From 623a05f1ee209a610db95b9c22ffb0593bcf4508 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:42:57 +0800 Subject: [PATCH 01/14] feat(orca): add bounded read-only current-turn Worker wait --- src/worker_wait.mjs | 54 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 src/worker_wait.mjs diff --git a/src/worker_wait.mjs b/src/worker_wait.mjs new file mode 100644 index 0000000..fbef925 --- /dev/null +++ b/src/worker_wait.mjs @@ -0,0 +1,54 @@ +import { OrcaError } from './cli.mjs'; +import { lifecycle } from './lifecycle.mjs'; + +// This is a bounded, read-only wait inside the *current* model tool call. +// It does not start a new model turn and is not an unattended Host wake. +const VALID_DISPATCH = /^[A-Za-z0-9_-]{1,128}$/; +const wait = ms => new Promise(resolve => setTimeout(resolve, ms)); + +export async function waitForWorkers({ + dispatchIds, waitSeconds = 10, read, + now = () => performance.now(), sleep = wait, +}) { + if (!Array.isArray(dispatchIds) || dispatchIds.length < 1 || dispatchIds.length > 3 + || dispatchIds.some(id => typeof id !== 'string' || !VALID_DISPATCH.test(id)) + || new Set(dispatchIds).size !== dispatchIds.length) { + throw new OrcaError('invalid_arguments', 'Provide 1-3 distinct exact Dispatch IDs'); + } + if (!Number.isInteger(waitSeconds) || waitSeconds < 0 || waitSeconds > 12) { + throw new OrcaError('invalid_arguments', 'waitSeconds must be 0-12'); + } + if (typeof read !== 'function') throw new OrcaError('invalid_arguments', 'Worker reader is required'); + + const started = now(); + const deadline = started + waitSeconds * 1000; + let polls = 0; + while (true) { + const workers = []; + // Only read the selected Dispatch attempts. Never create, stop, retry or + // release anything in this call. Each CLI read has its own short timeout. + for (const dispatchId of dispatchIds) { + const raw = await read(dispatchId); + const state = lifecycle(raw); + workers.push({ + dispatchId, phase: state.phase, settled: state.settled, + workerState: state.workerState, dispatchStatus: state.dispatchStatus, + userTakeover: state.userTakeover, + }); + } + polls += 1; + const allSettled = workers.every(worker => worker.settled); + const allSucceeded = workers.every(worker => worker.phase === 'worker_done'); + const needsAttention = workers.some(worker => + worker.userTakeover || worker.phase === 'start_unknown'); + const elapsedMilliseconds = Math.max(0, Math.floor(now() - started)); + if (allSettled || needsAttention || now() >= deadline) { + return { + waitOutcome: allSettled ? 'all_settled' : needsAttention ? 'needs_attention' : 'wait_elapsed', + allSettled, allSucceeded, polls, elapsedMilliseconds, + workers, readOnly: true, newModelTurnStarted: false, + }; + } + await sleep(Math.min(1500, Math.max(0, deadline - now()))); + } +} From 27c2161b642a24db935ce0541be2db83be174a63 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:43:09 +0800 Subject: [PATCH 02/14] feat(orca): expose read-only Worker wait through native Plugin v1 --- src/tools.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tools.mjs b/src/tools.mjs index 6d42e38..f0f28d0 100644 --- a/src/tools.mjs +++ b/src/tools.mjs @@ -4,6 +4,7 @@ import { integrity, readPages } from './transcript.mjs'; import { once, intentStatus } from './journal.mjs'; import { lifecycle } from './lifecycle.mjs'; import { findCoordinator, COORDINATOR_TITLE } from './coordinator.mjs'; +import { waitForWorkers } from './worker_wait.mjs'; const f = field; const r = async (op, args, mutating=false, timeoutMs=20000) => normalizeResult(op, await runOrca(args,{mutating,timeoutMs})); const readOnly = {readOnlyHint:true,destructiveHint:false,idempotentHint:true,openWorldHint:false}; @@ -76,6 +77,15 @@ export const tools = [ const receipt=await r('list_workers',args); return {...receipt, lifecycle:(receipt.result?.workers||[]).map(row=>({dispatchId:row.dispatchId,...lifecycle(row)}))}; },readOnly), + tool('orca_wait_workers','Bounded read-only wait in the current turn; NOT an unattended Host wake. Never launches or retries Workers.',{ + dispatchIds:{type:'array',items:{type:'string',minLength:1,maxLength:128},minItems:1,maxItems:3,description:'1-3 distinct exact Dispatch IDs'}, + waitSeconds:f.int('Wait in current tool call, 0-12 seconds; default 10'), + },['dispatchIds'],async(a)=>({ + ok:true,operation:'wait_workers',result:await waitForWorkers({ + dispatchIds:a.dispatchIds,waitSeconds:a.waitSeconds??10, + read:async dispatchId => (await runOrca(['orchestration','worker-show','--dispatch',dispatchId],{timeoutMs:3000})).result, + }), + }),readOnly), tool('orca_read_worker','Read a bounded page of one worker output, preserving continuation cursor.',{ dispatchId:f.str('Exact Dispatch attempt ID',128),cursor:f.optionalStr('Previous cursor',2048), limit:f.int('Rows per page, 1-50'),source:f.variant('Output source',['auto','transcript','terminal']), From 78d3561b98df2a2cc1c930fcfc0ac2402edaef9e Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:43:28 +0800 Subject: [PATCH 03/14] test(orca): verify current-turn wait state, timeout and no relaunch --- tests/worker_wait.test.mjs | 52 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 tests/worker_wait.test.mjs diff --git a/tests/worker_wait.test.mjs b/tests/worker_wait.test.mjs new file mode 100644 index 0000000..c6d9723 --- /dev/null +++ b/tests/worker_wait.test.mjs @@ -0,0 +1,52 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { waitForWorkers } from '../src/worker_wait.mjs'; +import { interact, boot, call } from './helpers.mjs'; + +const good = {worker:{state:'succeeded'},dispatch:{status:'completed'}}; +const working = {worker:{state:'running'},dispatch:{status:'active'}}; +test('immediate settled result is read-only and not a new Host turn',async()=>{ + const seen=[]; + const result=await waitForWorkers({dispatchIds:['dispatch_a','dispatch_b'],waitSeconds:0, + read:async id=>{seen.push(id);return good;}}); + assert.deepEqual(seen,['dispatch_a','dispatch_b']); + assert.equal(result.waitOutcome,'all_settled'); + assert.equal(result.allSucceeded,true); + assert.equal(result.newModelTurnStarted,false); +}); +test('polls while running and returns one settled snapshot',async()=>{ + let clock=0;let reads=0;const sleeps=[]; + const result=await waitForWorkers({dispatchIds:['dispatch_a'],waitSeconds:4, + read:async()=>{reads++;return reads===3?good:working;}, + now:()=>clock,sleep:async ms=>{sleeps.push(ms);clock+=ms;}}); + assert.equal(result.polls,3); + assert.deepEqual(sleeps,[1500,1500]); + assert.equal(result.waitOutcome,'all_settled'); +}); +test('deadline is an observation, not a successful completion',async()=>{ + let clock=0; + const result=await waitForWorkers({dispatchIds:['dispatch_a'],waitSeconds:2, + read:async()=>working,now:()=>clock,sleep:async ms=>{clock+=ms;}}); + assert.equal(result.waitOutcome,'wait_elapsed'); + assert.equal(result.allSettled,false); + assert.equal(result.allSucceeded,false); +}); +test('unknown Worker state requires attention, never restart',async()=>{ + const result=await waitForWorkers({dispatchIds:['dispatch_a'],waitSeconds:3, + read:async()=>({worker:{state:'start_unknown'}})}); + assert.equal(result.waitOutcome,'needs_attention'); + assert.equal(result.polls,1); +}); +test('invalid IDs and budgets are rejected before any read',async()=>{ + let reads=0;const read=async()=>{reads++;return good;}; + for(const args of [{dispatchIds:[]},{dispatchIds:['x','x']},{dispatchIds:['x','a/b']}, + {dispatchIds:['x'],waitSeconds:-1},{dispatchIds:['x'],waitSeconds:13}]) + await assert.rejects(waitForWorkers({...args,read}),/invalid_arguments|Dispatch IDs|waitSeconds/); + assert.equal(reads,0); +}); +test('Plugin v1 exposes bounded Worker wait and only issues worker-show',async()=>{ + const response=await interact([boot,call('orca_wait_workers',{dispatchIds:['dispatch_demo'],waitSeconds:0})]); + assert.equal(response.records[1].result.structuredContent.result.waitOutcome,'all_settled'); + assert.equal(response.records[1].result.structuredContent.result.newModelTurnStarted,false); + assert.deepEqual(response.commands,[['orchestration','worker-show','--dispatch','dispatch_demo','--json']]); +}); From cd261ecdc0ef01d4f842ff5f2a8d9cf2f518b8e7 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 15:57:40 +0800 Subject: [PATCH 04/14] ci(orca): validate native plugin contracts on PR --- .github/workflows/orca-plugin-tests.yml | 34 +++++++++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/orca-plugin-tests.yml diff --git a/.github/workflows/orca-plugin-tests.yml b/.github/workflows/orca-plugin-tests.yml new file mode 100644 index 0000000..692f824 --- /dev/null +++ b/.github/workflows/orca-plugin-tests.yml @@ -0,0 +1,34 @@ +name: Orca native plugin tests +on: + push: + branches: + - "feat/active-turn-worker-wait-*" + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + node-tests: + runs-on: ubuntu-22.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + node-version: ['18', '22'] + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4 + with: + persist-credentials: false + - name: Setup Node.js + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ matrix.node-version }} + - name: Verify syntax + run: | + node --check src/worker_wait.mjs + node --check src/tools.mjs + - name: Run entire plugin regression suite + run: npm test From 4829896cd9cc82223d0af35ae31251f585d4750d Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:00:42 +0800 Subject: [PATCH 05/14] fix(orca): comply with Native Plugin v1 scalar-only input schema --- src/tools.mjs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/tools.mjs b/src/tools.mjs index f0f28d0..9e4c3d3 100644 --- a/src/tools.mjs +++ b/src/tools.mjs @@ -78,11 +78,13 @@ export const tools = [ return {...receipt, lifecycle:(receipt.result?.workers||[]).map(row=>({dispatchId:row.dispatchId,...lifecycle(row)}))}; },readOnly), tool('orca_wait_workers','Bounded read-only wait in the current turn; NOT an unattended Host wake. Never launches or retries Workers.',{ - dispatchIds:{type:'array',items:{type:'string',minLength:1,maxLength:128},minItems:1,maxItems:3,description:'1-3 distinct exact Dispatch IDs'}, + dispatchId:f.str('First exact Dispatch ID',128), + dispatchId2:f.optionalStr('Optional second exact Dispatch ID',128), + dispatchId3:f.optionalStr('Optional third exact Dispatch ID',128), waitSeconds:f.int('Wait in current tool call, 0-12 seconds; default 10'), - },['dispatchIds'],async(a)=>({ + },['dispatchId'],async(a)=>({ ok:true,operation:'wait_workers',result:await waitForWorkers({ - dispatchIds:a.dispatchIds,waitSeconds:a.waitSeconds??10, + dispatchIds:[a.dispatchId,a.dispatchId2,a.dispatchId3].filter(x=>x!==undefined),waitSeconds:a.waitSeconds??10, read:async dispatchId => (await runOrca(['orchestration','worker-show','--dispatch',dispatchId],{timeoutMs:3000})).result, }), }),readOnly), From 4bb2f6f3a49e7e4cb61e9d9e1e32a0298f405311 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:00:52 +0800 Subject: [PATCH 06/14] test(orca): update tool-count and scalar-only protocol contract --- tests/protocol.test.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/protocol.test.mjs b/tests/protocol.test.mjs index eaac45c..8710022 100644 --- a/tests/protocol.test.mjs +++ b/tests/protocol.test.mjs @@ -7,7 +7,7 @@ test('webcodex v1 initializes and discovers real tool schema',async()=>{ assert.equal(result.exit,0); assert.equal(result.records[0].result.protocolVersion,'webcodex-plugin-v1'); const ts=result.records[1].result.tools; - assert.equal(ts.length,21);assert.equal(ts[0].name,'orca_health'); + assert.equal(ts.length,22);assert.equal(ts[0].name,'orca_health'); assert.equal(new Set(ts.map(x=>x.name)).size,ts.length); assert.equal(ts[0].inputSchema.type,'object'); }); @@ -16,7 +16,7 @@ test('rejects invalid JSON-RPC method before initialization',async()=>{ assert.equal(r.records[0].error.message,'Not initialized'); }); -test('all 21 tools stay within WebCodex native v1 schema keyword profile',async()=>{ +test('all 22 tools stay within WebCodex native v1 schema keyword profile',async()=>{ const r=await interact([boot,{method:'tools/list',params:{}}]); const defs=r.records[1].result.tools; const allowed=new Set(['type','description','title','enum','const','properties','required','additionalProperties','minLength','maxLength','items','minItems','maxItems']); From 3d780f6be3fec280b55a918accad7b42496a4d03 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:01:05 +0800 Subject: [PATCH 07/14] fix(orca): surface takeover even when Dispatch reports completion --- src/worker_wait.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/worker_wait.mjs b/src/worker_wait.mjs index fbef925..00cca5c 100644 --- a/src/worker_wait.mjs +++ b/src/worker_wait.mjs @@ -38,13 +38,13 @@ export async function waitForWorkers({ } polls += 1; const allSettled = workers.every(worker => worker.settled); - const allSucceeded = workers.every(worker => worker.phase === 'worker_done'); const needsAttention = workers.some(worker => worker.userTakeover || worker.phase === 'start_unknown'); + const allSucceeded = !needsAttention && workers.every(worker => worker.phase === 'worker_done'); const elapsedMilliseconds = Math.max(0, Math.floor(now() - started)); if (allSettled || needsAttention || now() >= deadline) { return { - waitOutcome: allSettled ? 'all_settled' : needsAttention ? 'needs_attention' : 'wait_elapsed', + waitOutcome: needsAttention ? 'needs_attention' : allSettled ? 'all_settled' : 'wait_elapsed', allSettled, allSucceeded, polls, elapsedMilliseconds, workers, readOnly: true, newModelTurnStarted: false, }; From 2e42e580d96cbc8c65eec52b8ed1baed0ec25464 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:01:20 +0800 Subject: [PATCH 08/14] test(orca): cover v1 scalar inputs, deduplication and takeover --- tests/worker_wait.test.mjs | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/tests/worker_wait.test.mjs b/tests/worker_wait.test.mjs index c6d9723..9f55b6d 100644 --- a/tests/worker_wait.test.mjs +++ b/tests/worker_wait.test.mjs @@ -45,8 +45,35 @@ test('invalid IDs and budgets are rejected before any read',async()=>{ assert.equal(reads,0); }); test('Plugin v1 exposes bounded Worker wait and only issues worker-show',async()=>{ - const response=await interact([boot,call('orca_wait_workers',{dispatchIds:['dispatch_demo'],waitSeconds:0})]); + const response=await interact([boot,call('orca_wait_workers',{dispatchId:'dispatch_demo',waitSeconds:0})]); assert.equal(response.records[1].result.structuredContent.result.waitOutcome,'all_settled'); assert.equal(response.records[1].result.structuredContent.result.newModelTurnStarted,false); assert.deepEqual(response.commands,[['orchestration','worker-show','--dispatch','dispatch_demo','--json']]); }); + +test('Plugin v1 accepts two distinct scalar Dispatch IDs and issues two read-only queries',async()=>{ + const r=await interact([boot,call('orca_wait_workers',{dispatchId:'dispatch_a',dispatchId2:'dispatch_b',waitSeconds:0})]); + assert.equal(r.records[1].result.structuredContent.result.workers.length,2); + assert.deepEqual(r.commands,[ + ['orchestration','worker-show','--dispatch','dispatch_a','--json'], + ['orchestration','worker-show','--dispatch','dispatch_b','--json'], + ]); +}); +test('Plugin v1 rejects duplicate Dispatch IDs without CLI calls',async()=>{ + const r=await interact([boot,call('orca_wait_workers',{dispatchId:'same',dispatchId2:'same',waitSeconds:0})]); + assert.equal(r.records[1].result.structuredContent.code,'invalid_arguments'); + assert.deepEqual(r.commands,[]); +}); +test('takeover is attention even after success; cannot imply clean continuation',async()=>{ + const result=await waitForWorkers({dispatchIds:['dispatch_a'],waitSeconds:0, + read:async()=>({...good,terminalResource:{retainedReason:'user_takeover'}})}); + assert.equal(result.waitOutcome,'needs_attention'); + assert.equal(result.allSucceeded,false); + assert.equal(result.workers[0].userTakeover,true); +}); +test('failed worker is settled but not successful',async()=>{ + const result=await waitForWorkers({dispatchIds:['dispatch_a'],waitSeconds:0, + read:async()=>({worker:{state:'failed'},dispatch:{status:'failed'}})}); + assert.equal(result.waitOutcome,'all_settled'); + assert.equal(result.allSucceeded,false); +}); From 4003e677670bd494a4ba47fe1522781564af788e Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:03:59 +0800 Subject: [PATCH 09/14] docs(orca): distinguish current-turn polling from Host native wake --- README.md | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 404527a..7e7cd2e 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - **恢复与诊断**:使用 Orca 官方 Run/Task/Dispatch/Terminal 查询恢复状态,复用有效 Coordinator,避免无证据地新建终端。 - **对话分页读取**:对读取来源、分页游标和内容完整性进行验证;无法获取完整原始对话时明确标记为不完整。 -共提供 **21 个工具**,分为只读查询和有副作用的编排操作。 +当前实验分支提供 **22 个工具**(主分支 v0.3.1 为 21 个),分为只读查询和有副作用的编排操作。 ## 环境要求 @@ -57,20 +57,34 @@ timeout_secs = 30 | 分类 | 工具 | | --- | --- | -| 环境与查询 | `orca_health`、`orca_list_workspaces`、`orca_list_workers`、`orca_read_worker`、`orca_read_worker_pages`、`orca_show_worker` | +| 环境与查询 | `orca_health`、`orca_list_workspaces`、`orca_list_workers`、`orca_wait_workers`、`orca_read_worker`、`orca_read_worker_pages`、`orca_show_worker` | | 状态恢复 | `orca_worker_launch_intent`、`orca_request_status`、`orca_coordinator_lookup`、`orca_recovery_snapshot`、`orca_check_messages` | | 编排与通信 | `orca_create_run`、`orca_create_coordinator`、`orca_start_worker`、`orca_send_prompt`、`orca_send_message`、`orca_receive_delivery`、`orca_ack_messages`、`orca_reply_message` | | 回收与释放 | `orca_stop_worker`、`orca_release_worker` | 对有副作用的操作,不应在超时或连接中断后直接重试;先通过只读恢复工具核实真实状态。 +## 实验性:当前回合 Worker 等待(非 Host 主动唤醒) + +`orca_wait_workers` 仅在已有 ChatGPT 模型回合执行期间进行限时、只读状态查询,**不会**在本回合结束后自动启动新的 ChatGPT 回合。 + +输入遵循 Native Plugin v1 的标量字段限制: + +```json +{"dispatchId":"dispatch_a","dispatchId2":"dispatch_b","waitSeconds":10} +``` + +最多指定 3 个互不相同的 Dispatch ID(`dispatchId3` 可选),`waitSeconds` 为 0–12 秒。结果区分 `all_settled`、`needs_attention` 和 `wait_elapsed`,返回每个 Worker 状态和 `newModelTurnStarted:false`。查询时限不包含已开始的 CLI 查询收尾耗时。不得将 `all_settled` 直接等同所有 Worker 成功(另看 `allSucceeded`)。 + +Host 在后台无用户输入时自动唤醒模型仍然是独立的、未通过验收的能力;见 [Host 唤醒边界](docs/HOST_WAKE_BOUNDARY.md)。 + ## 测试与已知限制 ```bash npm test ``` -2026-10-13 本机验证:**61/61 项自动化测试通过(Node.js 24.13.0)**。测试使用模拟 Orca CLI,不能替代真实多 Agent 端到端验收。真实 Orca 只读回归、Plugin check/reload 记录见 [v0.3.1 测试报告](docs/TEST_REPORT_v0.3.1.md)。 +2026-10-14 本机实验分支验证:**71/71 项自动化测试通过(Node.js 24.13.0)**;v0.3.1 主分支历史记录为 2026-10-13 的 61/61。测试使用模拟 Orca CLI,不能替代真实多 Agent 端到端验收。真实 Orca 只读回归、Plugin check/reload 记录见 [v0.3.1 测试报告](docs/TEST_REPORT_v0.3.1.md)。 以下能力仍有限制: From c6b10918f5a1573f1a9ce419abbef65d1aa23973 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:04:47 +0800 Subject: [PATCH 10/14] docs(orca): establish Host wake authorization and six-case acceptance boundary --- docs/HOST_WAKE_BOUNDARY.md | 50 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 docs/HOST_WAKE_BOUNDARY.md diff --git a/docs/HOST_WAKE_BOUNDARY.md b/docs/HOST_WAKE_BOUNDARY.md new file mode 100644 index 0000000..df275bf --- /dev/null +++ b/docs/HOST_WAKE_BOUNDARY.md @@ -0,0 +1,50 @@ +# Host 后台主动唤醒:架构边界与验收门槛 + +更新:2026-10-14。本文只描述已审查源代码、已验证的替代方案和仍缺失的 Host 原生能力,不声明已经支持无人值守自动续轮。 + +## 目标与现有行为 + +用户希望 ChatGPT 通过 WebCodex 调度 Orca Coordinator + Claude Code / Antigravity Workers,结束当前回答后,在某 Worker 完成时由 Host **自动开启一个新模型回合**,准确恢复 Run / Dispatch,并在需要时继续分配任务。 + +目前可用的两条路径: + +1. **当前回合只读等待**:本仓库 `orca_wait_workers` 在模型回合尚活跃时,经 Native Tool Plugin v1 → Orca CLI 的 `orchestration worker-show` 对 1–3 个精确 Dispatch ID 轮询。成功完成此工具调用之后的下一步仍属于**原回合**。此方式不依赖 MCP iframe,但也不具备无人值守唤醒能力。 +2. **Job Terminal MCP App 通知**:WebCodex Fork 的 `present_job_terminal_continuation` 使用 iframe `setTimeout` 调用 Server 状态接口,并在一条持久 Wait 的 prepare fence 之后,通过 `ui/message` 尝试交付给 Host。该消息的成功 ACK **只证明交付被 Host 接受**,不证明 Host 创建了新的 ChatGPT 模型回合。后台页可能暂停 iframe 计时器。 + +审查代码位置: + +- WebCodex `src/job_terminal_attention.rs`:`JobTerminalContinuationAdapter` 是 Server 内部抽象;当前提交仅通过 `#[cfg(test)] install_adapter_for_tests` 安装测试实现。没有证明生产端存在可从无人值守后台调用的原生 Host 转轮接口。 +- WebCodex `src/agent_wake.rs`:`ContinuationAdapter` 以 `production_auto_resume_available` 明确区分真实生产模型唤醒与 mock / 手工调度。 +- WebCodex `src/mcp_job_terminal_continuation_app.html`:`ui/message` 的投递及其 ACK 与模型回合启动证据分离。 +- WebCodex `docs/implementation/mcp-app-auto-resume-restoration.md`:包含 Agent / Job 各三种可见性和延迟组合,共六项真实 Host 验收条件。 + +2026-10-14 的实机探测:Job 完成至 Host prepare 相差 2,608 秒,最终 `delivered`,但没有无用户操作、自动新模型回合的证据。不能将 `WEBCODEX_MCP_APP_RESUME_MODE` 强行改为 `unattended` 作为修复。 + +## 真正的原生 Host Wake 最小合同 + +该能力**必须由有权限的 Host 提供并确认**,不能由 Orca 插件伪造: + +1. **授权**:绑定当前用户、ChatGPT 会话/窗口、项目与精确 Wait(或 Agent Wake),且只接受一次明确授权。不能从 Run ID 猜测 conversation、也不能把浏览器页面令牌当成任意会话续轮权限。 +2. **唤醒请求**:Host 自身持久化监听 Job terminal 事件;不依赖网页 iframe 定时器存活。持久键为 `(principal, conversation, wake_id, generation)`,同一逻辑通知在状态不明时不盲发第二次。 +3. **投递与执行双重回执**:分别记录 `dispatch_accepted` 与 `fresh_turn_started`(关联精确对话和模型回合 ID)。新模型回合必须实际查询 `observe_jobs` 或消费 Agent Wake,才能记录 `continuation_observed`;缺任一层则仍是未验证。 +4. **冲突关闭**:用户撤销授权、原会话不可用、过期/跨主身份、stale generation、接管的终端、Host 响应未知时,一律停止自动操作,只保留可观测恢复路径。 +5. **运行不变式**:恢复原有 Orca Run / Coordinator / Worker;不得凭通知重复调用 `orca_start_worker`,也不得擅自新建 Coordinator 或清理用户接管的终端。 + +建议按独立的能力发现与授权合同接入 Host;在 Host 未提供此 API 之前,保持 readiness=unknown,而不是增加无人值守声明。 + +## 发布门槛(真实 ChatGPT Host) + +在隔离的测试会话和测试工作区中,按 WebCodex 的官方六项矩阵验证 Agent/Job × 前台短延迟/前台长延迟/后台长延迟。每项需记录: + +- Job terminal 或 Agent Wake 的 UTC 时间 +- Host 接受投递的时间及不确定结果 +- 原回合终止后的**新模型回合**起始时间 +- 新回合调用精确 `observe_jobs` / `consume_agent_wake` 的证据 +- 没有新用户消息、没有重复 Worker 启动、没有重复模型回合 +- 页面隐藏、重开、Runner 重启、网络抖动下的恢复和关闭行为 + +`npm test`、Node 回归和 Rust CI 只能检查各自的逻辑合同,不能替代这六项真实 Host 的自动唤醒验收。 + +## 本仓库目前的回退策略 + +插件测试将 `wait_elapsed`、`start_unknown`、`needs_attention` 和失败 Worker 单独报告。长任务应在原回合结束后由下一次**实际用户发起的回合**查询 Orca 快照恢复;未来如果 Host 提供被授权的 native wake,再由 Host 注入带精确身份的事件。未知状态绝不自动重启 Worker。 From 803b3320ea2a2b11413875fa1e5b416f8be8b86e Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:17 +0800 Subject: [PATCH 11/14] fix(orca): enforce exact Dispatch identity on aggregated snapshots --- src/worker_wait.mjs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/worker_wait.mjs b/src/worker_wait.mjs index 00cca5c..86d85f9 100644 --- a/src/worker_wait.mjs +++ b/src/worker_wait.mjs @@ -29,6 +29,15 @@ export async function waitForWorkers({ // release anything in this call. Each CLI read has its own short timeout. for (const dispatchId of dispatchIds) { const raw = await read(dispatchId); + if (!raw || typeof raw !== 'object' || Array.isArray(raw)) { + throw new OrcaError('invalid_worker_snapshot', 'Orca worker-show did not return an object'); + } + if (raw.dispatchId !== undefined && raw.dispatchId !== dispatchId) { + throw new OrcaError('dispatch_mismatch', 'Orca worker-show returned a different Dispatch identity'); + } + if (raw.dispatch?.id !== undefined && raw.dispatch.id !== dispatchId) { + throw new OrcaError('dispatch_mismatch', 'Orca worker-show returned a different Dispatch identity'); + } const state = lifecycle(raw); workers.push({ dispatchId, phase: state.phase, settled: state.settled, From 539ad76d652d3a662ac3c48c2770d432b376a343 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:35 +0800 Subject: [PATCH 12/14] test(orca): mock exact requested worker identity --- tests/fixtures/fake-orca.mjs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/fixtures/fake-orca.mjs b/tests/fixtures/fake-orca.mjs index 713155b..af2d5ea 100644 --- a/tests/fixtures/fake-orca.mjs +++ b/tests/fixtures/fake-orca.mjs @@ -8,6 +8,7 @@ if (mode === 'refuse') { console.log(JSON.stringify({id:'sim',ok:false,error:{me if (mode === 'silent-fail') process.exit(2); if (mode === 'delay') await new Promise(resolve=>setTimeout(resolve,2000)); const root = args[0]==='orchestration' ? args[1] : args[0]; +const requestedDispatch = args.includes('--dispatch') ? args[args.indexOf('--dispatch')+1] : 'dispatch_demo'; const result = root==='status' ? {runtime:{state:'ready'},app:{running:true},capabilities:['orchestration.contract.v1']} : root==='worktree' ? {worktrees:[{id:'workspace-demo',path:'D:\\example-workspace'}]} : root==='run-create' ? {runId:'run_demo'} : @@ -17,7 +18,7 @@ root==='task-list' ? {tasks:[{id:'task_demo',runId:'run_demo'}]} : root==='worker-start' ? {taskId:'task_demo',dispatchId:'dispatch_demo',stage:'ready'} : root==='worker-list' ? {workers:[{dispatchId:'dispatch_demo',workerState:'running'}]} : root==='worker-read' ? {rows:[{text:'工作完成'}],nextCursor:'cursor_2'} : -root==='worker-show' ? {dispatchId:'dispatch_demo',workerState:'succeeded',dispatch:{id:'dispatch_demo',status:'completed'},worker:{state:'succeeded'},terminalResource:{ownerDispatchId:'dispatch_demo',ownershipState:'owned',releaseState:'reclaimable',retainedReason:mode==='takeover'?'user_takeover':null}} : +root==='worker-show' ? {dispatchId:requestedDispatch,workerState:'succeeded',dispatch:{id:requestedDispatch,status:'completed'},worker:{state:'succeeded'},terminalResource:{ownerDispatchId:requestedDispatch,ownershipState:'owned',releaseState:'reclaimable',retainedReason:mode==='takeover'?'user_takeover':null}} : root==='send' ? {messageId:'message_demo'} : root==='check' ? {deliveryId:'delivery_demo',messages:[{id:'m1',body:'收到'}]} : root==='reply' ? {messageId:'reply_demo'} : From c043dfdf358830167e02e0ffea5a9c56745826f2 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:06:48 +0800 Subject: [PATCH 13/14] test(orca): reject foreign Dispatch and malformed worker snapshots --- tests/worker_wait.test.mjs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/worker_wait.test.mjs b/tests/worker_wait.test.mjs index 9f55b6d..23a0891 100644 --- a/tests/worker_wait.test.mjs +++ b/tests/worker_wait.test.mjs @@ -77,3 +77,23 @@ test('failed worker is settled but not successful',async()=>{ assert.equal(result.waitOutcome,'all_settled'); assert.equal(result.allSucceeded,false); }); + +test('foreign Dispatch result is never accepted as the requested Worker',async()=>{ + for(const bad of [ + {dispatchId:'dispatch_other',...good}, + {dispatch:{id:'dispatch_other',status:'completed'},worker:{state:'succeeded'}}, + ]) { + await assert.rejects( + waitForWorkers({dispatchIds:['dispatch_expected'],waitSeconds:0,read:async()=>bad}), + error=>error.code==='dispatch_mismatch', + ); + } +}); +test('malformed Orca response cannot be counted as Worker success',async()=>{ + for(const bad of [null,[],42]) { + await assert.rejects( + waitForWorkers({dispatchIds:['dispatch_expected'],waitSeconds:0,read:async()=>bad}), + error=>error.code==='invalid_worker_snapshot', + ); + } +}); From 23d77f270a493dbebd16854c42031462439750a7 Mon Sep 17 00:00:00 2001 From: Herbivore <59003602+LonelyHerbivore@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:07:26 +0800 Subject: [PATCH 14/14] docs(orca): record 73 passing regression tests --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7e7cd2e..bf30b54 100644 --- a/README.md +++ b/README.md @@ -84,7 +84,7 @@ Host 在后台无用户输入时自动唤醒模型仍然是独立的、未通过 npm test ``` -2026-10-14 本机实验分支验证:**71/71 项自动化测试通过(Node.js 24.13.0)**;v0.3.1 主分支历史记录为 2026-10-13 的 61/61。测试使用模拟 Orca CLI,不能替代真实多 Agent 端到端验收。真实 Orca 只读回归、Plugin check/reload 记录见 [v0.3.1 测试报告](docs/TEST_REPORT_v0.3.1.md)。 +2026-10-14 本机实验分支验证:**73/73 项自动化测试通过(Node.js 24.13.0)**;v0.3.1 主分支历史记录为 2026-10-13 的 61/61。测试使用模拟 Orca CLI,不能替代真实多 Agent 端到端验收。真实 Orca 只读回归、Plugin check/reload 记录见 [v0.3.1 测试报告](docs/TEST_REPORT_v0.3.1.md)。 以下能力仍有限制: