@gajae-code/coding-agent 0.12.21 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +67 -2
- package/dist/types/cli/args.d.ts +5 -2
- package/dist/types/cli/root-flags.d.ts +246 -0
- package/dist/types/cli.d.ts +11 -19
- package/dist/types/commands/daemon.d.ts +0 -30
- package/dist/types/commands/launch.d.ts +11 -19
- package/dist/types/commands/quick-lane.d.ts +32 -0
- package/dist/types/commands/sdk.d.ts +21 -15
- package/dist/types/config/file-lock-gc.d.ts +2 -0
- package/dist/types/config/model-equivalence.d.ts +20 -0
- package/dist/types/config/model-profile-activation.d.ts +62 -17
- package/dist/types/config/model-profiles.d.ts +10 -0
- package/dist/types/config/model-registry.d.ts +68 -1
- package/dist/types/config/model-resolver.d.ts +15 -4
- package/dist/types/config/models-config-schema.d.ts +7 -2
- package/dist/types/config/provider-selection-policy.d.ts +62 -0
- package/dist/types/config/settings-schema.d.ts +36 -78
- package/dist/types/config/settings.d.ts +2 -0
- package/dist/types/edit/renderer.d.ts +5 -2
- package/dist/types/edit/streaming.d.ts +13 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +2 -0
- package/dist/types/extensibility/extensions/compact-handler.d.ts +2 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +19 -5
- package/dist/types/internal-urls/local-protocol.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +0 -2
- package/dist/types/modes/components/provider-order-context.d.ts +45 -0
- package/dist/types/modes/components/provider-order-editor.d.ts +24 -0
- package/dist/types/modes/components/settings-defs.d.ts +5 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/quick-lane/classify.d.ts +40 -0
- package/dist/types/runtime/stt-settings-setup.d.ts +18 -0
- package/dist/types/sdk/broker/broker.d.ts +56 -2
- package/dist/types/sdk/broker/operation-receipt-ledger.d.ts +25 -0
- package/dist/types/sdk/broker/session-index.d.ts +83 -2
- package/dist/types/sdk/broker/state-version.d.ts +1 -1
- package/dist/types/sdk/bus/notification-service.d.ts +6 -0
- package/dist/types/sdk/bus/prompt-reconciliation.d.ts +8 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +4 -1
- package/dist/types/sdk/bus/telegram-daemon.d.ts +26 -0
- package/dist/types/sdk/cli/rows.d.ts +98 -0
- package/dist/types/sdk/cli/session-cli.d.ts +35 -3
- package/dist/types/sdk/elevation/allowlist.d.ts +20 -0
- package/dist/types/sdk/elevation/audit.d.ts +93 -0
- package/dist/types/sdk/elevation/capability.d.ts +3 -0
- package/dist/types/sdk/elevation/digest.d.ts +72 -0
- package/dist/types/sdk/elevation/dispatch-receipt.d.ts +85 -0
- package/dist/types/sdk/elevation/grant-ledger.d.ts +163 -0
- package/dist/types/sdk/elevation/owner.d.ts +48 -0
- package/dist/types/sdk/guides/cache.d.ts +98 -0
- package/dist/types/sdk/guides/catalog.d.ts +156 -0
- package/dist/types/sdk/guides/cli.d.ts +39 -0
- package/dist/types/sdk/guides/manifest.d.ts +66 -0
- package/dist/types/sdk/guides/verify.d.ts +66 -0
- package/dist/types/sdk/host/control/dispatch.d.ts +8 -0
- package/dist/types/sdk/host/control/index.d.ts +1 -1
- package/dist/types/sdk/host/control/operations.d.ts +3 -2
- package/dist/types/sdk/host/query/cursor.d.ts +10 -0
- package/dist/types/sdk/host/query/handlers.d.ts +21 -0
- package/dist/types/sdk/host/session-runtime.d.ts +9 -0
- package/dist/types/sdk/mcp/server.d.ts +2 -2
- package/dist/types/sdk/prompt-status.d.ts +11 -0
- package/dist/types/sdk/session.d.ts +4 -0
- package/dist/types/sdk/transport/index.d.ts +1 -0
- package/dist/types/sdk/transport/relay.d.ts +1 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +20 -1
- package/dist/types/session/agent-session.d.ts +32 -18
- package/dist/types/session/blob-store.d.ts +13 -7
- package/dist/types/session/fallback-chain-controller.d.ts +15 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +25 -1
- package/dist/types/session/internal/session-memory-sidecar.d.ts +5 -5
- package/dist/types/session/session-manager.d.ts +139 -10
- package/dist/types/session/session-storage.d.ts +48 -0
- package/dist/types/setup/provider-onboarding.d.ts +7 -1
- package/dist/types/slash-commands/builtin-registry.d.ts +14 -0
- package/dist/types/stt/setup.d.ts +2 -0
- package/dist/types/task/executor.d.ts +10 -2
- package/dist/types/tools/browser/runtime-diagnostics.d.ts +65 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +4 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker.d.ts +2 -0
- package/dist/types/tools/browser.d.ts +2 -0
- package/dist/types/tools/image-gen.d.ts +14 -18
- package/dist/types/tools/index.d.ts +2 -0
- package/dist/types/tools/resource-gc.d.ts +2 -0
- package/dist/types/utils/pasted-image-path.d.ts +6 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +8 -0
- package/scripts/run-test-manifest.ts +61 -21
- package/scripts/verify-gjc-sdk-canonicalization.ts +9 -4
- package/src/cli/args.ts +62 -9
- package/src/cli/completion-cli.ts +1 -0
- package/src/cli/fast-help.ts +1 -0
- package/src/cli/root-flags.ts +100 -0
- package/src/cli/setup-cli.ts +6 -2
- package/src/cli.ts +42 -65
- package/src/commands/acp.ts +1 -1
- package/src/commands/daemon.ts +2 -31
- package/src/commands/gc.ts +1 -1
- package/src/commands/launch.ts +8 -129
- package/src/commands/quick-lane.ts +51 -0
- package/src/commands/sdk.ts +200 -26
- package/src/commands/setup.ts +4 -2
- package/src/config/file-lock-gc.ts +4 -1
- package/src/config/model-equivalence.ts +76 -7
- package/src/config/model-profile-activation.ts +1115 -265
- package/src/config/model-profiles.ts +134 -2
- package/src/config/model-registry.ts +352 -50
- package/src/config/model-resolver.ts +289 -38
- package/src/config/models-config-schema.ts +11 -4
- package/src/config/provider-selection-policy.ts +146 -0
- package/src/config/settings-schema.ts +50 -77
- package/src/config/settings.ts +40 -0
- package/src/coordinator-mcp/server.ts +32 -2
- package/src/edit/renderer.ts +175 -223
- package/src/edit/streaming.ts +108 -1
- package/src/export/html/index.ts +57 -7
- package/src/extensibility/custom-tools/types.ts +2 -0
- package/src/extensibility/extensions/compact-handler.ts +3 -2
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/gjc-runtime/gc-runtime.ts +379 -20
- package/src/gjc-runtime/launch-worktree.ts +5 -1
- package/src/gjc-runtime/team-runtime.ts +59 -2
- package/src/gjc-runtime/tmux-owner-isolation.ts +19 -0
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +16 -1
- package/src/internal-urls/docs-index.generated.ts +17 -13
- package/src/internal-urls/local-protocol.ts +1 -0
- package/src/main.ts +40 -3
- package/src/memories/index.ts +4 -2
- package/src/modes/acp/acp-agent.ts +235 -91
- package/src/modes/components/model-selector.ts +206 -42
- package/src/modes/components/provider-order-context.ts +197 -0
- package/src/modes/components/provider-order-editor.ts +441 -0
- package/src/modes/components/settings-defs.ts +21 -1
- package/src/modes/components/settings-selector.ts +45 -2
- package/src/modes/components/tool-execution.ts +47 -15
- package/src/modes/controllers/command-controller.ts +6 -3
- package/src/modes/controllers/extension-ui-controller.ts +2 -0
- package/src/modes/controllers/input-controller.ts +73 -2
- package/src/modes/controllers/selector-controller.ts +260 -153
- package/src/modes/prompt-suggestion-controller.ts +1 -1
- package/src/modes/runtime-init.ts +1 -0
- package/src/modes/utils/ui-helpers.ts +18 -0
- package/src/prompts/tools/browser.md +1 -0
- package/src/quick-lane/classify.ts +257 -0
- package/src/rlm/index.ts +1 -1
- package/src/runtime/stt-settings-setup.ts +57 -0
- package/src/sdk/acp/adapter.ts +2 -1
- package/src/sdk/broker/broker.ts +780 -12
- package/src/sdk/broker/lifecycle.ts +70 -29
- package/src/sdk/broker/operation-receipt-ledger.ts +144 -0
- package/src/sdk/broker/session-index.ts +463 -48
- package/src/sdk/broker/state-version.ts +13 -9
- package/src/sdk/broker/transport.ts +14 -1
- package/src/sdk/bus/index.ts +34 -13
- package/src/sdk/bus/kind-aware-reconciliation.ts +21 -2
- package/src/sdk/bus/notification-service.ts +47 -1
- package/src/sdk/bus/prompt-reconciliation.ts +22 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +4 -1
- package/src/sdk/bus/telegram-daemon.ts +54 -3
- package/src/sdk/cli/rows.ts +247 -0
- package/src/sdk/cli/session-cli.ts +930 -79
- package/src/sdk/elevation/allowlist.ts +54 -0
- package/src/sdk/elevation/audit.ts +208 -0
- package/src/sdk/elevation/capability.ts +41 -0
- package/src/sdk/elevation/digest.ts +129 -0
- package/src/sdk/elevation/dispatch-receipt.ts +184 -0
- package/src/sdk/elevation/grant-ledger.ts +1142 -0
- package/src/sdk/elevation/owner.ts +109 -0
- package/src/sdk/guides/cache.ts +363 -0
- package/src/sdk/guides/catalog.ts +554 -0
- package/src/sdk/guides/cli.ts +235 -0
- package/src/sdk/guides/manifest.ts +152 -0
- package/src/sdk/guides/verify.ts +178 -0
- package/src/sdk/host/control/dispatch.ts +49 -1
- package/src/sdk/host/control/index.ts +1 -0
- package/src/sdk/host/control/operations.ts +8 -1
- package/src/sdk/host/query/cursor.ts +47 -3
- package/src/sdk/host/query/handlers.ts +155 -2
- package/src/sdk/host/sdk-surface-parity.test.ts +1 -0
- package/src/sdk/host/session-runtime.test.ts +391 -1
- package/src/sdk/host/session-runtime.ts +308 -73
- package/src/sdk/host/surface-policy.ts +3 -0
- package/src/sdk/host/websocket-transport.ts +7 -1
- package/src/sdk/mcp/server.ts +88 -15
- package/src/sdk/prompt-status.ts +8 -0
- package/src/sdk/protocol/operation-inventory.generated.json +62 -0
- package/src/sdk/protocol/operation-registry.ts +3 -2
- package/src/sdk/session.ts +87 -39
- package/src/sdk/transport/index.ts +1 -0
- package/src/sdk/transport/relay.ts +3 -0
- package/src/sdk/transport/serve-cli.ts +111 -24
- package/src/secrets/obfuscator.ts +92 -12
- package/src/session/agent-session.ts +322 -93
- package/src/session/blob-store.ts +60 -8
- package/src/session/fallback-chain-controller.ts +35 -0
- package/src/session/internal/managed-session-scope.ts +25 -3
- package/src/session/internal/managed-session-storage.ts +574 -73
- package/src/session/internal/session-memory-sidecar.ts +19 -29
- package/src/session/session-manager.ts +2166 -743
- package/src/session/session-storage.ts +715 -48
- package/src/setup/provider-onboarding.ts +19 -3
- package/src/setup/provider-presets.json +3 -1
- package/src/slash-commands/builtin-registry.ts +85 -3
- package/src/stt/downloader.ts +2 -3
- package/src/stt/setup.ts +29 -4
- package/src/task/executor.ts +22 -18
- package/src/task/index.ts +24 -1
- package/src/tools/archive-reader.ts +11 -2
- package/src/tools/browser/runtime-diagnostics.ts +249 -0
- package/src/tools/browser/tab-protocol.ts +4 -0
- package/src/tools/browser/tab-supervisor.ts +5 -0
- package/src/tools/browser/tab-worker.ts +36 -1
- package/src/tools/browser.ts +7 -0
- package/src/tools/image-gen.ts +110 -166
- package/src/tools/index.ts +2 -0
- package/src/tools/read.ts +1 -1
- package/src/tools/resource-gc.ts +6 -1
- package/src/tools/tool-catalog.generated.ts +5 -1
- package/src/utils/pasted-image-path.ts +93 -0
- package/src/web/search/index.ts +3 -3
|
@@ -741,6 +741,10 @@ async function reconcileReadyScope(broker: Broker, id: string, scope: string | u
|
|
|
741
741
|
locator: { ...record.locator, repo: scope },
|
|
742
742
|
endpointGeneration: record.endpointGeneration,
|
|
743
743
|
pid: record.pid,
|
|
744
|
+
// Reconciliation only re-scopes the locator, so every identity fact the host
|
|
745
|
+
// published about its own process has to survive it: dropping the incarnation
|
|
746
|
+
// here would silently disarm the teardown fence for every lifecycle session.
|
|
747
|
+
...(record.processIncarnation === undefined ? {} : { processIncarnation: record.processIncarnation }),
|
|
744
748
|
endpointMtimeMs: record.endpointMtimeMs,
|
|
745
749
|
});
|
|
746
750
|
}
|
|
@@ -1947,15 +1951,35 @@ export async function readSessionLifecycleFailureForTest(
|
|
|
1947
1951
|
: undefined;
|
|
1948
1952
|
}
|
|
1949
1953
|
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
pid: number
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1954
|
+
/** Everything a signal target must prove about itself before it can be signalled. */
|
|
1955
|
+
type SignalTarget = {
|
|
1956
|
+
locator: { stateRoot: string };
|
|
1957
|
+
pid: number;
|
|
1958
|
+
lifecycleRequestId?: string;
|
|
1959
|
+
processIncarnation?: string;
|
|
1960
|
+
};
|
|
1961
|
+
|
|
1962
|
+
/**
|
|
1963
|
+
* The spawn-time marker is the strongest evidence — the broker itself wrote it for
|
|
1964
|
+
* exactly this pid — but it lives in the session's own workspace state root. A
|
|
1965
|
+
* workspace deleted while its host keeps running takes that evidence with it, and
|
|
1966
|
+
* the host then survives every later `session.close` as an orphan still serving the
|
|
1967
|
+
* source it started with. The registration its host published into the broker-owned
|
|
1968
|
+
* index carries the same pid-to-incarnation binding, outlives the workspace, and is
|
|
1969
|
+
* therefore consulted when the marker is absent. A marker naming a different process
|
|
1970
|
+
* is contradiction rather than absence and still refuses the signal, and either
|
|
1971
|
+
* source is only accepted while the pid's current OS incarnation still matches it.
|
|
1972
|
+
*/
|
|
1973
|
+
async function hasDurableProcessIdentity(target: SignalTarget, id: string, expected?: EffectMarker): Promise<boolean> {
|
|
1974
|
+
const marker = await readEffectMarker(lifecycleMarkerPath(target.locator.stateRoot, id));
|
|
1975
|
+
if (marker)
|
|
1976
|
+
return (
|
|
1977
|
+
marker.pid === target.pid &&
|
|
1978
|
+
(!expected || sameEffectMarker(marker, expected)) &&
|
|
1979
|
+
marker.incarnation === processIncarnation(target.pid)
|
|
1980
|
+
);
|
|
1981
|
+
if (expected && (expected.pid !== target.pid || target.lifecycleRequestId !== expected.effectMarker)) return false;
|
|
1982
|
+
return target.processIncarnation !== undefined && target.processIncarnation === processIncarnation(target.pid);
|
|
1959
1983
|
}
|
|
1960
1984
|
|
|
1961
1985
|
async function hasOwnedReadinessEvidence(
|
|
@@ -2229,14 +2253,14 @@ async function terminateSpawnedChild(
|
|
|
2229
2253
|
}
|
|
2230
2254
|
|
|
2231
2255
|
async function signalVerifiedSession(
|
|
2232
|
-
record:
|
|
2256
|
+
record: SignalTarget,
|
|
2233
2257
|
id: string,
|
|
2234
2258
|
signal: NodeJS.Signals,
|
|
2235
2259
|
expected?: EffectMarker,
|
|
2236
2260
|
): Promise<boolean> {
|
|
2237
|
-
if (!(await hasDurableProcessIdentity(record
|
|
2261
|
+
if (!(await hasDurableProcessIdentity(record, id, expected))) return false;
|
|
2238
2262
|
try {
|
|
2239
|
-
if (!(await hasDurableProcessIdentity(record
|
|
2263
|
+
if (!(await hasDurableProcessIdentity(record, id, expected))) return false;
|
|
2240
2264
|
process.kill(record.pid, signal);
|
|
2241
2265
|
return true;
|
|
2242
2266
|
} catch {
|
|
@@ -2276,6 +2300,24 @@ async function waitForClose(
|
|
|
2276
2300
|
hasObservedProcessExit(record.pid)
|
|
2277
2301
|
)
|
|
2278
2302
|
return true;
|
|
2303
|
+
// A host that dies before it can withdraw its own registration — killed
|
|
2304
|
+
// mid-teardown, or unable to finish one because its workspace is gone —
|
|
2305
|
+
// leaves the index advertising a session whose process is provably absent.
|
|
2306
|
+
// That is exactly the evidence the dead-registration sweep retires records
|
|
2307
|
+
// on, so retire it here too instead of escalating signals at a pid that no
|
|
2308
|
+
// longer exists and then reporting the teardown as unfinished.
|
|
2309
|
+
if (registration && hasObservedProcessExit(record.pid)) {
|
|
2310
|
+
await broker.index.append({
|
|
2311
|
+
type: "host_unregistered",
|
|
2312
|
+
sessionId: id,
|
|
2313
|
+
locator: registration.locator,
|
|
2314
|
+
endpointGeneration: registration.endpointGeneration,
|
|
2315
|
+
pid: registration.pid,
|
|
2316
|
+
...(registration.endpointMtimeMs === undefined ? {} : { endpointMtimeMs: registration.endpointMtimeMs }),
|
|
2317
|
+
...(registration.lifecycleRequestId ? { lifecycleRequestId: registration.lifecycleRequestId } : {}),
|
|
2318
|
+
});
|
|
2319
|
+
return true;
|
|
2320
|
+
}
|
|
2279
2321
|
await timing.sleep(POLL_MS);
|
|
2280
2322
|
}
|
|
2281
2323
|
return false;
|
|
@@ -2996,27 +3038,22 @@ function sameCloseProcessIdentity(expected: CloseRecord, current: CloseRecord &
|
|
|
2996
3038
|
);
|
|
2997
3039
|
}
|
|
2998
3040
|
|
|
2999
|
-
function sameCloseGeneration(expected: CloseRecord, current: CloseRecord & { live: boolean }): boolean {
|
|
3000
|
-
return (
|
|
3001
|
-
current.live &&
|
|
3002
|
-
current.endpointGeneration === expected.endpointGeneration &&
|
|
3003
|
-
current.pid === expected.pid &&
|
|
3004
|
-
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
3005
|
-
path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
|
|
3006
|
-
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot)
|
|
3007
|
-
);
|
|
3008
|
-
}
|
|
3009
|
-
|
|
3010
3041
|
async function revalidateCloseGeneration(
|
|
3011
3042
|
broker: Broker,
|
|
3012
3043
|
id: string,
|
|
3013
3044
|
expected: CloseRecord,
|
|
3014
3045
|
authority: CloseAuthority | undefined,
|
|
3046
|
+
allowUnreachable = false,
|
|
3015
3047
|
): Promise<BrokerResponse | undefined> {
|
|
3016
3048
|
await broker.index.refresh();
|
|
3017
3049
|
const current = broker.index.listSessions().sessions.find(session => session.sessionId === id);
|
|
3018
3050
|
return current &&
|
|
3019
|
-
|
|
3051
|
+
(allowUnreachable || current.live) &&
|
|
3052
|
+
current.endpointGeneration === expected.endpointGeneration &&
|
|
3053
|
+
current.pid === expected.pid &&
|
|
3054
|
+
current.endpointMtimeMs === expected.endpointMtimeMs &&
|
|
3055
|
+
path.resolve(current.locator.repo) === path.resolve(expected.locator.repo) &&
|
|
3056
|
+
path.resolve(current.locator.stateRoot) === path.resolve(expected.locator.stateRoot) &&
|
|
3020
3057
|
(!authority || sameCloseAuthority(authority, current, id))
|
|
3021
3058
|
? undefined
|
|
3022
3059
|
: fail("endpoint_stale", "session endpoint is stale");
|
|
@@ -3427,9 +3464,13 @@ async function executeLifecycleResponse(
|
|
|
3427
3464
|
}
|
|
3428
3465
|
}
|
|
3429
3466
|
if (!endpointResult.ok) {
|
|
3430
|
-
if (endpointResult.error.code === "endpoint_stale")
|
|
3431
|
-
|
|
3432
|
-
|
|
3467
|
+
if (endpointResult.error.code === "endpoint_stale") {
|
|
3468
|
+
if (requestedAuthority.authority) return endpointResult;
|
|
3469
|
+
usedSignalFallback = true;
|
|
3470
|
+
} else {
|
|
3471
|
+
if (endpointResult.error.code !== "resource_gone") return endpointResult;
|
|
3472
|
+
usedSignalFallback = true;
|
|
3473
|
+
}
|
|
3433
3474
|
} else {
|
|
3434
3475
|
const endpoint = closeEndpoint(endpointResult.result);
|
|
3435
3476
|
if (!endpoint) return fail("close_refused", "Session endpoint is malformed.");
|
|
@@ -3470,7 +3511,7 @@ async function executeLifecycleResponse(
|
|
|
3470
3511
|
}
|
|
3471
3512
|
|
|
3472
3513
|
if (usedSignalFallback) {
|
|
3473
|
-
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority);
|
|
3514
|
+
const stale = await revalidateCloseGeneration(broker, id, record, requestedAuthority.authority, true);
|
|
3474
3515
|
if (stale) return stale;
|
|
3475
3516
|
if (!(await signalVerifiedSession(record, id, "SIGTERM")))
|
|
3476
3517
|
return fail(
|
|
@@ -4403,7 +4444,7 @@ export async function executeLifecycle(
|
|
|
4403
4444
|
}
|
|
4404
4445
|
: {
|
|
4405
4446
|
code: "spawn_failed",
|
|
4406
|
-
message:
|
|
4447
|
+
message: startupFailure.message,
|
|
4407
4448
|
endpoint: "unavailable" as const,
|
|
4408
4449
|
},
|
|
4409
4450
|
...(durableEffects ? { durableEffects } : {}),
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import * as fs from "node:fs/promises";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
import { canonicalElevationJson } from "../elevation/digest";
|
|
5
|
+
import type { BrokerResponse } from "./broker";
|
|
6
|
+
|
|
7
|
+
interface PendingReceipt {
|
|
8
|
+
version: 1;
|
|
9
|
+
clientRef: string;
|
|
10
|
+
digest: string;
|
|
11
|
+
state: "pending";
|
|
12
|
+
ts: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface CompletedReceipt {
|
|
16
|
+
version: 1;
|
|
17
|
+
clientRef: string;
|
|
18
|
+
digest: string;
|
|
19
|
+
state: "completed";
|
|
20
|
+
response: BrokerResponse;
|
|
21
|
+
ts: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type OperationReceipt = PendingReceipt | CompletedReceipt;
|
|
25
|
+
|
|
26
|
+
export type OperationReserveResult =
|
|
27
|
+
| { status: "reserved" }
|
|
28
|
+
| { status: "replay"; response: BrokerResponse }
|
|
29
|
+
| { status: "in_progress" }
|
|
30
|
+
| { status: "conflict" };
|
|
31
|
+
|
|
32
|
+
function isReceipt(value: unknown): value is OperationReceipt {
|
|
33
|
+
if (!value || typeof value !== "object") return false;
|
|
34
|
+
const row = value as Partial<OperationReceipt>;
|
|
35
|
+
return (
|
|
36
|
+
row.version === 1 &&
|
|
37
|
+
typeof row.clientRef === "string" &&
|
|
38
|
+
row.clientRef.length > 0 &&
|
|
39
|
+
typeof row.digest === "string" &&
|
|
40
|
+
/^[0-9a-f]{64}$/.test(row.digest) &&
|
|
41
|
+
(row.state === "pending" || (row.state === "completed" && typeof row.response === "object")) &&
|
|
42
|
+
typeof row.ts === "number"
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Target-session-scoped idempotency key for a clientRef. Identical refs on
|
|
48
|
+
* different sessions are independent, so a receipt can never replay or
|
|
49
|
+
* conflict across sessions.
|
|
50
|
+
*/
|
|
51
|
+
export function operationReceiptKey(sessionId: string, clientRef: string): string {
|
|
52
|
+
return `${sessionId}\u0000${clientRef}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function operationReceiptDigest(operation: string, input: Record<string, unknown>, sessionId?: string): string {
|
|
56
|
+
return createHash("sha256")
|
|
57
|
+
.update(canonicalElevationJson({ operation, input, ...(sessionId === undefined ? {} : { sessionId }) }))
|
|
58
|
+
.digest("hex");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export class OperationReceiptLedger {
|
|
62
|
+
readonly #file: string;
|
|
63
|
+
readonly #latest = new Map<string, OperationReceipt>();
|
|
64
|
+
#chain: Promise<void> = Promise.resolve();
|
|
65
|
+
|
|
66
|
+
constructor(agentDir: string) {
|
|
67
|
+
this.#file = path.join(agentDir, "sdk", "operation-receipts.jsonl");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async open(): Promise<void> {
|
|
71
|
+
await fs.mkdir(path.dirname(this.#file), { recursive: true, mode: 0o700 });
|
|
72
|
+
let source = "";
|
|
73
|
+
try {
|
|
74
|
+
source = await fs.readFile(this.#file, "utf8");
|
|
75
|
+
} catch (error) {
|
|
76
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
77
|
+
}
|
|
78
|
+
for (const line of source.split("\n")) {
|
|
79
|
+
if (!line) continue;
|
|
80
|
+
try {
|
|
81
|
+
const row: unknown = JSON.parse(line);
|
|
82
|
+
if (isReceipt(row)) this.#latest.set(row.clientRef, row);
|
|
83
|
+
} catch {
|
|
84
|
+
// A torn final append is fail-closed: any earlier pending row remains authoritative.
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async reserve(clientRef: string, digest: string): Promise<OperationReserveResult> {
|
|
90
|
+
return this.#serial(async () => {
|
|
91
|
+
const current = this.#latest.get(clientRef);
|
|
92
|
+
if (current) {
|
|
93
|
+
if (current.digest !== digest) return { status: "conflict" };
|
|
94
|
+
if (current.state === "completed") return { status: "replay", response: current.response };
|
|
95
|
+
return { status: "in_progress" };
|
|
96
|
+
}
|
|
97
|
+
const pending: PendingReceipt = { version: 1, clientRef, digest, state: "pending", ts: Date.now() };
|
|
98
|
+
await this.#append(pending);
|
|
99
|
+
this.#latest.set(clientRef, pending);
|
|
100
|
+
return { status: "reserved" };
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async complete(clientRef: string, digest: string, response: BrokerResponse): Promise<void> {
|
|
105
|
+
await this.#serial(async () => {
|
|
106
|
+
const current = this.#latest.get(clientRef);
|
|
107
|
+
if (!current || current.digest !== digest)
|
|
108
|
+
throw new Error("Operation receipt reservation is missing or mismatched");
|
|
109
|
+
if (current.state === "completed") return;
|
|
110
|
+
const completed: CompletedReceipt = {
|
|
111
|
+
version: 1,
|
|
112
|
+
clientRef,
|
|
113
|
+
digest,
|
|
114
|
+
state: "completed",
|
|
115
|
+
response,
|
|
116
|
+
ts: Date.now(),
|
|
117
|
+
};
|
|
118
|
+
await this.#append(completed);
|
|
119
|
+
this.#latest.set(clientRef, completed);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async #append(row: OperationReceipt): Promise<void> {
|
|
124
|
+
const handle = await fs.open(this.#file, "a", 0o600);
|
|
125
|
+
try {
|
|
126
|
+
await handle.writeFile(`${JSON.stringify(row)}\n`);
|
|
127
|
+
await handle.sync();
|
|
128
|
+
} finally {
|
|
129
|
+
await handle.close();
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async #serial<T>(operation: () => Promise<T>): Promise<T> {
|
|
134
|
+
const previous = this.#chain;
|
|
135
|
+
const next = Promise.withResolvers<void>();
|
|
136
|
+
this.#chain = next.promise;
|
|
137
|
+
await previous;
|
|
138
|
+
try {
|
|
139
|
+
return await operation();
|
|
140
|
+
} finally {
|
|
141
|
+
next.resolve();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|