Skip to content

Commit 680bf5a

Browse files
fix(tui): resubscribe RPC event streams after they end (#110)
* fix(tui): resubscribe RPC event streams after they end * docs(loop-system): distinguish confirmed from best-effort post-action completion
1 parent b382204 commit 680bf5a

22 files changed

Lines changed: 239 additions & 80 deletions

‎docs/api/_media/architecture.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ The TUI plugin provides:
6868
- Command palette integration (`Execute plan`, `Open web dashboard`, `Build sandbox template`, `Toggle sandbox`)
6969
- Model selection with recent-model tracking
7070

71-
The TUI talks to the server through the V2 plugin RPC port (`FORGE_RPC`): `executePlan` for plan launches, `loopDefaults` for the execution dialog's default iterations and sandbox resources, and `autoApproveState`/`autoApproveSet` for per-session auto-approve (served by the core in `src/host/forge-core.ts`), and, served by `src/services/tui-rpc-service.ts`, `loops`/`loopSidebar` for the loop list and sidebar, `sessionPlan` for the stored plan, `loopRestart` for loop restarts, `hostSandboxState`/`hostSandboxSet` for the host-session sandbox, and `worktrees` for the server's Forge worktree directories; `version` reports the server plugin version. The server pushes `toast`, `sessionDelete`, `loopsChanged`, `autoApproveChanged`, and `hostSandboxChanged` events; the TUI never polls — it fetches at startup, when one of these events arrives for its project, when the open session changes, and on `server.connected` after a reconnect. The TUI holds no Forge database of its own — its only `forge.db` reference is the path handed to the local dashboard launcher — so it works attached to a remote OpenCode server running Forge.
71+
The TUI talks to the server through the V2 plugin RPC port (`FORGE_RPC`): `executePlan` for plan launches, `loopDefaults` for the execution dialog's default iterations and sandbox resources, and `autoApproveState`/`autoApproveSet` for per-session auto-approve (served by the core in `src/host/forge-core.ts`), and, served by `src/services/tui-rpc-service.ts`, `loops`/`loopSidebar` for the loop list and sidebar, `sessionPlan` for the stored plan, `loopRestart` for loop restarts, `hostSandboxState`/`hostSandboxSet` for the host-session sandbox, and `worktrees` for the server's Forge worktree directories; `version` reports the server plugin version. The server pushes `toast`, `sessionDelete`, `loopsChanged`, `autoApproveChanged`, and `hostSandboxChanged` events; the TUI never polls — it fetches at startup, when one of these events arrives for its project, when the open session changes, and on `server.connected` after a reconnect. The OpenCode client ends an RPC event subscription for good when its shared event connection closes or the subscriber overflows, so `followRpcEvent` (`src/tui/v2-client.ts`) resubscribes each event stream and re-reads the state that stream drives. The TUI holds no Forge database of its own — its only `forge.db` reference is the path handed to the local dashboard launcher — so it works attached to a remote OpenCode server running Forge.
7272

7373
## Module Layout
7474

@@ -119,7 +119,7 @@ Sandbox is optional and controlled by `sandbox.enabled` (default `true`) with dr
119119
- **SandboxManager** (`sandbox/manager.ts`) - Sandbox lifecycle management
120120
- **SandboxContext** (`sandbox/context.ts`) - Tool call redirection
121121
- **SandboxTools** (`hooks/sandbox-tools.ts`) - Hooks for sandbox integration
122-
- **SandboxMessage** (`hooks/sandbox-message.ts`) - Tells the agent its tool calls run in a container
122+
- **SandboxMessage** (`hooks/sandbox-message.ts`) - Adds the sandbox context note to every sandboxed request, and after the sandbox is turned off repeats the host note on every request until it is turned back on
123123
- **SessionSandboxController** (`sandbox/session-controller.ts`) - Host-session sandbox selection and reconciliation
124124
- **Shell shim** (`sandbox/shell-shim.ts`) - Generated shim routing the native `shell` tool through `msb exec`
125125

‎docs/api/_media/configuration.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ The block applies to loop, audit, and post-action sessions.
9696

9797
### Post-Action
9898

99-
`loop.postAction` configures an optional post-completion action phase. It runs inside the worktree after a clean final audit and before teardown.
99+
`loop.postAction` configures an optional post-completion action phase. It runs inside the worktree after a clean audit (a clean final audit for sectioned loops, `audit-clear` for non-sectioned plan loops) and before teardown. Goal loops never run it. See [Post-Completion Action Phase](loop-system.md#post-completion-action-phase) for completion confirmation.
100100

101101
The phase is enabled only when `enabled === true` and at least one of `skill` or `prompt` is configured.
102102

‎docs/api/_media/loop-system.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ A loop completes when the active phase emits a clean audit result (optionally fo
236236
- Sectioned loops advance through clean section audits, then complete on `final-audit-clean`.
237237
- Dirty section audits rotate back to coding for the same section so findings can be addressed.
238238
- Dirty final audits rotate to a coding session in the `final_audit_fix` phase (no section rewind); when the fix coding pass goes idle, the loop returns straight to `final_auditing`.
239-
- After a clean audit — a clean final audit for sectioned loops, `audit-clear` for non-sectioned plan loops — if `loop.postAction.enabled` is `true` and specifies a `skill` or `prompt`, the loop enters a `post_action` phase that runs inside the worktree before teardown. Completion occurs when the post-action session goes idle (`post-action-complete` event). Goal loops never enter this phase.
239+
- After a clean audit — a clean final audit for sectioned loops, `audit-clear` for non-sectioned plan loops — if `loop.postAction.enabled` is `true` and specifies a `skill` or `prompt`, the loop enters a `post_action` phase that runs inside the worktree before teardown. Normal confirmed completion (`post-action-complete` event) occurs once the post-action session is idle, every loop session has settled, and the final reply confirms completion; see [Behavior](#behavior) for the best-effort completion that follows exhausted or failed continuation attempts and the abort-with-reply exceptions. Goal loops never enter this phase.
240240

241241
## Termination
242242

@@ -269,15 +269,18 @@ After a clean audit (a clean final audit for sectioned loops, `audit-clear` for
269269
| `loop.postAction.enabled` | `boolean` | `false` | Enable the post-completion action phase. |
270270
| `loop.postAction.skill` | `string` | — | Name of a skill to load via the Skill tool at action time (e.g. `"pr-review"`). Must be installed host-side. |
271271
| `loop.postAction.prompt` | `string` | — | Optional extra instruction text appended to the action prompt. Used standalone when no skill is set. |
272+
| `loop.postAction.model` | `string` | — | Optional `provider/model` for the post-action session. Falls back to the loop's auditor model when unset or when the configured model fails. |
272273

273274
### Behavior
274275

275276
- Runs only after a clean audit completes (a clean final audit for sectioned loops, `audit-clear` for non-sectioned plan loops); goal loops never run it.
276277
- Runs **inside the worktree** as the `code` agent, with access to the full worktree state (including uncommitted changes).
277278
- **Best-effort:** The post-action result is not re-audited; it applies only safe, scoped fixes. The question tool is blocked — any finding requiring clarification is auto-deferred.
278-
- On idle (`post-action-complete`), the loop terminates normally.
279+
- **Completion confirmation:** The prompt requires the agent to finish every step (subagents, merges, saved records) before ending its turn, and to end its final reply with the line `<!-- forge:post-action-complete -->` (`POST_ACTION_COMPLETE_MARKER` in `src/loop/prompts.ts`). When the session goes idle, Forge first waits until every loop session, subagents included, is idle and no prompt is queued in the session inbox; the wait is bounded by `busyStallTimeoutMs`, after which the loop terminates with an error because the worktree cannot be torn down safely. It then checks the final reply for the marker. Without it, Forge sends a continue prompt asking the agent to finish, up to 3 times; after the third, or if the continue prompt fails to send, the loop completes best-effort.
280+
- On a confirmed (or best-effort) `post-action-complete`, the loop terminates normally.
281+
- If the post-action session is aborted after it has replied, the reply is processed as-is: the settle wait and marker check are skipped.
279282
- If the post-action session fails to create, the loop terminates as completed without retrying.
280-
- **Outcome capture:** On `post-action-complete`, the post-action session's full assistant transcript is stored in the loop's `postActionReport`, and its raw final assistant message is stored verbatim in `completion_summary`. `loop-status` and the dashboard prefer the full **Post-Action Report** and fall back to **Completion Summary** when no report was captured. The loop status is **always** `completed` regardless of what the post-action reported — the plan itself was already cleared by the audit; the report only provides context (alternate-review verdict, CI result, etc.). Completion summary and report are captured only on the clean `post-action-complete` path; idle-exhausted, error, and abort-without-assistant terminations leave them empty.
283+
- **Outcome capture:** On `post-action-complete`, the post-action session's full assistant transcript (including any continue-prompt turns) is stored in the loop's `postActionReport`, and its final assistant message, with the completion marker removed, is stored in `completion_summary`. `loop-status` and the dashboard prefer the full **Post-Action Report** and fall back to **Completion Summary** when no report was captured. The loop status is **always** `completed` regardless of what the post-action reported — the plan itself was already cleared by the audit; the report only provides context (alternate-review verdict, CI result, etc.). Completion summary and report are captured only on the clean `post-action-complete` path; idle-exhausted, error, and abort-without-assistant terminations leave them empty.
281284

282285
## Cancellation
283286

‎docs/api/_media/modules.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ Translates OpenCode events into loop actions and manages lifecycle side-effects.
155155
| `forge-session-attach.ts` | Auto-attach loops on `session.created` and `chat.message` events |
156156
| `loop-permission.ts` | Patches subagent permission rulesets on `session.created` for active-loop sessions |
157157
| `sandbox-tools.ts` | Sandbox tool before/after redirection hooks |
158-
| `sandbox-message.ts` | Tells the agent its tool calls run in a container |
158+
| `sandbox-message.ts` | Adds the container note to every sandboxed request and, once the sandbox is turned off, the host note to every request until it is turned back on |
159159
| `group-orchestrator.ts` | Advances queued features when a group loop terminates |
160160
| `tool-hook-types.ts` | Shared tool before/after hook types |
161161

‎docs/api/functions/createParentSessionLookup.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
> **createParentSessionLookup**(`__namedParameters`): (`sessionId`) => `Promise`\<`string` \| `null`\>
1010
11-
Defined in: [host/forge-core.ts:162](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L162)
11+
Defined in: [host/forge-core.ts:162](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L162)
1212

1313
## Parameters
1414

‎docs/api/functions/createSessionDirectoryLookup.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
> **createSessionDirectoryLookup**(`options`): (`sessionId`) => `Promise`\<`string` \| `null`\>
1010
11-
Defined in: [host/forge-core.ts:264](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L264)
11+
Defined in: [host/forge-core.ts:264](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L264)
1212

1313
## Parameters
1414

‎docs/api/functions/setupForgeV2.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
> **setupForgeV2**(`ctx`): `Promise`\<() => `Promise`\<`void`\>\>
1010
11-
Defined in: [host/v2.ts:80](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/v2.ts#L80)
11+
Defined in: [host/v2.ts:80](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/v2.ts#L80)
1212

1313
## Parameters
1414

‎docs/api/interfaces/CompactionConfig.md‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
# Interface: CompactionConfig
88

9-
Defined in: [types.ts:224](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/types.ts#L224)
9+
Defined in: [types.ts:224](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/types.ts#L224)
1010

1111
Configuration for session compaction behavior.
1212

@@ -16,7 +16,7 @@ Configuration for session compaction behavior.
1616

1717
> `optional` **customPrompt?**: `boolean`
1818
19-
Defined in: [types.ts:226](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/types.ts#L226)
19+
Defined in: [types.ts:226](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/types.ts#L226)
2020

2121
Use a custom compaction prompt.
2222

@@ -26,6 +26,6 @@ Use a custom compaction prompt.
2626

2727
> `optional` **maxContextTokens?**: `number`
2828
29-
Defined in: [types.ts:228](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/types.ts#L228)
29+
Defined in: [types.ts:228](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/types.ts#L228)
3030

3131
Maximum context tokens for compaction. Currently unused by Forge's compaction hook, which reads only `customPrompt`.

‎docs/api/interfaces/CreateParentSessionLookupOptions.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,31 @@
66

77
# Interface: CreateParentSessionLookupOptions
88

9-
Defined in: [host/forge-core.ts:124](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L124)
9+
Defined in: [host/forge-core.ts:124](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L124)
1010

1111
## Properties
1212

1313
### client
1414

1515
> **client**: `ForgeClient`
1616
17-
Defined in: [host/forge-core.ts:125](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L125)
17+
Defined in: [host/forge-core.ts:125](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L125)
1818

1919
***
2020

2121
### directory
2222

2323
> **directory**: `string`
2424
25-
Defined in: [host/forge-core.ts:126](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L126)
25+
Defined in: [host/forge-core.ts:126](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L126)
2626

2727
***
2828

2929
### logger
3030

3131
> **logger**: `object`
3232
33-
Defined in: [host/forge-core.ts:128](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L128)
33+
Defined in: [host/forge-core.ts:128](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L128)
3434

3535
#### debug
3636

@@ -92,4 +92,4 @@ Defined in: [host/forge-core.ts:128](https://github.com/chriswritescode-dev/open
9292

9393
> **loop**: `Loop`
9494
95-
Defined in: [host/forge-core.ts:127](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L127)
95+
Defined in: [host/forge-core.ts:127](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L127)

‎docs/api/interfaces/CreateSessionDirectoryLookupOptions.md‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,36 +6,36 @@
66

77
# Interface: CreateSessionDirectoryLookupOptions
88

9-
Defined in: [host/forge-core.ts:210](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L210)
9+
Defined in: [host/forge-core.ts:210](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L210)
1010

1111
## Properties
1212

1313
### client
1414

1515
> **client**: `ForgeClient`
1616
17-
Defined in: [host/forge-core.ts:211](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L211)
17+
Defined in: [host/forge-core.ts:211](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L211)
1818

1919
***
2020

2121
### directory
2222

2323
> **directory**: `string`
2424
25-
Defined in: [host/forge-core.ts:212](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L212)
25+
Defined in: [host/forge-core.ts:212](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L212)
2626

2727
***
2828

2929
### loop
3030

3131
> **loop**: `Loop`
3232
33-
Defined in: [host/forge-core.ts:213](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L213)
33+
Defined in: [host/forge-core.ts:213](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L213)
3434

3535
***
3636

3737
### negativeTtlMs?
3838

3939
> `optional` **negativeTtlMs?**: `number`
4040
41-
Defined in: [host/forge-core.ts:214](https://github.com/chriswritescode-dev/opencode-forge/blob/60a77095f4a0787f7b93be817c313586da5e2cc2/src/host/forge-core.ts#L214)
41+
Defined in: [host/forge-core.ts:214](https://github.com/chriswritescode-dev/opencode-forge/blob/9f9f4b4938fa505ff72d179150b77baca67ba3e6/src/host/forge-core.ts#L214)

0 commit comments

Comments
 (0)