Просмотр исходного кода

fix: review fixes — task-id parser precedence, docs honesty, idle invariant

- parseTaskIdFromTaskOutput: v1 task_id line now wins over a stray
  bracketed (sessionID: x); v2 formats unchanged (tests added).
- docs/codemap: absent generate channel means secondary-model summaries
  are unavailable (logged), not a session-pipeline fallback; fix the
  stale 'shim does not shim' surface list in the wake-scheduler note.
- event-adapter: document the double-idle invariant (native
  session.status + synthesized session.idle; consumers must tolerate
  duplicate idle delivery) — comment + docs line, no behavior change.
- package.json: keep dist/tui.js bundling with a note — it is required
  by scripts/verify-release-artifact.ts (packagedRequiredFiles) and v1
  TUI hosts; the v2 entry is dist/tui2.js.
GoldJohnKing 2 недель назад
Родитель
Сommit
ec13fee5e9
6 измененных файлов с 42 добавлено и 6 удалено
  1. 9 3
      docs/opencode-v2-compatibility.md
  2. 1 0
      package.json
  3. 15 0
      src/utils/task.test.ts
  4. 6 2
      src/utils/task.ts
  5. 3 1
      src/v2/codemap.md
  6. 8 0
      src/v2/event-adapter.ts

+ 9 - 3
docs/opencode-v2-compatibility.md

@@ -116,7 +116,7 @@ the rest, and a zero-registration load logs a loud health-check warning.
 | Event handling (session tracking, lifecycle, cache telemetry) | ✅ | ✅ event pump + additive v2→v1 synthesis | — |
 | Tool execute hooks (apply-patch recovery, task-session, json-recovery) | ✅ | ✅ `createToolExecuteBridges` with subagent→task normalization | — |
 | Built-in MCPs (context7, gh_grep) auto-registered | ✅ | ✅ `ctx.mcp.transform` | `mcp.transform` ≥ #45408; older builds degrade to config-only — see [the snippet](#restoring-built-in-mcps-on-older-v2-builds) |
-| webfetch secondary-model summaries | ✅ | ✅ via `ctx.generate.text` | absent → summaries fall back to the session pipeline |
+| webfetch secondary-model summaries | ✅ | ✅ via `ctx.generate.text` | absent → secondary-model summaries are unavailable (logged) |
 | Foreground model fallback (rate-limit failover) | ✅ | ✅ shim translates re-prompt into `session.switchModel` + `delivery:"steer"` prompt | `switchModel` ≥ #43718; older builds steer on the current model with an honest log (fallback inactive) |
 | `/preset` (interactive switcher) | ✅ | ✅ TUI plugin entry (`./tui` → `dist/tui2.js`): sidebar + `/preset` dialog or `/preset <name>` fast path | TUI host needs `keymap.layer` + `ui.dialog.select`; config-file `preset` still applies at load |
 | Project directory | ✅ | ✅ `ctx.location.directory` | ≥ #45403; older builds use `process.cwd()` |
@@ -175,6 +175,12 @@ break this plugin:
 - **Command `execute` receives a prompt *object*, not a string**: v2 hands
   the handler a `PromptInput.Prompt`. The command bridge reads `.text`
   (`invocation?.prompt?.text ?? ''`) and never assumes a string.
+- **Duplicate idle delivery.** v2 deprecated `session.idle` in favor of
+  `session.status`; the adapter synthesizes `session.idle` additively, so an
+  idle-tolerant consumer watching both events sees idle twice per session.
+  Current consumers are idempotent per session (idle-reconciliation's
+  per-session timer guards); new idle consumers must tolerate duplicate
+  delivery.
 
 ## Installing on v2
 
@@ -268,8 +274,8 @@ spec while retaining frontmatter and Q&A history.
   `packages/core/src/tool/plugin/subagent.ts` sends a
   `session.synthetic` message with a `<subagent sessionID state …>`
   envelope to the parent). The capability stays v1-only (it also requires
-  host `session.get`/`todo`/`children`/`status`/`promptAsync` surfaces the
-  v2 shim does not shim).
+  host `todo`/`children` surfaces — and the v1 live session-`status` map —
+  that the v2 shim does not provide).
 - **`chat.headers`.** Not bridged (low value on v2 — an HTTP request hook
   exists if demand appears).
 

+ 1 - 0
package.json

