Skip to content
1 change: 0 additions & 1 deletion apps/desktop/src/main/client-settings-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,6 @@ export function buildClientSettingsTools(
'Runtime behavior, credentials, model configuration, network settings, and Bot settings are excluded.',
parameters: z.object({}).strict(),
categoryHint: 'read',
recoveryMode: 'replay_safe',
impl: async () => project(await authority.read()),
};
const updateTool: MakaTool<ClientSettingsPatch, ClientSettingsUpdateResult> = {
Expand Down
3 changes: 3 additions & 0 deletions docs/architecture/runtime-recovery-resolver-adr.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

# ADR:RuntimeEvent 唯一事实源与 RecoveryResolver 唯一判定权威

> **2026-10-16 退役说明:** 本文涉及工具专属 reconciliation、recovery bundle 写入及其后续 PR 的内容保留为历史设计,不再是当前实施要求。没有生产工具提供完整的 reconciliation 合同,公开 bundle writer 和预设工具恢复能力已清理。已有 recovery facts 仍保留读取、复制与投影重建兼容。当前合同是结束旧执行、保留未知事实、禁止自动重放,由新的明确用户轮检查现状;详见[当前恢复架构](./runtime-resume-architecture.zh-CN.md#未知结果与历史-recovery-bundle)。


- 状态:Accepted,Phase 2.5 开始实施
- 日期:2026-07-17
- 范围:Runtime Resume Phase 2.5–3
Expand Down
146 changes: 33 additions & 113 deletions docs/architecture/runtime-resume-architecture.md

Large diffs are not rendered by default.

155 changes: 35 additions & 120 deletions docs/architecture/runtime-resume-architecture.zh-CN.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions docs/architecture/runtime-resume-extraction-ledger.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

# Runtime Resume #1346 拆分与提取账本

> **2026-10-16 退役说明:** 本文涉及工具专属 reconciliation、recovery bundle 写入及其后续 PR 的内容保留为历史设计,不再是当前实施要求。没有生产工具提供完整的 reconciliation 合同,公开 bundle writer 和预设工具恢复能力已清理。已有 recovery facts 仍保留读取、复制与投影重建兼容。当前合同是结束旧执行、保留未知事实、禁止自动重放,由新的明确用户轮检查现状;详见[当前恢复架构](./runtime-resume-architecture.zh-CN.md#未知结果与历史-recovery-bundle)。


- 状态:Active
- 更新日期:2026-07-28
- 集成实验来源:`origin/codex/runtime-resume-phase3a@24bb5f33`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@

# Runtime Resume Phase 3–4 实施路线

> **2026-10-16 退役说明:** 本文涉及工具专属 reconciliation、recovery bundle 写入及其后续 PR 的内容保留为历史设计,不再是当前实施要求。没有生产工具提供完整的 reconciliation 合同,公开 bundle writer 和预设工具恢复能力已清理。已有 recovery facts 仍保留读取、复制与投影重建兼容。当前合同是结束旧执行、保留未知事实、禁止自动重放,由新的明确用户轮检查现状;详见[当前恢复架构](./runtime-resume-architecture.zh-CN.md#未知结果与历史-recovery-bundle)。


> 路线更新(2026-08-29):旧 Git executable managed-workspace path 已删除,后续 workspace 主线从
> **Gitoxide data plane** 重新建立 production admission。本文保留 Recovery/Continuation 已落地协议与旧路线的历史论证;
> 新实现不再以通用 per-file checkpoint、native manifest、CAS object store 或旧 Git CLI owner 为前置。
Expand Down
13 changes: 0 additions & 13 deletions packages/core/src/runtime-event-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,8 @@ import type {
} from './workspace-version-authority.js';
import { WORKSPACE_VERSION_AUTHORITY_CAPABILITY_V1 } from './workspace-version-authority.js';

export const TOOL_RECOVERY_BUNDLE_CAPABILITY_V1 = 'tool_recovery_bundle_v1' as const;
export const RUNTIME_CONTINUATION_AUTHORITY_V1 = 'runtime_continuation_authority_v1' as const;

export interface RuntimeRecoveryBundleCommit {
operationId: string;
reconcileRuntimeEvent: RuntimeEvent;
outcomeRuntimeEvent?: RuntimeEvent;
decisionRuntimeEvent: RuntimeEvent;
}

export interface RuntimeInvocationRecoveryInventoryEntry {
readonly sessionId: string;
readonly invocationId: string;
Expand Down Expand Up @@ -277,11 +269,6 @@ export async function readRunInvocation(
);
}

export interface RuntimeRecoveryBundleStore extends RuntimeEventStore {
readonly recoveryBundleCapability: typeof TOOL_RECOVERY_BUNDLE_CAPABILITY_V1;
commitToolRecoveryBundle(input: RuntimeRecoveryBundleCommit): Promise<void>;
}

export type ContinuationClaimResult =
| { kind: 'acquired'; claim: ContinuationClaimV1 }
| { kind: 'existing'; claim: ContinuationClaimV1 }
Expand Down
5 changes: 4 additions & 1 deletion packages/core/src/runtime-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,10 @@ export interface RuntimeEventRefs {
sourceRuntimeEventHighWater?: number;
}

/** Tool-owned contract for deciding what a later recovery phase may do. */
/** Current tools only choose how to report an unknown result; neither mode retries an effect. */
export type ToolOutcomeRecoveryMode = 'never_auto_retry' | 'outcome_unknown';

/** Persisted dispatch metadata, including legacy modes; never authorizes recovery by itself. */
export type ToolRecoveryMode =
| 'replay_safe'
| 'idempotent'
Expand Down
16 changes: 2 additions & 14 deletions packages/core/src/tool-ledger-scanner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,11 +141,7 @@ export type GenericToolLedgerAppendValidation =
eventId: string;
};

export type ToolLedgerTransitionKind =
| 'generic_append'
| 't1_prepare'
| 't2_outcome'
| 'recovery_bundle';
export type ToolLedgerTransitionKind = 'generic_append' | 't1_prepare' | 't2_outcome';

/** Every reason the ledger has for refusing a candidate event. */
export type ToolLedgerRejectionCode =
Expand Down Expand Up @@ -708,15 +704,7 @@ function validateTransitionShape(
(expectedTransition === 't2_outcome' &&
actual.length === 1 &&
actual[0] === 'function_response' &&
events[0]?.refs?.operationId !== undefined) ||
(expectedTransition === 'recovery_bundle' &&
((actual.length === 2 &&
actual[0] === 'reconcile_result' &&
actual[1] === 'recovery_decision') ||
(actual.length === 3 &&
actual[0] === 'reconcile_result' &&
actual[1] === 'function_response' &&
actual[2] === 'recovery_decision')));
events[0]?.refs?.operationId !== undefined);
if (valid) return { ok: true };
return {
ok: false,
Expand Down
21 changes: 1 addition & 20 deletions packages/core/src/tool-recovery-bundle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,6 @@ export type ScannedToolRecoveryInterpretation =
message: string;
};

export class ToolRecoveryBundleValidationError extends Error {
readonly name = 'ToolRecoveryBundleValidationError';

constructor(
readonly code: ToolRecoveryBundleValidationCode,
message: string,
) {
super(message);
}
}

export function validateToolRecoveryEventBundle(
input: ToolRecoveryEventBundle,
): ToolRecoveryBundleValidationResult {
Expand Down Expand Up @@ -275,16 +264,8 @@ export function validateToolRecoveryEventBundle(
return { ok: true, reconcile, decision };
}

export function assertToolRecoveryEventBundle(input: ToolRecoveryEventBundle): {
reconcile: ToolReconcileResultFact;
decision: ToolRecoveryDecisionFact;
} {
const result = validateToolRecoveryEventBundle(input);
if (!result.ok) throw new ToolRecoveryBundleValidationError(result.code, result.message);
return { reconcile: result.reconcile, decision: result.decision };
}

/**
* Read compatibility for historical recovery bundles. Current tools never produce these facts.
* Interprets the recovery tail of one scanner-owned operation. Resolver and
* projection rebuild must consume this result instead of independently
* assembling facts or defining their own causal-order rules.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1016,7 +1016,7 @@ test('Host reopens one projected image from its ArtifactStore authority', async
name: 'ProjectedImage',
description: 'Return one inline image.',
parameters: z.object({}),
recoveryMode: 'replay_safe',
recoveryMode: 'never_auto_retry',
impl: async () => ({ private: 'raw execution fact' }),
toModelOutput: () => ({
type: 'content',
Expand Down
1 change: 0 additions & 1 deletion packages/runtime-host/src/server/agent-settings-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ export function buildHostAgentSettingsTools(
'This never returns credentials, network proxy details, native client settings, or Bot settings.',
parameters: z.object({}).strict(),
categoryHint: 'read',
recoveryMode: 'replay_safe',
impl: async () => projectSettings((await authority.read()).policy),
};
const updateTool: MakaTool<AgentSettingsPatch, AgentSettingsUpdateResult> = {
Expand Down
1 change: 0 additions & 1 deletion packages/runtime/src/__tests__/plan-mode.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ import type { MakaTool } from '../tool-runtime.js';
describe('Plan Mode tool surface', () => {
test('requires plain-text step titles and descriptions', () => {
const submitPlan = buildSubmitPlanTool({} as never);
assert.equal(submitPlan.recoveryMode, 'idempotent');
const schema = submitPlan.parameters as {
safeParse(input: unknown): { success: boolean };
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { mkdtemp, rm } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, it } from 'node:test';
import { DatabaseSync } from 'node:sqlite';
import type { RuntimeEvent } from '@maka/core/runtime-event';
import { canonicalToolArgsHash } from '@maka/core/tool-args-identity';
import { createSqliteRuntimeStore } from '@maka/storage/sqlite-runtime-store';
Expand All @@ -36,7 +37,7 @@ const OBSERVATION_DIGEST =
'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' as const;

describe('recovery authority equivalence', () => {
it('gives online projection, reopen, rebuild, and Resolver one terminal interpretation', async () => {
it('gives historical projection, reopen, rebuild, and Resolver one terminal interpretation', async () => {
const root = await mkdtemp(join(tmpdir(), 'maka-recovery-equivalence-'));
const dbPath = join(root, 'runtime.sqlite');
const store = createSqliteRuntimeStore(dbPath);
Expand All @@ -52,12 +53,8 @@ describe('recovery authority equivalence', () => {
recoveryMode: 'reconcile',
committedAt: 10,
});
await store.commitToolRecoveryBundle({
operationId: 'operation-1',
reconcileRuntimeEvent: reconcileEvent(),
outcomeRuntimeEvent: outcomeEvent(),
decisionRuntimeEvent: decisionEvent(),
});
seedHistoricalRecoveryTail(dbPath, [reconcileEvent(), outcomeEvent(), decisionEvent()]);
await store.rebuildToolProjectionsFromRuntimeEvents();

const immutable = await store.readImmutableRuntimeEvents('session-1', 'run-1');
const online = await store.readToolOperation('operation-1');
Expand All @@ -77,7 +74,6 @@ describe('recovery authority equivalence', () => {
responseIsError: false,
},
]);
assert.equal(resolution.requiresReconciliation, false);

store.close();
const reopened = createSqliteRuntimeStore(dbPath);
Expand Down Expand Up @@ -149,11 +145,8 @@ describe('recovery authority equivalence', () => {
committedAt: 20,
});
} else if ('parked' in scenario) {
await store.commitToolRecoveryBundle({
operationId: 'operation-1',
reconcileRuntimeEvent: reconcileEvent('diverged'),
decisionRuntimeEvent: parkedDecisionEvent(),
});
seedHistoricalRecoveryTail(dbPath, [reconcileEvent('diverged'), parkedDecisionEvent()]);
await store.rebuildToolProjectionsFromRuntimeEvents();
}

const immutable = await store.readImmutableRuntimeEvents('session-1', 'run-1');
Expand Down Expand Up @@ -195,7 +188,6 @@ describe('recovery authority equivalence', () => {
['duplicate_event_id', 'semantic_lane_conflict'],
);
assert.equal(resolution.hasCorruption, true);
assert.equal(resolution.requiresReconciliation, false);
});

it('treats a valid parked recovery bundle as terminal', () => {
Expand All @@ -220,7 +212,6 @@ describe('recovery authority equivalence', () => {
},
]);
assert.equal(resolution.hasCorruption, false);
assert.equal(resolution.requiresReconciliation, false);

const plan = buildResumePlanFromRuntimeEvents(events);
assert.equal(plan.disposition, 'blocked');
Expand Down Expand Up @@ -394,3 +385,31 @@ function decisionEvent(): RuntimeEvent {
refs: { operationId: 'operation-1', toolCallId: 'provider-call-1' },
});
}

/** Read-compatibility fixture: these facts were persisted by the retired writer. */
function seedHistoricalRecoveryTail(dbPath: string, events: readonly RuntimeEvent[]): void {
const db = new DatabaseSync(dbPath);
try {
const insert = db.prepare(`
INSERT INTO runtime_events (
event_id, session_id, invocation_id, run_id, turn_id,
event_seq, event_kind, payload_json, committed_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
for (const [index, event] of events.entries()) {
insert.run(
event.id,
event.sessionId,
event.invocationId,
event.runId,
event.turnId,
index + 3,
event.content?.kind ?? 'runtime_fact',
JSON.stringify(event),
event.ts,
);
}
} finally {
db.close();
}
}
10 changes: 1 addition & 9 deletions packages/runtime/src/__tests__/recovery-resolver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,9 @@ describe('RecoveryResolver', () => {
},
]);
assert.equal(resolution.hasCorruption, false);
assert.equal(resolution.requiresReconciliation, false);
});

it('requires reconciliation after dispatch when no response was committed', () => {
it('keeps the outcome unknown after dispatch when no response was committed', () => {
const resolution = resolveRuntimeRecovery([
initialEvent('t1_after_preflight_v1'),
functionCallEvent(),
Expand All @@ -62,7 +61,6 @@ describe('RecoveryResolver', () => {
},
]);
assert.equal(resolution.hasCorruption, false);
assert.equal(resolution.requiresReconciliation, true);
});

it('keeps dispatched operations unresolved instead of inventing durable tool results', () => {
Expand Down Expand Up @@ -144,7 +142,6 @@ describe('RecoveryResolver', () => {

assert.equal(resolution.decisions[0]?.status, 'indeterminate');
assert.equal(resolution.decisions[0]?.reason, 'dispatch_without_response');
assert.equal(resolution.requiresReconciliation, true);
});

it('treats a matching response without dispatch as a completed pre-T1 result', () => {
Expand All @@ -166,7 +163,6 @@ describe('RecoveryResolver', () => {
settlementOrigin: 'pre_t1_synthetic',
},
]);
assert.equal(resolution.requiresReconciliation, false);
});

it('classifies a dispatch without its canonical function call as corruption', () => {
Expand All @@ -186,7 +182,6 @@ describe('RecoveryResolver', () => {
},
]);
assert.equal(resolution.hasCorruption, true);
assert.equal(resolution.requiresReconciliation, false);
});

it('classifies a response without its canonical function call as corruption', () => {
Expand Down Expand Up @@ -249,7 +244,6 @@ describe('RecoveryResolver', () => {
},
]);
assert.equal(resolution.hasCorruption, true);
assert.equal(resolution.requiresReconciliation, false);
});

it('classifies repeated response for one operation as corruption', () => {
Expand All @@ -275,7 +269,6 @@ describe('RecoveryResolver', () => {
},
]);
assert.equal(resolution.hasCorruption, true);
assert.equal(resolution.requiresReconciliation, false);
});

it('rejects a protocol marker added after the first canonical event', () => {
Expand Down Expand Up @@ -313,7 +306,6 @@ describe('RecoveryResolver', () => {
assert.equal(resolution.decisions[0]?.status, 'indeterminate');
assert.equal(resolution.decisions[0]?.reason, 'legacy_dispatch_unknown');
assert.equal(resolution.hasCorruption, true);
assert.equal(resolution.requiresReconciliation, false);
});

it('classifies a response linked to a different operation as corruption', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,6 @@ describe('stream graph supervisor tools', () => {
assert.equal(viewTool.name, VIEW_AGENT_GRAPH_TOOL_NAME);
assert.equal(updateTool.name, UPDATE_AGENT_GRAPH_TOOL_NAME);
assert.equal(yieldTool.name, YIELD_AGENT_GRAPH_TOOL_NAME);
assert.equal(viewTool.recoveryMode, 'replay_safe');
assert.equal(updateTool.recoveryMode, 'idempotent');
assert.equal(yieldTool.recoveryMode, 'replay_safe');

await assert.rejects(
async () =>
Expand Down
Loading
Loading