spexcode 0.6.6 → 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/session-core/dist/delivery-queue.d.ts +1 -0
- package/node_modules/@spexcode/session-core/dist/delivery-queue.js +12 -1
- package/node_modules/@spexcode/session-core/dist/index.d.ts +1 -1
- package/node_modules/@spexcode/session-core/dist/index.js +1 -1
- package/node_modules/@spexcode/session-core/dist/message.js +1 -0
- package/node_modules/@spexcode/session-core/dist/runtime-session.d.ts +12 -0
- package/node_modules/@spexcode/session-core/dist/runtime-session.js +42 -2
- package/node_modules/@spexcode/session-core/dist/session-timeline.d.ts +1 -0
- package/node_modules/@spexcode/session-core/package.json +2 -2
- package/node_modules/@spexcode/spec-cli/package.json +5 -5
- 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 +2 -2
|
@@ -63,6 +63,11 @@ function read(id) {
|
|
|
63
63
|
return raw.filter((m) => !!m && typeof m === 'object'
|
|
64
64
|
&& typeof m.mid === 'string'
|
|
65
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')))
|
|
66
71
|
&& (m.dispatch === undefined
|
|
67
72
|
|| (typeof m.dispatch?.operation === 'string'
|
|
68
73
|
&& typeof m.dispatch?.requestDigest === 'string')));
|
|
@@ -71,6 +76,11 @@ function read(id) {
|
|
|
71
76
|
return [];
|
|
72
77
|
} // absent, empty, or unparseable all mean the honest thing: nothing owed
|
|
73
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
|
+
}
|
|
74
84
|
// Written whole and atomically; an empty queue is REMOVED rather than left as `[]`, so "is anything owed?" is
|
|
75
85
|
// one existsSync on the sweep's hot path.
|
|
76
86
|
function write(id, msgs) {
|
|
@@ -165,7 +175,8 @@ export async function drain(id, insert, timeoutMs = 5_000) {
|
|
|
165
175
|
if (head.dispatch) {
|
|
166
176
|
const receipt = sentDispatchReceipt(id, head.dispatch.operation, head.dispatch.requestDigest);
|
|
167
177
|
if (!receipt || receipt.mid !== head.mid || !receipt.delivery
|
|
168
|
-
|| receipt.delivery.text !== head.text || receipt.delivery.from !== head.from
|
|
178
|
+
|| receipt.delivery.text !== head.text || receipt.delivery.from !== head.from
|
|
179
|
+
|| !sameAttributes(receipt.delivery.attributes, head.attributes))
|
|
169
180
|
return { delivered, remaining: queued.length };
|
|
170
181
|
if (receipt.delivered)
|
|
171
182
|
ok = true;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { acceptMessage, MessageKeyConflict, type AcceptMessageOptions, type MessageIdempotency, type PreparedMessage } from './message.js';
|
|
2
2
|
export { drain, owesDelivery, pendingMessages, type PendingMessage } from './delivery-queue.js';
|
|
3
|
-
export { publishRuntimeSessionState, readRuntimeSession, registerRuntimeSession, runtimeSessionChildren, RuntimeSessionConflict, type RuntimeSessionRecord, type RuntimeSessionRegistration, type RuntimeSessionState, } from './runtime-session.js';
|
|
3
|
+
export { publishRuntimeSessionState, readRuntimeSession, registerRuntimeSession, runtimeSessionNotification, runtimeSessionChildren, RuntimeSessionConflict, type RuntimeSessionRecord, type RuntimeSessionNotification, type RuntimeSessionRegistration, type RuntimeSessionState, } from './runtime-session.js';
|
|
4
4
|
export { advanceFollow, followedSessions, followCursor, readCursors, unreadSince, type Cursors } from './session-cursors.js';
|
|
5
5
|
export { currentHumanTurn, lastHumanSendVia, recordStatus, timelineEvents, timelineStamp, timelineTail, type SessionTurn, type TimelineEvent, } from './session-timeline.js';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { acceptMessage, MessageKeyConflict } from './message.js';
|
|
2
2
|
export { drain, owesDelivery, pendingMessages } from './delivery-queue.js';
|
|
3
|
-
export { publishRuntimeSessionState, readRuntimeSession, registerRuntimeSession, runtimeSessionChildren, RuntimeSessionConflict, } from './runtime-session.js';
|
|
3
|
+
export { publishRuntimeSessionState, readRuntimeSession, registerRuntimeSession, runtimeSessionNotification, runtimeSessionChildren, RuntimeSessionConflict, } from './runtime-session.js';
|
|
4
4
|
export { advanceFollow, followedSessions, followCursor, readCursors, unreadSince } from './session-cursors.js';
|
|
5
5
|
export { currentHumanTurn, lastHumanSendVia, recordStatus, timelineEvents, timelineStamp, timelineTail, } from './session-timeline.js';
|
|
@@ -12,6 +12,7 @@ const keyedPending = (receipt, mid, delivery) => ({
|
|
|
12
12
|
mid,
|
|
13
13
|
text: delivery.text,
|
|
14
14
|
from: delivery.from,
|
|
15
|
+
...(delivery.attributes ? { attributes: delivery.attributes } : {}),
|
|
15
16
|
dispatch: { operation: receipt.operation, requestDigest: receipt.requestDigest },
|
|
16
17
|
});
|
|
17
18
|
// The durable acceptance boundary. Product validation and prompt composition run while the record fence is
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type SessionLifecycle, type SessionProposal } from '@spexcode/spec-core';
|
|
2
|
+
import { type PendingMessage } from './delivery-queue.js';
|
|
2
3
|
export type RuntimeSessionRegistration = {
|
|
3
4
|
sessionId: string;
|
|
4
5
|
runtimeOwner: string;
|
|
@@ -35,6 +36,16 @@ export type RuntimeSessionRecord = {
|
|
|
35
36
|
note: string | null;
|
|
36
37
|
createdAt: number;
|
|
37
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
|
+
};
|
|
38
49
|
export declare class RuntimeSessionConflict extends Error {
|
|
39
50
|
readonly code = "runtime_session_conflict";
|
|
40
51
|
constructor(message: string);
|
|
@@ -42,6 +53,7 @@ export declare class RuntimeSessionConflict extends Error {
|
|
|
42
53
|
export declare function registerRuntimeSession(input: RuntimeSessionRegistration): Promise<{
|
|
43
54
|
replayed: boolean;
|
|
44
55
|
}>;
|
|
56
|
+
export declare function runtimeSessionNotification(parentSessionId: string, message: PendingMessage): RuntimeSessionNotification | null;
|
|
45
57
|
export declare function publishRuntimeSessionState(input: RuntimeSessionState): Promise<{
|
|
46
58
|
notified: string[];
|
|
47
59
|
replayed: boolean;
|
|
@@ -14,6 +14,7 @@ export class RuntimeSessionConflict extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
const digest = (value) => createHash('sha256').update(value).digest('hex');
|
|
16
16
|
const watchPath = (target) => sessionArtifactPath(target, 'watchers.json');
|
|
17
|
+
const RUNTIME_NOTIFICATION_KIND = 'spex.runtime-state.v1';
|
|
17
18
|
function scalar(value, field) {
|
|
18
19
|
const normalized = value.trim();
|
|
19
20
|
if (!normalized)
|
|
@@ -183,14 +184,52 @@ function stateMessage(record) {
|
|
|
183
184
|
const note = record.note ? ` — ${record.note}` : '';
|
|
184
185
|
return `[spex watch] ${record.sessionId} is ${record.runtimeState ?? record.lifecycle}${proposal}${note}`;
|
|
185
186
|
}
|
|
187
|
+
function notificationAttributes(record) {
|
|
188
|
+
if (!record.revision || !record.runtimeState)
|
|
189
|
+
throw new RuntimeSessionConflict(`runtime session ${record.sessionId} has no publishable state revision`);
|
|
190
|
+
return {
|
|
191
|
+
kind: RUNTIME_NOTIFICATION_KIND,
|
|
192
|
+
runtimeOwner: record.runtimeOwner,
|
|
193
|
+
runtimeState: record.runtimeState,
|
|
194
|
+
revision: record.revision,
|
|
195
|
+
lifecycle: record.lifecycle,
|
|
196
|
+
proposal: record.proposal ?? '',
|
|
197
|
+
note: record.note ?? '',
|
|
198
|
+
};
|
|
199
|
+
}
|
|
186
200
|
function pendingFor(receipt, mid) {
|
|
187
201
|
return {
|
|
188
202
|
mid,
|
|
189
203
|
text: receipt.delivery.text,
|
|
190
204
|
from: receipt.delivery.from,
|
|
205
|
+
...(receipt.delivery.attributes ? { attributes: receipt.delivery.attributes } : {}),
|
|
191
206
|
dispatch: { operation: receipt.operation, requestDigest: receipt.requestDigest },
|
|
192
207
|
};
|
|
193
208
|
}
|
|
209
|
+
export function runtimeSessionNotification(parentSessionId, message) {
|
|
210
|
+
const parent = scalar(parentSessionId, 'parentSessionId');
|
|
211
|
+
const child = message.from?.trim();
|
|
212
|
+
const attributes = message.attributes;
|
|
213
|
+
if (!child || !attributes || attributes.kind !== RUNTIME_NOTIFICATION_KIND)
|
|
214
|
+
return null;
|
|
215
|
+
const record = readRuntimeSession(child);
|
|
216
|
+
if (!record || record.parentSessionId !== parent || record.runtimeOwner !== attributes.runtimeOwner)
|
|
217
|
+
return null;
|
|
218
|
+
if (!attributes.revision || !attributes.runtimeState || !isSessionLifecycle(attributes.lifecycle))
|
|
219
|
+
return null;
|
|
220
|
+
if (attributes.proposal && !isSessionProposal(attributes.proposal))
|
|
221
|
+
return null;
|
|
222
|
+
return {
|
|
223
|
+
childSessionId: child,
|
|
224
|
+
runtimeOwner: attributes.runtimeOwner,
|
|
225
|
+
runtimeState: attributes.runtimeState,
|
|
226
|
+
revision: attributes.revision,
|
|
227
|
+
lifecycle: attributes.lifecycle,
|
|
228
|
+
proposal: isSessionProposal(attributes.proposal) ? attributes.proposal : null,
|
|
229
|
+
note: attributes.note || null,
|
|
230
|
+
runtimeMetadata: record.runtimeMetadata,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
194
233
|
export async function publishRuntimeSessionState(input) {
|
|
195
234
|
const id = scalar(input.sessionId, 'sessionId');
|
|
196
235
|
const owner = scalar(input.runtimeOwner, 'runtimeOwner');
|
|
@@ -222,10 +261,11 @@ export async function publishRuntimeSessionState(input) {
|
|
|
222
261
|
runtime_revision: revision,
|
|
223
262
|
});
|
|
224
263
|
const message = stateMessage(candidate);
|
|
264
|
+
const attributes = notificationAttributes(candidate);
|
|
225
265
|
const historical = watchers.map((watcher) => {
|
|
226
266
|
const operation = `runtime-state:${id}`;
|
|
227
267
|
const requestDigest = digest(`${id}\0${watcher}\0${revision}`);
|
|
228
|
-
const payloadHash = digest(`${operation}\0${requestDigest}\0${message}`);
|
|
268
|
+
const payloadHash = digest(`${operation}\0${requestDigest}\0${message}\0${JSON.stringify(attributes)}`);
|
|
229
269
|
const prior = sentDispatchReceipt(watcher, operation, requestDigest);
|
|
230
270
|
if (prior && prior.payloadHash !== payloadHash)
|
|
231
271
|
throw new RuntimeSessionConflict(`runtime revision ${revision} notification is already bound to different bytes`);
|
|
@@ -252,7 +292,7 @@ export async function publishRuntimeSessionState(input) {
|
|
|
252
292
|
operation,
|
|
253
293
|
requestDigest,
|
|
254
294
|
payloadHash,
|
|
255
|
-
delivery: { text: message, from: id },
|
|
295
|
+
delivery: { text: message, from: id, attributes },
|
|
256
296
|
};
|
|
257
297
|
if (prior) {
|
|
258
298
|
if (!prior.delivered)
|
|
@@ -21,6 +21,7 @@ export type SentDispatchReceipt = {
|
|
|
21
21
|
delivery?: {
|
|
22
22
|
text: string;
|
|
23
23
|
from: string | null;
|
|
24
|
+
attributes?: Record<string, string>;
|
|
24
25
|
};
|
|
25
26
|
};
|
|
26
27
|
export declare function recordStatus(id: string, status: SessionLifecycle, proposal: SessionProposal | null, note: string | null): void;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-core",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode's durable file-based session communication protocol.",
|
|
6
6
|
"files": [
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts && node --test scripts/public-boundary.test.mjs"
|
|
24
24
|
},
|
|
25
25
|
"dependencies": {
|
|
26
|
-
"@spexcode/spec-core": "0.6.
|
|
26
|
+
"@spexcode/spec-core": "0.6.7"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
29
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode CLI + server. The root spexcode package delegates to this compiled package; dashboard assets live in @spexcode/spec-dashboard.",
|
|
6
6
|
"bin": {
|
|
@@ -36,10 +36,10 @@
|
|
|
36
36
|
"test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
|
-
"@spexcode/session-core": "0.6.
|
|
40
|
-
"@spexcode/spec-core": "0.6.
|
|
41
|
-
"@spexcode/spec-eval": "0.6.
|
|
42
|
-
"@spexcode/spec-forge": "0.6.
|
|
39
|
+
"@spexcode/session-core": "0.6.7",
|
|
40
|
+
"@spexcode/spec-core": "0.6.7",
|
|
41
|
+
"@spexcode/spec-eval": "0.6.7",
|
|
42
|
+
"@spexcode/spec-forge": "0.6.7"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-eval",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@spexcode/spec-core": "0.6.
|
|
29
|
+
"@spexcode/spec-core": "0.6.7"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-forge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./dist/index.js",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"test": "tsx --test src/*.test.ts"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@spexcode/spec-core": "0.6.
|
|
25
|
+
"@spexcode/spec-core": "0.6.7"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.16.0",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spexcode",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "SpexCode — a spec-driven, self-developing dev tool. The `spex` CLI + spec server reads the .spec tree and its git history, and serves the dashboard.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,7 +38,7 @@
|
|
|
38
38
|
"prepack": "node scripts/sync-init-plugins.mjs --check && node scripts/prepack.mjs"
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
|
-
"@spexcode/spec-cli": "0.6.
|
|
41
|
+
"@spexcode/spec-cli": "0.6.7"
|
|
42
42
|
},
|
|
43
43
|
"publishConfig": {
|
|
44
44
|
"access": "public"
|