fix: make HoloLake stage relay strictly serial
This commit is contained in:
parent
5f86116303
commit
c6c578e494
10 changed files with 85 additions and 45 deletions
|
|
@ -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`。
|
||||
|
|
|
|||
|
|
@ -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`。
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue