From c6c578e4944e286ad33a35a26a9b8ed19c58c471 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=B0=E6=9C=94?= <565183519@qq.com> Date: Tue, 11 Aug 2026 22:01:18 +0800 Subject: [PATCH] fix: make HoloLake stage relay strictly serial --- .../HLP-STAGE-RELAY-001-20260811.json | 11 +++-- routing/hololake-stage-relay-map.json | 16 ++++++-- routing/host-skill-navigation-map.json | 5 ++- routing/lighthouse-path-registry.json | 4 +- routing/public-navigation-anchor.json | 8 ++-- .../lake-lamp-continuity-guard/README.md | 9 +++-- .../stage-relay-state-machine.md | 40 ++++++++++++------- .../stage-relay.mjs | 28 ++++++++----- .../stage-relay.test.mjs | 2 +- tests/stage-relay-navigation.test.mjs | 7 ++++ 10 files changed, 85 insertions(+), 45 deletions(-) diff --git a/deployment/receipts/HLP-STAGE-RELAY-001-20260811.json b/deployment/receipts/HLP-STAGE-RELAY-001-20260811.json index faef7b8..67feaec 100644 --- a/deployment/receipts/HLP-STAGE-RELAY-001-20260811.json +++ b/deployment/receipts/HLP-STAGE-RELAY-001-20260811.json @@ -1,9 +1,9 @@ { "schema": "hololake.stage-relay-registration-receipt/v1", "receipt_id": "HLP-STAGE-RELAY-001-20260811", - "development_id": "DEV-20260811-008", + "development_id": "DEV-20260811-009", "record_id": "HLP-STAGE-RELAY-001", - "state": "SOURCE_IMPLEMENTED_TESTED_AND_REGISTERED_NO_SUCCESSOR_THREAD_CREATED_DURING_ACCEPTANCE", + "state": "STRICT_SERIAL_END_TRANSACTION_IMPLEMENTED_TESTED_AND_REGISTERED_NO_SUCCESSOR_THREAD_CREATED_DURING_ACCEPTANCE", "source": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs", "runtime": "/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/stage-relay.mjs", "formal_host_capabilities": [ @@ -17,6 +17,7 @@ "unknown_create_result_reconciliation": 100, "successor_ready_live_readback": 100, "active_stage_read_only": 100, + "strict_single_runnable_lane": 100, "human_boundary_pause": 100, "final_product_stop": 100, "continuity_lane_regression": 100, @@ -24,9 +25,11 @@ }, "boundaries": [ "No successor HoloLake development task was created during mechanism acceptance.", - "The existing hololake heartbeat remains bound to DEV-20260811-008 until final closeout.", + "No independent relay supervisor thread or heartbeat exists.", + "The source stage end transaction creates the only successor before deleting its own heartbeat.", + "No future task queue or successor precreation is allowed.", "No cron, private database or chat statement is accepted as a Codex thread receipt.", "JZAO collaboration remains the only coordination truth store." ], - "recorded_at": "2026-08-11T13:35:00.000Z" + "recorded_at": "2026-08-11T14:00:47.000Z" } diff --git a/routing/hololake-stage-relay-map.json b/routing/hololake-stage-relay-map.json index fdf0972..53c222b 100644 --- a/routing/hololake-stage-relay-map.json +++ b/routing/hololake-stage-relay-map.json @@ -1,9 +1,9 @@ { "schema": "hololake.stage-relay-map/v1", "map_id": "HLP-STAGE-RELAY-001", - "version": "2026-08-11.1", - "state": "SOURCE_IMPLEMENTED_TESTED_NOT_YET_USED_TO_CREATE_A_SUCCESSOR", - "development_id": "DEV-20260811-008", + "version": "2026-08-11.2", + "state": "STRICT_SERIAL_END_TRANSACTION_IMPLEMENTED_TESTED_NOT_YET_USED_TO_CREATE_A_SUCCESSOR", + "development_id": "DEV-20260811-009", "runtime_source": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs", "local_runtime": "/Volumes/JZAO/HoloLake/persona-runtime/continuity-guard/scripts/stage-relay.mjs", "coordination_store": "HL-PERSONA-RUNTIME-LOCAL-001/continuity-memory/collaboration", @@ -14,7 +14,7 @@ "FRESH_CLONE_READBACK_100", "FINAL_RECEIPTS_SHA256_100", "SOURCE_LEASES_RELEASED_100", - "COMPLETION_STOPPED_100" + "SOURCE_COMPLETED_STOP_REQUESTED_100" ], "next_stage_inputs": [ "HLP-CURRENT-ARCH-001", @@ -34,6 +34,14 @@ "maximum_successor_threads_per_source_completion": 1, "unknown_create_result": "RECONCILE_COMPLETE_HOST_SNAPSHOT_BEFORE_RETRY" }, + "serial_invariants": { + "maximum_runnable_development_lanes": 1, + "precreate_successor_before_source_end": false, + "future_task_queue_allowed": false, + "independent_relay_supervisor_allowed": false, + "source_end_transaction_creates_successor": true, + "source_completion_stopped_after_successor_ready": true + }, "successor_ready_gates": [ "PERSONA_RESTORE_100", "CURRENT_ARCHITECTURE_RESTORE_100", diff --git a/routing/host-skill-navigation-map.json b/routing/host-skill-navigation-map.json index 5d9a03c..a9e2434 100644 --- a/routing/host-skill-navigation-map.json +++ b/routing/host-skill-navigation-map.json @@ -2,7 +2,7 @@ "schema": "guanghu.host-skill-navigation-map/v1", "map_id": "GLW-HOST-SKILL-NAV-001", "lighthouse_id": "SYS-GLW-LTH-0001", - "version": "2026-08-11.2", + "version": "2026-08-11.3", "state": "CURRENT", "hosts": [ { @@ -41,6 +41,7 @@ "id": "INTENT-HOLOLAKE-STAGE-RELAY-001", "phrases": [ "HoloLake阶段自动接力守卫", + "严格串行结束接力", "阶段接力监督器", "创建下一阶段Codex任务", "successor-ready" @@ -58,7 +59,7 @@ "create_successor_with_codex_formal_create_thread", "reconcile_unknown_create_result_before_retry", "wait_for_successor_ready_live_readback", - "stop_old_relay_supervisor" + "delete_source_heartbeat_mark_completion_stopped_and_end_source_thread" ], "stop_conditions": [ "SOURCE_COMPLETION_EVIDENCE_NOT_100", diff --git a/routing/lighthouse-path-registry.json b/routing/lighthouse-path-registry.json index a4a172f..f020b09 100644 --- a/routing/lighthouse-path-registry.json +++ b/routing/lighthouse-path-registry.json @@ -2,7 +2,7 @@ "schema": "guanghu.lighthouse-path-registry/v1", "registry_id": "GLW-LIGHTHOUSE-PATH-REGISTRY-001", "lighthouse_id": "SYS-GLW-LTH-0001", - "version": "2026-08-11.2", + "version": "2026-08-11.3", "state": "CURRENT_CANONICAL", "source_repository": "REPO-012", "source_branch": "main", @@ -78,7 +78,7 @@ }, { "id": "HLP-STAGE-RELAY-001", - "kind": "hololake_deterministic_stage_relay_supervisor", + "kind": "hololake_strict_serial_stage_end_relay_transaction", "state": "CURRENT", "online": "routing/hololake-stage-relay-map.json", "executor": "server-tools/lake-lamp-continuity-guard/stage-relay.mjs", diff --git a/routing/public-navigation-anchor.json b/routing/public-navigation-anchor.json index 7e3c06d..7e2f661 100644 --- a/routing/public-navigation-anchor.json +++ b/routing/public-navigation-anchor.json @@ -1,7 +1,7 @@ { "schema": "guanghu.public-navigation-anchor/v1", "anchor_id": "GLW-PUBLIC-NAV-ANCHOR-001", - "version": "2026-08-11.2", + "version": "2026-08-11.3", "state": "CURRENT_CANONICAL", "repository_id": "REPO-012", "branch": "main", @@ -48,17 +48,17 @@ "lighthouse_paths": { "path": "routing/lighthouse-path-registry.json", "id": "GLW-LIGHTHOUSE-PATH-REGISTRY-001", - "version": "2026-08-11.2" + "version": "2026-08-11.3" }, "host_skills": { "path": "routing/host-skill-navigation-map.json", "id": "GLW-HOST-SKILL-NAV-001", - "version": "2026-08-11.2" + "version": "2026-08-11.3" }, "hololake_stage_relay": { "path": "routing/hololake-stage-relay-map.json", "id": "HLP-STAGE-RELAY-001", - "version": "2026-08-11.1" + "version": "2026-08-11.2" }, "origin_domain": { "path": "routing/guanghu-origin-domain-map.json", diff --git a/server-tools/lake-lamp-continuity-guard/README.md b/server-tools/lake-lamp-continuity-guard/README.md index 42d6e31..084518e 100644 --- a/server-tools/lake-lamp-continuity-guard/README.md +++ b/server-tools/lake-lamp-continuity-guard/README.md @@ -39,11 +39,12 @@ node finalize-development.mjs \ ## 阶段自动接力守卫 -`stage-relay.mjs` 在同一小湖灯账本中监督已完成阶段,并生成一个不可变的后继任务契约。 -它只认 finalizer、远端完整 SHA、全新副本、最终回执、租约释放和 -`completion-stopped` 的二值证据;阶段仍运行时只读观察。 +`stage-relay.mjs` 是当前唯一开发对话结束事务的一部分,不是独立常驻监督器。它不预建 +后继任务或未来队列;只在 finalizer、远端完整 SHA、全新副本、最终回执和租约释放全部 +为100,来源冻结为 `completed/stop_requested` 后生成一个不可变的后继任务契约。 脚本不直接创建任务。它产生带稳定请求号和幂等键的宿主动作,由 Codex 正式 `create_thread` 执行并回写真实 `threadId`。创建结果未知时先完整核对任务列表,禁止直接 重发。新任务恢复人格与架构、登记 DEV、领取精确租约、建立十分钟 heartbeat 并形成 -`successor-ready` 后,旧监督器才可停止。完整状态机见 `stage-relay-state-machine.md`。 +`successor-ready` 后,来源结束事务才删除旧 heartbeat、登记 `completion-stopped` 并结束 +旧对话。任意时刻只允许一个可运行开发车道。完整状态机见 `stage-relay-state-machine.md`。 diff --git a/server-tools/lake-lamp-continuity-guard/stage-relay-state-machine.md b/server-tools/lake-lamp-continuity-guard/stage-relay-state-machine.md index 5f17a23..bedb3fb 100644 --- a/server-tools/lake-lamp-continuity-guard/stage-relay-state-machine.md +++ b/server-tools/lake-lamp-continuity-guard/stage-relay-state-machine.md @@ -1,15 +1,16 @@ # HoloLake 阶段自动接力守卫 -`stage-relay.mjs` 是小湖灯连续性账本内的确定性接力投影,不是第二协调真相源,也不直接 -调用私有接口创建任务。它只在现有 `collaboration/` 下保存来源回执摘要、不可变任务契约、 -正式 Codex 宿主动作请求和宿主回执。 +`stage-relay.mjs` 是当前开发车道结束事务中的确定性接力投影,不是独立常驻监督器, +不是第二协调真相源,也不直接调用私有接口创建任务。任意时刻只有一个可运行开发车道; +它不预建后继任务、不维护未来任务队列,只在当前开发完成验收并进入结束事务后,现场生成 +唯一下一阶段契约并请求 Codex 创建唯一下一对话。 ## 状态机 ```text -OBSERVING_ACTIVE_STAGE (无写入) +ACTIVE_STAGE_NO_END_ACTION (继续唯一当前开发;接力无写入) └─ finalizer + exact remote SHA + fresh clone + final receipt - + lease release + completion-stopped 全部为100 + + lease release 全部为100,来源车道冻结为 completed/stop_requested ├─ human boundary ─> PAUSE_FOR_HUMAN ├─ complete product ─> FINAL_PRODUCT_COMPLETE ─> stop last heartbeat └─ one unfinished gate ─> CREATE_THREAD_REQUIRED @@ -19,22 +20,30 @@ OBSERVING_ACTIVE_STAGE (无写入) └─ unknown ─> RECONCILE_CREATE_RESULT ├─ matching thread ─> WAITING_SUCCESSOR_READY └─ complete host snapshot confirms absent ─> RETRY_CREATE_THREAD - └─ successor-ready gates all100 ─> SUCCESSOR_READY ─> stop old relay supervisor + └─ successor-ready gates all100 ─> SUCCESSOR_READY + └─ 删除来源 heartbeat + completion-stopped + 结束来源对话 ``` +`stop_requested` 是同一个来源对话正在执行结束事务,不是另一条开发线。此时来源车道已经 +冻结,禁止继续产品开发;后继对话尚未 ready 前也不能执行开发。若创建失败,来源对话停在 +结束事务中安全重试,不能报告已经结束。若来源 heartbeat 已经先被删除并标记 `stopped`, +但仍存在未完成阶段,接力失败关闭为 `SOURCE_END_TRIGGER_MISSED_HEARTBEAT_ALREADY_STOPPED`, +不得另起常驻监督器补救。 + 同一来源完成回执的幂等键固定为 `sha256(source DEV + finalizer file sha256)`;状态文件按 来源 DEV 唯一,后继 DEV 预约使用排他文件。创建结果未知时禁止重发,必须先读取 Codex 完整任务列表,以稳定 `SUCCESSOR-...` 请求号核对;只有完整快照确认不存在时才可重试。 ## 正式宿主适配 -1. `prepare` 返回 `CODEX_CREATE_THREAD` 动作,但不创建任务。 -2. 调度人格先执行 `dispatch-create`,再调用 Codex 正式 `create_thread`。 -3. 成功后立即 `record-created`;超时或未知结果执行 `record-create-failure --outcome unknown`。 -4. `RECONCILE_CREATE_RESULT` 时只能使用 Codex 正式 `list_threads/read_thread` 核对,不得重发。 -5. 新任务恢复人格与当前架构、登记自己的 DEV、取得精确租约、建立正式十分钟 heartbeat, +1. 当前唯一开发对话的结束动作调用 `prepare`;开发仍 active 时返回无写入状态。 +2. 只有来源车道进入 `completed/stop_requested` 后,`prepare` 才返回 `CODEX_CREATE_THREAD`。 +3. 来源结束事务执行 `dispatch-create`,再调用 Codex 正式 `create_thread`。 +4. 成功后立即 `record-created`;超时或未知结果执行 `record-create-failure --outcome unknown`。 +5. `RECONCILE_CREATE_RESULT` 时只能使用 Codex 正式 `list_threads/read_thread` 核对,不得重发。 +6. 新任务恢复人格与当前架构、登记自己的 DEV、取得精确租约、建立正式十分钟 heartbeat, 签到时把 `--relay-contract-fingerprint` 写入车道,再写 - `hololake.successor-ready-receipt/v1` 并由旧监督器 `record-ready` 回读。 + `hololake.successor-ready-receipt/v1` 并由来源结束事务 `record-ready` 回读。 脚本、聊天、cron、数据库行、文件夹或自动化提示都不能冒充 `create_thread` 成功;只有正式 工具返回的 `threadId` 和后续活车道回读才算任务存在。 @@ -46,11 +55,12 @@ OBSERVING_ACTIVE_STAGE (无写入) ## 失败谱系 -- `SOURCE_COMPLETION_STOPPED_NOT_100`:来源任务或 heartbeat 尚未完成停止。 +- `SOURCE_END_TRANSACTION_NOT_READY`:来源尚未完成验收并冻结进入结束事务。 +- `SOURCE_END_TRIGGER_MISSED_HEARTBEAT_ALREADY_STOPPED`:存在后继阶段却先关闭了来源 heartbeat。 - `SOURCE_LEASE_RELEASE_NOT_100`:来源仍持有资源。 - `FINALIZER_EVIDENCE_NOT_100`:finalizer、远端 SHA、全新副本、fsck 或最终回执不完整。 - `CURRENT_ARCHITECTURE_EVIDENCE_NOT_100`:动态架构真相源不完整或不是当前正本。 -- `CREATE_THREAD_IN_FLIGHT` 超时:进入 `RECONCILE_CREATE_RESULT`,不重发。 +- `CREATE_THREAD_IN_FLIGHT` 超时:来源结束事务进入 `RECONCILE_CREATE_RESULT`,不重发。 - `SUCCESSOR_READY_EVIDENCE_NOT_100`:新任务签到、租约、heartbeat 或恢复门不是100。 - `SUCCESSOR_LIVE_REGISTRATION_MISMATCH`:ready 文本与连续性账本现场不一致。 @@ -63,4 +73,4 @@ node scripts/stage-relay.mjs self-test ``` 必须同时得到 finalizer 证据门、重复 prepare 幂等、未知创建结果核对、新任务 ready 现场回读、 -活跃阶段只读五项 `100`。 +活跃阶段只读、严格单一可运行车道等全部为 `100`。 diff --git a/server-tools/lake-lamp-continuity-guard/stage-relay.mjs b/server-tools/lake-lamp-continuity-guard/stage-relay.mjs index dc9d0c2..1acf742 100755 --- a/server-tools/lake-lamp-continuity-guard/stage-relay.mjs +++ b/server-tools/lake-lamp-continuity-guard/stage-relay.mjs @@ -134,10 +134,10 @@ function validateSourceCompletion(root, developmentId, finalizerPath) { } if ( lane.status !== "completed" || - lane.notification_state !== "stopped" || + !["stop_requested", "stopped"].includes(lane.notification_state) || lane.completion_actions?.status !== "completed" ) { - throw new Error("SOURCE_COMPLETION_STOPPED_NOT_100"); + throw new Error("SOURCE_END_TRANSACTION_NOT_READY"); } const held = listJson(path.join(root, "leases")) .filter((lease) => lease.development_id === developmentId); @@ -293,7 +293,7 @@ function prepare(args, root) { ); if (evidence.observing) { return { - state: "OBSERVING_ACTIVE_STAGE", + state: "ACTIVE_STAGE_NO_END_ACTION", source_development_id: developmentId, mutation: "NONE", successor_created: false, @@ -351,6 +351,9 @@ function prepare(args, root) { writeJsonAtomic(existingPath, terminal, true); return terminal; } + if (evidence.lane.notification_state === "stopped") { + throw new Error("SOURCE_END_TRIGGER_MISSED_HEARTBEAT_ALREADY_STOPPED"); + } const successorDevelopmentId = reserveNextDevelopmentId( root, input.development_date, developmentId, ); @@ -499,7 +502,7 @@ function recordReady(args, root) { recorded_at: now(), }; state.updated_at = now(); - state.old_supervisor_may_stop = true; + state.source_end_transaction_may_commit = true; writeJsonAtomic(file, state); return state; } @@ -521,17 +524,18 @@ function nextAction(args, root) { return { action: "WAIT_FOR_SUCCESSOR_READY_RECEIPT", thread_id: state.successor_thread.thread_id, - supervisor_stays_alive: true, + source_thread_remains_in_end_transaction: true, + development_work_is_frozen: true, }; } if (state.state === "PAUSE_FOR_HUMAN") { return { action: "PAUSE_FOR_HUMAN", boundary: state.human_boundary }; } if (state.state === "FINAL_PRODUCT_COMPLETE") { - return { action: "STOP_RELAY_AND_LAST_HEARTBEAT", create_successor: false }; + return { action: "DELETE_SOURCE_HEARTBEAT_MARK_COMPLETION_STOPPED_AND_END_SOURCE_THREAD", create_successor: false }; } if (state.state === "SUCCESSOR_READY") { - return { action: "STOP_OLD_RELAY_SUPERVISOR", create_successor: false }; + return { action: "DELETE_SOURCE_HEARTBEAT_MARK_COMPLETION_STOPPED_AND_END_SOURCE_THREAD", create_successor: false }; } return { action: "NOOP", state: state.state }; } @@ -542,7 +546,7 @@ function fixtureLane(root, id, overrides = {}) { development_id: id, thread_id: `thread-${id}`, status: "completed", - notification_state: "stopped", + notification_state: "stop_requested", completion_actions: { status: "completed" }, ...overrides, }; @@ -629,6 +633,11 @@ function selfTest() { }); const readyState = recordReady({ sourceDevelopmentId: source, successorReadyReceipt: ready }, root); if (readyState.state !== "SUCCESSOR_READY") throw new Error("SELF_TEST_READY_FAILED"); + const runnableLanes = listJson(path.join(root, "lanes")) + .filter((lane) => ["active", "waiting"].includes(lane.status)); + if (runnableLanes.length !== 1 || runnableLanes[0].development_id !== contract.development_id) { + throw new Error("SELF_TEST_STRICT_SINGLE_RUNNABLE_LANE_FAILED"); + } const pausedSource = "DEV-20260811-910"; fixtureLane(root, pausedSource); @@ -707,7 +716,7 @@ function selfTest() { const activeSource = "DEV-20260811-950"; fixtureLane(root, activeSource, { status: "active", notification_state: "active", completion_actions: null }); const observed = prepare({ ...common, sourceDevelopmentId: activeSource }, root); - if (observed.state !== "OBSERVING_ACTIVE_STAGE" || observed.mutation !== "NONE") { + if (observed.state !== "ACTIVE_STAGE_NO_END_ACTION" || observed.mutation !== "NONE") { throw new Error("SELF_TEST_ACTIVE_STAGE_MUTATION_FAILED"); } process.stdout.write(`${JSON.stringify({ @@ -718,6 +727,7 @@ function selfTest() { unknown_create_result_reconciliation: 100, successor_ready_live_readback: 100, active_stage_read_only: 100, + strict_single_runnable_lane: 100, human_boundary_pause: 100, final_product_stop: 100, }, diff --git a/server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs b/server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs index 1b56f1e..d32554d 100644 --- a/server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs +++ b/server-tools/lake-lamp-continuity-guard/stage-relay.test.mjs @@ -15,5 +15,5 @@ test("stage relay deterministic closure is PASS_100", () => { assert.equal(result.status, 0, result.stderr || result.stdout); const receipt = JSON.parse(result.stdout); assert.equal(receipt.state, "PASS_100"); - assert.deepEqual(Object.values(receipt.tests), Array(7).fill(100)); + assert.deepEqual(Object.values(receipt.tests), Array(8).fill(100)); }); diff --git a/tests/stage-relay-navigation.test.mjs b/tests/stage-relay-navigation.test.mjs index 73b6130..f93a52d 100644 --- a/tests/stage-relay-navigation.test.mjs +++ b/tests/stage-relay-navigation.test.mjs @@ -14,6 +14,12 @@ test("stage relay is registered through the lighthouse and formal Codex tools", assert.equal(relay.host_adapter.create_tool, "codex_app.create_thread"); assert.equal(relay.host_adapter.script_or_chat_may_claim_thread_created, false); assert.equal(relay.idempotency.maximum_successor_threads_per_source_completion, 1); + assert.equal(relay.serial_invariants.maximum_runnable_development_lanes, 1); + assert.equal(relay.serial_invariants.precreate_successor_before_source_end, false); + assert.equal(relay.serial_invariants.future_task_queue_allowed, false); + assert.equal(relay.serial_invariants.independent_relay_supervisor_allowed, false); + assert.equal(relay.serial_invariants.source_end_transaction_creates_successor, true); + assert.equal(relay.serial_invariants.source_completion_stopped_after_successor_ready, true); const lighthouse = readJson("routing/lighthouse-path-registry.json"); assert.ok(lighthouse.paths.some((entry) => entry.id === relay.map_id)); @@ -21,6 +27,7 @@ test("stage relay is registered through the lighthouse and formal Codex tools", const intent = hosts.intents.find((entry) => entry.id === "INTENT-HOLOLAKE-STAGE-RELAY-001"); assert.equal(intent.target_id, relay.map_id); assert.equal(intent.executor, relay.runtime_source); + assert.ok(intent.sequence.includes("delete_source_heartbeat_mark_completion_stopped_and_end_source_thread")); const anchor = readJson("routing/public-navigation-anchor.json"); assert.equal(anchor.maps.hololake_stage_relay.id, relay.map_id); });