@@ -53,6 +53,7 @@
     "README.ko-KR.md",
     "LICENSE"
   ],
+  "// build:plugin": "Keeps bundling src/tui.ts -> dist/tui.js: required by scripts/verify-release-artifact.ts (packagedRequiredFiles) and loaded by v1 TUI hosts. The v2 TUI entry is dist/tui2.js (see build:tui / the ./tui export).",
   "scripts": {
     "clean:dist": "bun -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\"",
     "build:plugin": "bun build src/index.ts src/tui.ts --outdir dist --target node --format esm --external @opencode-ai/plugin --external @opencode-ai/plugin/tui --external @opencode-ai/sdk --external @opencode-ai/sdk/v2 --external @opentui/core --external @opentui/solid --external solid-js --external jsdom --external zod",

+ 15 - 0
src/utils/task.test.ts

@@ -318,4 +318,19 @@ describe('v2 subagent output formats', () => {
     expect(parseTaskIdFromTaskOutput(out)).toBe('ses_e');
     expect(parseTaskStateFromOutput(out)).toBe('running');
   });
+
+  test('v1 task_id line wins over a stray bracketed sessionID', () => {
+    const out = [
+      'Launched background task.',
+      'task_id: ses_v1',
+      'Related discussion mentions (sessionID: ses_other) in passing.',
+    ].join('\n');
+    expect(parseTaskIdFromTaskOutput(out)).toBe('ses_v1');
+  });
+
+  test('v2 background-launch text still parses after precedence reorder', () => {
+    const out =
+      'The subagent is working in the background (sessionID: ses_c). You will be notified automatically when it finishes.';
+    expect(parseTaskIdFromTaskOutput(out)).toBe('ses_c');
+  });
 });

+ 6 - 2
src/utils/task.ts

@@ -47,9 +47,10 @@ export function parseTaskIdFromTaskOutput(output: string): string | undefined {
   const failed =
     /Subagent (?:failed|cancelled) \(sessionID:\s*([^\s)]+)\)/i.exec(output);
   if (failed) return failed[1];
-  const background = /\(sessionID:\s*([^\s)]+)\)/.exec(output);
-  if (background) return background[1];
 
+  // v1 `task_id:` line before the generic bracket pattern: a v1 output can
+  // quote a foreign `(sessionID: x)` in passing, and the explicit marker
+  // is the authoritative id.
   const lines = output.split(/\r?\n/);
 
   for (const line of lines) {
@@ -63,6 +64,9 @@ export function parseTaskIdFromTaskOutput(output: string): string | undefined {
     return match[1];
   }
 
+  const background = /\(sessionID:\s*([^\s)]+)\)/.exec(output);
+  if (background) return background[1];
+
   return undefined;
 }
 

+ 3 - 1
src/v2/codemap.md

@@ -118,7 +118,9 @@ expanding the global v2 client surface.
   `sessionManagerMultiplexerConfig`).
 - `src/tools/smartfetch/secondary-model.ts`: consumes the
   `experimental_v2.generateText` channel threaded by `setup` for one-shot
-  summaries; absent channel → v1 session path.
+  summaries; absent channel → secondary-model summaries are unavailable
+  (logged) — the v2 shim has no `session.create`/`tool.ids`, so the v1
+  session pipeline cannot substitute.
 - Build: `build:v2` bundles `src/index.ts` (which pulls in `src/v2/`) into
   `dist/server.js` (self-contained except `jsdom`); `build:tui` bundles
   `src/v2/tui.ts` into `dist/tui2.js`.

+ 8 - 0
src/v2/event-adapter.ts

@@ -116,6 +116,14 @@ export function mapV2EventToV1(
         : undefined
       : undefined;
     if (statusType === 'idle' && typeof props.sessionID === 'string') {
+      // Double-idle invariant: on v2 an idle-tolerant consumer that watches
+      // BOTH the native `session.status` event and the synthesized
+      // `session.idle` receives idle twice per session. Safe today because
+      // every idle consumer is idempotent per session — idle-reconciliation
+      // guards repeats via its per-session timer maps
+      // (`idleReconcileTimers.has` / `childIdleReconcileTimers.has`,
+      // idle-reconciliation.ts:42,64). Any NEW idle consumer must tolerate
+      // duplicate idle delivery.
       out.push({
         type: 'session.idle',
         properties: { sessionID: props.sessionID },