@anhlp/alp-runtime 0.0.1-beta.3 → 0.0.1-beta.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/dist/assignment-results.d.ts +18 -0
- package/dist/assignment-results.js +77 -0
- package/dist/attachments.d.ts +84 -0
- package/dist/attachments.js +213 -0
- package/dist/claude-transport.d.ts +28 -0
- package/dist/claude-transport.js +87 -4
- package/dist/configure.d.ts +44 -0
- package/dist/configure.js +64 -0
- package/dist/contracts.d.ts +160 -3
- package/dist/contracts.js +11 -0
- package/dist/conversation.d.ts +45 -0
- package/dist/conversation.js +62 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +10 -1
- package/dist/mailbox.d.ts +2 -0
- package/dist/mailbox.js +3 -1
- package/dist/runtime.d.ts +7 -0
- package/dist/runtime.js +960 -115
- package/dist/session-config.d.ts +105 -0
- package/dist/session-config.js +2 -2
- package/dist/session-state.d.ts +22 -6
- package/dist/tools.d.ts +37 -0
- package/dist/tools.js +19 -0
- package/dist/worktree-domain.d.ts +2 -1
- package/dist/worktree-domain.js +4 -1
- package/package.json +11 -3
- package/src/assignment-results.ts +64 -0
- package/src/attachments.ts +254 -0
- package/src/claude-transport.ts +100 -5
- package/src/configure.ts +93 -0
- package/src/contracts.ts +150 -4
- package/src/conversation.ts +81 -0
- package/src/index.ts +6 -2
- package/src/mailbox.ts +4 -1
- package/src/runtime.ts +1049 -123
- package/src/session-config.ts +2 -2
- package/src/session-state.ts +15 -2
- package/src/tools.ts +16 -0
- package/src/worktree-domain.ts +5 -1
package/dist/session-config.d.ts
CHANGED
|
@@ -615,6 +615,41 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
|
|
|
615
615
|
required: string[];
|
|
616
616
|
additionalProperties: boolean;
|
|
617
617
|
};
|
|
618
|
+
} | {
|
|
619
|
+
type: string;
|
|
620
|
+
name: string;
|
|
621
|
+
description: string;
|
|
622
|
+
inputSchema: {
|
|
623
|
+
type: string;
|
|
624
|
+
properties: {
|
|
625
|
+
action: {
|
|
626
|
+
type: string;
|
|
627
|
+
enum: string[];
|
|
628
|
+
};
|
|
629
|
+
dueAt: {
|
|
630
|
+
type: string;
|
|
631
|
+
description: string;
|
|
632
|
+
};
|
|
633
|
+
prompt: {
|
|
634
|
+
type: string;
|
|
635
|
+
description: string;
|
|
636
|
+
};
|
|
637
|
+
requestId: {
|
|
638
|
+
type: string;
|
|
639
|
+
description: string;
|
|
640
|
+
};
|
|
641
|
+
id: {
|
|
642
|
+
type: string;
|
|
643
|
+
description: string;
|
|
644
|
+
};
|
|
645
|
+
status: {
|
|
646
|
+
type: string;
|
|
647
|
+
enum: string[];
|
|
648
|
+
};
|
|
649
|
+
};
|
|
650
|
+
required: string[];
|
|
651
|
+
additionalProperties: boolean;
|
|
652
|
+
};
|
|
618
653
|
} | {
|
|
619
654
|
type: string;
|
|
620
655
|
name: string;
|
|
@@ -1265,6 +1300,41 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
|
|
|
1265
1300
|
required: string[];
|
|
1266
1301
|
additionalProperties: boolean;
|
|
1267
1302
|
};
|
|
1303
|
+
} | {
|
|
1304
|
+
type: string;
|
|
1305
|
+
name: string;
|
|
1306
|
+
description: string;
|
|
1307
|
+
inputSchema: {
|
|
1308
|
+
type: string;
|
|
1309
|
+
properties: {
|
|
1310
|
+
action: {
|
|
1311
|
+
type: string;
|
|
1312
|
+
enum: string[];
|
|
1313
|
+
};
|
|
1314
|
+
dueAt: {
|
|
1315
|
+
type: string;
|
|
1316
|
+
description: string;
|
|
1317
|
+
};
|
|
1318
|
+
prompt: {
|
|
1319
|
+
type: string;
|
|
1320
|
+
description: string;
|
|
1321
|
+
};
|
|
1322
|
+
requestId: {
|
|
1323
|
+
type: string;
|
|
1324
|
+
description: string;
|
|
1325
|
+
};
|
|
1326
|
+
id: {
|
|
1327
|
+
type: string;
|
|
1328
|
+
description: string;
|
|
1329
|
+
};
|
|
1330
|
+
status: {
|
|
1331
|
+
type: string;
|
|
1332
|
+
enum: string[];
|
|
1333
|
+
};
|
|
1334
|
+
};
|
|
1335
|
+
required: string[];
|
|
1336
|
+
additionalProperties: boolean;
|
|
1337
|
+
};
|
|
1268
1338
|
} | {
|
|
1269
1339
|
type: string;
|
|
1270
1340
|
name: string;
|
|
@@ -1917,6 +1987,41 @@ export declare function nativeSessionConfig(runtimeKind: RuntimeKind, mapping: R
|
|
|
1917
1987
|
required: string[];
|
|
1918
1988
|
additionalProperties: boolean;
|
|
1919
1989
|
};
|
|
1990
|
+
} | {
|
|
1991
|
+
type: string;
|
|
1992
|
+
name: string;
|
|
1993
|
+
description: string;
|
|
1994
|
+
inputSchema: {
|
|
1995
|
+
type: string;
|
|
1996
|
+
properties: {
|
|
1997
|
+
action: {
|
|
1998
|
+
type: string;
|
|
1999
|
+
enum: string[];
|
|
2000
|
+
};
|
|
2001
|
+
dueAt: {
|
|
2002
|
+
type: string;
|
|
2003
|
+
description: string;
|
|
2004
|
+
};
|
|
2005
|
+
prompt: {
|
|
2006
|
+
type: string;
|
|
2007
|
+
description: string;
|
|
2008
|
+
};
|
|
2009
|
+
requestId: {
|
|
2010
|
+
type: string;
|
|
2011
|
+
description: string;
|
|
2012
|
+
};
|
|
2013
|
+
id: {
|
|
2014
|
+
type: string;
|
|
2015
|
+
description: string;
|
|
2016
|
+
};
|
|
2017
|
+
status: {
|
|
2018
|
+
type: string;
|
|
2019
|
+
enum: string[];
|
|
2020
|
+
};
|
|
2021
|
+
};
|
|
2022
|
+
required: string[];
|
|
2023
|
+
additionalProperties: boolean;
|
|
2024
|
+
};
|
|
1920
2025
|
} | {
|
|
1921
2026
|
type: string;
|
|
1922
2027
|
name: string;
|
package/dist/session-config.js
CHANGED
|
@@ -5,7 +5,7 @@ import { anyModel, profileModel } from "@anhlp/alp-core/profiles";
|
|
|
5
5
|
import { models, thinkingOptionsFor } from "./catalog.js";
|
|
6
6
|
import { claudeSandboxAvailable, SKILL_PLUGIN } from "./claude-transport.js";
|
|
7
7
|
import { languageInstruction } from "./language.js";
|
|
8
|
-
import { HANDOFF_TOOL, WAIT_TOOL, SEND_TOOL, MERGE_TOOL, VERIFY_TOOL, DISCARD_TOOL, CANCEL_TOOL, RECALL_TOOL, PIN_TOOL, BOARD_TOOL, UNPIN_TOOL, ASK_TOOL, LESSON_TOOL, SKILL_TOOL, ISSUE_TOOL, mainOf, taskActions, taskTool } from "./tools.js";
|
|
8
|
+
import { HANDOFF_TOOL, WAIT_TOOL, SEND_TOOL, MERGE_TOOL, VERIFY_TOOL, DISCARD_TOOL, CANCEL_TOOL, RECALL_TOOL, PIN_TOOL, BOARD_TOOL, UNPIN_TOOL, ASK_TOOL, LESSON_TOOL, SKILL_TOOL, ISSUE_TOOL, SCHEDULE_TOOL, mainOf, taskActions, taskTool } from "./tools.js";
|
|
9
9
|
import { COMPACT_SHARE } from "./runtime-context.js";
|
|
10
10
|
var nativeMode = (mapping) => mapping.runtimeKind === "antigravity" && mapping.mode === "read-only" && mapping.antigravity?.readOnlyMode === "instructions" ? "full-access" : mapping.copy ? "workspace-write" : mapping.mode;
|
|
11
11
|
function claudeFloor(mapping) {
|
|
@@ -182,7 +182,7 @@ function nativeSessionConfig(runtimeKind, mapping, targets, parentAgent, role, s
|
|
|
182
182
|
...targets.length || parentAgent ? [SEND_TOOL] : [],
|
|
183
183
|
...parentAgent ? [HANDOFF_TOOL] : [],
|
|
184
184
|
...supervised ? [LESSON_TOOL, SKILL_TOOL] : [],
|
|
185
|
-
...!parentAgent && mapping.agent.name === mainOf(mapping) ? [ISSUE_TOOL] : [],
|
|
185
|
+
...!parentAgent && mapping.agent.name === mainOf(mapping) ? [ISSUE_TOOL, SCHEDULE_TOOL] : [],
|
|
186
186
|
ASK_TOOL,
|
|
187
187
|
PIN_TOOL,
|
|
188
188
|
BOARD_TOOL,
|
package/dist/session-state.d.ts
CHANGED
|
@@ -1,15 +1,28 @@
|
|
|
1
|
-
import type { RuntimeTransport } from './contracts.js';
|
|
1
|
+
import type { NativeBase, RuntimeTransport } from './contracts.js';
|
|
2
2
|
import type { RuntimeKind, SessionSpec, ResolvedSession } from './resolve.js';
|
|
3
3
|
import type { MailEvent } from './mailbox.js';
|
|
4
4
|
import type { Worktree, WorktreeChange, Copy } from './workspace.js';
|
|
5
5
|
import type { Verification } from '@anhlp/alp-core/verify';
|
|
6
6
|
import type { Handoff } from './handoff.js';
|
|
7
7
|
import type { SessionUsage } from './events.js';
|
|
8
|
+
/** The pause that stopped a turn; a matching resume may arrive before its interruption settles. */
|
|
9
|
+
export type ParkPause = {
|
|
10
|
+
scope: RuntimeKind | 'all';
|
|
11
|
+
resumed?: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type Parked = {
|
|
14
|
+
reason: string;
|
|
15
|
+
since: number;
|
|
16
|
+
prompt?: string;
|
|
17
|
+
pause?: ParkPause;
|
|
18
|
+
};
|
|
8
19
|
export type Session = {
|
|
9
20
|
runtimeKind: RuntimeKind;
|
|
10
21
|
runtime: RuntimeTransport;
|
|
11
22
|
mapping: ResolvedSession;
|
|
12
23
|
threadId: string;
|
|
24
|
+
/** Claude: the conversation threadId is made from on its first prompt, until then (a fork or rewind, §12.7). */
|
|
25
|
+
base?: NativeBase;
|
|
13
26
|
active?: string;
|
|
14
27
|
/** A continued turn accepts a simultaneous user auto prompt as steering. */
|
|
15
28
|
resumedTurn?: string;
|
|
@@ -44,11 +57,7 @@ export type Session = {
|
|
|
44
57
|
/** Set by interrupt: pending mail waits for the next user prompt. */
|
|
45
58
|
wakeBlocked: boolean;
|
|
46
59
|
/** An assignment a usage limit or a pause stopped: it waits, open, until its runtime resumes. prompt is what continues it. */
|
|
47
|
-
parked?:
|
|
48
|
-
reason: string;
|
|
49
|
-
since: number;
|
|
50
|
-
prompt?: string;
|
|
51
|
-
};
|
|
60
|
+
parked?: Parked;
|
|
52
61
|
/** What the requester of a session parked by its turn's end is told happens next, instead of waiting for alp resume. */
|
|
53
62
|
parkThen?: string;
|
|
54
63
|
/** When ALP restarted this session's native process, within the last RESTART_WINDOW_MS. */
|
|
@@ -67,8 +76,15 @@ export type Session = {
|
|
|
67
76
|
skillsUsed?: Set<string>;
|
|
68
77
|
/** Why the running turn is being stopped, so its end parks the assignment instead of ending it. */
|
|
69
78
|
parkReason?: string;
|
|
79
|
+
parkPause?: ParkPause;
|
|
70
80
|
/** Mail arrived while its runtime was paused; it is delivered on resume. */
|
|
71
81
|
wakeHeld?: boolean;
|
|
82
|
+
/** How many lifecycle operations (a rewind) hold its wakes now; mail waits until the last releases (AlpRuntime.hold). */
|
|
83
|
+
lifecycleHolds?: number;
|
|
84
|
+
/** Its parked work was to continue during a lifecycle hold; it continues when the last hold releases. */
|
|
85
|
+
resumeHeld?: Parked;
|
|
86
|
+
/** An assignment opened but not yet sent its brief: mail waits and rides with the brief's turn. */
|
|
87
|
+
awaitingBrief?: boolean;
|
|
72
88
|
/** Permission questions to the user, one at a time. */
|
|
73
89
|
permissionQueue?: Promise<unknown>;
|
|
74
90
|
toolCalls: Map<string, Promise<unknown>>;
|
package/dist/tools.d.ts
CHANGED
|
@@ -680,3 +680,40 @@ export declare function taskTool(actions: TaskAction[]): {
|
|
|
680
680
|
};
|
|
681
681
|
/** The task an assignment takes, as the start of its brief. */
|
|
682
682
|
export declare function taskBrief(task: Task, writing: boolean): string;
|
|
683
|
+
/** Only the project's main root may schedule a future continuation. */
|
|
684
|
+
export declare const SCHEDULE_TOOL: {
|
|
685
|
+
type: string;
|
|
686
|
+
name: string;
|
|
687
|
+
description: string;
|
|
688
|
+
inputSchema: {
|
|
689
|
+
type: string;
|
|
690
|
+
properties: {
|
|
691
|
+
action: {
|
|
692
|
+
type: string;
|
|
693
|
+
enum: string[];
|
|
694
|
+
};
|
|
695
|
+
dueAt: {
|
|
696
|
+
type: string;
|
|
697
|
+
description: string;
|
|
698
|
+
};
|
|
699
|
+
prompt: {
|
|
700
|
+
type: string;
|
|
701
|
+
description: string;
|
|
702
|
+
};
|
|
703
|
+
requestId: {
|
|
704
|
+
type: string;
|
|
705
|
+
description: string;
|
|
706
|
+
};
|
|
707
|
+
id: {
|
|
708
|
+
type: string;
|
|
709
|
+
description: string;
|
|
710
|
+
};
|
|
711
|
+
status: {
|
|
712
|
+
type: string;
|
|
713
|
+
enum: string[];
|
|
714
|
+
};
|
|
715
|
+
};
|
|
716
|
+
required: string[];
|
|
717
|
+
additionalProperties: boolean;
|
|
718
|
+
};
|
|
719
|
+
};
|
package/dist/tools.js
CHANGED
|
@@ -348,6 +348,24 @@ function taskBrief(task, writing) {
|
|
|
348
348
|
"Your handoff moves this task to review for your requester to accept. List work you found outside the task under discovered; only main and the user create tasks."
|
|
349
349
|
].join("\n");
|
|
350
350
|
}
|
|
351
|
+
var SCHEDULE_TOOL = {
|
|
352
|
+
type: "function",
|
|
353
|
+
name: "alp_schedule",
|
|
354
|
+
description: "Create, list, inspect or cancel a one-shot continuation of this main/root session. create proposes the exact prompt and ISO time with UTC offset for user approval. Jobs wake an idle main without a user message; timer gates only unblock tasks, and assignment notes do not schedule a wake. Scheduling a quota-reset continuation never guarantees provider availability: recheck actual availability before delegating.",
|
|
355
|
+
inputSchema: {
|
|
356
|
+
type: "object",
|
|
357
|
+
properties: {
|
|
358
|
+
action: { type: "string", enum: ["create", "list", "get", "cancel"] },
|
|
359
|
+
dueAt: { type: "string", description: "For create: ISO timestamp with Z or an explicit UTC offset." },
|
|
360
|
+
prompt: { type: "string", description: "For create: the exact continuation, at most 8000 characters." },
|
|
361
|
+
requestId: { type: "string", description: "For create: stable id for this approval proposal; reuse only for an identical draft." },
|
|
362
|
+
id: { type: "string", description: "For get/cancel: the scheduled job id." },
|
|
363
|
+
status: { type: "string", enum: ["pending", "delivered", "failed", "cancelled"] }
|
|
364
|
+
},
|
|
365
|
+
required: ["action"],
|
|
366
|
+
additionalProperties: false
|
|
367
|
+
}
|
|
368
|
+
};
|
|
351
369
|
export {
|
|
352
370
|
ASK_TOOL,
|
|
353
371
|
BOARD_TOOL,
|
|
@@ -359,6 +377,7 @@ export {
|
|
|
359
377
|
MERGE_TOOL,
|
|
360
378
|
PIN_TOOL,
|
|
361
379
|
RECALL_TOOL,
|
|
380
|
+
SCHEDULE_TOOL,
|
|
362
381
|
SEND_TOOL,
|
|
363
382
|
SKILL_TOOL,
|
|
364
383
|
TASK_ACTIONS,
|
|
@@ -4,6 +4,7 @@ import type { RuntimeOptions } from './contracts.js';
|
|
|
4
4
|
import type { Session, Assignment } from './session-state.js';
|
|
5
5
|
type WorktreeContext = {
|
|
6
6
|
options: RuntimeOptions;
|
|
7
|
+
releaseResult(assignmentId: string): Promise<void>;
|
|
7
8
|
leases: Map<string, {
|
|
8
9
|
assignment: string;
|
|
9
10
|
agent: string;
|
|
@@ -15,7 +16,7 @@ type WorktreeContext = {
|
|
|
15
16
|
touchTask(rootId: string, id: string): void;
|
|
16
17
|
};
|
|
17
18
|
/** Checkout mutation and verification retain one shared queue per checkout. */
|
|
18
|
-
export declare function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHooks, touchTask }: WorktreeContext): {
|
|
19
|
+
export declare function createWorktreeDomain({ options, leases, releaseResult, rootOf, lineage, runLog, runHooks, touchTask }: WorktreeContext): {
|
|
19
20
|
settleWorktree: (parentId: string, parent: Session, assignment: Assignment) => Promise<{
|
|
20
21
|
branch: string;
|
|
21
22
|
files: never[];
|
package/dist/worktree-domain.js
CHANGED
|
@@ -8,7 +8,7 @@ import { writes } from "./catalog.js";
|
|
|
8
8
|
import { nativeEnvironment } from "./providers.js";
|
|
9
9
|
import { mainOf } from "./tools.js";
|
|
10
10
|
import { errorData, plainObject, toolResult } from "./tool-results.js";
|
|
11
|
-
function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHooks, touchTask }) {
|
|
11
|
+
function createWorktreeDomain({ options, leases, releaseResult, rootOf, lineage, runLog, runHooks, touchTask }) {
|
|
12
12
|
const merging = /* @__PURE__ */ new Map();
|
|
13
13
|
async function settleWorktree(parentId, parent, assignment) {
|
|
14
14
|
const worktree = assignment.worktree;
|
|
@@ -16,6 +16,7 @@ function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHoo
|
|
|
16
16
|
const change = await commitWorktree(worktree, `alp: ${assignment.agent} assignment ${assignment.id}`);
|
|
17
17
|
if (!change.files.length) {
|
|
18
18
|
await removeWorktree(worktree, { deleteBranch: true });
|
|
19
|
+
await releaseResult(assignment.id);
|
|
19
20
|
return { branch: worktree.branch, files: [], note: "No changes; the worktree was removed" };
|
|
20
21
|
}
|
|
21
22
|
const described = { branch: worktree.branch, base: worktree.base, commit: change.commit, files: change.files, stat: change.stat };
|
|
@@ -44,6 +45,7 @@ function createWorktreeDomain({ options, leases, rootOf, lineage, runLog, runHoo
|
|
|
44
45
|
if (action === "discard") {
|
|
45
46
|
session.worktrees.delete(args.assignmentId);
|
|
46
47
|
await removeWorktree(worktree, { deleteBranch: true });
|
|
48
|
+
await releaseResult(args.assignmentId);
|
|
47
49
|
runLog(rootOf(sessionId), { event: "worktree.discarded", assignmentId: args.assignmentId, branch: worktree.branch });
|
|
48
50
|
return toolResult(true, { discarded: args.assignmentId, branch: worktree.branch });
|
|
49
51
|
}
|
|
@@ -104,6 +106,7 @@ ${refused}`, branch: worktree.branch, next: `Fix what it reports in a new assign
|
|
|
104
106
|
return toolResult(false, { error: errorData(error).message, branch: worktree.branch, next: "Merge the branch yourself, or alp_discard it" });
|
|
105
107
|
}
|
|
106
108
|
await removeWorktree(worktree, { deleteBranch: merged.status !== "conflicts" });
|
|
109
|
+
if (merged.status !== "conflicts") await releaseResult(args.assignmentId);
|
|
107
110
|
runLog(rootId, { event: "worktree.merged", assignmentId: args.assignmentId, branch: worktree.branch, ...merged });
|
|
108
111
|
let checked;
|
|
109
112
|
if (config && args.skipVerify === void 0 && merged.status === "applied" && moved) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@anhlp/alp-runtime",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "ALP runtime module",
|
|
6
6
|
"files": [
|
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
"types": "./dist/antigravity-transport.d.ts",
|
|
26
26
|
"default": "./dist/antigravity-transport.js"
|
|
27
27
|
},
|
|
28
|
+
"./attachments": {
|
|
29
|
+
"alp-source": {
|
|
30
|
+
"types": "./src/attachments.ts",
|
|
31
|
+
"default": "./src/attachments.ts"
|
|
32
|
+
},
|
|
33
|
+
"types": "./dist/attachments.d.ts",
|
|
34
|
+
"default": "./dist/attachments.js"
|
|
35
|
+
},
|
|
28
36
|
"./board": {
|
|
29
37
|
"alp-source": {
|
|
30
38
|
"types": "./src/board.ts",
|
|
@@ -222,8 +230,8 @@
|
|
|
222
230
|
"node": ">=22.12"
|
|
223
231
|
},
|
|
224
232
|
"dependencies": {
|
|
225
|
-
"@anhlp/alp-core": "0.0.1-beta.
|
|
226
|
-
"@anhlp/alp-protocol": "0.0.1-beta.
|
|
233
|
+
"@anhlp/alp-core": "0.0.1-beta.4",
|
|
234
|
+
"@anhlp/alp-protocol": "0.0.1-beta.4",
|
|
227
235
|
"@anthropic-ai/claude-agent-sdk": "^0.3.292",
|
|
228
236
|
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
229
237
|
"@agentclientprotocol/sdk": "1.7.0",
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { readFile, mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { writeAtomic } from '@anhlp/alp-core/atomic';
|
|
4
|
+
|
|
5
|
+
export type AssignmentResultOwner = { assignmentId: string; checkout: string; worktree?: string };
|
|
6
|
+
type Ledger = { version: 1; workspaces: string[]; results: AssignmentResultOwner[] };
|
|
7
|
+
const empty = (): Ledger => ({ version: 1, workspaces: [], results: [] });
|
|
8
|
+
const absolute = (value: unknown): value is string => typeof value === 'string' && path.isAbsolute(value);
|
|
9
|
+
const valid = (value: any): value is Ledger => value?.version === 1 && Array.isArray(value.workspaces) && value.workspaces.every(absolute) &&
|
|
10
|
+
Array.isArray(value.results) && value.results.every((entry: any) => entry && typeof entry.assignmentId === 'string' && absolute(entry.checkout) && (entry.worktree === undefined || absolute(entry.worktree)));
|
|
11
|
+
|
|
12
|
+
/** Permanent ownership, independent of native-thread recall expiry. Missing initialized data is never recreated. */
|
|
13
|
+
export function createAssignmentResults(file?: string) {
|
|
14
|
+
let memory = empty();
|
|
15
|
+
let queue = Promise.resolve();
|
|
16
|
+
const marker = file ? `${file}.initialized` : undefined;
|
|
17
|
+
async function load(initialize = false): Promise<Ledger> {
|
|
18
|
+
if (!file) return memory;
|
|
19
|
+
let value: unknown;
|
|
20
|
+
try { value = JSON.parse(await readFile(file, 'utf8')); }
|
|
21
|
+
catch (error) {
|
|
22
|
+
if ((error as NodeJS.ErrnoException).code !== 'ENOENT' || !initialize) throw new Error(`Cannot inspect assignment result ledger ${file}`);
|
|
23
|
+
try { await readFile(marker!, 'utf8'); }
|
|
24
|
+
catch (missing) {
|
|
25
|
+
if ((missing as NodeJS.ErrnoException).code === 'ENOENT') return empty();
|
|
26
|
+
throw missing;
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`Assignment result ledger ${file} was lost; restore it before managing workspaces`);
|
|
29
|
+
}
|
|
30
|
+
if (!valid(value)) throw new Error(`Invalid assignment result ledger ${file}`);
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
33
|
+
async function save(value: Ledger) {
|
|
34
|
+
if (!file) { memory = value; return; }
|
|
35
|
+
await mkdir(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
36
|
+
await writeAtomic(file, JSON.stringify(value) + '\n');
|
|
37
|
+
// The marker survives a lost ledger; registration must never silently reset ownership.
|
|
38
|
+
await writeFile(marker!, '1\n', { flag: 'wx', mode: 0o600 }).catch(error => { if (error.code !== 'EEXIST') throw error; });
|
|
39
|
+
}
|
|
40
|
+
function serialize<T>(run: () => Promise<T>): Promise<T> {
|
|
41
|
+
const result = queue.then(run);
|
|
42
|
+
queue = result.then(() => {}, () => {});
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
register(directory: string) { return serialize(async () => {
|
|
47
|
+
const value = await load(true);
|
|
48
|
+
if (!value.workspaces.includes(directory)) value.workspaces.push(directory);
|
|
49
|
+
await save(value);
|
|
50
|
+
}); },
|
|
51
|
+
put(entry: AssignmentResultOwner) { return serialize(async () => {
|
|
52
|
+
// Initial assignment activity can establish the index, but never certify old workspaces.
|
|
53
|
+
const value = await load(true);
|
|
54
|
+
value.results = [...value.results.filter(existing => existing.assignmentId !== entry.assignmentId), entry];
|
|
55
|
+
await save(value);
|
|
56
|
+
}); },
|
|
57
|
+
remove(assignmentId: string) { return serialize(async () => {
|
|
58
|
+
const value = await load();
|
|
59
|
+
value.results = value.results.filter(entry => entry.assignmentId !== assignmentId);
|
|
60
|
+
await save(value);
|
|
61
|
+
}); },
|
|
62
|
+
inspect() { return serialize(() => load()); },
|
|
63
|
+
};
|
|
64
|
+
}
|