spexcode 0.6.5 → 0.6.7
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/node_modules/@spexcode/{spec-cli → session-core}/dist/delivery-queue.d.ts +2 -1
- package/node_modules/@spexcode/{spec-cli → session-core}/dist/delivery-queue.js +29 -2
- package/node_modules/@spexcode/session-core/dist/index.d.ts +5 -0
- package/node_modules/@spexcode/session-core/dist/index.js +5 -0
- package/node_modules/@spexcode/session-core/dist/internal.d.ts +3 -0
- package/node_modules/@spexcode/session-core/dist/internal.js +3 -0
- package/node_modules/@spexcode/session-core/dist/message.d.ts +22 -0
- package/node_modules/@spexcode/session-core/dist/message.js +53 -0
- package/node_modules/@spexcode/session-core/dist/record-lock.d.ts +7 -0
- package/node_modules/@spexcode/session-core/dist/record-lock.js +152 -0
- package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +62 -0
- package/node_modules/@spexcode/session-core/dist/runtime-session.js +326 -0
- package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +47 -0
- package/node_modules/@spexcode/session-core/dist/session-timeline.js +216 -0
- package/node_modules/@spexcode/session-core/package.json +33 -0
- package/node_modules/@spexcode/spec-cli/bin/spex.mjs +2 -1
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.d.ts +4 -1
- package/node_modules/@spexcode/spec-cli/dist/claude-headless.js +13 -4
- package/node_modules/@spexcode/spec-cli/dist/cli.js +72 -23
- package/node_modules/@spexcode/spec-cli/dist/client.d.ts +2 -1
- package/node_modules/@spexcode/spec-cli/dist/client.js +13 -8
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.d.ts +5 -0
- package/node_modules/@spexcode/spec-cli/dist/codex-runtime-generations.js +112 -0
- package/node_modules/@spexcode/spec-cli/dist/doctor.js +7 -1
- package/node_modules/@spexcode/spec-cli/dist/gateway-hub.js +7 -5
- package/node_modules/@spexcode/spec-cli/dist/gateway.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/gateway.js +44 -20
- package/node_modules/@spexcode/spec-cli/dist/graphCache.js +2 -1
- package/node_modules/@spexcode/spec-cli/dist/graphSnapshot.js +2 -1
- package/node_modules/@spexcode/spec-cli/dist/harness.d.ts +15 -2
- package/node_modules/@spexcode/spec-cli/dist/harness.js +174 -54
- package/node_modules/@spexcode/spec-cli/dist/help.d.ts +5 -0
- package/node_modules/@spexcode/spec-cli/dist/help.js +26 -6
- package/node_modules/@spexcode/spec-cli/dist/index.js +3 -3
- package/node_modules/@spexcode/spec-cli/dist/listen.d.ts +2 -1
- package/node_modules/@spexcode/spec-cli/dist/listen.js +10 -10
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/opencode-headless.js +7 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/runtime-rotate.js +58 -0
- package/node_modules/@spexcode/spec-cli/dist/session-follow.js +1 -1
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.d.ts +6 -46
- package/node_modules/@spexcode/spec-cli/dist/session-timeline.js +8 -221
- package/node_modules/@spexcode/spec-cli/dist/sessions.d.ts +25 -4
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +822 -394
- package/node_modules/@spexcode/spec-cli/dist/supervise.js +3 -3
- package/node_modules/@spexcode/spec-cli/package.json +6 -4
- package/node_modules/@spexcode/spec-core/dist/git.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/dist/git.js +32 -0
- package/node_modules/@spexcode/spec-core/dist/layout.d.ts +4 -0
- package/node_modules/@spexcode/spec-core/package.json +1 -1
- package/node_modules/@spexcode/spec-eval/package.json +2 -2
- package/node_modules/@spexcode/spec-forge/package.json +2 -2
- package/package.json +3 -3
- /package/node_modules/@spexcode/{spec-cli → session-core}/dist/session-cursors.d.ts +0 -0
- /package/node_modules/@spexcode/{spec-cli → session-core}/dist/session-cursors.js +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync, writeSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { runtimeRoot, sessionArtifactPath, sessionStoreDir } from '@spexcode/spec-core';
|
|
4
|
+
import { sentDispatchReceipt, settleSentDispatch } from './session-timeline.js';
|
|
4
5
|
const queuePath = (id) => sessionArtifactPath(id, 'pending.json');
|
|
5
6
|
const revokedSenderRoot = () => join(runtimeRoot(), '.revoked-senders');
|
|
6
7
|
const revokedSenderPath = (id) => join(revokedSenderRoot(), id);
|
|
@@ -62,14 +63,24 @@ function read(id) {
|
|
|
62
63
|
return raw.filter((m) => !!m && typeof m === 'object'
|
|
63
64
|
&& typeof m.mid === 'string'
|
|
64
65
|
&& typeof m.text === 'string'
|
|
66
|
+
&& (m.attributes === undefined
|
|
67
|
+
|| (!!m.attributes
|
|
68
|
+
&& typeof m.attributes === 'object'
|
|
69
|
+
&& !Array.isArray(m.attributes)
|
|
70
|
+
&& Object.values(m.attributes).every((value) => typeof value === 'string')))
|
|
65
71
|
&& (m.dispatch === undefined
|
|
66
|
-
|| (m.dispatch?.operation === '
|
|
72
|
+
|| (typeof m.dispatch?.operation === 'string'
|
|
67
73
|
&& typeof m.dispatch?.requestDigest === 'string')));
|
|
68
74
|
}
|
|
69
75
|
catch {
|
|
70
76
|
return [];
|
|
71
77
|
} // absent, empty, or unparseable all mean the honest thing: nothing owed
|
|
72
78
|
}
|
|
79
|
+
function sameAttributes(left, right) {
|
|
80
|
+
const leftEntries = Object.entries(left ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
81
|
+
const rightEntries = Object.entries(right ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
82
|
+
return JSON.stringify(leftEntries) === JSON.stringify(rightEntries);
|
|
83
|
+
}
|
|
73
84
|
// Written whole and atomically; an empty queue is REMOVED rather than left as `[]`, so "is anything owed?" is
|
|
74
85
|
// one existsSync on the sweep's hot path.
|
|
75
86
|
function write(id, msgs) {
|
|
@@ -160,7 +171,23 @@ export async function drain(id, insert, timeoutMs = 5_000) {
|
|
|
160
171
|
}
|
|
161
172
|
let ok = false;
|
|
162
173
|
try {
|
|
163
|
-
|
|
174
|
+
const head = queued[0];
|
|
175
|
+
if (head.dispatch) {
|
|
176
|
+
const receipt = sentDispatchReceipt(id, head.dispatch.operation, head.dispatch.requestDigest);
|
|
177
|
+
if (!receipt || receipt.mid !== head.mid || !receipt.delivery
|
|
178
|
+
|| receipt.delivery.text !== head.text || receipt.delivery.from !== head.from
|
|
179
|
+
|| !sameAttributes(receipt.delivery.attributes, head.attributes))
|
|
180
|
+
return { delivered, remaining: queued.length };
|
|
181
|
+
if (receipt.delivered)
|
|
182
|
+
ok = true;
|
|
183
|
+
else {
|
|
184
|
+
ok = await insert(head);
|
|
185
|
+
if (ok)
|
|
186
|
+
settleSentDispatch(id, head.mid);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
else
|
|
190
|
+
ok = await insert(head);
|
|
164
191
|
}
|
|
165
192
|
catch {
|
|
166
193
|
ok = false;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { acceptMessage, MessageKeyConflict, type AcceptMessageOptions, type MessageIdempotency, type PreparedMessage } from './message.js';
|
|
2
|
+
export { drain, owesDelivery, pendingMessages, type PendingMessage } from './delivery-queue.js';
|
|
3
|
+
export { publishRuntimeSessionState, readRuntimeSession, registerRuntimeSession, runtimeSessionNotification, runtimeSessionChildren, RuntimeSessionConflict, type RuntimeSessionRecord, type RuntimeSessionNotification, type RuntimeSessionRegistration, type RuntimeSessionState, } from './runtime-session.js';
|
|
4
|
+
export { advanceFollow, followedSessions, followCursor, readCursors, unreadSince, type Cursors } from './session-cursors.js';
|
|
5
|
+
export { currentHumanTurn, lastHumanSendVia, recordStatus, timelineEvents, timelineStamp, timelineTail, type SessionTurn, type TimelineEvent, } from './session-timeline.js';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { acceptMessage, MessageKeyConflict } from './message.js';
|
|
2
|
+
export { drain, owesDelivery, pendingMessages } from './delivery-queue.js';
|
|
3
|
+
export { publishRuntimeSessionState, readRuntimeSession, registerRuntimeSession, runtimeSessionNotification, runtimeSessionChildren, RuntimeSessionConflict, } from './runtime-session.js';
|
|
4
|
+
export { advanceFollow, followedSessions, followCursor, readCursors, unreadSince } from './session-cursors.js';
|
|
5
|
+
export { currentHumanTurn, lastHumanSendVia, recordStatus, timelineEvents, timelineStamp, timelineTail, } from './session-timeline.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { enqueue, ensurePendingWhileLocked, pendingSnapshot, replacePendingWhileLocked, revokePendingFromWhileLocked, revokeSenderDelivery, senderDeliveryRevoked, withDeliveryLocks, } from './delivery-queue.js';
|
|
2
|
+
export { appendSent, sentDispatchReceipt, settleSentDispatch, type SentDispatchReceipt, type SentDispatchState, } from './session-timeline.js';
|
|
3
|
+
export { trySessionRecordLockSync, withSessionRecordLock, withSessionRecordLocks, withSessionRecordLockSync, } from './record-lock.js';
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
export { enqueue, ensurePendingWhileLocked, pendingSnapshot, replacePendingWhileLocked, revokePendingFromWhileLocked, revokeSenderDelivery, senderDeliveryRevoked, withDeliveryLocks, } from './delivery-queue.js';
|
|
2
|
+
export { appendSent, sentDispatchReceipt, settleSentDispatch, } from './session-timeline.js';
|
|
3
|
+
export { trySessionRecordLockSync, withSessionRecordLock, withSessionRecordLocks, withSessionRecordLockSync, } from './record-lock.js';
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type SentDispatchReceipt } from './session-timeline.js';
|
|
2
|
+
export type MessageIdempotency = SentDispatchReceipt;
|
|
3
|
+
export type PreparedMessage = {
|
|
4
|
+
text: string;
|
|
5
|
+
replyVia?: 'note';
|
|
6
|
+
};
|
|
7
|
+
export type AcceptMessageOptions = {
|
|
8
|
+
target: string;
|
|
9
|
+
text: string;
|
|
10
|
+
from?: string;
|
|
11
|
+
idempotency?: MessageIdempotency;
|
|
12
|
+
validate?: () => Promise<void>;
|
|
13
|
+
prepare: () => Promise<PreparedMessage>;
|
|
14
|
+
};
|
|
15
|
+
export declare class MessageKeyConflict extends Error {
|
|
16
|
+
readonly code = "dispatch_key_reused";
|
|
17
|
+
constructor(operation: string);
|
|
18
|
+
}
|
|
19
|
+
export declare function acceptMessage(options: AcceptMessageOptions): Promise<{
|
|
20
|
+
mid: string;
|
|
21
|
+
replayed: boolean;
|
|
22
|
+
}>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { ensurePendingWhileLocked, enqueue, senderDeliveryRevoked, withDeliveryLocks } from './delivery-queue.js';
|
|
2
|
+
import { appendSent, sentDispatchReceipt } from './session-timeline.js';
|
|
3
|
+
import { withSessionRecordLocks } from './record-lock.js';
|
|
4
|
+
export class MessageKeyConflict extends Error {
|
|
5
|
+
code = 'dispatch_key_reused';
|
|
6
|
+
constructor(operation) {
|
|
7
|
+
super(`idempotency key is already bound to another ${operation} payload`);
|
|
8
|
+
this.name = 'MessageKeyConflict';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const keyedPending = (receipt, mid, delivery) => ({
|
|
12
|
+
mid,
|
|
13
|
+
text: delivery.text,
|
|
14
|
+
from: delivery.from,
|
|
15
|
+
...(delivery.attributes ? { attributes: delivery.attributes } : {}),
|
|
16
|
+
dispatch: { operation: receipt.operation, requestDigest: receipt.requestDigest },
|
|
17
|
+
});
|
|
18
|
+
// The durable acceptance boundary. Product validation and prompt composition run while the record fence is
|
|
19
|
+
// held, but the package alone owns append + debt publication and keyed crash recovery.
|
|
20
|
+
export async function acceptMessage(options) {
|
|
21
|
+
const from = options.from ?? null;
|
|
22
|
+
let result = null;
|
|
23
|
+
await withSessionRecordLocks([options.target, ...(from ? [from] : [])], async () => {
|
|
24
|
+
if (from && senderDeliveryRevoked(from))
|
|
25
|
+
throw new Error(`sender session ${from} is closed; prompt NOT delivered`);
|
|
26
|
+
await options.validate?.();
|
|
27
|
+
const accept = async () => {
|
|
28
|
+
if (options.idempotency) {
|
|
29
|
+
const prior = sentDispatchReceipt(options.target, options.idempotency.operation, options.idempotency.requestDigest);
|
|
30
|
+
if (prior) {
|
|
31
|
+
if (prior.payloadHash !== options.idempotency.payloadHash)
|
|
32
|
+
throw new MessageKeyConflict(options.idempotency.operation);
|
|
33
|
+
if (prior.delivery && !prior.delivered)
|
|
34
|
+
ensurePendingWhileLocked(options.target, keyedPending(options.idempotency, prior.mid, prior.delivery));
|
|
35
|
+
result = { mid: prior.mid, replayed: true };
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
const prepared = await options.prepare();
|
|
40
|
+
const receipt = options.idempotency
|
|
41
|
+
? { ...options.idempotency, delivery: { text: prepared.text, from } }
|
|
42
|
+
: undefined;
|
|
43
|
+
const { mid } = appendSent(options.target, options.text, from, prepared.replyVia, receipt);
|
|
44
|
+
enqueue(options.target, receipt ? keyedPending(receipt, mid, receipt.delivery) : { mid, text: prepared.text, from });
|
|
45
|
+
result = { mid, replayed: false };
|
|
46
|
+
};
|
|
47
|
+
if (options.idempotency)
|
|
48
|
+
await withDeliveryLocks([options.target], accept);
|
|
49
|
+
else
|
|
50
|
+
await accept();
|
|
51
|
+
});
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare class SessionRecordLockTimeout extends Error {
|
|
2
|
+
constructor(id: string);
|
|
3
|
+
}
|
|
4
|
+
export declare function withSessionRecordLock<T>(id: string, body: () => Promise<T>, signal?: AbortSignal): Promise<T>;
|
|
5
|
+
export declare function withSessionRecordLockSync<T>(id: string, body: () => T): T;
|
|
6
|
+
export declare function trySessionRecordLockSync(id: string): (() => void) | null;
|
|
7
|
+
export declare function withSessionRecordLocks<T>(rawIds: string[], body: () => Promise<T>, index?: number, ids?: string[]): Promise<T>;
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { closeSync, mkdirSync, openSync, readFileSync, unlinkSync, writeSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { runtimeRoot } from '@spexcode/spec-core';
|
|
4
|
+
const lockRoot = () => join(runtimeRoot(), '.session-locks');
|
|
5
|
+
const lockPath = (id) => join(lockRoot(), `${id}.lock`);
|
|
6
|
+
const syncPause = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
7
|
+
export class SessionRecordLockTimeout extends Error {
|
|
8
|
+
constructor(id) {
|
|
9
|
+
super(`session ${id}: lifecycle transition lock timed out; refusing a stale write`);
|
|
10
|
+
this.name = 'SessionRecordLockTimeout';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
function acquireSync(id, timeoutMs = 30_000) {
|
|
14
|
+
mkdirSync(lockRoot(), { recursive: true });
|
|
15
|
+
const path = lockPath(id), deadline = Date.now() + timeoutMs;
|
|
16
|
+
for (;;) {
|
|
17
|
+
try {
|
|
18
|
+
const fd = openSync(path, 'wx');
|
|
19
|
+
writeSync(fd, String(process.pid));
|
|
20
|
+
closeSync(fd);
|
|
21
|
+
return () => { try {
|
|
22
|
+
unlinkSync(path);
|
|
23
|
+
}
|
|
24
|
+
catch { /* another recovery already removed it */ } };
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error.code !== 'EEXIST')
|
|
28
|
+
throw error;
|
|
29
|
+
let owner = 0;
|
|
30
|
+
try {
|
|
31
|
+
owner = Number(readFileSync(path, 'utf8').trim()) || 0;
|
|
32
|
+
}
|
|
33
|
+
catch { /* race */ }
|
|
34
|
+
if (owner && owner !== process.pid) {
|
|
35
|
+
try {
|
|
36
|
+
process.kill(owner, 0);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
try {
|
|
40
|
+
unlinkSync(path);
|
|
41
|
+
}
|
|
42
|
+
catch { /* race */ }
|
|
43
|
+
;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (Date.now() >= deadline)
|
|
48
|
+
throw new SessionRecordLockTimeout(id);
|
|
49
|
+
syncPause(10);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const aborted = (signal) => signal.reason instanceof Error
|
|
54
|
+
? signal.reason
|
|
55
|
+
: Object.assign(new Error('The operation was aborted'), { name: 'AbortError', code: 'ABORT_ERR' });
|
|
56
|
+
async function pause(signal) {
|
|
57
|
+
if (!signal) {
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (signal.aborted)
|
|
62
|
+
throw aborted(signal);
|
|
63
|
+
await new Promise((resolve, reject) => {
|
|
64
|
+
const timer = setTimeout(done, 10);
|
|
65
|
+
const abort = () => { clearTimeout(timer); signal.removeEventListener('abort', abort); reject(aborted(signal)); };
|
|
66
|
+
function done() { signal.removeEventListener('abort', abort); resolve(); }
|
|
67
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
async function acquire(id, timeoutMs = 30_000, signal) {
|
|
71
|
+
mkdirSync(lockRoot(), { recursive: true });
|
|
72
|
+
const path = lockPath(id), deadline = Date.now() + timeoutMs;
|
|
73
|
+
for (;;) {
|
|
74
|
+
if (signal?.aborted)
|
|
75
|
+
throw aborted(signal);
|
|
76
|
+
try {
|
|
77
|
+
const fd = openSync(path, 'wx');
|
|
78
|
+
writeSync(fd, String(process.pid));
|
|
79
|
+
closeSync(fd);
|
|
80
|
+
return () => { try {
|
|
81
|
+
unlinkSync(path);
|
|
82
|
+
}
|
|
83
|
+
catch { /* another recovery already removed it */ } };
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
if (error.code !== 'EEXIST')
|
|
87
|
+
throw error;
|
|
88
|
+
let owner = 0;
|
|
89
|
+
try {
|
|
90
|
+
owner = Number(readFileSync(path, 'utf8').trim()) || 0;
|
|
91
|
+
}
|
|
92
|
+
catch { /* race */ }
|
|
93
|
+
if (owner && owner !== process.pid) {
|
|
94
|
+
try {
|
|
95
|
+
process.kill(owner, 0);
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
try {
|
|
99
|
+
unlinkSync(path);
|
|
100
|
+
}
|
|
101
|
+
catch { /* race */ }
|
|
102
|
+
;
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (Date.now() >= deadline)
|
|
107
|
+
throw new SessionRecordLockTimeout(id);
|
|
108
|
+
await pause(signal);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
export async function withSessionRecordLock(id, body, signal) {
|
|
113
|
+
const release = await acquire(id, 30_000, signal);
|
|
114
|
+
try {
|
|
115
|
+
return await body();
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
release();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function withSessionRecordLockSync(id, body) {
|
|
122
|
+
const release = acquireSync(id);
|
|
123
|
+
try {
|
|
124
|
+
return body();
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
release();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
export function trySessionRecordLockSync(id) {
|
|
131
|
+
mkdirSync(lockRoot(), { recursive: true });
|
|
132
|
+
const path = lockPath(id);
|
|
133
|
+
try {
|
|
134
|
+
const fd = openSync(path, 'wx');
|
|
135
|
+
writeSync(fd, String(process.pid));
|
|
136
|
+
closeSync(fd);
|
|
137
|
+
return () => { try {
|
|
138
|
+
unlinkSync(path);
|
|
139
|
+
}
|
|
140
|
+
catch { /* another recovery already removed it */ } };
|
|
141
|
+
}
|
|
142
|
+
catch (error) {
|
|
143
|
+
if (error.code === 'EEXIST')
|
|
144
|
+
return null;
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function withSessionRecordLocks(rawIds, body, index = 0, ids = [...new Set(rawIds)].sort()) {
|
|
149
|
+
if (index >= ids.length)
|
|
150
|
+
return body();
|
|
151
|
+
return withSessionRecordLock(ids[index], () => withSessionRecordLocks(ids, body, index + 1, ids));
|
|
152
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type SessionLifecycle, type SessionProposal } from '@spexcode/spec-core';
|
|
2
|
+
import { type PendingMessage } from './delivery-queue.js';
|
|
3
|
+
export type RuntimeSessionRegistration = {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
runtimeOwner: string;
|
|
6
|
+
worktreePath: string;
|
|
7
|
+
branch: string | null;
|
|
8
|
+
parentSessionId?: string | null;
|
|
9
|
+
title?: string | null;
|
|
10
|
+
node?: string | null;
|
|
11
|
+
runtimeMetadata?: Record<string, string>;
|
|
12
|
+
createdAt?: number;
|
|
13
|
+
};
|
|
14
|
+
export type RuntimeSessionState = {
|
|
15
|
+
sessionId: string;
|
|
16
|
+
runtimeOwner: string;
|
|
17
|
+
revision: string;
|
|
18
|
+
runtimeState: string;
|
|
19
|
+
lifecycle: SessionLifecycle;
|
|
20
|
+
proposal?: SessionProposal | null;
|
|
21
|
+
note?: string | null;
|
|
22
|
+
};
|
|
23
|
+
export type RuntimeSessionRecord = {
|
|
24
|
+
sessionId: string;
|
|
25
|
+
runtimeOwner: string;
|
|
26
|
+
runtimeState: string | null;
|
|
27
|
+
revision: string | null;
|
|
28
|
+
worktreePath: string;
|
|
29
|
+
branch: string | null;
|
|
30
|
+
parentSessionId: string | null;
|
|
31
|
+
title: string | null;
|
|
32
|
+
node: string | null;
|
|
33
|
+
runtimeMetadata: Record<string, string>;
|
|
34
|
+
lifecycle: SessionLifecycle;
|
|
35
|
+
proposal: SessionProposal | null;
|
|
36
|
+
note: string | null;
|
|
37
|
+
createdAt: number;
|
|
38
|
+
};
|
|
39
|
+
export type RuntimeSessionNotification = {
|
|
40
|
+
childSessionId: string;
|
|
41
|
+
runtimeOwner: string;
|
|
42
|
+
runtimeState: string;
|
|
43
|
+
revision: string;
|
|
44
|
+
lifecycle: SessionLifecycle;
|
|
45
|
+
proposal: SessionProposal | null;
|
|
46
|
+
note: string | null;
|
|
47
|
+
runtimeMetadata: Record<string, string>;
|
|
48
|
+
};
|
|
49
|
+
export declare class RuntimeSessionConflict extends Error {
|
|
50
|
+
readonly code = "runtime_session_conflict";
|
|
51
|
+
constructor(message: string);
|
|
52
|
+
}
|
|
53
|
+
export declare function registerRuntimeSession(input: RuntimeSessionRegistration): Promise<{
|
|
54
|
+
replayed: boolean;
|
|
55
|
+
}>;
|
|
56
|
+
export declare function runtimeSessionNotification(parentSessionId: string, message: PendingMessage): RuntimeSessionNotification | null;
|
|
57
|
+
export declare function publishRuntimeSessionState(input: RuntimeSessionState): Promise<{
|
|
58
|
+
notified: string[];
|
|
59
|
+
replayed: boolean;
|
|
60
|
+
}>;
|
|
61
|
+
export declare function readRuntimeSession(sessionId: string): RuntimeSessionRecord | null;
|
|
62
|
+
export declare function runtimeSessionChildren(parentSessionId: string, runtimeOwner?: string): RuntimeSessionRecord[];
|