@codetreker/daynight 0.21.9 → 0.21.11
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/README.md +204 -27
- package/dist/api/client.d.ts +3 -1
- package/dist/api/client.js +12 -1
- package/dist/api/client.js.map +1 -1
- package/dist/api/parse.d.ts +2 -1
- package/dist/api/parse.js +8 -0
- package/dist/api/parse.js.map +1 -1
- package/dist/api/types.d.ts +7 -0
- package/dist/cli/commands.js +2 -0
- package/dist/cli/commands.js.map +1 -1
- package/dist/commands/worker/cleanup.d.ts +6 -0
- package/dist/commands/worker/cleanup.js +173 -0
- package/dist/commands/worker/cleanup.js.map +1 -0
- package/dist/commands/worker/config.d.ts +21 -0
- package/dist/commands/worker/config.js +144 -0
- package/dist/commands/worker/config.js.map +1 -0
- package/dist/commands/worker/coordinator.d.ts +34 -0
- package/dist/commands/worker/coordinator.js +508 -0
- package/dist/commands/worker/coordinator.js.map +1 -0
- package/dist/commands/worker/definition.d.ts +2 -0
- package/dist/commands/worker/definition.js +14 -0
- package/dist/commands/worker/definition.js.map +1 -0
- package/dist/commands/worker/diagnostics.d.ts +20 -0
- package/dist/commands/worker/diagnostics.js +56 -0
- package/dist/commands/worker/diagnostics.js.map +1 -0
- package/dist/commands/worker/execution.d.ts +41 -0
- package/dist/commands/worker/execution.js +255 -0
- package/dist/commands/worker/execution.js.map +1 -0
- package/dist/commands/worker/herdr/agents.d.ts +185 -0
- package/dist/commands/worker/herdr/agents.js +654 -0
- package/dist/commands/worker/herdr/agents.js.map +1 -0
- package/dist/commands/worker/herdr/arguments.d.ts +8 -0
- package/dist/commands/worker/herdr/arguments.js +58 -0
- package/dist/commands/worker/herdr/arguments.js.map +1 -0
- package/dist/commands/worker/herdr/command.d.ts +22 -0
- package/dist/commands/worker/herdr/command.js +115 -0
- package/dist/commands/worker/herdr/command.js.map +1 -0
- package/dist/commands/worker/herdr/instance.d.ts +56 -0
- package/dist/commands/worker/herdr/instance.js +322 -0
- package/dist/commands/worker/herdr/instance.js.map +1 -0
- package/dist/commands/worker/herdr/preflight.d.ts +40 -0
- package/dist/commands/worker/herdr/preflight.js +251 -0
- package/dist/commands/worker/herdr/preflight.js.map +1 -0
- package/dist/commands/worker/herdr/rpc.d.ts +29 -0
- package/dist/commands/worker/herdr/rpc.js +186 -0
- package/dist/commands/worker/herdr/rpc.js.map +1 -0
- package/dist/commands/worker/herdr.d.ts +95 -0
- package/dist/commands/worker/herdr.js +35 -0
- package/dist/commands/worker/herdr.js.map +1 -0
- package/dist/commands/worker/index.d.ts +10 -0
- package/dist/commands/worker/index.js +14 -0
- package/dist/commands/worker/index.js.map +1 -0
- package/dist/commands/worker/prompts.d.ts +4 -0
- package/dist/commands/worker/prompts.js +64 -0
- package/dist/commands/worker/prompts.js.map +1 -0
- package/dist/commands/worker/remote.d.ts +18 -0
- package/dist/commands/worker/remote.js +134 -0
- package/dist/commands/worker/remote.js.map +1 -0
- package/dist/commands/worker/runtime.d.ts +8 -0
- package/dist/commands/worker/runtime.js +246 -0
- package/dist/commands/worker/runtime.js.map +1 -0
- package/dist/commands/worker/store.d.ts +55 -5
- package/dist/commands/worker/store.js +381 -26
- package/dist/commands/worker/store.js.map +1 -1
- package/dist/commands/worker/types.d.ts +37 -1
- package/dist/commands/worker/worktrees.d.ts +1 -0
- package/dist/commands/worker/worktrees.js +9 -0
- package/dist/commands/worker/worktrees.js.map +1 -1
- package/dist/config/document.d.ts +1 -1
- package/dist/config/document.js +1 -0
- package/dist/config/document.js.map +1 -1
- package/package.json +2 -2
|
@@ -2,6 +2,7 @@ import path from "node:path";
|
|
|
2
2
|
import Database from "better-sqlite3";
|
|
3
3
|
import { canonicalId } from "../../api/parse.js";
|
|
4
4
|
import { normalizeServerUrl } from "../../config/server.js";
|
|
5
|
+
import { parseHerdrExecutionLabel } from "./herdr/agents.js";
|
|
5
6
|
const integer = (value, name, minimum = 0) => {
|
|
6
7
|
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum)
|
|
7
8
|
throw new Error(`${name} must be a safe integer of at least ${minimum}.`);
|
|
@@ -61,6 +62,10 @@ const parseConfiguration = (value) => {
|
|
|
61
62
|
return { agent: oneOf(data.agent, ["copilot", "codex"], "Agent"), model: nullable(data.model, (entry) => text(entry, "Model")),
|
|
62
63
|
args: data.args.map((entry) => text(entry, "Agent argument", true)), recoveryLimit: integer(data.recoveryLimit, "Recovery limit") };
|
|
63
64
|
};
|
|
65
|
+
const parseClaimAttempt = (value) => {
|
|
66
|
+
const data = record(value, ["taskId", "claimId", "worktreeId", "workerId", "userId", "expectedVersion", "configuration"], "Claim attempt");
|
|
67
|
+
return { taskId: id(data.taskId, "Attempt task ID"), claimId: id(data.claimId, "Attempt claim ID"), worktreeId: id(data.worktreeId, "Attempt worktree ID"), workerId: id(data.workerId, "Attempt worker ID"), userId: id(data.userId, "Attempt user ID"), expectedVersion: version(data.expectedVersion), configuration: parseConfiguration(data.configuration) };
|
|
68
|
+
};
|
|
64
69
|
const parseWorktree = (value) => {
|
|
65
70
|
const data = record(value, ["repositoryPath", "commonDirectory", "worktreePath", "branch"], "Worktree binding");
|
|
66
71
|
return { repositoryPath: absolutePath(data.repositoryPath, "Repository path"), commonDirectory: absolutePath(data.commonDirectory, "Git common directory"), worktreePath: absolutePath(data.worktreePath, "Worktree path"), branch: text(data.branch, "Worktree branch") };
|
|
@@ -77,20 +82,91 @@ const parseResources = (value) => {
|
|
|
77
82
|
const data = record(value, ["worktree", "herdr", "nativeSession"], "Execution resources");
|
|
78
83
|
return { worktree: nullable(data.worktree, parseWorktree), herdr: nullable(data.herdr, parseHerdr), nativeSession: nullable(data.nativeSession, parseNative) };
|
|
79
84
|
};
|
|
85
|
+
const parseTaskBinding = (value) => {
|
|
86
|
+
const data = record(value, ["session", "workspaceId", "paneId", "terminalId"], "Local Herdr binding");
|
|
87
|
+
return { session: text(data.session, "Herdr session"), workspaceId: text(data.workspaceId, "Herdr workspace ID"), paneId: text(data.paneId, "Herdr pane ID"), terminalId: text(data.terminalId, "Herdr terminal ID") };
|
|
88
|
+
};
|
|
89
|
+
const parseWorkspaceEvidence = (value) => {
|
|
90
|
+
const data = record(value, ["binding", "ownerLabel", "environment"], "Herdr workspace evidence");
|
|
91
|
+
const environment = record(data.environment, ["XDG_CONFIG_HOME", "XDG_STATE_HOME"], "Herdr workspace environment");
|
|
92
|
+
const config = text(environment.XDG_CONFIG_HOME, "Workspace XDG_CONFIG_HOME", true);
|
|
93
|
+
const state = text(environment.XDG_STATE_HOME, "Workspace XDG_STATE_HOME", true);
|
|
94
|
+
if (config.includes("\0") || state.includes("\0"))
|
|
95
|
+
throw new Error("The workspace environment cannot contain NUL.");
|
|
96
|
+
return { binding: parseTaskBinding(data.binding), ownerLabel: text(data.ownerLabel, "Workspace owner label"), environment: { XDG_CONFIG_HOME: config, XDG_STATE_HOME: state } };
|
|
97
|
+
};
|
|
98
|
+
const parseLaunchEvidence = (value) => {
|
|
99
|
+
const data = record(value, ["binding", "ownerLabel", "agent", "argv", "referenceBaseline", "workspaceEvidence"], "Herdr launch evidence");
|
|
100
|
+
if (!Array.isArray(data.argv) || data.argv.length === 0)
|
|
101
|
+
throw new Error("Herdr launch arguments must be a nonempty array.");
|
|
102
|
+
return { binding: parseTaskBinding(data.binding), ownerLabel: text(data.ownerLabel, "Launch owner label"), agent: oneOf(data.agent, ["codex", "copilot"], "Launch agent"), argv: data.argv.map((value) => text(value, "Launch argument", true)), referenceBaseline: oneOf(data.referenceBaseline, ["empty"], "Launch reference baseline"), workspaceEvidence: parseWorkspaceEvidence(data.workspaceEvidence) };
|
|
103
|
+
};
|
|
104
|
+
const processId = (value) => {
|
|
105
|
+
const result = integer(value, "Herdr process ID", 1);
|
|
106
|
+
if (result > 2_147_483_647)
|
|
107
|
+
throw new Error("Herdr process ID exceeds the supported process identifier range.");
|
|
108
|
+
return result;
|
|
109
|
+
};
|
|
110
|
+
const parseStopObservation = (value) => {
|
|
111
|
+
const data = record(value, ["binding", "ownerLabel", "agent", "nativeSessionId", "shellPid", "processIds", "observedAt"], "Herdr stop observation");
|
|
112
|
+
if (!Array.isArray(data.processIds) || data.processIds.length === 0)
|
|
113
|
+
throw new Error("The original native process observation must be nonempty.");
|
|
114
|
+
const shellPid = processId(data.shellPid);
|
|
115
|
+
const processIds = data.processIds.map(processId);
|
|
116
|
+
if (new Set(processIds).size !== processIds.length || processIds.includes(shellPid))
|
|
117
|
+
throw new Error("The original native process observation contains duplicate or shell process IDs.");
|
|
118
|
+
return { binding: parseTaskBinding(data.binding), ownerLabel: text(data.ownerLabel, "Stop owner label"), agent: oneOf(data.agent, ["codex", "copilot"], "Stop agent"), nativeSessionId: nullable(data.nativeSessionId, (value) => id(value, "Stopped native session ID")), shellPid, processIds, observedAt: integer(data.observedAt, "Stop observation time") };
|
|
119
|
+
};
|
|
120
|
+
const parseExecutionSummary = (value) => {
|
|
121
|
+
const data = record(value, ["version", "herdr", "nativeSessionId", "phase", "submission"], "Execution summary mutation");
|
|
122
|
+
return { version: version(data.version), herdr: nullable(data.herdr, parseHerdr), nativeSessionId: nullable(data.nativeSessionId, (value) => id(value, "Summary native session ID")), phase: oneOf(data.phase, ["preparing", "executing", "verifying", "completing", "waiting", "paused"], "Summary phase"), submission: oneOf(data.submission, ["unknown", "confirmed", "rejected"], "Summary submission") };
|
|
123
|
+
};
|
|
124
|
+
const parseNativeExecution = (value) => {
|
|
125
|
+
const data = record(value, ["terminalId", "workspaceSubmission", "workspaceEvidence", "launchEvidence", "launchSubmission", "promptSubmission", "stopObservation", "summary"], "Native execution state");
|
|
126
|
+
const result = { terminalId: nullable(data.terminalId, (value) => text(value, "Herdr terminal ID")), workspaceSubmission: oneOf(data.workspaceSubmission, ["not-submitted", "unknown", "acknowledged"], "Herdr workspace submission"), workspaceEvidence: nullable(data.workspaceEvidence, parseWorkspaceEvidence), launchEvidence: nullable(data.launchEvidence, parseLaunchEvidence), launchSubmission: oneOf(data.launchSubmission, ["never-submitted", "unknown", "acknowledged"], "Native launch submission"), promptSubmission: oneOf(data.promptSubmission, ["not-submitted", "unknown", "submitted"], "Native prompt submission"), stopObservation: nullable(data.stopObservation, parseStopObservation), summary: nullable(data.summary, parseExecutionSummary) };
|
|
127
|
+
if ((result.launchSubmission === "acknowledged") !== (result.launchEvidence !== null))
|
|
128
|
+
throw new Error("Acknowledged native launch requires its original launch evidence.");
|
|
129
|
+
if (result.launchSubmission === "never-submitted" && (result.promptSubmission !== "not-submitted" || result.stopObservation !== null))
|
|
130
|
+
throw new Error("An unlaunched native execution cannot have submitted input or original native process evidence.");
|
|
131
|
+
return result;
|
|
132
|
+
};
|
|
133
|
+
const freshNativeExecution = () => ({ terminalId: null, workspaceSubmission: "not-submitted", workspaceEvidence: null, launchEvidence: null, launchSubmission: "never-submitted", promptSubmission: "not-submitted", stopObservation: null, summary: null });
|
|
134
|
+
const unknownNativeExecution = () => ({ ...freshNativeExecution(), workspaceSubmission: "unknown", launchSubmission: "unknown", promptSubmission: "unknown" });
|
|
135
|
+
const withUserScope = (task, userId) => {
|
|
136
|
+
const owner = id(userId, "Claim user ID");
|
|
137
|
+
if (task.userId !== null && task.userId !== owner)
|
|
138
|
+
throw new Error("The original authenticated claim owner cannot be replaced.");
|
|
139
|
+
if (task.userId === null && (task.cleanup?.kind === "terminal" || task.control !== null))
|
|
140
|
+
throw new Error("An existing terminal or control intent with unknown authenticated scope cannot acquire an inferred owner.");
|
|
141
|
+
return { ...task, userId: owner };
|
|
142
|
+
};
|
|
143
|
+
export const isUnstartedPending = (task) => {
|
|
144
|
+
const native = task.nativeExecution;
|
|
145
|
+
return task.status === "pending" && task.cleanup === null && task.control === null && task.resources.worktree === null && task.resources.herdr === null && task.resources.nativeSession === null
|
|
146
|
+
&& native !== null && native.terminalId === null && native.workspaceSubmission === "not-submitted" && native.workspaceEvidence === null
|
|
147
|
+
&& native.launchSubmission === "never-submitted" && native.launchEvidence === null && native.promptSubmission === "not-submitted" && native.stopObservation === null && native.summary === null;
|
|
148
|
+
};
|
|
80
149
|
const parseRecovery = (value) => {
|
|
81
150
|
const data = record(value, ["count", "pauseReason", "retryAuthorized"], "Recovery state");
|
|
82
151
|
return { count: integer(data.count, "Recovery count"), pauseReason: nullable(data.pauseReason, (entry) => text(entry, "Pause reason")), retryAuthorized: bool(data.retryAuthorized, "Recovery retry authorization") };
|
|
83
152
|
};
|
|
153
|
+
const parseCleanupBaseline = (value) => {
|
|
154
|
+
const data = record(value, ["stateChangeSeq", "completionSeq"], "Cleanup completion baseline");
|
|
155
|
+
return { stateChangeSeq: integer(data.stateChangeSeq, "Cleanup state-change sequence"), completionSeq: integer(data.completionSeq, "Cleanup completion sequence") };
|
|
156
|
+
};
|
|
84
157
|
const parseCleanup = (value) => {
|
|
85
|
-
const data = record(value, ["kind", "startedAt", "deadline", "input", "result", "error", "agentStopped", "workspaceClosed", "worktreeDeleted"], "Cleanup state");
|
|
158
|
+
const data = record(value, ["kind", "startedAt", "deadline", "input", "baseline", "result", "error", "agentStopped", "workspaceClosed", "worktreeDeleted"], "Cleanup state");
|
|
86
159
|
const result = { kind: oneOf(data.kind, ["terminal", "shutdown"], "Cleanup kind"), startedAt: integer(data.startedAt, "Cleanup start"), deadline: integer(data.deadline, "Cleanup deadline"),
|
|
87
160
|
input: oneOf(data.input, ["not-submitted", "unknown", "submitted", "rejected", "unavailable"], "Cleanup input state"),
|
|
161
|
+
baseline: nullable(data.baseline, parseCleanupBaseline),
|
|
88
162
|
result: nullable(data.result, (entry) => oneOf(entry, ["completed", "timed-out", "failed", "unavailable"], "Cleanup result")), error: nullable(data.error, (entry) => text(entry, "Cleanup error")),
|
|
89
163
|
agentStopped: bool(data.agentStopped, "Agent stopped"), workspaceClosed: bool(data.workspaceClosed, "Workspace closed"), worktreeDeleted: bool(data.worktreeDeleted, "Worktree deleted") };
|
|
90
164
|
if (result.deadline < result.startedAt || result.deadline - result.startedAt > 600_000)
|
|
91
165
|
throw new Error("Cleanup deadline must be within ten minutes of its original start.");
|
|
92
166
|
if ((result.workspaceClosed && !result.agentStopped) || (result.worktreeDeleted && !result.workspaceClosed))
|
|
93
167
|
throw new Error("Cleanup progress is out of order.");
|
|
168
|
+
if (result.baseline !== null && result.input !== "submitted")
|
|
169
|
+
throw new Error("A cleanup completion baseline requires confirmed input submission.");
|
|
94
170
|
return result;
|
|
95
171
|
};
|
|
96
172
|
const parseIntent = (value) => {
|
|
@@ -120,32 +196,73 @@ const parseControl = (value) => {
|
|
|
120
196
|
return result;
|
|
121
197
|
};
|
|
122
198
|
const same = (left, right) => JSON.stringify(left) === JSON.stringify(right);
|
|
123
|
-
const
|
|
199
|
+
const validateNativeExecution = (task) => {
|
|
200
|
+
const state = task.nativeExecution;
|
|
201
|
+
if (state === null)
|
|
202
|
+
return task;
|
|
203
|
+
const binding = task.resources.herdr === null || state.terminalId === null ? null : { ...task.resources.herdr, terminalId: state.terminalId };
|
|
204
|
+
if (state.terminalId !== null && task.resources.herdr === null)
|
|
205
|
+
throw new Error("The local terminal requires its core Herdr binding.");
|
|
206
|
+
if (state.workspaceSubmission === "acknowledged" && binding === null)
|
|
207
|
+
throw new Error("An acknowledged workspace requires its full original Herdr binding.");
|
|
208
|
+
const owner = { ...claimKey(task), worktreeId: task.worktreeId };
|
|
209
|
+
const evidence = (value) => {
|
|
210
|
+
if (binding === null || !same(value.binding, binding))
|
|
211
|
+
throw new Error("Native execution evidence differs from its original Herdr binding.");
|
|
212
|
+
if (!same(parseHerdrExecutionLabel(value.ownerLabel), owner))
|
|
213
|
+
throw new Error("Native execution evidence belongs to another claim or worktree.");
|
|
214
|
+
};
|
|
215
|
+
if (state.workspaceEvidence !== null)
|
|
216
|
+
evidence(state.workspaceEvidence);
|
|
217
|
+
if (state.launchEvidence !== null) {
|
|
218
|
+
evidence(state.launchEvidence);
|
|
219
|
+
evidence(state.launchEvidence.workspaceEvidence);
|
|
220
|
+
if (!same(state.workspaceEvidence, state.launchEvidence.workspaceEvidence))
|
|
221
|
+
throw new Error("The native launch does not preserve its original workspace evidence.");
|
|
222
|
+
if (task.configuration !== null && state.launchEvidence.agent !== task.configuration.agent)
|
|
223
|
+
throw new Error("The native launch agent differs from its frozen configuration.");
|
|
224
|
+
if (state.launchEvidence.argv[0] !== state.launchEvidence.agent)
|
|
225
|
+
throw new Error("The native launch arguments name another executable.");
|
|
226
|
+
}
|
|
227
|
+
if (state.stopObservation !== null) {
|
|
228
|
+
evidence(state.stopObservation);
|
|
229
|
+
if (task.configuration !== null && state.stopObservation.agent !== task.configuration.agent)
|
|
230
|
+
throw new Error("The stopped agent differs from its frozen configuration.");
|
|
231
|
+
if (state.stopObservation.nativeSessionId !== null && task.resources.nativeSession !== null && state.stopObservation.nativeSessionId !== task.resources.nativeSession.sessionId)
|
|
232
|
+
throw new Error("The stopped native session differs from the original session.");
|
|
233
|
+
}
|
|
234
|
+
return task;
|
|
235
|
+
};
|
|
236
|
+
const projectColumns = "server_origin AS serverOrigin, project_id AS projectId, repo_url AS repoUrl, local_path AS localPath, claim_attempt AS claimAttempt";
|
|
237
|
+
const asProject = (row) => ({ ...row, claimAttempt: row.claimAttempt === null ? null : parseClaimAttempt(JSON.parse(row.claimAttempt)) });
|
|
124
238
|
const taskColumns = `server_origin AS serverOrigin, project_id AS projectId, task_id AS taskId, claim_id AS claimId,
|
|
125
|
-
worker_id AS workerId, worktree_id AS worktreeId, status, repo_url AS repoUrl, repository_path AS repositoryPath,
|
|
126
|
-
configuration, resources, recovery, cleanup, control, created_at AS createdAt, updated_at AS updatedAt`;
|
|
127
|
-
const asTask = (row) => ({ ...row,
|
|
239
|
+
worker_id AS workerId, worktree_id AS worktreeId, user_id AS userId, status, repo_url AS repoUrl, repository_path AS repositoryPath,
|
|
240
|
+
configuration, resources, native_execution AS nativeExecution, recovery, cleanup, control, created_at AS createdAt, updated_at AS updatedAt`;
|
|
241
|
+
const asTask = (row) => validateNativeExecution({ ...row,
|
|
242
|
+
userId: row.userId === null ? null : id(row.userId, "Claim user ID"),
|
|
128
243
|
configuration: row.configuration === null ? null : parseConfiguration(JSON.parse(row.configuration)),
|
|
244
|
+
nativeExecution: row.nativeExecution === null ? null : parseNativeExecution(JSON.parse(row.nativeExecution)),
|
|
129
245
|
resources: parseResources(JSON.parse(row.resources)), recovery: parseRecovery(JSON.parse(row.recovery)),
|
|
130
246
|
cleanup: row.cleanup === null ? null : parseCleanup(JSON.parse(row.cleanup)), control: row.control === null ? null : parseControl(JSON.parse(row.control)) });
|
|
131
247
|
const schema = `
|
|
132
248
|
CREATE TABLE projects (
|
|
133
249
|
server_origin TEXT NOT NULL, project_id TEXT NOT NULL, repo_url TEXT NOT NULL, local_path TEXT NOT NULL,
|
|
250
|
+
claim_attempt TEXT CHECK(claim_attempt IS NULL OR json_valid(claim_attempt)),
|
|
134
251
|
PRIMARY KEY(server_origin, project_id)
|
|
135
252
|
) STRICT;
|
|
136
253
|
CREATE TABLE tasks (
|
|
137
254
|
server_origin TEXT NOT NULL, project_id TEXT NOT NULL, task_id TEXT NOT NULL, claim_id TEXT NOT NULL,
|
|
138
|
-
worker_id TEXT NOT NULL, worktree_id TEXT NOT NULL,
|
|
255
|
+
worker_id TEXT NOT NULL, worktree_id TEXT NOT NULL, user_id TEXT,
|
|
139
256
|
status TEXT NOT NULL CHECK(status IN ('pending', 'active', 'done')),
|
|
140
257
|
repo_url TEXT NOT NULL, repository_path TEXT NOT NULL,
|
|
141
258
|
configuration TEXT CHECK(configuration IS NULL OR json_valid(configuration)),
|
|
142
|
-
resources TEXT NOT NULL CHECK(json_valid(resources)), recovery TEXT NOT NULL CHECK(json_valid(recovery)),
|
|
259
|
+
resources TEXT NOT NULL CHECK(json_valid(resources)), native_execution TEXT CHECK(native_execution IS NULL OR json_valid(native_execution)), recovery TEXT NOT NULL CHECK(json_valid(recovery)),
|
|
143
260
|
cleanup TEXT CHECK(cleanup IS NULL OR json_valid(cleanup)), control TEXT CHECK(control IS NULL OR json_valid(control)),
|
|
144
261
|
created_at INTEGER NOT NULL CHECK(created_at >= 0), updated_at INTEGER NOT NULL CHECK(updated_at >= 0),
|
|
145
262
|
PRIMARY KEY(server_origin, project_id, task_id, claim_id), UNIQUE(server_origin, claim_id)
|
|
146
263
|
) STRICT;
|
|
147
264
|
CREATE UNIQUE INDEX tasks_one_active_per_task ON tasks(server_origin, project_id, task_id) WHERE status = 'active';
|
|
148
|
-
PRAGMA user_version =
|
|
265
|
+
PRAGMA user_version = 2;
|
|
149
266
|
`;
|
|
150
267
|
export const openWorkerStore = (databaseFile, options = {}) => {
|
|
151
268
|
absolutePath(databaseFile, "The SQLite database path");
|
|
@@ -162,8 +279,8 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
162
279
|
throw new Error("SQLite refused synchronous=FULL.");
|
|
163
280
|
db.transaction(() => {
|
|
164
281
|
const currentVersion = db.pragma("user_version", { simple: true });
|
|
165
|
-
if (currentVersion !== 0 && currentVersion !== 1)
|
|
166
|
-
throw new Error(`Unsupported worker SQLite schema version ${currentVersion}; supported version is
|
|
282
|
+
if (currentVersion !== 0 && currentVersion !== 1 && currentVersion !== 2)
|
|
283
|
+
throw new Error(`Unsupported worker SQLite schema version ${currentVersion}; supported version is 2.`);
|
|
167
284
|
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name").all();
|
|
168
285
|
if (currentVersion === 0) {
|
|
169
286
|
if (tables.length !== 0)
|
|
@@ -172,6 +289,8 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
172
289
|
}
|
|
173
290
|
else if (!same(tables.map(({ name }) => name), ["projects", "tasks"]))
|
|
174
291
|
throw new Error("The worker database does not contain its projects/tasks schema.");
|
|
292
|
+
if (currentVersion === 1)
|
|
293
|
+
db.exec("ALTER TABLE tasks ADD COLUMN native_execution TEXT CHECK(native_execution IS NULL OR json_valid(native_execution)); ALTER TABLE tasks ADD COLUMN user_id TEXT; ALTER TABLE projects ADD COLUMN claim_attempt TEXT CHECK(claim_attempt IS NULL OR json_valid(claim_attempt)); UPDATE tasks SET cleanup = json_set(cleanup, '$.baseline', json('null')) WHERE cleanup IS NOT NULL; PRAGMA user_version = 2;");
|
|
175
294
|
db.prepare(`SELECT ${projectColumns} FROM projects LIMIT 0`).all();
|
|
176
295
|
db.prepare(`SELECT ${taskColumns} FROM tasks LIMIT 0`).all();
|
|
177
296
|
}).immediate();
|
|
@@ -186,7 +305,10 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
186
305
|
throw error;
|
|
187
306
|
}
|
|
188
307
|
const atomic = (operation) => db.transaction(operation).immediate();
|
|
189
|
-
const getProject = (identity) =>
|
|
308
|
+
const getProject = (identity) => {
|
|
309
|
+
const row = db.prepare(`SELECT ${projectColumns} FROM projects WHERE server_origin = @serverOrigin AND project_id = @projectId`).get(projectKey(identity));
|
|
310
|
+
return row === undefined ? null : asProject(row);
|
|
311
|
+
};
|
|
190
312
|
const getTask = (identity) => {
|
|
191
313
|
const row = db.prepare(`SELECT ${taskColumns} FROM tasks WHERE server_origin = @serverOrigin AND project_id = @projectId AND task_id = @taskId AND claim_id = @claimId`).get(claimKey(identity));
|
|
192
314
|
return row === undefined ? null : asTask(row);
|
|
@@ -198,9 +320,10 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
198
320
|
return task;
|
|
199
321
|
};
|
|
200
322
|
const writeTask = (task) => {
|
|
201
|
-
|
|
323
|
+
validateNativeExecution(task);
|
|
324
|
+
const values = { ...task, configuration: task.configuration === null ? null : JSON.stringify(parseConfiguration(task.configuration)), resources: JSON.stringify(parseResources(task.resources)), nativeExecution: task.nativeExecution === null ? null : JSON.stringify(parseNativeExecution(task.nativeExecution)), recovery: JSON.stringify(parseRecovery(task.recovery)),
|
|
202
325
|
cleanup: task.cleanup === null ? null : JSON.stringify(parseCleanup(task.cleanup)), control: task.control === null ? null : JSON.stringify(parseControl(task.control)) };
|
|
203
|
-
db.prepare(`UPDATE tasks SET status = @status, configuration = @configuration, resources = @resources, recovery = @recovery,
|
|
326
|
+
db.prepare(`UPDATE tasks SET user_id = @userId, status = @status, configuration = @configuration, resources = @resources, native_execution = @nativeExecution, recovery = @recovery,
|
|
204
327
|
cleanup = @cleanup, control = @control, updated_at = @updatedAt
|
|
205
328
|
WHERE server_origin = @serverOrigin AND project_id = @projectId AND task_id = @taskId AND claim_id = @claimId`).run(values);
|
|
206
329
|
return needTask(task);
|
|
@@ -231,44 +354,109 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
231
354
|
return {
|
|
232
355
|
close: () => { db.close(); },
|
|
233
356
|
getProject,
|
|
234
|
-
listProjects: () => db.prepare(`SELECT ${projectColumns} FROM projects ORDER BY server_origin, project_id`).all(),
|
|
357
|
+
listProjects: () => db.prepare(`SELECT ${projectColumns} FROM projects ORDER BY server_origin, project_id`).all().map(asProject),
|
|
235
358
|
registerProject: (project) => atomic(() => {
|
|
236
359
|
const value = { ...projectKey(project), repoUrl: text(project.repoUrl, "Repository URL"), localPath: absolutePath(project.localPath, "Project local path") };
|
|
237
360
|
const existing = getProject(value);
|
|
238
361
|
if (existing !== null && (existing.repoUrl !== value.repoUrl || existing.localPath !== value.localPath)) {
|
|
362
|
+
if (existing.claimAttempt !== null)
|
|
363
|
+
throw new Error("An unresolved claim attempt prevents changing its project repository binding.");
|
|
239
364
|
const unfinished = db.prepare("SELECT 1 FROM tasks WHERE server_origin = @serverOrigin AND project_id = @projectId AND status <> 'done' LIMIT 1").get(value);
|
|
240
365
|
if (unfinished !== undefined)
|
|
241
366
|
throw new Error("An unfinished execution prevents changing its project repository binding.");
|
|
242
367
|
}
|
|
243
368
|
db.prepare(`INSERT INTO projects(server_origin, project_id, repo_url, local_path) VALUES (@serverOrigin, @projectId, @repoUrl, @localPath)
|
|
244
369
|
ON CONFLICT(server_origin, project_id) DO UPDATE SET repo_url = excluded.repo_url, local_path = excluded.local_path`).run(value);
|
|
245
|
-
return value;
|
|
370
|
+
return getProject(value);
|
|
371
|
+
}),
|
|
372
|
+
getClaimAttempt: (identity) => {
|
|
373
|
+
const project = getProject(identity);
|
|
374
|
+
if (project === null)
|
|
375
|
+
throw new Error("The project repository binding is not registered.");
|
|
376
|
+
return project.claimAttempt;
|
|
377
|
+
},
|
|
378
|
+
listClaimAttempts: () => db.prepare(`SELECT ${projectColumns} FROM projects WHERE claim_attempt IS NOT NULL ORDER BY server_origin, project_id`).all().map(asProject).map((project) => ({ ...projectKey(project), attempt: project.claimAttempt })),
|
|
379
|
+
beginClaimAttempt: (identity, input) => atomic(() => {
|
|
380
|
+
const key = projectKey(identity);
|
|
381
|
+
const project = getProject(key);
|
|
382
|
+
if (project === null)
|
|
383
|
+
throw new Error("The project repository binding must be persisted before claiming a task.");
|
|
384
|
+
const attempt = parseClaimAttempt(input);
|
|
385
|
+
if (project.claimAttempt !== null)
|
|
386
|
+
throw new Error("The project already has an unresolved claim attempt; it cannot be replayed or replaced.");
|
|
387
|
+
db.prepare("UPDATE projects SET claim_attempt = @claimAttempt WHERE server_origin = @serverOrigin AND project_id = @projectId").run({ ...key, claimAttempt: JSON.stringify(attempt) });
|
|
388
|
+
return attempt;
|
|
389
|
+
}),
|
|
390
|
+
rejectClaimAttempt: (identity, expectedClaimId, proof) => atomic(() => {
|
|
391
|
+
const key = projectKey(identity);
|
|
392
|
+
const project = getProject(key);
|
|
393
|
+
if (project === null || project.claimAttempt === null || project.claimAttempt.claimId !== id(expectedClaimId, "Rejected claim ID"))
|
|
394
|
+
throw new Error("The original unresolved claim attempt does not match.");
|
|
395
|
+
if (!("definiteRejection" in proof && proof.definiteRejection === true) && !("notSubmitted" in proof && proof.notSubmitted === true))
|
|
396
|
+
throw new Error("Clearing a claim attempt requires definite rejection or proven non-submission of the original mutation.");
|
|
397
|
+
db.prepare("UPDATE projects SET claim_attempt = NULL WHERE server_origin = @serverOrigin AND project_id = @projectId").run(key);
|
|
246
398
|
}),
|
|
247
399
|
getTask,
|
|
248
400
|
listTasks: () => db.prepare(`SELECT ${taskColumns} FROM tasks ORDER BY created_at, server_origin, project_id, task_id, claim_id`).all().map(asTask),
|
|
249
401
|
listUnfinishedTasks: () => db.prepare(`SELECT ${taskColumns} FROM tasks WHERE status <> 'done' ORDER BY created_at, server_origin, project_id, task_id, claim_id`).all().map(asTask),
|
|
250
|
-
|
|
402
|
+
recordObservedUnattemptedWorkspace: (owner, now) => atomic(() => {
|
|
403
|
+
const key = { ...claimKey(owner), workerId: id(owner.workerId, "Observed worker ID"), worktreeId: id(owner.worktreeId, "Observed worktree ID") };
|
|
404
|
+
integer(now, "Workspace observation time");
|
|
405
|
+
const task = getTask(key);
|
|
406
|
+
if (task === null)
|
|
407
|
+
return null;
|
|
408
|
+
if (task.workerId !== key.workerId || task.worktreeId !== key.worktreeId)
|
|
409
|
+
throw new Error("The observed workspace does not match the original execution's worker and worktree identity.");
|
|
410
|
+
const native = task.nativeExecution;
|
|
411
|
+
if (task.status === "done" || task.resources.herdr !== null || native === null || native.workspaceSubmission !== "not-submitted" || native.launchSubmission !== "never-submitted")
|
|
412
|
+
return task;
|
|
413
|
+
return writeTask({ ...task, nativeExecution: { ...unknownNativeExecution(), summary: native.summary }, updatedAt: now });
|
|
414
|
+
}),
|
|
415
|
+
registerPending: (claim, input, now, verifiedUserId = null) => atomic(() => {
|
|
251
416
|
const key = { ...claimKey(claim), workerId: id(claim.workerId, "Worker ID"), worktreeId: id(claim.worktreeId, "Worktree ID") };
|
|
417
|
+
const owner = verifiedUserId === null ? null : id(verifiedUserId, "Verified claim user ID");
|
|
252
418
|
integer(now, "Task creation time");
|
|
253
419
|
oneOf(input.kind, ["new", "recovered"], "Pending registration kind");
|
|
254
420
|
if (input.kind === "new" && input.configuration === null)
|
|
255
421
|
throw new Error("A new execution requires its frozen configuration.");
|
|
256
422
|
const configuration = input.configuration === null ? null : parseConfiguration(input.configuration);
|
|
423
|
+
const project = getProject(key);
|
|
424
|
+
if (project === null)
|
|
425
|
+
throw new Error("The project repository binding must be persisted before claiming a task.");
|
|
426
|
+
const confirmAttempt = (originalConfiguration) => {
|
|
427
|
+
const attempt = project.claimAttempt;
|
|
428
|
+
if (attempt === null || attempt.claimId !== key.claimId)
|
|
429
|
+
return originalConfiguration;
|
|
430
|
+
if (attempt.taskId !== key.taskId || attempt.worktreeId !== key.worktreeId || attempt.workerId !== key.workerId || (originalConfiguration !== null && !same(attempt.configuration, originalConfiguration)))
|
|
431
|
+
throw new Error("The actual granted claim does not preserve its original claim attempt and frozen configuration.");
|
|
432
|
+
if (owner === null)
|
|
433
|
+
return originalConfiguration;
|
|
434
|
+
if (attempt.userId !== owner)
|
|
435
|
+
throw new Error("The actual granted claim does not preserve its original authenticated claim attempt owner.");
|
|
436
|
+
db.prepare("UPDATE projects SET claim_attempt = NULL WHERE server_origin = @serverOrigin AND project_id = @projectId").run(projectKey(key));
|
|
437
|
+
return originalConfiguration === null ? attempt.configuration : originalConfiguration;
|
|
438
|
+
};
|
|
257
439
|
const existing = getTask(key);
|
|
258
440
|
if (existing !== null) {
|
|
259
441
|
if (existing.workerId !== key.workerId || existing.worktreeId !== key.worktreeId)
|
|
260
442
|
throw new Error("The claim is already bound to a different worker or worktree identity.");
|
|
443
|
+
const scoped = owner === null ? existing : withUserScope(existing, owner);
|
|
444
|
+
const originalConfiguration = confirmAttempt(existing.configuration);
|
|
445
|
+
if (scoped.userId !== existing.userId || originalConfiguration !== existing.configuration) {
|
|
446
|
+
if (existing.status === "done")
|
|
447
|
+
throw new Error("The local execution is already done; its unknown authenticated scope cannot be replaced.");
|
|
448
|
+
return writeTask({ ...scoped, configuration: originalConfiguration, updatedAt: now });
|
|
449
|
+
}
|
|
261
450
|
return existing;
|
|
262
451
|
}
|
|
263
|
-
const
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
db.prepare(`INSERT INTO tasks(server_origin, project_id, task_id, claim_id, worker_id, worktree_id, status, repo_url, repository_path, configuration, resources, recovery, created_at, updated_at)
|
|
269
|
-
VALUES (@serverOrigin, @projectId, @taskId, @claimId, @workerId, @worktreeId, 'pending', @repoUrl, @repositoryPath, @configuration, @resources, @recovery, @now, @now)`).run(values);
|
|
452
|
+
const originalConfiguration = confirmAttempt(configuration);
|
|
453
|
+
const values = { ...key, userId: owner, configuration: originalConfiguration === null ? null : JSON.stringify(originalConfiguration), repoUrl: project.repoUrl, repositoryPath: project.localPath,
|
|
454
|
+
resources: JSON.stringify({ worktree: null, herdr: null, nativeSession: null }), nativeExecution: input.kind === "new" ? JSON.stringify(freshNativeExecution()) : null, recovery: JSON.stringify({ count: 0, pauseReason: originalConfiguration === null ? "Original frozen execution configuration is missing." : null, retryAuthorized: false }), now };
|
|
455
|
+
db.prepare(`INSERT INTO tasks(server_origin, project_id, task_id, claim_id, worker_id, worktree_id, user_id, status, repo_url, repository_path, configuration, resources, native_execution, recovery, created_at, updated_at)
|
|
456
|
+
VALUES (@serverOrigin, @projectId, @taskId, @claimId, @workerId, @worktreeId, @userId, 'pending', @repoUrl, @repositoryPath, @configuration, @resources, @nativeExecution, @recovery, @now, @now)`).run(values);
|
|
270
457
|
return needTask(key);
|
|
271
458
|
}),
|
|
459
|
+
bindUserScope: (identity, userId, now) => change(identity, now, (task) => withUserScope(task, userId)),
|
|
272
460
|
restoreOriginalConfiguration: (identity, configuration, now) => change(identity, now, (task) => {
|
|
273
461
|
const original = parseConfiguration(configuration);
|
|
274
462
|
if (task.configuration !== null && !same(task.configuration, original))
|
|
@@ -281,6 +469,13 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
281
469
|
throw new Error("The execution is paused.");
|
|
282
470
|
return { ...task, status: "active" };
|
|
283
471
|
}),
|
|
472
|
+
activatePausedRecovery: (identity, reason, now) => change(identity, now, (task) => {
|
|
473
|
+
if (task.control !== null || task.cleanup !== null)
|
|
474
|
+
throw new Error("A control or cleanup intent prevents recovery promotion.");
|
|
475
|
+
if (task.nativeExecution?.launchSubmission === "never-submitted" && task.resources.worktree === null && task.resources.herdr === null && task.resources.nativeSession === null)
|
|
476
|
+
throw new Error("Paused recovery requires unknown original provenance or existing execution resources.");
|
|
477
|
+
return { ...task, status: "active", recovery: { ...task.recovery, pauseReason: text(reason, "Recovery pause reason") } };
|
|
478
|
+
}),
|
|
284
479
|
bindWorktree: (identity, binding, now) => change(identity, now, (task) => {
|
|
285
480
|
if (task.status !== "active")
|
|
286
481
|
throw new Error("Resource creation requires an active execution.");
|
|
@@ -299,6 +494,125 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
299
494
|
throw new Error("The execution already has a different Herdr binding.");
|
|
300
495
|
return { ...task, resources: { ...task.resources, herdr: value } };
|
|
301
496
|
}),
|
|
497
|
+
bindHerdrExecution: (identity, binding, workspaceEvidence, now) => change(identity, now, (task) => {
|
|
498
|
+
if (task.status !== "active" || task.resources.worktree === null)
|
|
499
|
+
throw new Error("Herdr binding requires an active execution and its original worktree.");
|
|
500
|
+
const value = parseTaskBinding(binding);
|
|
501
|
+
const core = { session: value.session, workspaceId: value.workspaceId, paneId: value.paneId };
|
|
502
|
+
if (task.resources.herdr !== null && !same(task.resources.herdr, core))
|
|
503
|
+
throw new Error("The execution already has a different Herdr binding.");
|
|
504
|
+
const state = task.nativeExecution === null ? unknownNativeExecution() : task.nativeExecution;
|
|
505
|
+
if (state.terminalId !== null && state.terminalId !== value.terminalId)
|
|
506
|
+
throw new Error("The original terminal cannot change without confirmed stop and workspace closure.");
|
|
507
|
+
if (state.workspaceSubmission === "unknown" && state.terminalId !== null && task.resources.herdr !== null)
|
|
508
|
+
throw new Error("An unresolved replacement workspace cannot be acknowledged by its previous binding.");
|
|
509
|
+
const evidence = workspaceEvidence === null ? state.workspaceEvidence : parseWorkspaceEvidence(workspaceEvidence);
|
|
510
|
+
if (state.workspaceEvidence !== null && !same(state.workspaceEvidence, evidence))
|
|
511
|
+
throw new Error("The original workspace evidence cannot be replaced.");
|
|
512
|
+
return { ...task, resources: { ...task.resources, herdr: core }, nativeExecution: { ...state, terminalId: value.terminalId, workspaceSubmission: "acknowledged", workspaceEvidence: evidence } };
|
|
513
|
+
}),
|
|
514
|
+
markWorkspaceSubmitting: (identity, now) => change(identity, now, (task) => {
|
|
515
|
+
requireOrdinary(task);
|
|
516
|
+
const state = task.nativeExecution;
|
|
517
|
+
if (task.status !== "active" || task.resources.worktree === null || state === null)
|
|
518
|
+
throw new Error("Workspace creation requires its active original worktree and persisted provenance.");
|
|
519
|
+
if (state.workspaceSubmission === "unknown")
|
|
520
|
+
throw new Error("The original workspace creation is unresolved and cannot be replayed.");
|
|
521
|
+
if (state.launchSubmission !== "never-submitted" && task.resources.nativeSession === null)
|
|
522
|
+
throw new Error("The original native session ID is missing; workspace replacement cannot start a new conversation.");
|
|
523
|
+
if (task.resources.herdr !== null && state.launchSubmission !== "never-submitted" && state.stopObservation === null)
|
|
524
|
+
throw new Error("Workspace replacement requires its durable original native stop observation.");
|
|
525
|
+
return { ...task, nativeExecution: { ...state, workspaceSubmission: "unknown" } };
|
|
526
|
+
}),
|
|
527
|
+
recordWorkspaceNotSubmitted: (identity, proof, now) => change(identity, now, (task) => {
|
|
528
|
+
const state = task.nativeExecution;
|
|
529
|
+
if (state === null || state.workspaceSubmission !== "unknown")
|
|
530
|
+
throw new Error("Workspace non-submission requires its original unresolved create attempt.");
|
|
531
|
+
if (!("notSubmitted" in proof && proof.notSubmitted === true) && !("definiteRejection" in proof && proof.definiteRejection === true))
|
|
532
|
+
throw new Error("Resetting workspace creation requires proven non-submission or definite rejection before dispatch.");
|
|
533
|
+
return { ...task, nativeExecution: { ...state, workspaceSubmission: "not-submitted" } };
|
|
534
|
+
}),
|
|
535
|
+
markNativeLaunchSubmitting: (identity, now) => change(identity, now, (task) => {
|
|
536
|
+
requireOrdinary(task);
|
|
537
|
+
const state = task.nativeExecution;
|
|
538
|
+
if (task.status !== "active" || task.recovery.pauseReason !== null || state === null || state.terminalId === null || state.workspaceSubmission !== "acknowledged" || state.workspaceEvidence === null || state.launchSubmission !== "never-submitted")
|
|
539
|
+
throw new Error("Native launch requires a proven unlaunched active execution and its verified workspace.");
|
|
540
|
+
return { ...task, nativeExecution: { ...state, launchSubmission: "unknown", promptSubmission: "unknown" } };
|
|
541
|
+
}),
|
|
542
|
+
recordNativeLaunchNotSubmitted: (identity, proof, now) => change(identity, now, (task) => {
|
|
543
|
+
const state = task.nativeExecution;
|
|
544
|
+
if (task.status !== "active" || state === null || state.launchSubmission !== "unknown" || state.launchEvidence !== null || state.stopObservation !== null || state.promptSubmission === "submitted")
|
|
545
|
+
throw new Error("A native launch can be unsubmitted only before acknowledged launch, prompt submission or native process evidence.");
|
|
546
|
+
if (!("notSubmitted" in proof && proof.notSubmitted === true) && !("definiteRejection" in proof && proof.definiteRejection === true))
|
|
547
|
+
throw new Error("Resetting a native launch requires proven non-submission or definite rejection before native dispatch.");
|
|
548
|
+
return { ...task, nativeExecution: { ...state, launchSubmission: "never-submitted", promptSubmission: "not-submitted" } };
|
|
549
|
+
}),
|
|
550
|
+
markNativePromptSubmitting: (identity, now) => change(identity, now, (task) => {
|
|
551
|
+
requireOrdinary(task);
|
|
552
|
+
const state = task.nativeExecution;
|
|
553
|
+
if (task.status !== "active" || task.recovery.pauseReason !== null || state === null || state.workspaceSubmission !== "acknowledged" || state.launchSubmission !== "acknowledged" || state.launchEvidence === null || state.promptSubmission !== "not-submitted")
|
|
554
|
+
throw new Error("Deferred startup input requires its original acknowledged launch and a proven unsubmitted prompt.");
|
|
555
|
+
return { ...task, nativeExecution: { ...state, promptSubmission: "unknown" } };
|
|
556
|
+
}),
|
|
557
|
+
recordNativeStartOutcome: (identity, input, now) => change(identity, now, (task) => {
|
|
558
|
+
const state = task.nativeExecution;
|
|
559
|
+
if (task.status !== "active" || state === null || state.terminalId === null || state.launchSubmission === "never-submitted")
|
|
560
|
+
throw new Error("Native startup outcome requires the original persisted submission attempt.");
|
|
561
|
+
const promptSubmission = oneOf(input.promptSubmission, ["not-submitted", "unknown", "submitted"], "Native prompt outcome");
|
|
562
|
+
if (state.promptSubmission === "submitted" && promptSubmission !== "submitted")
|
|
563
|
+
throw new Error("A confirmed startup prompt cannot become unsubmitted or unknown.");
|
|
564
|
+
const launchEvidence = input.launchEvidence === undefined ? state.launchEvidence : parseLaunchEvidence(input.launchEvidence);
|
|
565
|
+
if (state.launchEvidence !== null && !same(state.launchEvidence, launchEvidence))
|
|
566
|
+
throw new Error("The original native launch evidence cannot be replaced.");
|
|
567
|
+
return { ...task, nativeExecution: { ...state, launchEvidence, launchSubmission: launchEvidence === null ? state.launchSubmission : "acknowledged", promptSubmission } };
|
|
568
|
+
}),
|
|
569
|
+
recordStopObservation: (identity, observation, now) => change(identity, now, (task) => {
|
|
570
|
+
const state = task.nativeExecution;
|
|
571
|
+
if (task.status !== "active" || state === null || state.terminalId === null || state.launchSubmission === "never-submitted")
|
|
572
|
+
throw new Error("A native stop observation requires the original possibly launched terminal.");
|
|
573
|
+
const evidence = parseStopObservation(observation);
|
|
574
|
+
if (state.stopObservation !== null && !same(state.stopObservation, evidence))
|
|
575
|
+
throw new Error("The original native process observation cannot be replaced before confirmed closure.");
|
|
576
|
+
return { ...task, nativeExecution: { ...state, stopObservation: evidence } };
|
|
577
|
+
}),
|
|
578
|
+
markExecutionSummarySubmitting: (identity, input, now) => change(identity, now, (task) => {
|
|
579
|
+
requireOrdinary(task);
|
|
580
|
+
if (task.status !== "active")
|
|
581
|
+
throw new Error("Execution summary publication requires an active execution.");
|
|
582
|
+
const state = task.nativeExecution === null ? unknownNativeExecution() : task.nativeExecution;
|
|
583
|
+
if (state.summary?.submission === "unknown")
|
|
584
|
+
throw new Error("The original execution summary mutation is unresolved and cannot be replayed or replaced.");
|
|
585
|
+
const summary = parseExecutionSummary({ ...input, submission: "unknown" });
|
|
586
|
+
if (!same(summary.herdr, task.resources.herdr) || summary.nativeSessionId !== (task.resources.nativeSession === null ? null : task.resources.nativeSession.sessionId))
|
|
587
|
+
throw new Error("Execution summary must describe its original actual resource bindings.");
|
|
588
|
+
return { ...task, nativeExecution: { ...state, summary } };
|
|
589
|
+
}),
|
|
590
|
+
recordExecutionSummaryOutcome: (identity, expectedVersion, submission, now) => change(identity, now, (task) => {
|
|
591
|
+
const state = task.nativeExecution;
|
|
592
|
+
const summary = state === null ? null : state.summary;
|
|
593
|
+
const outcome = oneOf(submission, ["confirmed", "rejected"], "Summary mutation outcome");
|
|
594
|
+
if (summary === null || summary.version !== version(expectedVersion) || (summary.submission !== "unknown" && summary.submission !== outcome))
|
|
595
|
+
throw new Error("The original execution summary submission does not match this outcome.");
|
|
596
|
+
return { ...task, nativeExecution: { ...state, summary: { ...summary, submission: outcome } } };
|
|
597
|
+
}),
|
|
598
|
+
recordExecutionSummaryNotSubmitted: (identity, expectedVersion, proof, now) => change(identity, now, (task) => {
|
|
599
|
+
const state = task.nativeExecution;
|
|
600
|
+
const summary = state === null ? null : state.summary;
|
|
601
|
+
if (summary === null || summary.submission !== "unknown" || summary.version !== version(expectedVersion))
|
|
602
|
+
throw new Error("The original unresolved execution summary does not match this non-submission.");
|
|
603
|
+
if (proof.notSubmitted !== true)
|
|
604
|
+
throw new Error("Clearing an execution summary requires proven non-submission before dispatch.");
|
|
605
|
+
return { ...task, nativeExecution: { ...state, summary: null } };
|
|
606
|
+
}),
|
|
607
|
+
reconcileExecutionSummary: (identity, input, now) => change(identity, now, (task) => {
|
|
608
|
+
const state = task.nativeExecution;
|
|
609
|
+
const summary = state === null ? null : state.summary;
|
|
610
|
+
const observed = parseExecutionSummary({ ...input, submission: "confirmed" });
|
|
611
|
+
if (summary === null || summary.submission !== "unknown" || BigInt(observed.version) <= BigInt(summary.version))
|
|
612
|
+
return task;
|
|
613
|
+
const matched = same(observed.herdr, summary.herdr) && observed.nativeSessionId === summary.nativeSessionId && observed.phase === summary.phase;
|
|
614
|
+
return { ...task, nativeExecution: { ...state, summary: matched ? { ...summary, submission: "confirmed" } : null } };
|
|
615
|
+
}),
|
|
302
616
|
rebindHerdr: (identity, expectedPrevious, next, proof, now) => change(identity, now, (task) => {
|
|
303
617
|
requireOrdinary(task);
|
|
304
618
|
if (task.status !== "active" || task.resources.worktree === null || task.resources.herdr === null)
|
|
@@ -307,7 +621,26 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
307
621
|
throw new Error("The previous Herdr binding does not match the original execution.");
|
|
308
622
|
if (proof.originalAgentStopped !== true || proof.oldWorkspaceClosed !== true)
|
|
309
623
|
throw new Error("Herdr rebinding requires confirmed original agent stop and old workspace closure.");
|
|
310
|
-
|
|
624
|
+
if (task.resources.nativeSession === null && task.nativeExecution?.launchSubmission !== "never-submitted")
|
|
625
|
+
throw new Error("The original native session ID is missing; rebinding cannot start a new conversation.");
|
|
626
|
+
return { ...task, resources: { ...task.resources, herdr: parseHerdr(next) }, nativeExecution: { ...unknownNativeExecution(), summary: task.nativeExecution === null ? null : task.nativeExecution.summary } };
|
|
627
|
+
}),
|
|
628
|
+
rebindHerdrExecution: (identity, expectedPrevious, next, workspaceEvidence, proof, now) => change(identity, now, (task) => {
|
|
629
|
+
requireOrdinary(task);
|
|
630
|
+
const previous = parseTaskBinding(expectedPrevious);
|
|
631
|
+
const state = task.nativeExecution;
|
|
632
|
+
if (task.status !== "active" || task.resources.worktree === null || task.resources.herdr === null || state === null || !same({ ...task.resources.herdr, terminalId: state.terminalId }, previous))
|
|
633
|
+
throw new Error("The previous full Herdr binding does not match the active execution.");
|
|
634
|
+
if (proof.originalAgentStopped !== true || proof.oldWorkspaceClosed !== true || (state.stopObservation === null && state.launchSubmission !== "never-submitted"))
|
|
635
|
+
throw new Error("Herdr rebinding requires durable original stop provenance and confirmed old workspace closure.");
|
|
636
|
+
if (state.launchSubmission !== "never-submitted" && task.resources.nativeSession === null)
|
|
637
|
+
throw new Error("The original native session ID is missing; rebinding cannot start a new conversation.");
|
|
638
|
+
if (state.workspaceSubmission !== "unknown")
|
|
639
|
+
throw new Error("Herdr rebinding requires the persisted new workspace creation attempt.");
|
|
640
|
+
const binding = parseTaskBinding(next);
|
|
641
|
+
if (same(binding, previous))
|
|
642
|
+
throw new Error("A fresh Herdr execution must use a new workspace or terminal binding.");
|
|
643
|
+
return { ...task, resources: { ...task.resources, herdr: { session: binding.session, workspaceId: binding.workspaceId, paneId: binding.paneId } }, nativeExecution: { ...freshNativeExecution(), terminalId: binding.terminalId, workspaceSubmission: "acknowledged", workspaceEvidence: workspaceEvidence === null ? null : parseWorkspaceEvidence(workspaceEvidence), summary: state.summary } };
|
|
311
644
|
}),
|
|
312
645
|
bindNativeSession: (identity, reference, now) => change(identity, now, (task) => {
|
|
313
646
|
if (task.status !== "active" || task.resources.herdr === null)
|
|
@@ -348,7 +681,7 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
348
681
|
return { ...task, cleanup: { ...existing, kind: existing.kind === "terminal" ? "terminal" : input.kind } };
|
|
349
682
|
if (task.control !== null && input.kind !== "terminal")
|
|
350
683
|
throw new Error("A source control intent requires terminal cleanup.");
|
|
351
|
-
return { ...task, status: "active", cleanup: parseCleanup({ kind: input.kind, startedAt: now, deadline: input.deadline, input: "not-submitted", result: null, error: null, agentStopped: false, workspaceClosed: false, worktreeDeleted: false }) };
|
|
684
|
+
return { ...task, status: "active", cleanup: parseCleanup({ kind: input.kind, startedAt: now, deadline: input.deadline, input: "not-submitted", baseline: null, result: null, error: null, agentStopped: false, workspaceClosed: false, worktreeDeleted: false }) };
|
|
352
685
|
}),
|
|
353
686
|
markCleanupInputSubmitting: (identity, now) => change(identity, now, (task) => {
|
|
354
687
|
const cleanup = needCleanup(task);
|
|
@@ -365,6 +698,15 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
365
698
|
throw new Error("The cleanup input outcome conflicts with the original submission.");
|
|
366
699
|
return { ...task, cleanup: { ...cleanup, input } };
|
|
367
700
|
}),
|
|
701
|
+
recordCleanupInputEvidence: (identity, baseline, now) => change(identity, now, (task) => {
|
|
702
|
+
const cleanup = needCleanup(task);
|
|
703
|
+
if (cleanup.input !== "submitted")
|
|
704
|
+
throw new Error("Cleanup completion evidence requires the original confirmed input submission.");
|
|
705
|
+
const evidence = parseCleanupBaseline(baseline);
|
|
706
|
+
if (cleanup.baseline !== null && !same(cleanup.baseline, evidence))
|
|
707
|
+
throw new Error("The original cleanup completion baseline cannot be replaced.");
|
|
708
|
+
return { ...task, cleanup: { ...cleanup, baseline: evidence } };
|
|
709
|
+
}),
|
|
368
710
|
recordCleanupResult: (identity, result, error, now) => change(identity, now, (task) => {
|
|
369
711
|
const cleanup = needCleanup(task);
|
|
370
712
|
oneOf(result, ["completed", "timed-out", "failed", "unavailable"], "Cleanup result");
|
|
@@ -398,7 +740,9 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
398
740
|
throw new Error("Shutdown resume requires the original agent stopped, workspace closed and its cleanup result recorded.");
|
|
399
741
|
if (task.configuration === null)
|
|
400
742
|
throw new Error("The original frozen execution configuration is missing.");
|
|
401
|
-
|
|
743
|
+
if (task.resources.nativeSession === null && task.nativeExecution?.launchSubmission !== "never-submitted")
|
|
744
|
+
return { ...task, cleanup: null, recovery: { ...task.recovery, pauseReason: "The original native session ID is missing; recover its exact history identity before resuming this claim." } };
|
|
745
|
+
return { ...task, cleanup: null, resources: { ...task.resources, herdr: null }, nativeExecution: { ...freshNativeExecution(), summary: task.nativeExecution === null ? null : task.nativeExecution.summary } };
|
|
402
746
|
}),
|
|
403
747
|
recordControlIntent: (identity, input, now) => change(identity, now, (task) => {
|
|
404
748
|
const intent = parseIntent(input);
|
|
@@ -455,6 +799,17 @@ export const openWorkerStore = (databaseFile, options = {}) => {
|
|
|
455
799
|
throw new Error("The source control outcome must be reconciled before local completion.");
|
|
456
800
|
return { ...task, status: "done" };
|
|
457
801
|
}),
|
|
802
|
+
completeUnstartedPending: (identity, proof, now) => change(identity, now, (task) => {
|
|
803
|
+
if (proof.completeOwnClaimInventory !== true || proof.claimAbsent !== true)
|
|
804
|
+
throw new Error("Completing an unstarted pending claim requires its absence from a successful complete own-claim inventory.");
|
|
805
|
+
if (!isUnstartedPending(task))
|
|
806
|
+
throw new Error("Only a proven unstarted pending execution with no resources or outstanding actions can complete directly.");
|
|
807
|
+
if (task.userId === null || task.userId !== id(proof.authenticatedUserId, "Authenticated user ID"))
|
|
808
|
+
throw new Error("The complete own-claim inventory must belong to the original authenticated claim owner.");
|
|
809
|
+
const completed = { ...task, status: "done", cleanup: { kind: "terminal", startedAt: now, deadline: now, input: "unavailable", baseline: null, result: "completed", error: null, agentStopped: true, workspaceClosed: true, worktreeDeleted: true } };
|
|
810
|
+
fullyCleaned(completed);
|
|
811
|
+
return completed;
|
|
812
|
+
}),
|
|
458
813
|
};
|
|
459
814
|
};
|
|
460
815
|
//# sourceMappingURL=store.js.map
|