@astrosheep/keiyaku 2.9.2 → 2.9.4
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/build/.tsbuildinfo +1 -1
- package/build/agents/call-terms.js +13 -94
- package/build/agents/harness/activity-values.js +16 -0
- package/build/agents/harness/event-persistence.js +218 -0
- package/build/agents/harness/execution-handle.js +25 -222
- package/build/agents/harness/index.js +3 -1
- package/build/agents/harness/outcome.js +47 -4
- package/build/agents/harness/projection.js +42 -59
- package/build/agents/harness/provider-adapter.js +58 -0
- package/build/agents/harness/router.js +11 -187
- package/build/agents/harness/runtime.js +17 -4
- package/build/agents/harness/session.js +15 -0
- package/build/agents/launch-snapshot/model.js +15 -236
- package/build/agents/launch-snapshot/parse-helpers.js +114 -0
- package/build/agents/launch-snapshot/resolve.js +17 -43
- package/build/agents/opencode-sdk.js +45 -12
- package/build/agents/providers/claude-agent-sdk/adapter.js +6 -17
- package/build/agents/providers/claude-agent-sdk/registration.js +214 -0
- package/build/agents/providers/claude-agent-sdk/session-schema.js +2 -0
- package/build/agents/providers/codex-app-server/adapter.js +50 -53
- package/build/agents/providers/codex-app-server/events.js +67 -11
- package/build/agents/providers/codex-app-server/identity.js +1 -0
- package/build/agents/providers/codex-app-server/policy.js +29 -0
- package/build/agents/providers/codex-app-server/registration.js +248 -0
- package/build/agents/providers/codex-app-server/session-schema.js +6 -0
- package/build/agents/providers/codex-app-server/session.js +23 -17
- package/build/agents/providers/codex-app-server/terms.js +1 -0
- package/build/agents/providers/opencode-sdk/adapter.js +94 -50
- package/build/agents/providers/opencode-sdk/events.js +8 -4
- package/build/agents/providers/opencode-sdk/registration.js +135 -0
- package/build/agents/providers/opencode-sdk/session-schema.js +2 -0
- package/build/agents/providers/opencode-sdk/session.js +21 -14
- package/build/agents/providers/opencode-sdk/terms.js +1 -0
- package/build/agents/providers/pi/adapter.js +36 -30
- package/build/agents/providers/pi/registration.js +145 -0
- package/build/agents/providers/pi/session-schema.js +6 -0
- package/build/agents/providers/pi/terms.js +1 -0
- package/build/agents/providers/registration.js +7 -0
- package/build/agents/providers/registry.js +76 -0
- package/build/agents/selector.js +2 -29
- package/build/agents/types.js +8 -0
- package/build/artifact-directories.js +22 -0
- package/build/cli/commands/akuma/akuma/handler.js +1 -1
- package/build/cli/commands/akuma/view/handler.js +5 -4
- package/build/cli/commands/akuma.js +6 -0
- package/build/cli/commands/contract/amend/handler.js +8 -2
- package/build/cli/commands/contract/arc/handler.js +7 -1
- package/build/cli/commands/contract/bind/handler.js +4 -3
- package/build/cli/commands/contract/bind/meta.js +34 -7
- package/build/cli/commands/contract/forfeit/handler.js +4 -4
- package/build/cli/commands/contract/log/handler.js +3 -3
- package/build/cli/commands/contract/petition/handler.js +4 -6
- package/build/cli/commands/contract/renew/handler.js +20 -6
- package/build/cli/commands/contract/renew/meta.js +5 -4
- package/build/cli/commands/metadata.js +58 -0
- package/build/cli/commands/projection/call/handler.js +6 -5
- package/build/cli/commands/projection/kill/handler.js +16 -32
- package/build/cli/commands/projection/shared.js +28 -0
- package/build/cli/commands/projection/status/handler.js +1 -1
- package/build/cli/commands/projection/tell/handler.js +14 -30
- package/build/cli/commands/projection/wait/handler.js +45 -22
- package/build/cli/commands/projection/wait/meta.js +7 -2
- package/build/cli/commands/registry.js +2 -54
- package/build/cli/commands/shared.js +10 -7
- package/build/cli/commands/skills/install/handler.js +2 -5
- package/build/cli/commands/skills/skills/handler.js +1 -1
- package/build/cli/commands/task/add/handler.js +5 -13
- package/build/cli/commands/task/catalog.js +2 -2
- package/build/cli/commands/task/done/handler.js +4 -8
- package/build/cli/commands/task/drop/handler.js +3 -6
- package/build/cli/commands/task/ls/handler.js +3 -5
- package/build/cli/commands/task/shared.js +14 -4
- package/build/cli/commands/task/{view → show}/handler.js +7 -8
- package/build/cli/commands/task/show/meta.js +8 -0
- package/build/cli/commands/task/start/handler.js +3 -6
- package/build/cli/commands/task/stop/handler.js +4 -8
- package/build/cli/commands/task/task/handler.js +1 -1
- package/build/cli/commands/task/update/handler.js +4 -8
- package/build/cli/completion.js +1 -1
- package/build/cli/flags.js +19 -3
- package/build/cli/help.js +4 -11
- package/build/cli/index.js +57 -83
- package/build/cli/missing-subcommand.js +7 -0
- package/build/cli/parse-flags.js +14 -5
- package/build/cli/parse-metadata.js +3 -3
- package/build/cli/parse-selectors.js +1 -1
- package/build/cli/parse.js +21 -0
- package/build/cli/projection-address.js +7 -12
- package/build/cli/render/address.js +41 -37
- package/build/cli/render/arc.js +24 -12
- package/build/cli/render/call.js +11 -5
- package/build/cli/render/errors.js +36 -1
- package/build/cli/render/format.js +4 -0
- package/build/cli/render/kanshi.js +6 -12
- package/build/cli/render/kill.js +9 -0
- package/build/cli/render/misc.js +55 -12
- package/build/cli/render/petition.js +20 -15
- package/build/cli/render/projection-activity.js +3 -0
- package/build/cli/render/shared.js +60 -21
- package/build/cli/render/skills.js +10 -0
- package/build/cli/render/status.js +37 -14
- package/build/cli/render/success-response.js +77 -0
- package/build/cli/render/task.js +54 -14
- package/build/cli/render/tell.js +5 -0
- package/build/cli/render/terminal-failure.js +58 -0
- package/build/cli/render/types.js +1 -0
- package/build/cli/render/wait.js +38 -36
- package/build/cli/types.js +1 -1
- package/build/config/akuma-loader.js +0 -5
- package/build/config/env-keys.js +1 -1
- package/build/config/provider-policy-ownership.js +3 -32
- package/build/config/settings/disease.js +8 -1
- package/build/config/settings/loader.js +11 -3
- package/build/config/settings/provider-instance-fields.js +51 -0
- package/build/config/settings/schema.js +6 -176
- package/build/core/addressing.js +66 -16
- package/build/core/amend.js +135 -18
- package/build/core/arc.js +19 -67
- package/build/core/bind.js +82 -35
- package/build/core/call/call.js +15 -9
- package/build/core/call/context.js +6 -51
- package/build/core/call/execution.js +21 -22
- package/build/core/call/prompt.js +14 -1
- package/build/core/command-markdown.js +45 -0
- package/build/core/context.js +13 -7
- package/build/core/contract-view.js +20 -3
- package/build/core/draft.js +123 -16
- package/build/core/forfeit.js +5 -2
- package/build/core/hints.js +1 -75
- package/build/core/identifier-slug.js +66 -0
- package/build/core/ids.js +25 -11
- package/build/core/ledger.js +20 -68
- package/build/core/markdown/parser.js +0 -68
- package/build/core/markdown/render.js +68 -0
- package/build/core/markdown/sections.js +2 -1
- package/build/core/outcome-base.js +4 -0
- package/build/core/projection/generation/database.js +2 -38
- package/build/core/projection/generation/model.js +42 -1
- package/build/core/{projection-generation-continuation.js → projection/generation/projection-generation-continuation.js} +3 -3
- package/build/core/{projection-generation-execution.js → projection/generation/projection-generation-execution.js} +19 -49
- package/build/core/{projection-generation-identity.js → projection/generation/projection-generation-identity.js} +2 -2
- package/build/core/{projection-generation-launcher.js → projection/generation/projection-generation-launcher.js} +5 -3
- package/build/core/{projection-generation-process.js → projection/generation/projection-generation-process.js} +2 -1
- package/build/core/{projection-generation-runner.js → projection/generation/projection-generation-runner.js} +18 -3
- package/build/core/projection/generation/store.js +2 -2
- package/build/core/projection/generation/transitions.js +1 -1
- package/build/core/projection/index.js +27 -0
- package/build/core/{projection-activity.js → projection/projection-activity.js} +12 -0
- package/build/core/{projection-alias.js → projection/projection-alias.js} +14 -19
- package/build/core/projection/projection-coordinate.js +61 -0
- package/build/core/{projection-core.js → projection/projection-core.js} +9 -9
- package/build/core/{projection-execution-observer.js → projection/projection-execution-observer.js} +2 -57
- package/build/core/{projection-identity.js → projection/projection-identity.js} +1 -1
- package/build/core/{projection-kill.js → projection/projection-kill.js} +1 -1
- package/build/core/{projection-life-observer.js → projection/projection-life-observer.js} +1 -1
- package/build/core/{projection-mint.js → projection/projection-mint.js} +10 -10
- package/build/core/{projection-status.js → projection/projection-status.js} +98 -25
- package/build/core/projection/projection-terminal-failure.js +104 -0
- package/build/core/projection/projection-wait.js +278 -0
- package/build/core/projection/projection-wake.js +203 -0
- package/build/core/provider-loader-ports.js +4 -0
- package/build/core/ref-log.js +65 -40
- package/build/core/registry.js +63 -10
- package/build/core/renew-session.js +128 -0
- package/build/core/renew.js +426 -99
- package/build/core/scope.js +143 -66
- package/build/core/seal.js +139 -35
- package/build/core/{claim-delivery.js → settlement/claim-delivery.js} +9 -14
- package/build/core/{claim.js → settlement/claim.js} +32 -8
- package/build/core/settlement/index.js +19 -0
- package/build/core/{petition-claim-gates.js → settlement/petition-claim-gates.js} +22 -18
- package/build/core/{petition-forfeit.js → settlement/petition-forfeit.js} +2 -2
- package/build/core/{petition-head-guard.js → settlement/petition-head-guard.js} +2 -2
- package/build/core/settlement/petition-io.js +1 -0
- package/build/core/{petition-preview.js → settlement/petition-preview.js} +1 -1
- package/build/core/{petition.js → settlement/petition.js} +12 -10
- package/build/core/settlement/queue-read-model.js +143 -0
- package/build/core/settlement/queue-seat-allocation.js +156 -0
- package/build/core/settlement/queue.js +6 -0
- package/build/core/settlement/settlement.js +295 -0
- package/build/core/{verdict.js → settlement/verdict.js} +9 -37
- package/build/core/status/board.js +24 -23
- package/build/core/status/ledger-batch.js +158 -0
- package/build/core/status/reconciliation.js +10 -4
- package/build/core/stored-agent-event.js +3 -0
- package/build/core/task/board.js +40 -4
- package/build/core/task/commands.js +30 -21
- package/build/core/task/index.js +12 -0
- package/build/core/task/settlement-git.js +157 -65
- package/build/core/task/settlement-policy.js +54 -24
- package/build/core/task/task-contract.js +370 -0
- package/build/core/{task-doctor.js → task/task-doctor.js} +1 -1
- package/build/core/{task-git-actor.js → task/task-git-actor.js} +5 -10
- package/build/core/{task-git-runtime.js → task/task-git-runtime.js} +3 -3
- package/build/core/{task-git-store.js → task/task-git-store.js} +92 -29
- package/build/core/task/task-repository.js +1 -0
- package/build/core/{task-store-repository.js → task/task-store-repository.js} +5 -4
- package/build/core/task/task-store.js +1 -0
- package/build/core/{task-worktree.js → task/task-worktree.js} +2 -1
- package/build/core/task/task.js +5 -0
- package/build/core/task/tree.js +37 -0
- package/build/core/task/validation.js +5 -2
- package/build/core/transcripts.js +15 -3
- package/build/core/worktree-path.js +43 -28
- package/build/flow-error.js +85 -39
- package/build/fs/durability.js +27 -0
- package/build/fs/path-coordinate.js +16 -0
- package/build/generated/version.js +3 -2
- package/build/git/branches.js +2 -1
- package/build/git/core.js +106 -2
- package/build/git/path-coordinate.js +17 -0
- package/build/git/refs.js +57 -0
- package/build/git/streaming-batch.js +301 -0
- package/build/git/worktree.js +65 -0
- package/build/index.js +3 -3
- package/build/keiyaku.js +2 -36
- package/package.json +4 -10
- package/skills/keiyaku/SKILL.md +3 -2
- package/skills/keiyaku-akuma/SKILL.md +10 -6
- package/skills/keiyaku-task/SKILL.md +1 -1
- package/skills/keiyaku-workflow/SKILL.md +25 -4
- package/build/agents/providers/codex-sdk.js +0 -329
- package/build/cli/commands/task/view/meta.js +0 -8
- package/build/core/command-io.js +0 -174
- package/build/core/path-coordinate.js +0 -27
- package/build/core/petition-claim.js +0 -117
- package/build/core/petition-run.js +0 -83
- package/build/core/projection-coordinate.js +0 -63
- package/build/core/projection-wait.js +0 -165
- package/build/core/projection-wake.js +0 -136
- package/build/core/queue.js +0 -343
- package/build/core/task-contract.js +0 -237
- package/build/core/task.js +0 -4
- /package/build/{core/task-repository.js → agents/launch-snapshot/types.js} +0 -0
- /package/build/{core/task-store.js → agents/providers/claude-agent-sdk/terms.js} +0 -0
- /package/build/core/{projection-generation-runtime.js → projection/generation/projection-generation-runtime.js} +0 -0
- /package/build/core/{projection-life-protocol.js → projection/projection-life-protocol.js} +0 -0
- /package/build/core/{projection-runner-lock.js → projection/projection-runner-lock.js} +0 -0
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { foldPublicProjectionActivity } from "./projection-activity.js";
|
|
4
|
+
import { readGenerationEvents, isProjectionId, projectionRoot, } from "./projection-core.js";
|
|
5
|
+
import { readProjectionIdentity } from "./projection-identity.js";
|
|
6
|
+
import { RESPONSE_ARTIFACT_ID_RE } from "../response-artifact-id.js";
|
|
7
|
+
import { PROJECTION_ADOPTION_TIMEOUT_MS } from "./generation/projection-generation-launcher.js";
|
|
8
|
+
import { observeProjectionLifeSnapshot } from "./projection-life-observer.js";
|
|
9
|
+
import { selectCurrentGeneration } from "./projection-life-protocol.js";
|
|
10
|
+
import { readProjectionTellCounts } from "./tell/store.js";
|
|
11
|
+
import { projectTerminalFailure, } from "./projection-terminal-failure.js";
|
|
12
|
+
import { invalidActivityEventDiagnostic, selectValidStoredAgentEvents, } from "../stored-agent-event.js";
|
|
13
|
+
/** Return the commission-bound projection IDs visible at one admission instant. */
|
|
14
|
+
export async function snapshotCommissionProjectionIds(cwd, commissionId) {
|
|
15
|
+
const root = await projectionRoot(cwd);
|
|
16
|
+
let akumas;
|
|
17
|
+
try {
|
|
18
|
+
akumas = fs.readdirSync(root).sort();
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT")
|
|
22
|
+
return [];
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
const ids = [];
|
|
26
|
+
for (const akuma of akumas) {
|
|
27
|
+
if (akuma.startsWith("."))
|
|
28
|
+
continue;
|
|
29
|
+
const akumaDirectory = path.join(root, akuma);
|
|
30
|
+
try {
|
|
31
|
+
if (!fs.lstatSync(akumaDirectory).isDirectory())
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
let projections;
|
|
38
|
+
try {
|
|
39
|
+
projections = fs.readdirSync(akumaDirectory).sort();
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
for (const hex of projections) {
|
|
45
|
+
const id = `${akuma}/${hex}`;
|
|
46
|
+
if (!isProjectionId(id))
|
|
47
|
+
continue;
|
|
48
|
+
const directory = path.join(akumaDirectory, hex);
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.lstatSync(directory).isDirectory())
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
let pact;
|
|
57
|
+
try {
|
|
58
|
+
pact = readProjectionIdentity(directory);
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// A commission snapshot admits only identities that can prove their
|
|
62
|
+
// binding. Missing or corrupt unrelated storage is not such proof.
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (pact.binding.kind === "commission" && pact.binding.commissionId === commissionId)
|
|
66
|
+
ids.push(id);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return ids;
|
|
70
|
+
}
|
|
71
|
+
const WAIT_POLL_MS = 100;
|
|
72
|
+
function sleep(ms) {
|
|
73
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
74
|
+
}
|
|
75
|
+
function waitingTellCount(projectionDirectory) {
|
|
76
|
+
const counts = readProjectionTellCounts(projectionDirectory);
|
|
77
|
+
return counts.inbox + counts.inflight + counts.submitted + counts.delivered;
|
|
78
|
+
}
|
|
79
|
+
function latestValidEventAgeMs(events, nowMs) {
|
|
80
|
+
let latest;
|
|
81
|
+
for (const event of events) {
|
|
82
|
+
const atMs = Date.parse(event.at);
|
|
83
|
+
if (!Number.isFinite(atMs))
|
|
84
|
+
continue;
|
|
85
|
+
if (latest === undefined || atMs > latest)
|
|
86
|
+
latest = atMs;
|
|
87
|
+
}
|
|
88
|
+
return latest === undefined ? undefined : Math.max(0, nowMs - latest);
|
|
89
|
+
}
|
|
90
|
+
function adoptionAgeMs(current, nowMs) {
|
|
91
|
+
const adoptedAt = current?.adoption?.facts.adoptedAt;
|
|
92
|
+
if (typeof adoptedAt !== "string")
|
|
93
|
+
return undefined;
|
|
94
|
+
const adoptedAtMs = Date.parse(adoptedAt);
|
|
95
|
+
return Number.isFinite(adoptedAtMs)
|
|
96
|
+
? Math.max(0, nowMs - adoptedAtMs)
|
|
97
|
+
: undefined;
|
|
98
|
+
}
|
|
99
|
+
function relativeEventLedgerFile(executionId) {
|
|
100
|
+
return `${executionId}.events.jsonl`;
|
|
101
|
+
}
|
|
102
|
+
function timestampFact(current, nowMs) {
|
|
103
|
+
const facts = current.verdict?.facts;
|
|
104
|
+
for (const key of ["completedAt", "failedAt", "dismissedAt", "killedAt"]) {
|
|
105
|
+
const value = facts?.[key];
|
|
106
|
+
if (typeof value === "string" && Number.isFinite(Date.parse(value)))
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
return new Date(nowMs).toISOString();
|
|
110
|
+
}
|
|
111
|
+
function verdictFromCurrent(current, nowMs) {
|
|
112
|
+
if (!current.verdict)
|
|
113
|
+
return undefined;
|
|
114
|
+
const state = current.verdict.facts.state;
|
|
115
|
+
if (typeof state !== "string")
|
|
116
|
+
return undefined;
|
|
117
|
+
const terminalFailure = projectTerminalFailure(state, current.verdict.facts);
|
|
118
|
+
return {
|
|
119
|
+
state: state,
|
|
120
|
+
at: timestampFact(current, nowMs),
|
|
121
|
+
...(terminalFailure ? { terminalFailure } : {}),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function outcomeText(current) {
|
|
125
|
+
if (current.verdict?.facts.state !== "completed")
|
|
126
|
+
return undefined;
|
|
127
|
+
const finalMessage = current.verdict?.facts.finalMessage;
|
|
128
|
+
if (typeof finalMessage === "string" && finalMessage.trim())
|
|
129
|
+
return finalMessage;
|
|
130
|
+
return undefined;
|
|
131
|
+
}
|
|
132
|
+
function buildSnapshot(input) {
|
|
133
|
+
const { projectionDirectory, pact, phase, current, events, malformedEvents, nowMs } = input;
|
|
134
|
+
const anchorAt = current?.verdict ? timestampFact(current, nowMs) : new Date(nowMs).toISOString();
|
|
135
|
+
const anchorMs = Date.parse(anchorAt);
|
|
136
|
+
const mintedAtMs = Date.parse(pact.mintedAt);
|
|
137
|
+
const activityAgeMs = latestValidEventAgeMs(events, nowMs)
|
|
138
|
+
?? adoptionAgeMs(current, nowMs);
|
|
139
|
+
const invalidActivity = invalidActivityEventDiagnostic(malformedEvents);
|
|
140
|
+
const artifactId = current?.verdict?.facts.artifactId;
|
|
141
|
+
const responsePath = current?.verdict?.facts.responsePath;
|
|
142
|
+
const completed = current?.verdict?.facts.state === "completed";
|
|
143
|
+
return {
|
|
144
|
+
phase,
|
|
145
|
+
observedAtMs: Number.isFinite(anchorMs) ? anchorMs : nowMs,
|
|
146
|
+
durationMs: Number.isFinite(mintedAtMs) && Number.isFinite(anchorMs)
|
|
147
|
+
? Math.max(0, anchorMs - mintedAtMs)
|
|
148
|
+
: 0,
|
|
149
|
+
...(activityAgeMs !== undefined ? { activityAgeMs } : {}),
|
|
150
|
+
tellsWaiting: waitingTellCount(projectionDirectory),
|
|
151
|
+
activity: foldPublicProjectionActivity(events),
|
|
152
|
+
...(invalidActivity ? { eventDiagnostics: [invalidActivity] } : {}),
|
|
153
|
+
workspaceRoot: pact.workspaceRoot,
|
|
154
|
+
...(current ? {
|
|
155
|
+
executionId: current.launch.executionId,
|
|
156
|
+
eventLedgerFile: relativeEventLedgerFile(current.launch.executionId),
|
|
157
|
+
} : {}),
|
|
158
|
+
...(completed && typeof responsePath === "string" && responsePath.trim() ? { responsePath } : {}),
|
|
159
|
+
...(completed && typeof artifactId === "string" && RESPONSE_ARTIFACT_ID_RE.test(artifactId) ? { artifactId } : {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
export async function waitForProjection(input) {
|
|
163
|
+
const now = input.now ?? Date.now;
|
|
164
|
+
let pact;
|
|
165
|
+
try {
|
|
166
|
+
pact = readProjectionIdentity(input.projectionDirectory);
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
return {
|
|
170
|
+
state: "unknown",
|
|
171
|
+
diagnostic: `projection identity could not be observed: ${error instanceof Error ? error.message : String(error)}`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const deadline = input.deadlineMs
|
|
175
|
+
?? (input.timeoutMs === undefined ? undefined : now() + input.timeoutMs);
|
|
176
|
+
for (;;) {
|
|
177
|
+
input.signal?.throwIfAborted();
|
|
178
|
+
const observedAtMs = now();
|
|
179
|
+
const observation = observeProjectionLifeSnapshot(input.projectionDirectory, {
|
|
180
|
+
nowMs: observedAtMs,
|
|
181
|
+
startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
|
|
182
|
+
});
|
|
183
|
+
const current = observation.records
|
|
184
|
+
? selectCurrentGeneration(observation.records)
|
|
185
|
+
: null;
|
|
186
|
+
const life = observation.life;
|
|
187
|
+
const verdict = current ? verdictFromCurrent(current, observedAtMs) : undefined;
|
|
188
|
+
const terminal = verdict !== undefined;
|
|
189
|
+
const terminalAtMs = verdict === undefined ? undefined : Date.parse(verdict.at);
|
|
190
|
+
const terminalWithinDeadline = terminal && (input.deadlineMs === undefined
|
|
191
|
+
|| (typeof terminalAtMs === "number" && Number.isFinite(terminalAtMs) && terminalAtMs < input.deadlineMs));
|
|
192
|
+
if (terminalWithinDeadline || (deadline !== undefined && observedAtMs >= deadline)) {
|
|
193
|
+
const stored = current
|
|
194
|
+
? readGenerationEvents(input.projectionDirectory, current.launch.executionId)
|
|
195
|
+
: [];
|
|
196
|
+
const { events, malformed } = selectValidStoredAgentEvents(stored);
|
|
197
|
+
const snapshot = buildSnapshot({
|
|
198
|
+
projectionDirectory: input.projectionDirectory,
|
|
199
|
+
pact,
|
|
200
|
+
phase: life.phase,
|
|
201
|
+
current,
|
|
202
|
+
events,
|
|
203
|
+
malformedEvents: malformed,
|
|
204
|
+
nowMs: observedAtMs,
|
|
205
|
+
});
|
|
206
|
+
if (terminalWithinDeadline && current) {
|
|
207
|
+
const text = outcomeText(current);
|
|
208
|
+
return {
|
|
209
|
+
state: "terminal",
|
|
210
|
+
pact,
|
|
211
|
+
verdict,
|
|
212
|
+
snapshot,
|
|
213
|
+
...(text ? { outcomeText: text } : {}),
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
return {
|
|
217
|
+
state: "timeout",
|
|
218
|
+
pact,
|
|
219
|
+
snapshot,
|
|
220
|
+
...(terminal ? { terminalSnapshot: true } : {}),
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
const waitMs = deadline === undefined
|
|
224
|
+
? WAIT_POLL_MS
|
|
225
|
+
: Math.max(0, Math.min(WAIT_POLL_MS, deadline - observedAtMs));
|
|
226
|
+
await sleep(waitMs);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Observe a fixed projection set. Admission owns the set snapshot; this reader
|
|
231
|
+
* never discovers or adds a projection while it is waiting.
|
|
232
|
+
*/
|
|
233
|
+
export async function waitForProjectionSet(input) {
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
const abort = () => controller.abort(input.signal?.reason);
|
|
236
|
+
if (input.signal?.aborted)
|
|
237
|
+
abort();
|
|
238
|
+
input.signal?.addEventListener("abort", abort, { once: true });
|
|
239
|
+
const members = [...input.projections].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0);
|
|
240
|
+
const now = input.now ?? Date.now;
|
|
241
|
+
const deadlineMs = input.timeoutMs === undefined ? undefined : now() + input.timeoutMs;
|
|
242
|
+
try {
|
|
243
|
+
const waits = members.map((member, index) => waitForProjection({
|
|
244
|
+
projectionDirectory: member.projectionDirectory,
|
|
245
|
+
...(deadlineMs === undefined ? {} : { deadlineMs }),
|
|
246
|
+
now,
|
|
247
|
+
signal: controller.signal,
|
|
248
|
+
}).then((result) => ({ index, id: member.id, result }), (error) => ({ index, id: member.id, error })));
|
|
249
|
+
if (input.selection === "all") {
|
|
250
|
+
const settled = await Promise.all(waits);
|
|
251
|
+
for (const item of settled) {
|
|
252
|
+
if ("error" in item)
|
|
253
|
+
throw item.error;
|
|
254
|
+
}
|
|
255
|
+
return {
|
|
256
|
+
selection: "all",
|
|
257
|
+
results: settled.map((item) => ({ id: item.id, result: item.result })),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
const pending = new Set(waits.keys());
|
|
261
|
+
const observed = [];
|
|
262
|
+
while (pending.size > 0) {
|
|
263
|
+
const next = await Promise.race([...pending].map((index) => waits[index]));
|
|
264
|
+
pending.delete(next.index);
|
|
265
|
+
if ("error" in next)
|
|
266
|
+
throw next.error;
|
|
267
|
+
if (next.result.state === "terminal") {
|
|
268
|
+
controller.abort();
|
|
269
|
+
return { selection: "any", selectedId: next.id, results: [{ id: next.id, result: next.result }] };
|
|
270
|
+
}
|
|
271
|
+
observed.push({ id: next.id, result: next.result });
|
|
272
|
+
}
|
|
273
|
+
return { selection: "any", results: observed.sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0) };
|
|
274
|
+
}
|
|
275
|
+
finally {
|
|
276
|
+
input.signal?.removeEventListener("abort", abort);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { FlowError } from "../../flow-error.js";
|
|
2
|
+
import { parseResolvedAkumaLaunchSnapshot } from "../../agents/launch-snapshot/model.js";
|
|
3
|
+
import { isRecoverableSessionEnvelope } from "../../agents/harness/index.js";
|
|
4
|
+
import { ProjectionStateError } from "../atomic-publish.js";
|
|
5
|
+
import { applyContinuationEffort, snapshotPendingTells, } from "./generation/projection-generation-continuation.js";
|
|
6
|
+
import { createProjectionExecutionId } from "./generation/projection-generation-identity.js";
|
|
7
|
+
import { PROJECTION_ADOPTION_POLL_MS, PROJECTION_ADOPTION_TIMEOUT_MS, startProjectionGeneration, } from "./generation/projection-generation-launcher.js";
|
|
8
|
+
import { openProjectionGenerationStore, openProjectionGenerationStoreReadOnly, } from "./generation/store.js";
|
|
9
|
+
import { observeProjectionLife } from "./projection-life-observer.js";
|
|
10
|
+
import { observeProjectionRunnerLock } from "./projection-runner-lock.js";
|
|
11
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
12
|
+
function readCurrentGeneration(projectionDirectory) {
|
|
13
|
+
const store = openProjectionGenerationStoreReadOnly(projectionDirectory);
|
|
14
|
+
try {
|
|
15
|
+
return store.readCurrentGeneration();
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
store.close();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function successorFacts(current, snapshot, nowMs, requireContinuation) {
|
|
22
|
+
const verdictFacts = current.verdict?.facts;
|
|
23
|
+
const originalInputs = current.launch.facts.inputs;
|
|
24
|
+
const inputs = originalInputs
|
|
25
|
+
&& typeof originalInputs === "object"
|
|
26
|
+
&& !Array.isArray(originalInputs)
|
|
27
|
+
? originalInputs
|
|
28
|
+
: {};
|
|
29
|
+
const session = verdictFacts?.session ?? inputs.session;
|
|
30
|
+
const launchSnapshot = parseResolvedAkumaLaunchSnapshot(inputs.launchSnapshot);
|
|
31
|
+
const recoverableSession = isRecoverableSessionEnvelope(launchSnapshot.provider.name, session);
|
|
32
|
+
if (requireContinuation && !recoverableSession) {
|
|
33
|
+
throw new FlowError("PROJECTION_WAKE_FAILED", "The tell was saved, but the successor could not be started because its provider session is not recoverable.");
|
|
34
|
+
}
|
|
35
|
+
const { sourceArtifactId: _sourceArtifactId, ...inheritedInputs } = inputs;
|
|
36
|
+
const resumePath = typeof verdictFacts?.responsePath === "string"
|
|
37
|
+
? verdictFacts.responsePath
|
|
38
|
+
: typeof inputs.resumePath === "string"
|
|
39
|
+
? inputs.resumePath
|
|
40
|
+
: undefined;
|
|
41
|
+
const continuedInputs = {
|
|
42
|
+
...inheritedInputs,
|
|
43
|
+
...(recoverableSession ? {
|
|
44
|
+
session,
|
|
45
|
+
...(resumePath ? { resumePath } : {}),
|
|
46
|
+
} : {}),
|
|
47
|
+
};
|
|
48
|
+
const { tellFence: _oldFence, ...inherited } = current.launch.facts;
|
|
49
|
+
return {
|
|
50
|
+
...inherited,
|
|
51
|
+
createdAt: new Date(nowMs).toISOString(),
|
|
52
|
+
launchedBy: "tell-wake",
|
|
53
|
+
parentExecutionId: current.launch.executionId,
|
|
54
|
+
tellFence: snapshot.tellIds,
|
|
55
|
+
inputs: applyContinuationEffort(continuedInputs, snapshot.effort),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function assertTellEffortSupported(projectionDirectory, effort) {
|
|
59
|
+
if (!effort)
|
|
60
|
+
return;
|
|
61
|
+
const current = readCurrentGeneration(projectionDirectory);
|
|
62
|
+
if (!current)
|
|
63
|
+
throw new ProjectionStateError("projection has no immutable generation launch");
|
|
64
|
+
const inputs = current.launch.facts.inputs;
|
|
65
|
+
if (!inputs || typeof inputs !== "object" || Array.isArray(inputs)) {
|
|
66
|
+
throw new ProjectionStateError("generation launch inputs are missing");
|
|
67
|
+
}
|
|
68
|
+
let launchSnapshot;
|
|
69
|
+
try {
|
|
70
|
+
launchSnapshot = parseResolvedAkumaLaunchSnapshot(inputs.launchSnapshot);
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
throw new ProjectionStateError("generation launch has an invalid immutable launchSnapshot", { cause: error });
|
|
74
|
+
}
|
|
75
|
+
if (launchSnapshot.akuma.model === undefined) {
|
|
76
|
+
throw new FlowError("INVALID_EFFORT_OVERRIDE", `Effort '${effort}' requires an explicit model.`);
|
|
77
|
+
}
|
|
78
|
+
const effortOptions = launchSnapshot.akuma.effortOptions;
|
|
79
|
+
if (effortOptions === undefined)
|
|
80
|
+
return;
|
|
81
|
+
if (!effortOptions.includes(effort)) {
|
|
82
|
+
throw new FlowError("INVALID_EFFORT_OVERRIDE", `Effort '${effort}' is not permitted; legal values: ${effortOptions.join(", ")}.`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function startCommittedSuccessor(projectionDirectory, projectionId, executionId, startGeneration) {
|
|
86
|
+
const started = await startGeneration(projectionDirectory, executionId);
|
|
87
|
+
if (started.status === "adopted")
|
|
88
|
+
return "woken";
|
|
89
|
+
throw new FlowError("PROJECTION_WAKE_FAILED", `The tell was saved for '${projectionId}', but its successor could not be started.`, {
|
|
90
|
+
facts: {
|
|
91
|
+
kind: "projection_wake_failure",
|
|
92
|
+
projectionId,
|
|
93
|
+
cause: started.cause,
|
|
94
|
+
tellRetained: true,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function sessionWakeFailure(projectionId, cause) {
|
|
99
|
+
return new FlowError("PROJECTION_WAKE_FAILED", `The tell was saved for '${projectionId}', but its successor could not be started.`, {
|
|
100
|
+
cause,
|
|
101
|
+
facts: {
|
|
102
|
+
kind: "projection_wake_failure",
|
|
103
|
+
projectionId,
|
|
104
|
+
cause: "session_unrecoverable",
|
|
105
|
+
tellRetained: true,
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
export async function tellProjection(projectionDirectory, projectionId, effort, dependencies = {}) {
|
|
110
|
+
const startGeneration = dependencies.startGeneration ?? startProjectionGeneration;
|
|
111
|
+
const sleep = dependencies.sleep ?? ((milliseconds) => delay(milliseconds));
|
|
112
|
+
const observationStartedAt = Date.now();
|
|
113
|
+
for (;;) {
|
|
114
|
+
const nowMs = Date.now();
|
|
115
|
+
const life = observeProjectionLife(projectionDirectory, {
|
|
116
|
+
nowMs,
|
|
117
|
+
startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
|
|
118
|
+
});
|
|
119
|
+
if (life.phase === "active")
|
|
120
|
+
return "active";
|
|
121
|
+
if (life.phase === "minting" || life.phase === "quiescent") {
|
|
122
|
+
if (life.phase === "quiescent" && nowMs - observationStartedAt >= PROJECTION_ADOPTION_TIMEOUT_MS) {
|
|
123
|
+
throw sessionWakeFailure(projectionId, new Error("projection has no generation continuation"));
|
|
124
|
+
}
|
|
125
|
+
await sleep(PROJECTION_ADOPTION_POLL_MS);
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (life.phase === "unknown") {
|
|
129
|
+
return "pending";
|
|
130
|
+
}
|
|
131
|
+
const current = readCurrentGeneration(projectionDirectory);
|
|
132
|
+
if (!current)
|
|
133
|
+
return "pending";
|
|
134
|
+
if (current.killIntent && !current.verdict)
|
|
135
|
+
return "pending";
|
|
136
|
+
const snapshot = snapshotPendingTells(projectionDirectory);
|
|
137
|
+
const successorExecutionId = createProjectionExecutionId(nowMs);
|
|
138
|
+
let facts;
|
|
139
|
+
try {
|
|
140
|
+
facts = successorFacts(current, { ...snapshot, ...(effort ? { effort } : {}) }, nowMs, life.phase !== "startup-timeout");
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error instanceof FlowError && error.code === "PROJECTION_WAKE_FAILED") {
|
|
144
|
+
throw sessionWakeFailure(projectionId, error);
|
|
145
|
+
}
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
const store = openProjectionGenerationStore(projectionDirectory);
|
|
149
|
+
try {
|
|
150
|
+
if (life.phase === "startup-timeout") {
|
|
151
|
+
const transition = store.supersedeStartupTimeout({
|
|
152
|
+
executionId: current.launch.executionId,
|
|
153
|
+
observedAtMs: nowMs,
|
|
154
|
+
startupTimeoutMs: PROJECTION_ADOPTION_TIMEOUT_MS,
|
|
155
|
+
diagnostic: { detail: "runner did not adopt before startup timeout" },
|
|
156
|
+
successorExecutionId,
|
|
157
|
+
successorFacts: facts,
|
|
158
|
+
});
|
|
159
|
+
if (transition.status === "committed") {
|
|
160
|
+
return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
|
|
161
|
+
}
|
|
162
|
+
if (transition.reason === "kill-requested")
|
|
163
|
+
return "pending";
|
|
164
|
+
await sleep(PROJECTION_ADOPTION_POLL_MS);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
if (life.phase === "lost" && !current.verdict) {
|
|
168
|
+
const lock = observeProjectionRunnerLock(projectionDirectory);
|
|
169
|
+
if (lock.state !== "released")
|
|
170
|
+
return lock.state === "held" ? "active" : "pending";
|
|
171
|
+
const transition = store.replaceDeadRunner({
|
|
172
|
+
executionId: current.launch.executionId,
|
|
173
|
+
releasedLockEvidence: { observedAt: new Date(nowMs).toISOString() },
|
|
174
|
+
successorExecutionId,
|
|
175
|
+
successorFacts: facts,
|
|
176
|
+
});
|
|
177
|
+
if (transition.status === "committed") {
|
|
178
|
+
return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
|
|
179
|
+
}
|
|
180
|
+
if (transition.reason === "kill-requested")
|
|
181
|
+
return "pending";
|
|
182
|
+
await sleep(PROJECTION_ADOPTION_POLL_MS);
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
if (!["done", "failed", "lost", "dismissed", "killed"].includes(life.phase)) {
|
|
186
|
+
return "pending";
|
|
187
|
+
}
|
|
188
|
+
const transition = store.launchIfSettled({
|
|
189
|
+
executionId: successorExecutionId,
|
|
190
|
+
facts,
|
|
191
|
+
});
|
|
192
|
+
if (transition.status === "committed") {
|
|
193
|
+
return await startCommittedSuccessor(projectionDirectory, projectionId, successorExecutionId, startGeneration);
|
|
194
|
+
}
|
|
195
|
+
if (transition.reason === "kill-requested")
|
|
196
|
+
return "pending";
|
|
197
|
+
await sleep(PROJECTION_ADOPTION_POLL_MS);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
store.close();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
package/build/core/ref-log.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, wrapGitError } from "../git/core.js";
|
|
1
|
+
import { createGit, errorContainsAnyPattern, MISSING_HEAD_PATTERNS, parseRevListOutput, wrapGitError } from "../git/core.js";
|
|
2
2
|
const EMPTY_TREE_HASH = "4b825dc642cb6eb9a060e54bf8d69288fbee4904";
|
|
3
3
|
const MISSING_REF_SENTINEL = "0".repeat(40);
|
|
4
|
-
|
|
4
|
+
/** Resolve a ref log head, or null when the ref is missing. */
|
|
5
|
+
export async function resolveRefLogHead(cwd, ref, options = {}) {
|
|
5
6
|
const git = createGit(cwd);
|
|
7
|
+
const peel = options.peel ?? "commit";
|
|
8
|
+
const target = peel === "commit" ? `${ref}^{commit}` : ref;
|
|
6
9
|
try {
|
|
7
|
-
return (await git.raw(["rev-parse", "--verify",
|
|
10
|
+
return (await git.raw(["rev-parse", "--verify", target])).trim();
|
|
8
11
|
}
|
|
9
12
|
catch (error) {
|
|
10
13
|
if (errorContainsAnyPattern(error, MISSING_HEAD_PATTERNS))
|
|
@@ -12,7 +15,8 @@ async function resolveRefHead(cwd, ref) {
|
|
|
12
15
|
throw wrapGitError(`rev-parse ${ref}`, error, cwd);
|
|
13
16
|
}
|
|
14
17
|
}
|
|
15
|
-
|
|
18
|
+
/** Read the raw commit message bytes used by ref-log parsers (`show %B`). */
|
|
19
|
+
export async function readRefLogCommitMessage(cwd, commit) {
|
|
16
20
|
const git = createGit(cwd);
|
|
17
21
|
try {
|
|
18
22
|
return await git.raw(["show", "-s", "--format=%B", commit]);
|
|
@@ -21,71 +25,92 @@ async function readCommitMessage(cwd, commit) {
|
|
|
21
25
|
throw wrapGitError(`show commit message ${commit}`, error, cwd);
|
|
22
26
|
}
|
|
23
27
|
}
|
|
24
|
-
|
|
28
|
+
/**
|
|
29
|
+
* List commits reachable from `head` oldest-first.
|
|
30
|
+
* `errorLabel` is the ref or range name used in wrapGitError text.
|
|
31
|
+
*/
|
|
32
|
+
export async function listRefLogCommits(cwd, head, errorLabel = head) {
|
|
25
33
|
const git = createGit(cwd);
|
|
26
|
-
let output;
|
|
27
34
|
try {
|
|
28
|
-
|
|
35
|
+
return parseRevListOutput(await git.raw(["rev-list", "--reverse", head]));
|
|
29
36
|
}
|
|
30
37
|
catch (error) {
|
|
31
|
-
throw wrapGitError(`rev-list ${
|
|
32
|
-
}
|
|
33
|
-
const commits = output
|
|
34
|
-
.trim()
|
|
35
|
-
.split("\n")
|
|
36
|
-
.map((line) => line.trim())
|
|
37
|
-
.filter(Boolean);
|
|
38
|
-
const entries = [];
|
|
39
|
-
for (const commit of commits) {
|
|
40
|
-
entries.push(parse(await readCommitMessage(cwd, commit)));
|
|
38
|
+
throw wrapGitError(`rev-list ${errorLabel}`, error, cwd);
|
|
41
39
|
}
|
|
42
|
-
return entries;
|
|
43
40
|
}
|
|
44
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Create an empty-tree commit for a ref log entry.
|
|
43
|
+
* Callers own subject/body byte layout so message formats stay domain-local.
|
|
44
|
+
*/
|
|
45
|
+
export async function createRefLogCommit(cwd, input) {
|
|
45
46
|
const git = createGit(cwd);
|
|
46
47
|
const args = ["commit-tree", EMPTY_TREE_HASH];
|
|
47
|
-
if (parentHead)
|
|
48
|
-
args.push("-p", parentHead);
|
|
49
|
-
args.push("-m", subject, "-m",
|
|
48
|
+
if (input.parentHead)
|
|
49
|
+
args.push("-p", input.parentHead);
|
|
50
|
+
args.push("-m", input.subject, "-m", input.body);
|
|
50
51
|
try {
|
|
51
52
|
return (await git.raw(args)).trim();
|
|
52
53
|
}
|
|
53
54
|
catch (error) {
|
|
54
|
-
throw wrapGitError(`commit-tree ${subject}`, error, cwd);
|
|
55
|
+
throw wrapGitError(`commit-tree ${input.errorLabel ?? input.subject}`, error, cwd);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Compare-and-swap a ref to `newHead`.
|
|
60
|
+
* Returns false on CAS failure; callers own re-resolve / degrade policy.
|
|
61
|
+
*/
|
|
62
|
+
export async function casUpdateRefLog(cwd, ref, newHead, expectedOld) {
|
|
63
|
+
const git = createGit(cwd);
|
|
64
|
+
const oldValue = expectedOld ?? MISSING_REF_SENTINEL;
|
|
65
|
+
try {
|
|
66
|
+
await git.raw(["update-ref", ref, newHead, oldValue]);
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function readRefLogAt(cwd, ref, head, parse) {
|
|
74
|
+
const commits = await listRefLogCommits(cwd, head, ref);
|
|
75
|
+
const entries = [];
|
|
76
|
+
for (const commit of commits) {
|
|
77
|
+
entries.push(parse(await readRefLogCommitMessage(cwd, commit)));
|
|
55
78
|
}
|
|
79
|
+
return entries;
|
|
56
80
|
}
|
|
57
81
|
export async function readRefLog(cwd, ref, parse) {
|
|
58
|
-
const head = await
|
|
82
|
+
const head = await resolveRefLogHead(cwd, ref);
|
|
59
83
|
if (!head)
|
|
60
84
|
return [];
|
|
61
85
|
return readRefLogAt(cwd, ref, head, parse);
|
|
62
86
|
}
|
|
63
87
|
export async function appendRefLogWithRetry(cwd, ref, subjectFor, parse, build, options = {}) {
|
|
64
88
|
const maxRetries = options.maxRetries ?? 3;
|
|
65
|
-
let currentHead = await
|
|
89
|
+
let currentHead = await resolveRefLogHead(cwd, ref);
|
|
66
90
|
const startedMissing = currentHead === null;
|
|
67
91
|
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
|
|
68
92
|
const entries = currentHead ? await readRefLogAt(cwd, ref, currentHead, parse) : [];
|
|
69
93
|
const entry = await build(entries);
|
|
70
94
|
if (entry === null)
|
|
71
95
|
return { ok: false, reason: "aborted", currentHead };
|
|
72
|
-
const newHead = await createRefLogCommit(cwd,
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
96
|
+
const newHead = await createRefLogCommit(cwd, {
|
|
97
|
+
subject: subjectFor(entry),
|
|
98
|
+
body: JSON.stringify(entry, null, 2),
|
|
99
|
+
parentHead: currentHead,
|
|
100
|
+
});
|
|
101
|
+
const casOk = await casUpdateRefLog(cwd, ref, newHead, currentHead);
|
|
102
|
+
if (casOk)
|
|
77
103
|
return { ok: true, head: newHead };
|
|
104
|
+
// Intentional degrade: re-resolve after CAS loss; missing-ref mid-flight is sticky
|
|
105
|
+
// when the ref existed at start (do not resurrect).
|
|
106
|
+
const resolved = await resolveRefLogHead(cwd, ref);
|
|
107
|
+
if (resolved === null) {
|
|
108
|
+
if (!startedMissing)
|
|
109
|
+
return { ok: false, reason: "missing-ref" };
|
|
110
|
+
currentHead = null;
|
|
78
111
|
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if (resolved === null) {
|
|
82
|
-
if (!startedMissing)
|
|
83
|
-
return { ok: false, reason: "missing-ref" };
|
|
84
|
-
currentHead = null;
|
|
85
|
-
}
|
|
86
|
-
else {
|
|
87
|
-
currentHead = resolved;
|
|
88
|
-
}
|
|
112
|
+
else {
|
|
113
|
+
currentHead = resolved;
|
|
89
114
|
}
|
|
90
115
|
}
|
|
91
116
|
if (currentHead === null)
|