fix: make HoloLake stage relay strictly serial

This commit is contained in:
冰朔 2026-08-11 22:01:18 +08:00
commit c6c578e494
10 changed files with 85 additions and 45 deletions

View file

@ -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"
}

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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",

View file

@ -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`

View file

@ -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`

View file

@ -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,
},

View file

@ -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));
});

View file

@ -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);
});