spexcode 0.7.0-next.5 → 0.7.0-next.6
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-application/package.json +5 -5
- package/node_modules/@spexcode/session-events/package.json +2 -2
- package/node_modules/@spexcode/session-protocol/package.json +1 -1
- package/node_modules/@spexcode/session-runtime/package.json +2 -2
- package/node_modules/@spexcode/session-selflaunch/package.json +3 -3
- package/node_modules/@spexcode/session-topology/package.json +2 -2
- package/node_modules/@spexcode/spec-cli/dist/client.js +7 -5
- package/node_modules/@spexcode/spec-cli/dist/host-resources.js +20 -3
- package/node_modules/@spexcode/spec-cli/dist/session-transcript.d.ts +1 -0
- package/node_modules/@spexcode/spec-cli/dist/sessions.js +31 -6
- package/node_modules/@spexcode/spec-cli/package.json +7 -7
- 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/node_modules/@spexcode/transcript/dist/parsers.d.ts +3 -0
- package/node_modules/@spexcode/transcript/dist/parsers.js +16 -7
- package/node_modules/@spexcode/transcript/dist/turns.d.ts +1 -0
- package/node_modules/@spexcode/transcript/package.json +1 -1
- package/package.json +2 -2
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-application",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Adopter-owned composition of session protocol and topology transactions.",
|
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
|
19
|
-
"@spexcode/session-events": "0.7.0-next.
|
|
20
|
-
"@spexcode/session-protocol": "0.7.0-next.
|
|
21
|
-
"@spexcode/session-runtime": "0.7.0-next.
|
|
22
|
-
"@spexcode/session-topology": "0.7.0-next.
|
|
19
|
+
"@spexcode/session-events": "0.7.0-next.6",
|
|
20
|
+
"@spexcode/session-protocol": "0.7.0-next.6",
|
|
21
|
+
"@spexcode/session-runtime": "0.7.0-next.6",
|
|
22
|
+
"@spexcode/session-topology": "0.7.0-next.6"
|
|
23
23
|
},
|
|
24
24
|
"devDependencies": {
|
|
25
25
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-events",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Append-only session facts and deterministic ordered replay inputs.",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.6" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-runtime",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Adopter-owned bindings between protocol addresses and native runtime identities.",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.6" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-selflaunch",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A fail-closed self-launch adopter for the SpexCode session protocol.",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
25
25
|
},
|
|
26
26
|
"dependencies": {
|
|
27
|
-
"@spexcode/session-protocol": "0.7.0-next.
|
|
28
|
-
"@spexcode/session-runtime": "0.7.0-next.
|
|
27
|
+
"@spexcode/session-protocol": "0.7.0-next.6",
|
|
28
|
+
"@spexcode/session-runtime": "0.7.0-next.6"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^20.16.0",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/session-topology",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
4
4
|
"publishConfig": { "access": "public" },
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "A neutral SQLite relation model for session addresses.",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"prepublishOnly": "node ../../scripts/release-publish.mjs --from-package-publish",
|
|
16
16
|
"test": "npm run build && tsx --import ../../scripts/test-home.mjs --test src/*.test.ts"
|
|
17
17
|
},
|
|
18
|
-
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.
|
|
18
|
+
"dependencies": { "@spexcode/session-protocol": "0.7.0-next.6" },
|
|
19
19
|
"devDependencies": {
|
|
20
20
|
"@types/node": "^20.16.0",
|
|
21
21
|
"tsx": "^4.19.2",
|
|
@@ -132,10 +132,10 @@ const post = (body) => ({ method: 'POST', headers: { 'content-type': 'applicatio
|
|
|
132
132
|
const seg = (id) => encodeURIComponent(id);
|
|
133
133
|
// A rendezvous liveness probe can displace the real delivery client.
|
|
134
134
|
function cachedStatus(rec) {
|
|
135
|
-
if (!rec.worktreePath || !existsSync(rec.worktreePath))
|
|
136
|
-
return 'retired';
|
|
137
135
|
if (rec.archived)
|
|
138
136
|
return 'offline';
|
|
137
|
+
if (!rec.worktreePath || !existsSync(rec.worktreePath))
|
|
138
|
+
return 'retired';
|
|
139
139
|
if (rec.status === 'awaiting')
|
|
140
140
|
return displayStatusForProposal(rec.proposal);
|
|
141
141
|
return rec.status === 'active' || rec.status === 'idle' ? 'unknown' : rec.status;
|
|
@@ -179,9 +179,11 @@ export function localCachedSessions(includeArchived = false) {
|
|
|
179
179
|
if (!state)
|
|
180
180
|
continue;
|
|
181
181
|
const lifecycle = state.status;
|
|
182
|
-
const status = state.status === '
|
|
183
|
-
?
|
|
184
|
-
: state.status === '
|
|
182
|
+
const status = state.status === 'archived'
|
|
183
|
+
? 'offline'
|
|
184
|
+
: state.status === 'awaiting'
|
|
185
|
+
? displayStatusForProposal(state.proposal)
|
|
186
|
+
: state.status;
|
|
185
187
|
rows.push({
|
|
186
188
|
id,
|
|
187
189
|
node: null,
|
|
@@ -145,6 +145,23 @@ const descendants = (root, procs) => {
|
|
|
145
145
|
}
|
|
146
146
|
return ids;
|
|
147
147
|
};
|
|
148
|
+
// tmux is a shared host for many independent session windows. Its inherited environment is therefore
|
|
149
|
+
// not a launch receipt: a token on the server or one of its panes cannot charge the whole hosted tree.
|
|
150
|
+
const isTmuxServer = (proc) => proc?.command === 'tmux: server';
|
|
151
|
+
const hasTmuxAncestor = (pid, procs) => {
|
|
152
|
+
const seen = new Set();
|
|
153
|
+
let next = procs.get(pid)?.ppid;
|
|
154
|
+
while (next && !seen.has(next)) {
|
|
155
|
+
seen.add(next);
|
|
156
|
+
const parent = procs.get(next);
|
|
157
|
+
if (!parent)
|
|
158
|
+
return false;
|
|
159
|
+
if (isTmuxServer(parent))
|
|
160
|
+
return true;
|
|
161
|
+
next = parent.ppid;
|
|
162
|
+
}
|
|
163
|
+
return false;
|
|
164
|
+
};
|
|
148
165
|
const publicRecordInventory = () => {
|
|
149
166
|
const entries = listSessionIds().map(readPublicRecordEntry);
|
|
150
167
|
const application = configuredSessionApplicationIfCutover();
|
|
@@ -345,13 +362,13 @@ const buildInventory = (procs, publicRecords = publicRecordInventory()) => {
|
|
|
345
362
|
for (const p of procs.values()) {
|
|
346
363
|
const acting = actingSession(p);
|
|
347
364
|
const fallback = p.env.SPEXCODE_SESSION_ID;
|
|
348
|
-
const sid = acting ?? (fallback ? byId.get(fallback) : undefined);
|
|
365
|
+
const sid = acting ?? (!hasTmuxAncestor(p.pid, procs) && !isTmuxServer(p) && fallback ? byId.get(fallback) : undefined);
|
|
349
366
|
if (sid)
|
|
350
367
|
ownership.set(p.pid, `session:${sid}`);
|
|
351
368
|
}
|
|
352
369
|
for (const rec of activeRecs) {
|
|
353
370
|
const root = runtimePid(join(runtimeRoot(), 'sessions', rec.session_id, 'agent.pid'));
|
|
354
|
-
if (root && procs.has(root))
|
|
371
|
+
if (root && procs.has(root) && !isTmuxServer(procs.get(root)))
|
|
355
372
|
for (const pid of descendants(root, procs))
|
|
356
373
|
if (!ownership.has(pid))
|
|
357
374
|
ownership.set(pid, `session:${rec.session_id}`);
|
|
@@ -391,7 +408,7 @@ const buildInventory = (procs, publicRecords = publicRecordInventory()) => {
|
|
|
391
408
|
}
|
|
392
409
|
const root = repoRoot();
|
|
393
410
|
for (const p of procs.values()) {
|
|
394
|
-
if (ownership.has(p.pid) || p.env.SPEXCODE_PROJECT_ROOT !== root)
|
|
411
|
+
if (ownership.has(p.pid) || p.env.SPEXCODE_PROJECT_ROOT !== root || hasTmuxAncestor(p.pid, procs) || isTmuxServer(p))
|
|
395
412
|
continue;
|
|
396
413
|
const claimed = p.env.SPEXCODE_SESSION_ID;
|
|
397
414
|
if (claimed && !byId.has(claimed))
|
|
@@ -30,6 +30,7 @@ export declare function readSessionTranscript(c: Context): Promise<(Response & i
|
|
|
30
30
|
readonly output?: string | undefined;
|
|
31
31
|
readonly outputLines: number;
|
|
32
32
|
readonly outputBytes: number;
|
|
33
|
+
readonly outcome?: "failed" | "rejected" | undefined;
|
|
33
34
|
}[] | undefined;
|
|
34
35
|
}[];
|
|
35
36
|
readonly truncated: boolean;
|
|
@@ -1102,7 +1102,7 @@ export function toSession(rec, status, lv, activity = null) {
|
|
|
1102
1102
|
const pp = prompt ? oneLinePreview(prompt) : null;
|
|
1103
1103
|
const parts = { id: rec.session, name: rec.name, node: rec.node, title: rec.title, branch: rec.branch, activity: act, note: rec.note, promptPreview: pp };
|
|
1104
1104
|
const harness = harnessById(rec.harness || defaultHarness.id);
|
|
1105
|
-
return { id: rec.session, node: rec.node, branch: rec.branch, label: deriveLabel(parts), title: deriveTitle(parts), raw: { name: rec.name, title: rec.title }, path: rec.worktreePath, parent: rec.parent, harness: harness.id, capabilities: { headless: harness.headless }, launcher: rec.launcher, lifecycle: rec.status, proposal: rec.proposal, merges: rec.merges, note: rec.note, status, liveness: lv, archived: rec.archived
|
|
1105
|
+
return { id: rec.session, node: rec.node, branch: rec.branch, label: deriveLabel(parts), title: deriveTitle(parts), raw: { name: rec.name, title: rec.title }, path: rec.worktreePath, parent: rec.parent, harness: harness.id, capabilities: { headless: harness.headless }, launcher: rec.launcher, lifecycle: rec.closedAt ? 'archived' : rec.status, proposal: rec.closedAt ? null : rec.proposal, merges: rec.merges, note: rec.note, status, liveness: lv, archived: rec.archived || !!rec.closedAt, closedAt: rec.closedAt, archiveHazard: null, prompt, promptPreview: pp, created: rec.createdAt, activity: act, sortKey: rec.sortKey, files: readSessionFiles(rec.session), web: readSessionWebs(rec.session), ...(rec.zcodeChildSessionIds?.length ? { zcodeChildSessionIds: [...rec.zcodeChildSessionIds] } : {}) };
|
|
1106
1106
|
}
|
|
1107
1107
|
// @@@zcode child identity - ZCode owns the child id and SpexCode owns the session record. The writer accepts
|
|
1108
1108
|
// only their exact declared pair; names, worktrees, branches, and timestamps are deliberately not candidates.
|
|
@@ -1176,7 +1176,7 @@ export async function listArchivedSessionIndex(probe) {
|
|
|
1176
1176
|
if (entry.kind !== 'ok')
|
|
1177
1177
|
continue;
|
|
1178
1178
|
const rec = fromRaw(entry.raw);
|
|
1179
|
-
if (!rec.governed || !rec.archived)
|
|
1179
|
+
if (!rec.governed || (!rec.archived && !rec.closedAt))
|
|
1180
1180
|
continue;
|
|
1181
1181
|
const parts = {
|
|
1182
1182
|
id: rec.session, name: rec.name, node: rec.node, title: rec.title, branch: rec.branch,
|
|
@@ -1305,7 +1305,11 @@ export async function listSessions(includeArchived = false) {
|
|
|
1305
1305
|
// missing durable cold proof is also legacy: leaf liveness alone cannot prove a Codex loaded thread was
|
|
1306
1306
|
// unloaded, so it remains visible until an explicit archive repair.
|
|
1307
1307
|
const cleanCold = projectedRecord.archived && !changedDuringCensus.has(id) && hasValidColdProof(projectedRecord) && physical === 'offline' && (!residentRequired || resident?.healthy === true);
|
|
1308
|
-
|
|
1308
|
+
// A published close is terminal public history even if a later census cannot prove the old adapter fully
|
|
1309
|
+
// unloaded. Only legacy archived rows without closedAt may be exposed as a working hazard for repair.
|
|
1310
|
+
const projected = projectedRecord.archived && !cleanCold && !projectedRecord.closedAt
|
|
1311
|
+
? { ...projectedRecord, archived: false, stopped: false }
|
|
1312
|
+
: projectedRecord;
|
|
1309
1313
|
const projectedLv = projected === projectedRecord
|
|
1310
1314
|
? sessionHarness.runtimeOwnership === 'adapter'
|
|
1311
1315
|
? adapterResidentLiveness(projectedRecord, resident)
|
|
@@ -1824,12 +1828,16 @@ export function canonicalRecordProjection(rec, canonical) {
|
|
|
1824
1828
|
// The application row is the only lifecycle fact after cutover. A JSON status is historical envelope data,
|
|
1825
1829
|
// so it must not win merely because it says waiting/error/archived while the canonical row says otherwise.
|
|
1826
1830
|
if (!canonical) {
|
|
1827
|
-
return rec
|
|
1831
|
+
return ('closedAt' in rec && rec.closedAt
|
|
1832
|
+
? { ...rec, archived: true }
|
|
1833
|
+
: rec);
|
|
1828
1834
|
}
|
|
1835
|
+
const closed = 'closedAt' in rec && !!rec.closedAt;
|
|
1829
1836
|
return {
|
|
1830
1837
|
...rec,
|
|
1831
|
-
|
|
1832
|
-
|
|
1838
|
+
archived: closed ? true : rec.archived,
|
|
1839
|
+
status: (closed ? 'archived' : canonical.status),
|
|
1840
|
+
proposal: (closed ? null : canonical.proposal),
|
|
1833
1841
|
note: canonical.note,
|
|
1834
1842
|
parent: canonical.parentSessionId,
|
|
1835
1843
|
};
|
|
@@ -4789,12 +4797,25 @@ async function closeOwnedSessionUnlocked(id, wt, _source, unboundStopped = false
|
|
|
4789
4797
|
throw new ResourceConflict(`refusing to finish close for ${id}: session record disappeared before publication`);
|
|
4790
4798
|
writeRecord({
|
|
4791
4799
|
...latest,
|
|
4800
|
+
proposal: null,
|
|
4792
4801
|
archived: true,
|
|
4793
4802
|
closedAt: latest.closedAt || new Date().toISOString(),
|
|
4794
4803
|
stopped: true,
|
|
4795
4804
|
coldProof: latest.coldProof || coldProofFor(latest),
|
|
4796
4805
|
adapterRecovery: null,
|
|
4797
4806
|
});
|
|
4807
|
+
// The canonical lifecycle must settle at the same terminal boundary as the durable close fact. `archived`
|
|
4808
|
+
// is an internal terminal marker; public projections render its closed record as `retired`.
|
|
4809
|
+
const application = configuredSessionApplicationIfCutover();
|
|
4810
|
+
if (application?.readState(id)) {
|
|
4811
|
+
application.transitionSession(id, {
|
|
4812
|
+
status: 'archived',
|
|
4813
|
+
proposal: null,
|
|
4814
|
+
note: latest.note,
|
|
4815
|
+
parentSessionId: latest.parent,
|
|
4816
|
+
recipientSessionIds: canonicalWatchRecipients(application, id, 'archived'),
|
|
4817
|
+
});
|
|
4818
|
+
}
|
|
4798
4819
|
let slot = null;
|
|
4799
4820
|
try {
|
|
4800
4821
|
slot = existsSync(wt.path) ? treeSlotDir(wt.path) : null;
|
|
@@ -5367,6 +5388,8 @@ export async function drainSession(id) {
|
|
|
5367
5388
|
const removed = application.dequeuePendingMessage(id, msg.messageId);
|
|
5368
5389
|
if (!removed || removed.messageId !== msg.messageId)
|
|
5369
5390
|
throw new ResourceConflict(`canonical queue head changed while delivering ${id}`);
|
|
5391
|
+
if (!msg.senderSessionId)
|
|
5392
|
+
markHumanPromptActive(id);
|
|
5370
5393
|
}
|
|
5371
5394
|
});
|
|
5372
5395
|
return;
|
|
@@ -5393,6 +5416,8 @@ export async function drainSession(id) {
|
|
|
5393
5416
|
const removed = application.dequeueForRuntime(id, 'spex-governed', binding.bindingGeneration, msg.messageId);
|
|
5394
5417
|
if (!removed || removed.messageId !== msg.messageId)
|
|
5395
5418
|
throw new ResourceConflict(`canonical queue head changed while delivering ${id}`);
|
|
5419
|
+
if (!msg.senderSessionId)
|
|
5420
|
+
markHumanPromptActive(id);
|
|
5396
5421
|
}
|
|
5397
5422
|
});
|
|
5398
5423
|
return;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-cli",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
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": {
|
|
@@ -35,12 +35,12 @@
|
|
|
35
35
|
"test": "tsx --import ../scripts/test-home.mjs --test src/*.test.ts"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@spexcode/session-application": "0.7.0-next.
|
|
39
|
-
"@spexcode/session-selflaunch": "0.7.0-next.
|
|
40
|
-
"@spexcode/spec-core": "0.7.0-next.
|
|
41
|
-
"@spexcode/spec-eval": "0.7.0-next.
|
|
42
|
-
"@spexcode/spec-forge": "0.7.0-next.
|
|
43
|
-
"@spexcode/transcript": "0.7.0-next.
|
|
38
|
+
"@spexcode/session-application": "0.7.0-next.6",
|
|
39
|
+
"@spexcode/session-selflaunch": "0.7.0-next.6",
|
|
40
|
+
"@spexcode/spec-core": "0.7.0-next.6",
|
|
41
|
+
"@spexcode/spec-eval": "0.7.0-next.6",
|
|
42
|
+
"@spexcode/spec-forge": "0.7.0-next.6",
|
|
43
|
+
"@spexcode/transcript": "0.7.0-next.6",
|
|
44
44
|
"smol-toml": "^1.8.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/spec-eval",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
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.7.0-next.
|
|
29
|
+
"@spexcode/spec-core": "0.7.0-next.6"
|
|
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.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
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.7.0-next.
|
|
25
|
+
"@spexcode/spec-core": "0.7.0-next.6"
|
|
26
26
|
},
|
|
27
27
|
"devDependencies": {
|
|
28
28
|
"@types/node": "^20.16.0",
|
|
@@ -8,6 +8,7 @@ export type MutableTool = {
|
|
|
8
8
|
output?: string;
|
|
9
9
|
outputLines: number;
|
|
10
10
|
outputBytes: number;
|
|
11
|
+
outcome?: ToolOutcome;
|
|
11
12
|
};
|
|
12
13
|
export type MutableTurn = {
|
|
13
14
|
id: string | null;
|
|
@@ -16,12 +17,14 @@ export type MutableTurn = {
|
|
|
16
17
|
text?: string;
|
|
17
18
|
tools: MutableTool[];
|
|
18
19
|
};
|
|
20
|
+
export type ToolOutcome = 'failed' | 'rejected';
|
|
19
21
|
export type ParsedEvent = {
|
|
20
22
|
at: number | null;
|
|
21
23
|
turn: MutableTurn | null;
|
|
22
24
|
toolOutputs?: readonly {
|
|
23
25
|
id: string;
|
|
24
26
|
text: string;
|
|
27
|
+
outcome?: ToolOutcome;
|
|
25
28
|
}[];
|
|
26
29
|
};
|
|
27
30
|
export type Parse = (value: unknown) => ParsedEvent | null;
|
|
@@ -82,7 +82,7 @@ export function claudeEvent(value) {
|
|
|
82
82
|
const outputs = blocks.flatMap((block) => {
|
|
83
83
|
const b = object(block);
|
|
84
84
|
const id = string(b?.tool_use_id);
|
|
85
|
-
return b?.type === 'tool_result' && id ? [{ id, text: compact(b?.content ?? '') }] : [];
|
|
85
|
+
return b?.type === 'tool_result' && id ? [{ id, text: compact(b?.content ?? ''), ...(b?.is_error === true ? { outcome: 'failed' } : {}) }] : [];
|
|
86
86
|
});
|
|
87
87
|
if (outputs.length)
|
|
88
88
|
return { at: eventAt, turn: null, toolOutputs: outputs };
|
|
@@ -204,9 +204,14 @@ export function codexAppServerEvent(value) {
|
|
|
204
204
|
output = item.result ?? item.error;
|
|
205
205
|
else if (type === 'dynamicToolCall')
|
|
206
206
|
output = item.contentItems ?? item.output ?? item.error;
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
207
|
+
// the item status is the app-server's own verdict: `failed`, or `declined` when the person refused the call —
|
|
208
|
+
// a declined call has no output, so the empty result is what ends its "running"
|
|
209
|
+
const status = string(item.status);
|
|
210
|
+
const outcome = status === 'failed' ? 'failed' : status === 'declined' ? 'rejected' : undefined;
|
|
211
|
+
if (output === undefined || output === null) {
|
|
212
|
+
return outcome ? { at: eventAt, turn: null, toolOutputs: [{ id, text: '', outcome }] } : { at: eventAt, turn: null };
|
|
213
|
+
}
|
|
214
|
+
return { at: eventAt, turn: null, toolOutputs: [{ id, text: compact(output), ...(outcome ? { outcome } : {}) }] };
|
|
210
215
|
}
|
|
211
216
|
// Agent-message deltas are fragments of one native item. The closure remembers only that item's text and
|
|
212
217
|
// re-emits its native id, allowing IntervalCollector to replace the earlier turn in place.
|
|
@@ -268,7 +273,7 @@ export function piEvent(value) {
|
|
|
268
273
|
}
|
|
269
274
|
if (message.role === 'toolResult') {
|
|
270
275
|
const id = string(message.toolCallId);
|
|
271
|
-
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? '') }] } : null;
|
|
276
|
+
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? ''), ...(message.isError === true ? { outcome: 'failed' } : {}) }] } : null;
|
|
272
277
|
}
|
|
273
278
|
return null;
|
|
274
279
|
}
|
|
@@ -303,7 +308,7 @@ export function geminiEvent(value) {
|
|
|
303
308
|
for (const callValue of items(message.toolCalls)) {
|
|
304
309
|
const call = object(callValue);
|
|
305
310
|
const id = string(call?.id) ?? `tool-${turn.tools.length}`;
|
|
306
|
-
turn.tools.push({ id, name: string(call?.name) ?? 'tool', input: call?.args === undefined ? undefined : compact(call.args), outputLines: 0, outputBytes: 0 });
|
|
311
|
+
turn.tools.push({ id, name: string(call?.name) ?? 'tool', input: call?.args === undefined ? undefined : compact(call.args), outputLines: 0, outputBytes: 0, ...(call?.status === 'error' ? { outcome: 'failed' } : {}) });
|
|
307
312
|
}
|
|
308
313
|
if (!turn.text && !turn.tools.length)
|
|
309
314
|
return null;
|
|
@@ -325,7 +330,7 @@ export function openclawEvent(value) {
|
|
|
325
330
|
}
|
|
326
331
|
if (message.role === 'toolResult') {
|
|
327
332
|
const id = string(message.toolCallId);
|
|
328
|
-
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? '') }] } : null;
|
|
333
|
+
return id ? { at: eventAt, turn: null, toolOutputs: [{ id, text: blockText(message.content) ?? compact(message.content ?? ''), ...(message.isError === true ? { outcome: 'failed' } : {}) }] } : null;
|
|
329
334
|
}
|
|
330
335
|
if (message.role === 'assistant') {
|
|
331
336
|
const turn = { id: idOf(entry) ?? idOf(message), at: eventAt, role: 'assistant', tools: [] };
|
|
@@ -414,6 +419,8 @@ export function opencodeEvents(value) {
|
|
|
414
419
|
tool.output = output.slice(0, MAX_OUTPUT_BYTES);
|
|
415
420
|
tool.outputBytes = Buffer.byteLength(output);
|
|
416
421
|
tool.outputLines = lineCount(output);
|
|
422
|
+
if (status === 'error')
|
|
423
|
+
tool.outcome = 'failed';
|
|
417
424
|
}
|
|
418
425
|
turn.tools.push(tool);
|
|
419
426
|
}
|
|
@@ -466,6 +473,8 @@ export class IntervalCollector {
|
|
|
466
473
|
tool.outputLines += lineCount(output.text);
|
|
467
474
|
if (tool.output === undefined)
|
|
468
475
|
tool.output = '';
|
|
476
|
+
if (output.outcome)
|
|
477
|
+
tool.outcome = output.outcome;
|
|
469
478
|
const remaining = Math.max(0, MAX_OUTPUT_BYTES - Buffer.byteLength(tool.output));
|
|
470
479
|
tool.output += output.text.slice(0, remaining);
|
|
471
480
|
if (bytes > remaining)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spexcode/transcript",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Normalized agent transcripts: one parser per harness, a bounded interval reader over a native thread file or an in-memory event stream, and the full/delta frame protocol every transport and renderer share.",
|
|
6
6
|
"files": [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spexcode",
|
|
3
|
-
"version": "0.7.0-next.
|
|
3
|
+
"version": "0.7.0-next.6",
|
|
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",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"prepack": "node scripts/sync-init-plugins.mjs --check && node scripts/prepack.mjs"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@spexcode/spec-cli": "0.7.0-next.
|
|
47
|
+
"@spexcode/spec-cli": "0.7.0-next.6"
|
|
48
48
|
},
|
|
49
49
|
"publishConfig": {
|
|
50
50
|
"access": "public"
|