agent-relay-server 0.124.0 → 0.126.0
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/docs/openapi.json +1 -1
- package/package.json +9 -5
- package/public/assets/{MessageBubble-SQK8-Mzp.js → MessageBubble-B6YgulnX.js} +2 -2
- package/public/assets/{MessageBubble-SQK8-Mzp.js.map → MessageBubble-B6YgulnX.js.map} +1 -1
- package/public/assets/{PlanGraphPanel-CS-aow4S.js → PlanGraphPanel--DNxHn1T.js} +2 -2
- package/public/assets/{PlanGraphPanel-CS-aow4S.js.map → PlanGraphPanel--DNxHn1T.js.map} +1 -1
- package/public/assets/{activity-W31XvhC2.js → activity-Fo5aLYD6.js} +2 -2
- package/public/assets/{activity-W31XvhC2.js.map → activity-Fo5aLYD6.js.map} +1 -1
- package/public/assets/agent-profiles-C3p0xclG.js +2 -0
- package/public/assets/{agent-profiles-DXysA9To.js.map → agent-profiles-C3p0xclG.js.map} +1 -1
- package/public/assets/{agent-type-icon-BxUopALO.js → agent-type-icon-BmBR-9CH.js} +2 -2
- package/public/assets/{agent-type-icon-BxUopALO.js.map → agent-type-icon-BmBR-9CH.js.map} +1 -1
- package/public/assets/{agents-B5GRNRnN.js → agents-_GBQyF5b.js} +2 -2
- package/public/assets/{agents-B5GRNRnN.js.map → agents-_GBQyF5b.js.map} +1 -1
- package/public/assets/{analytics-DqAuUAwX.js → analytics-BZtRycyL.js} +2 -2
- package/public/assets/{analytics-DqAuUAwX.js.map → analytics-BZtRycyL.js.map} +1 -1
- package/public/assets/{automation-DYlgHYcb.js → automation-Cf_Ewi9f.js} +2 -2
- package/public/assets/{automation-DYlgHYcb.js.map → automation-Cf_Ewi9f.js.map} +1 -1
- package/public/assets/{badge-XEru6wto.js → badge-BCvp7LK3.js} +2 -2
- package/public/assets/{badge-XEru6wto.js.map → badge-BCvp7LK3.js.map} +1 -1
- package/public/assets/{branch-state-badge-Mf0Ow77K.js → branch-state-badge-Cscjqf-E.js} +2 -2
- package/public/assets/{branch-state-badge-Mf0Ow77K.js.map → branch-state-badge-Cscjqf-E.js.map} +1 -1
- package/public/assets/{button-C3CQSr4p.js → button-DyHjsHY3.js} +2 -2
- package/public/assets/{button-C3CQSr4p.js.map → button-DyHjsHY3.js.map} +1 -1
- package/public/assets/{card-C5XTIPvq.js → card-BoqYxpVW.js} +2 -2
- package/public/assets/{card-C5XTIPvq.js.map → card-BoqYxpVW.js.map} +1 -1
- package/public/assets/{channels-DVRkVeky.js → channels-DAtOnA6o.js} +2 -2
- package/public/assets/{channels-DVRkVeky.js.map → channels-DAtOnA6o.js.map} +1 -1
- package/public/assets/chat-BdbIWZpQ.js +2 -0
- package/public/assets/chat-BdbIWZpQ.js.map +1 -0
- package/public/assets/{connectors-Cqubkw9C.js → connectors-B2grC-hl.js} +2 -2
- package/public/assets/{connectors-Cqubkw9C.js.map → connectors-B2grC-hl.js.map} +1 -1
- package/public/assets/{copy-button-Bil2CjzT.js → copy-button-C67FUG3E.js} +2 -2
- package/public/assets/{copy-button-Bil2CjzT.js.map → copy-button-C67FUG3E.js.map} +1 -1
- package/public/assets/{display-GFbGUG0O.js → display-aTnwsqQh.js} +3 -3
- package/public/assets/{display-GFbGUG0O.js.map → display-aTnwsqQh.js.map} +1 -1
- package/public/assets/{dist-V3wu3BHw.js → dist-BD4Xc13v.js} +2 -2
- package/public/assets/{dist-V3wu3BHw.js.map → dist-BD4Xc13v.js.map} +1 -1
- package/public/assets/{dist-C5z05Vkz.js → dist-BFDfbfNC.js} +2 -2
- package/public/assets/{dist-C5z05Vkz.js.map → dist-BFDfbfNC.js.map} +1 -1
- package/public/assets/{dist-CjyfQThq.js → dist-BJKxqIQd.js} +2 -2
- package/public/assets/{dist-CjyfQThq.js.map → dist-BJKxqIQd.js.map} +1 -1
- package/public/assets/{dist-B8tD9ZcR.js → dist-BNB3fTO-.js} +2 -2
- package/public/assets/{dist-B8tD9ZcR.js.map → dist-BNB3fTO-.js.map} +1 -1
- package/public/assets/{dist-CFtFIhu3.js → dist-CHKra2JV.js} +2 -2
- package/public/assets/{dist-CFtFIhu3.js.map → dist-CHKra2JV.js.map} +1 -1
- package/public/assets/{dist-r8wyUTkK.js → dist-CQMQ5iku.js} +2 -2
- package/public/assets/{dist-r8wyUTkK.js.map → dist-CQMQ5iku.js.map} +1 -1
- package/public/assets/{dist-DKmTXfpP.js → dist-C_VOBSaK.js} +2 -2
- package/public/assets/{dist-DKmTXfpP.js.map → dist-C_VOBSaK.js.map} +1 -1
- package/public/assets/{dist-CGaHpxnN.js → dist-D5w04z0l.js} +2 -2
- package/public/assets/{dist-CGaHpxnN.js.map → dist-D5w04z0l.js.map} +1 -1
- package/public/assets/{dist-4VwhnLtj.js → dist-DG75bvPp.js} +2 -2
- package/public/assets/{dist-4VwhnLtj.js.map → dist-DG75bvPp.js.map} +1 -1
- package/public/assets/{dist-CrUl2aQp.js → dist-G8YBD-wp.js} +2 -2
- package/public/assets/{dist-CrUl2aQp.js.map → dist-G8YBD-wp.js.map} +1 -1
- package/public/assets/{dist-BJaV2BD4.js → dist-f6MW74eX.js} +2 -2
- package/public/assets/{dist-BJaV2BD4.js.map → dist-f6MW74eX.js.map} +1 -1
- package/public/assets/{es2015-5DTJe35n.js → es2015-a6fysEVz.js} +2 -2
- package/public/assets/{es2015-5DTJe35n.js.map → es2015-a6fysEVz.js.map} +1 -1
- package/public/assets/{formatted-body-impl-L1EMnNW6.js → formatted-body-impl-gOLBKeyW.js} +2 -2
- package/public/assets/{formatted-body-impl-L1EMnNW6.js.map → formatted-body-impl-gOLBKeyW.js.map} +1 -1
- package/public/assets/formatted-body-jjhFXTap.js +3 -0
- package/public/assets/{formatted-body-BYQ-SPtj.js.map → formatted-body-jjhFXTap.js.map} +1 -1
- package/public/assets/index-CgH1BZwq.js +26 -0
- package/public/assets/index-CgH1BZwq.js.map +1 -0
- package/public/assets/index-DHzXsQot.css +2 -0
- package/public/assets/{input-CXMbJ848.js → input-Rc4oWv4q.js} +2 -2
- package/public/assets/{input-CXMbJ848.js.map → input-Rc4oWv4q.js.map} +1 -1
- package/public/assets/{insights-DwZxq1Dn.js → insights-CTW85An9.js} +2 -2
- package/public/assets/{insights-DwZxq1Dn.js.map → insights-CTW85An9.js.map} +1 -1
- package/public/assets/{integrations-9G5TJnnU.js → integrations-9c6IdRU4.js} +2 -2
- package/public/assets/{integrations-9G5TJnnU.js.map → integrations-9c6IdRU4.js.map} +1 -1
- package/public/assets/{lib-BcUW1l-W.js → lib-BZ6WPg2t.js} +2 -2
- package/public/assets/{lib-BcUW1l-W.js.map → lib-BZ6WPg2t.js.map} +1 -1
- package/public/assets/lucide-react-NXHPIMsX.js +10 -0
- package/public/assets/lucide-react-NXHPIMsX.js.map +1 -0
- package/public/assets/{maintenance-COcmUDnf.js → maintenance-D5PEygZz.js} +2 -2
- package/public/assets/{maintenance-COcmUDnf.js.map → maintenance-D5PEygZz.js.map} +1 -1
- package/public/assets/{managed-agents-D9-dgMeX.js → managed-agents-pkpAzZy8.js} +2 -2
- package/public/assets/{managed-agents-D9-dgMeX.js.map → managed-agents-pkpAzZy8.js.map} +1 -1
- package/public/assets/{markdown-preview-impl-DH372bL-.js → markdown-preview-impl-DMzFE-Lo.js} +2 -2
- package/public/assets/{markdown-preview-impl-DH372bL-.js.map → markdown-preview-impl-DMzFE-Lo.js.map} +1 -1
- package/public/assets/{memory-B9hRK8sk.js → memory-BnquVYQz.js} +2 -2
- package/public/assets/{memory-B9hRK8sk.js.map → memory-BnquVYQz.js.map} +1 -1
- package/public/assets/{messages-BHkHlXzF.js → messages-DgxLlndC.js} +2 -2
- package/public/assets/{messages-BHkHlXzF.js.map → messages-DgxLlndC.js.map} +1 -1
- package/public/assets/{orchestrators-CXy3SDSn.js → orchestrators-BaWxeAlK.js} +2 -2
- package/public/assets/{orchestrators-CXy3SDSn.js.map → orchestrators-BaWxeAlK.js.map} +1 -1
- package/public/assets/{overview-D2yRyCQq.js → overview-Kvs-j2gg.js} +2 -2
- package/public/assets/{overview-D2yRyCQq.js.map → overview-Kvs-j2gg.js.map} +1 -1
- package/public/assets/{pairs-5tXdHylI.js → pairs-eDzbqcER.js} +2 -2
- package/public/assets/{pairs-5tXdHylI.js.map → pairs-eDzbqcER.js.map} +1 -1
- package/public/assets/{projects-CquHTMo1.js → projects-iDkxAGBW.js} +2 -2
- package/public/assets/{projects-CquHTMo1.js.map → projects-iDkxAGBW.js.map} +1 -1
- package/public/assets/{prompt-settings-Bn5mmi9w.js → prompt-settings-CjcqBG_r.js} +2 -2
- package/public/assets/{prompt-settings-Bn5mmi9w.js.map → prompt-settings-CjcqBG_r.js.map} +1 -1
- package/public/assets/{react-dom-BQICbUuF.js → react-dom-8yaD9iJG.js} +2 -2
- package/public/assets/{react-dom-BQICbUuF.js.map → react-dom-8yaD9iJG.js.map} +1 -1
- package/public/assets/{registry-C3sB7qv5.js → registry-C1DSnjjg.js} +2 -2
- package/public/assets/{registry-C3sB7qv5.js.map → registry-C1DSnjjg.js.map} +1 -1
- package/public/assets/{security-CmD-YE-_.js → security-VbEe_jwm.js} +2 -2
- package/public/assets/{security-CmD-YE-_.js.map → security-VbEe_jwm.js.map} +1 -1
- package/public/assets/{select-C0UlhK2k.js → select-CVfwgfKl.js} +2 -2
- package/public/assets/{select-C0UlhK2k.js.map → select-CVfwgfKl.js.map} +1 -1
- package/public/assets/{settings-DrqMG5nG.js → settings-ujA9hZW1.js} +4 -4
- package/public/assets/{settings-DrqMG5nG.js.map → settings-ujA9hZW1.js.map} +1 -1
- package/public/assets/store-D-Frk_EC.js +9 -0
- package/public/assets/store-D-Frk_EC.js.map +1 -0
- package/public/assets/{task-chip-BHM-e3I8.js → task-chip-CeujtXYT.js} +2 -2
- package/public/assets/{task-chip-BHM-e3I8.js.map → task-chip-CeujtXYT.js.map} +1 -1
- package/public/assets/{tasks-ChQjtXHu.js → tasks-BtbKaa-b.js} +2 -2
- package/public/assets/{tasks-ChQjtXHu.js.map → tasks-BtbKaa-b.js.map} +1 -1
- package/public/assets/{teams-itjNlDLp.js → teams-CrVDQ99v.js} +2 -2
- package/public/assets/{teams-itjNlDLp.js.map → teams-CrVDQ99v.js.map} +1 -1
- package/public/assets/{terminal-viewer-impl-B9M6s2vB.js → terminal-viewer-impl-Aays_e7-.js} +2 -2
- package/public/assets/{terminal-viewer-impl-B9M6s2vB.js.map → terminal-viewer-impl-Aays_e7-.js.map} +1 -1
- package/public/assets/{types-CRptbSYh.js → types-BqeSkWrv.js} +2 -2
- package/public/assets/types-BqeSkWrv.js.map +1 -0
- package/public/assets/{use-keyboard-viewport-CAa7IQTm.js → use-keyboard-viewport-cGEmrGE7.js} +2 -2
- package/public/assets/{use-keyboard-viewport-CAa7IQTm.js.map → use-keyboard-viewport-cGEmrGE7.js.map} +1 -1
- package/public/assets/{user-avatar-kYPfLgXp.js → user-avatar-qbo-B3dQ.js} +2 -2
- package/public/assets/{user-avatar-kYPfLgXp.js.map → user-avatar-qbo-B3dQ.js.map} +1 -1
- package/public/assets/{work-queue-Kw0uMkEF.js → work-queue-DDSGZO4Z.js} +2 -2
- package/public/assets/{work-queue-Kw0uMkEF.js.map → work-queue-DDSGZO4Z.js.map} +1 -1
- package/public/assets/{workspaces-CR6f7G0R.js → workspaces-CoDz-iZd.js} +2 -2
- package/public/assets/{workspaces-CR6f7G0R.js.map → workspaces-CoDz-iZd.js.map} +1 -1
- package/public/index.html +26 -26
- package/runner/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/runner/plugins/claude/hooks/relay-status.sh +11 -1
- package/runner/plugins/claude/hooks/stop.sh +6 -1
- package/runner/plugins/claude/hooks/user-prompt-submit.sh +6 -1
- package/runner/src/adapter.ts +5 -1
- package/runner/src/adapters/claude-transcript.ts +52 -58
- package/runner/src/adapters/codex-agent-message-capture.ts +33 -31
- package/runner/src/adapters/codex.ts +30 -12
- package/runner/src/control-server.ts +9 -4
- package/runner/src/response-capture-report.ts +4 -1
- package/runner/src/runner-core.ts +108 -46
- package/runner/src/runner-helpers.ts +31 -0
- package/src/agent-lifecycle-events.ts +8 -0
- package/src/bus.ts +0 -24
- package/src/db/agents.ts +15 -1
- package/src/db/messages.ts +150 -43
- package/src/maintenance/jobs.ts +8 -2
- package/src/mcp/tools-workspace.ts +4 -0
- package/src/routes/agent-sessions.ts +10 -7
- package/src/routes/agents.ts +0 -4
- package/src/routes/orchestrator.ts +5 -2
- package/src/routes/settings.ts +31 -2
- package/src/services/restart-instructions.ts +36 -0
- package/src/services/send-message.ts +7 -18
- package/src/services/shutdown-agent.ts +6 -0
- package/src/services/task-entity.ts +4 -0
- package/src/services/transient-agent-reaper.ts +40 -1
- package/src/services/work-finality.ts +25 -0
- package/src/settings/registry.ts +14 -3
- package/public/assets/agent-profiles-DXysA9To.js +0 -2
- package/public/assets/chat-Clz6qOtn.js +0 -2
- package/public/assets/chat-Clz6qOtn.js.map +0 -1
- package/public/assets/formatted-body-BYQ-SPtj.js +0 -3
- package/public/assets/index-0uIGXT14.js +0 -25
- package/public/assets/index-0uIGXT14.js.map +0 -1
- package/public/assets/index-DOJ7KidA.css +0 -2
- package/public/assets/lucide-react-CoU_Xts2.js +0 -10
- package/public/assets/lucide-react-CoU_Xts2.js.map +0 -1
- package/public/assets/store-zFLzd0Jd.js +0 -9
- package/public/assets/store-zFLzd0Jd.js.map +0 -1
- package/public/assets/types-CRptbSYh.js.map +0 -1
|
@@ -46,6 +46,16 @@ interface TranscriptEntry {
|
|
|
46
46
|
// Events/countSubstantiveTurns) intentionally do NOT filter — changing them
|
|
47
47
|
// would shift the #184/#185 baselines, a separate concern.
|
|
48
48
|
isSidechain?: boolean;
|
|
49
|
+
// Every entry carries its own `uuid` and its `parentUuid` (the entry it was
|
|
50
|
+
// appended after), forming a causal chain for the whole session. A `user`-type
|
|
51
|
+
// entry — whether the real prompt or a synthetic tool_result — is stamped with
|
|
52
|
+
// `promptId`: the same UUID Claude's Stop-hook payload reports as `prompt_id`,
|
|
53
|
+
// shared by every entry from that prompt until the next one. Assistant entries
|
|
54
|
+
// don't carry `promptId` directly; their turn is resolved by walking `parentUuid`
|
|
55
|
+
// back to the nearest ancestor that has one (see resolveEntryPromptId).
|
|
56
|
+
uuid?: string;
|
|
57
|
+
parentUuid?: string;
|
|
58
|
+
promptId?: string;
|
|
49
59
|
}
|
|
50
60
|
|
|
51
61
|
function blocks(message: TranscriptMessage | undefined): TranscriptBlock[] {
|
|
@@ -147,59 +157,39 @@ export function extractLastAssistantTurnAfterEntry(jsonl: string, afterEntry: nu
|
|
|
147
157
|
}
|
|
148
158
|
|
|
149
159
|
/**
|
|
150
|
-
*
|
|
151
|
-
*
|
|
160
|
+
* Resolve the `promptId` (Stop hook's `prompt_id`) a transcript entry belongs to.
|
|
161
|
+
* A `user`-type entry (the real prompt, or a synthetic tool_result) carries its own
|
|
162
|
+
* `promptId`; an assistant entry doesn't, so its turn is resolved by walking
|
|
163
|
+
* `parentUuid` back through the causal chain until an ancestor with one is found.
|
|
164
|
+
* Returns undefined for an entry with no resolvable ancestor (e.g. the very first
|
|
165
|
+
* turn of a process, before any `promptId` has ever been stamped).
|
|
152
166
|
*/
|
|
153
|
-
|
|
154
|
-
let
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* Returns only the text from the LAST assistant entry in the current turn.
|
|
164
|
-
* Unlike extractLastAssistantTurn which collects all intermediate text between
|
|
165
|
-
* tool calls, this returns just the final response — what the user sees as the
|
|
166
|
-
* concluding message.
|
|
167
|
-
*/
|
|
168
|
-
export function extractFinalAssistantMessage(jsonl: string): string {
|
|
169
|
-
const lines = jsonl.split("\n");
|
|
170
|
-
let lastText = "";
|
|
171
|
-
let pastLastUserPrompt = false;
|
|
172
|
-
for (const line of lines) {
|
|
173
|
-
const trimmed = line.trim();
|
|
174
|
-
if (!trimmed) continue;
|
|
175
|
-
let entry: TranscriptEntry;
|
|
176
|
-
try {
|
|
177
|
-
entry = JSON.parse(trimmed) as TranscriptEntry;
|
|
178
|
-
} catch {
|
|
179
|
-
continue;
|
|
180
|
-
}
|
|
181
|
-
if (isSidechainEntry(entry)) continue;
|
|
182
|
-
if (isRealUserPrompt(entry)) {
|
|
183
|
-
pastLastUserPrompt = true;
|
|
184
|
-
lastText = "";
|
|
185
|
-
continue;
|
|
186
|
-
}
|
|
187
|
-
if (!pastLastUserPrompt) continue;
|
|
188
|
-
const text = assistantText(entry);
|
|
189
|
-
if (text) lastText = text;
|
|
167
|
+
function resolveEntryPromptId(entry: TranscriptEntry, byUuid: ReadonlyMap<string, TranscriptEntry>): string | undefined {
|
|
168
|
+
let current: TranscriptEntry | undefined = entry;
|
|
169
|
+
const visited = new Set<string>();
|
|
170
|
+
while (current) {
|
|
171
|
+
if (typeof current.promptId === "string" && current.promptId) return current.promptId;
|
|
172
|
+
const parentUuid = current.parentUuid;
|
|
173
|
+
if (!parentUuid || visited.has(parentUuid)) return undefined;
|
|
174
|
+
visited.add(parentUuid);
|
|
175
|
+
current = byUuid.get(parentUuid);
|
|
190
176
|
}
|
|
191
|
-
return
|
|
177
|
+
return undefined;
|
|
192
178
|
}
|
|
193
179
|
|
|
194
180
|
/**
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
181
|
+
* Deterministic replacement for extractLastAssistantTurnAfterEntry's "walk back to
|
|
182
|
+
* the last real user prompt" heuristic: collects assistant `text` blocks for entries
|
|
183
|
+
* whose resolved promptId (see resolveEntryPromptId) equals the Claude-native
|
|
184
|
+
* `promptId` the Stop hook reported for this turn. Skips the first `afterEntry`
|
|
185
|
+
* entries the same way, for pre-flush resume. Since entries never move once
|
|
186
|
+
* written, a turn's promptId membership is exact — no text-shape guessing about
|
|
187
|
+
* what counts as a "real" prompt.
|
|
198
188
|
*/
|
|
199
|
-
export function
|
|
189
|
+
export function extractAssistantTurnForPrompt(jsonl: string, promptId: string, afterEntry = 0): string {
|
|
200
190
|
const lines = jsonl.split("\n");
|
|
201
|
-
|
|
202
|
-
|
|
191
|
+
const byUuid = new Map<string, TranscriptEntry>();
|
|
192
|
+
const collected: string[] = [];
|
|
203
193
|
let entryIndex = 0;
|
|
204
194
|
for (const line of lines) {
|
|
205
195
|
const trimmed = line.trim();
|
|
@@ -211,26 +201,30 @@ export function extractFinalAssistantMessageAfterEntry(jsonl: string, afterEntry
|
|
|
211
201
|
continue;
|
|
212
202
|
}
|
|
213
203
|
entryIndex++;
|
|
204
|
+
if (typeof entry.uuid === "string" && entry.uuid) byUuid.set(entry.uuid, entry);
|
|
214
205
|
if (isSidechainEntry(entry)) continue;
|
|
215
206
|
if (entryIndex <= afterEntry) continue;
|
|
216
|
-
if (
|
|
217
|
-
|
|
218
|
-
lastText = "";
|
|
219
|
-
continue;
|
|
220
|
-
}
|
|
221
|
-
if (!pastLastUserPrompt) continue;
|
|
207
|
+
if (entry.type !== "assistant") continue;
|
|
208
|
+
if (resolveEntryPromptId(entry, byUuid) !== promptId) continue;
|
|
222
209
|
const text = assistantText(entry);
|
|
223
|
-
if (text)
|
|
210
|
+
if (text) collected.push(text);
|
|
224
211
|
}
|
|
225
|
-
return
|
|
212
|
+
return collected.join("\n\n").trim();
|
|
226
213
|
}
|
|
227
214
|
|
|
228
215
|
/**
|
|
229
|
-
*
|
|
230
|
-
*
|
|
231
|
-
* string or an array of content blocks (same shape as transcript entries).
|
|
232
|
-
* Thinking and tool_use blocks are dropped, matching extractLastAssistantTurn.
|
|
216
|
+
* Count valid JSON entries in a JSONL string. Used to record the high-water-mark
|
|
217
|
+
* after a pre-flush so the Stop hook knows where to resume in full-capture mode.
|
|
233
218
|
*/
|
|
219
|
+
export function countTranscriptEntries(jsonl: string): number {
|
|
220
|
+
let count = 0;
|
|
221
|
+
for (const line of jsonl.split("\n")) {
|
|
222
|
+
if (!line.trim()) continue;
|
|
223
|
+
try { JSON.parse(line); count++; } catch {}
|
|
224
|
+
}
|
|
225
|
+
return count;
|
|
226
|
+
}
|
|
227
|
+
|
|
234
228
|
/**
|
|
235
229
|
* Extract the ordered narration, reasoning, and tool steps for the most recent
|
|
236
230
|
* turn (since the last real user prompt). Used by the reasoning tailer to stream
|
|
@@ -4,7 +4,6 @@ type CompleteItemOptions = { includeInResponse?: boolean; phase?: unknown };
|
|
|
4
4
|
|
|
5
5
|
const MAX_CAPTURED_RESPONSE_CHARS = 16_000;
|
|
6
6
|
const TRUNCATION_MARKER_PREFIX = "[truncated:";
|
|
7
|
-
const TERSE_FINAL_MAX_CHARS = 280;
|
|
8
7
|
|
|
9
8
|
export class CodexAgentMessageCapture {
|
|
10
9
|
private segments: ResponseEntry[][] = [];
|
|
@@ -46,11 +45,16 @@ export class CodexAgentMessageCapture {
|
|
|
46
45
|
this.currentSegment = [];
|
|
47
46
|
}
|
|
48
47
|
|
|
48
|
+
// Deterministic turn-final response: prefer the trailing structured report span, not a generic
|
|
49
|
+
// post-handoff coda. The original #1079/#1086 trailing-run rule correctly kept early narration
|
|
50
|
+
// out, but it over-trimmed Codex turns shaped "summary -> workspace ready -> Landed..." and made
|
|
51
|
+
// report-up deliver only the coda (#1136). We still drop leading narration; we just let structured
|
|
52
|
+
// report markers beat a terse final status line.
|
|
49
53
|
finish(emit: EmitAgentMessage): string | undefined {
|
|
50
54
|
this.closeSegment(emit);
|
|
51
|
-
const
|
|
55
|
+
const trailingSegment = selectFinalResponseSegments(this.segments);
|
|
52
56
|
this.segments = [];
|
|
53
|
-
const text =
|
|
57
|
+
const text = trailingSegment ? trailingSegment.map(segmentBody).join("\n\n").trim() : undefined;
|
|
54
58
|
return truncateCapturedResponse(text);
|
|
55
59
|
}
|
|
56
60
|
|
|
@@ -96,41 +100,39 @@ export class CodexAgentMessageCapture {
|
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
|
|
99
|
-
function
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
for (let
|
|
111
|
-
|
|
112
|
-
if (!candidate || !isSubstantiveResponseSegment(segmentBody(candidate))) break;
|
|
113
|
-
selectedSegments.unshift(candidate);
|
|
103
|
+
function segmentBody(segment: ResponseEntry[]): string {
|
|
104
|
+
return segment.map((entry) => entry.body).join("\n\n").trim();
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function selectFinalResponseSegments(segments: ResponseEntry[][]): ResponseEntry[][] | undefined {
|
|
108
|
+
const bodies = segments.map(segmentBody).filter(Boolean);
|
|
109
|
+
if (!bodies.length) return undefined;
|
|
110
|
+
let end = bodies.length - 1;
|
|
111
|
+
while (end > 0 && isGenericFinalCoda(bodies[end]!) && bodies.slice(0, end).some(isSubstantiveReportSegment)) end--;
|
|
112
|
+
const firstReport = bodies.slice(0, end + 1).findIndex(isReportBoundarySegment);
|
|
113
|
+
if (firstReport >= 0) return segments.slice(firstReport, end + 1);
|
|
114
|
+
for (let i = end; i >= 0; i--) {
|
|
115
|
+
if (isSubstantiveReportSegment(bodies[i]!)) return [segments[i]!];
|
|
114
116
|
}
|
|
115
|
-
return [
|
|
117
|
+
return [segments[end]!];
|
|
116
118
|
}
|
|
117
119
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
+
const REPORT_BOUNDARY = /(^|\n)(#{1,6}\s|\*\*(summary|what changed|rca|fix|tests?|gates?|verification|risk)\b|(?:summary|what changed|rca|fix|tests?|gates?|verification|risk):)/i;
|
|
121
|
+
const REPORT_STRUCTURE = /(^|\n)\s*[-*]\s+\S|`[^`]+`|\b(typecheck|bun test|tests? pass(?:ed)?|gates? green|rca|verified|implemented)\b/i;
|
|
122
|
+
const GENERIC_FINAL_CODA = /^(?:done|landed(?: on main)?|committed|merged|ready|no further action|nothing further|all set)\b|(?:\bno further action needed\b|\bnothing further needed\b)$/i;
|
|
123
|
+
|
|
124
|
+
function isReportBoundarySegment(body: string): boolean {
|
|
125
|
+
return REPORT_BOUNDARY.test(body.trim());
|
|
120
126
|
}
|
|
121
127
|
|
|
122
|
-
function
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
return /\b(?:landed|ready for review|handed off|no further action|done|complete|completed|committed|merged)\b/i.test(body);
|
|
128
|
+
function isSubstantiveReportSegment(body: string): boolean {
|
|
129
|
+
const trimmed = body.trim();
|
|
130
|
+
return !isGenericFinalCoda(trimmed) && (trimmed.length >= 120 || isReportBoundarySegment(trimmed) || REPORT_STRUCTURE.test(trimmed));
|
|
126
131
|
}
|
|
127
132
|
|
|
128
|
-
function
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
if (/\*\*(?:what changed|changes?|tests?|gates?|verification|risks?|residual risk|summary)\b/i.test(body)) return true;
|
|
132
|
-
if (/^#{1,3}\s+\S/m.test(body)) return true;
|
|
133
|
-
return body.split(/\n{2,}/).length >= 2;
|
|
133
|
+
function isGenericFinalCoda(body: string): boolean {
|
|
134
|
+
const trimmed = body.trim();
|
|
135
|
+
return trimmed.length <= 180 && GENERIC_FINAL_CODA.test(trimmed) && !isReportBoundarySegment(trimmed);
|
|
134
136
|
}
|
|
135
137
|
|
|
136
138
|
function truncateCapturedResponse(text: string | undefined): string | undefined {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { accessSync, constants, existsSync, readFileSync, realpathSync, readdirSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { basename, join, resolve } from "node:path";
|
|
4
|
-
import type { ContextState, InteractivePrompt, LivenessSignal, Message, ProviderState } from "agent-relay-sdk";
|
|
4
|
+
import type { ContextState, InteractivePrompt, LivenessSignal, Message, MessageTurnEndReason, ProviderState } from "agent-relay-sdk";
|
|
5
5
|
import { isRecord, stringValue } from "agent-relay-sdk";
|
|
6
6
|
import { isPidAlive, killPid, processTreePids, processTreePidsFromTable, waitForPidsExit } from "agent-relay-sdk/process-utils";
|
|
7
7
|
import { profileAllowsRelayFeature, providerMessageText, relayContext, type ManagedProcess, type ProviderAdapter, type ProviderConfig, type ProviderPermissionDecisionInput, type ProviderSessionEvent, type ProviderStatusUpdate, type RunnerSpawnConfig, type SpawnArgs, type TerminalAttachSpec } from "../adapter";
|
|
@@ -144,13 +144,13 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
144
144
|
const turnStatus = isRecord(activeTurn) ? stringValue(activeTurn.status) : undefined;
|
|
145
145
|
if (turnStatus === "inProgress") return "busy";
|
|
146
146
|
if (turnStatus === "completed" || turnStatus === "interrupted" || turnStatus === "failed") {
|
|
147
|
-
this.finishMainTurn();
|
|
147
|
+
this.finishMainTurn(codexEndedByFrom(turnStatus));
|
|
148
148
|
return "idle";
|
|
149
149
|
}
|
|
150
150
|
const threadStatus = statusType(thread?.status);
|
|
151
151
|
if (threadStatus === "active") return "busy";
|
|
152
152
|
if (threadStatus === "idle" || threadStatus === "notLoaded" || threadStatus === "systemError") {
|
|
153
|
-
this.finishMainTurn();
|
|
153
|
+
this.finishMainTurn(codexEndedByFrom(threadStatus));
|
|
154
154
|
return "idle";
|
|
155
155
|
}
|
|
156
156
|
} catch {
|
|
@@ -508,7 +508,7 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
508
508
|
if (threadId && this.subagentThreads.has(threadId)) {
|
|
509
509
|
this.statusCb({ status: "idle", reason: "subagent", id: threadId, ...this.subagentThreads.get(threadId) });
|
|
510
510
|
} else {
|
|
511
|
-
this.finishMainTurn();
|
|
511
|
+
this.finishMainTurn(codexEndedByFrom(method));
|
|
512
512
|
}
|
|
513
513
|
}
|
|
514
514
|
if ((method.includes("thread/compacted") || method.includes("thread.compacted")) && threadId) {
|
|
@@ -527,15 +527,16 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
527
527
|
if (status === "idle" || status === "notLoaded") this.statusCb({ status: "idle", reason: "subagent", id: threadId, ...this.subagentThreads.get(threadId) });
|
|
528
528
|
} else {
|
|
529
529
|
if (status === "active") this.statusCb({ status: "busy", reason: "provider-turn", providerState: this.providerStateFromThreadStatus(params?.status, params) });
|
|
530
|
-
if (status === "idle" || status === "notLoaded" || status === "systemError") this.finishMainTurn();
|
|
530
|
+
if (status === "idle" || status === "notLoaded" || status === "systemError") this.finishMainTurn(codexEndedByFrom(status));
|
|
531
531
|
}
|
|
532
532
|
}
|
|
533
533
|
}
|
|
534
534
|
|
|
535
535
|
// Turn one completed Codex thread item into a session-mirror event. agentMessage text is
|
|
536
|
-
// mirrored as narration as it lands; the captured turn-final response repeats the
|
|
537
|
-
//
|
|
538
|
-
//
|
|
536
|
+
// mirrored as narration as it lands; the captured turn-final response repeats only the
|
|
537
|
+
// TRAILING agentMessage run — the segment after the last tool/file-change/reasoning
|
|
538
|
+
// boundary (design #1079 §3.2) — never earlier mid-work narration, which was already
|
|
539
|
+
// mirrored live. The rest (user prompt echo, reasoning, tool steps) is surfaced as it lands.
|
|
539
540
|
private handleCodexItem(item: Record<string, unknown> | undefined): void {
|
|
540
541
|
if (!item) return;
|
|
541
542
|
const type = stringValue(item.type);
|
|
@@ -676,14 +677,23 @@ export class CodexAdapter implements ProviderAdapter {
|
|
|
676
677
|
if (method.includes("outputDelta") || method.includes("output_delta") || method.includes("progress")) return;
|
|
677
678
|
}
|
|
678
679
|
|
|
679
|
-
|
|
680
|
+
// A turn-final capture is emitted ONLY for a genuine `completed` turn (design #1079 §3.2):
|
|
681
|
+
// failed/interrupted turns must never produce a turn-final. Every "response"-type
|
|
682
|
+
// ProviderSessionEvent is unconditionally dual-stamped turnFinal/final downstream
|
|
683
|
+
// (publishCapturedResponse's provider-agnostic stamp), so the only way to keep a
|
|
684
|
+
// failed/interrupted partial OUT of D's work-final ring is to never emit it as a
|
|
685
|
+
// "response" event at all (#1083 Finding 1). The partial text was already mirrored live
|
|
686
|
+
// as narration by the `emit` callback inside agentMessageCapture.finish(), so it's still
|
|
687
|
+
// visible in the dashboard.
|
|
688
|
+
private flushTurnResponse(endedBy: MessageTurnEndReason): void {
|
|
680
689
|
const text = this.agentMessageCapture.finish((body) => this.emitAgentNarration(body));
|
|
681
|
-
if (text
|
|
690
|
+
if (!text || endedBy !== "completed") return;
|
|
691
|
+
this.sessionEventCb({ type: "response", origin: "provider", body: text, final: true, confidence: "definitive", endedBy, ...(this.activeTurnId ? { turnId: this.activeTurnId } : {}) });
|
|
682
692
|
}
|
|
683
693
|
|
|
684
|
-
private finishMainTurn(): void {
|
|
694
|
+
private finishMainTurn(endedBy: MessageTurnEndReason): void {
|
|
685
695
|
this.flushBufferedReasoning(); // surface any trailing reasoning before the closing response
|
|
686
|
-
this.flushTurnResponse();
|
|
696
|
+
this.flushTurnResponse(endedBy);
|
|
687
697
|
const turnId = this.activeTurnId;
|
|
688
698
|
this.activeTurnId = undefined;
|
|
689
699
|
this.pendingApprovals.clear();
|
|
@@ -1151,6 +1161,14 @@ function statusType(value: unknown): string | undefined {
|
|
|
1151
1161
|
return isRecord(value) ? stringValue(value.type) : undefined;
|
|
1152
1162
|
}
|
|
1153
1163
|
|
|
1164
|
+
// Maps any Codex turn-ending signal — a turn/failed|interrupted (dot-notation too) app-server
|
|
1165
|
+
// method name, a turn status ("completed"/"interrupted"/"failed"), or a thread status
|
|
1166
|
+
// ("idle"/"notLoaded"/"systemError") — to the Part A (#1082) turn-end reason. "completed" is
|
|
1167
|
+
// the default for every signal that isn't explicitly a failure or an interruption.
|
|
1168
|
+
function codexEndedByFrom(value: string | undefined): MessageTurnEndReason {
|
|
1169
|
+
return value === "systemError" || value?.includes("failed") ? "failed" : value?.includes("interrupted") ? "interrupted" : "completed";
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1154
1172
|
function activeFlags(value: unknown): string[] {
|
|
1155
1173
|
if (!isRecord(value) || !Array.isArray(value.activeFlags)) return [];
|
|
1156
1174
|
return value.activeFlags.filter((flag): flag is string => typeof flag === "string" && flag.length > 0);
|
|
@@ -49,12 +49,15 @@ interface ControlServerOptions {
|
|
|
49
49
|
// dashboard chat without the agent re-emitting it via /reply.
|
|
50
50
|
// isPreFlush: true signals a mid-turn call from a PreToolUse hook — flush any
|
|
51
51
|
// un-mirrored narrative before the interactive form appears in chat (#435).
|
|
52
|
-
|
|
52
|
+
// promptId (#1086): Claude's native `prompt_id` from the Stop hook payload — the
|
|
53
|
+
// stable, provider-native turn identifier used instead of a minted random UUID.
|
|
54
|
+
onSessionTurn?(input: { transcriptPath: string; lastAssistantMessage?: unknown; promptId?: string; isPreFlush?: boolean }): Promise<void>;
|
|
53
55
|
onPermissionResolved?(input: ResolvedPermissionPrompt): Promise<void> | void;
|
|
54
56
|
// A provider UserPromptSubmit hook hands over the prompt the human typed
|
|
55
57
|
// directly into the session (web terminal / TUI) so the runner can mirror it
|
|
56
58
|
// into the dashboard chat and start tailing the turn transcript for reasoning.
|
|
57
|
-
|
|
59
|
+
// promptId (#1086): Claude's native `prompt_id` for the turn this prompt starts.
|
|
60
|
+
onUserPrompt?(input: { prompt: string; transcriptPath?: string; promptId?: string }): Promise<void>;
|
|
58
61
|
// A provider session-boundary hook (Claude PreCompact / SessionEnd) signals an imminent
|
|
59
62
|
// context reset or termination so the runner can run end-of-session work (#183 pre-destroy
|
|
60
63
|
// seam: #184 context-ratio capture) before the invasive operation. `reason` is the raw
|
|
@@ -501,6 +504,7 @@ async function handleSessionTurn(req: Request, options: ControlServerOptions): P
|
|
|
501
504
|
const body = await req.json().catch(() => null);
|
|
502
505
|
const transcriptPath = isRecord(body) && typeof body.transcriptPath === "string" ? body.transcriptPath : "";
|
|
503
506
|
const lastAssistantMessage = isRecord(body) ? body.lastAssistantMessage : undefined;
|
|
507
|
+
const promptId = isRecord(body) && typeof body.promptId === "string" && body.promptId ? body.promptId : undefined;
|
|
504
508
|
// transcriptPath is the primary source, but it can be absent (known CC Stop edge case).
|
|
505
509
|
// lastAssistantMessage is the fallback for exactly that — accept either; reject only when
|
|
506
510
|
// both are missing so the turn still reaches the dashboard chat (#264).
|
|
@@ -511,7 +515,7 @@ async function handleSessionTurn(req: Request, options: ControlServerOptions): P
|
|
|
511
515
|
// reply-obligation check, so the captured turn must be persisted (and the
|
|
512
516
|
// obligation cleared) before this response returns.
|
|
513
517
|
try {
|
|
514
|
-
await options.onSessionTurn({ transcriptPath, lastAssistantMessage });
|
|
518
|
+
await options.onSessionTurn({ transcriptPath, lastAssistantMessage, ...(promptId ? { promptId } : {}) });
|
|
515
519
|
return Response.json({ ok: true });
|
|
516
520
|
} catch (error) {
|
|
517
521
|
return Response.json({ ok: false, reason: errMessage(error) }, { status: 500 });
|
|
@@ -524,8 +528,9 @@ async function handleUserPrompt(req: Request, options: ControlServerOptions): Pr
|
|
|
524
528
|
const prompt = isRecord(body) && typeof body.prompt === "string" ? body.prompt : "";
|
|
525
529
|
if (!prompt.trim()) return Response.json({ ok: false, reason: "prompt required" }, { status: 400 });
|
|
526
530
|
const transcriptPath = isRecord(body) && typeof body.transcriptPath === "string" ? body.transcriptPath : undefined;
|
|
531
|
+
const promptId = isRecord(body) && typeof body.promptId === "string" && body.promptId ? body.promptId : undefined;
|
|
527
532
|
// Fire-and-forget: the hook must not block Claude's turn on relay round-trips.
|
|
528
|
-
void Promise.resolve(options.onUserPrompt({ prompt, transcriptPath })).catch(() => {});
|
|
533
|
+
void Promise.resolve(options.onUserPrompt({ prompt, transcriptPath, ...(promptId ? { promptId } : {}) })).catch(() => {});
|
|
529
534
|
return Response.json({ ok: true });
|
|
530
535
|
}
|
|
531
536
|
|
|
@@ -12,8 +12,11 @@ type SessionPublisher = (input: {
|
|
|
12
12
|
replyTo?: number;
|
|
13
13
|
}) => void | Promise<void>;
|
|
14
14
|
|
|
15
|
+
// #1086: dual-stamp the Part A (#1082) turn-finality field alongside the legacy
|
|
16
|
+
// `final` alias so old consumers keep working while new ones can key off the
|
|
17
|
+
// typed `turnFinal` flag directly instead of every writer's own `final: true`.
|
|
15
18
|
function finalSession(session: MessageSessionMeta): MessageSessionMeta {
|
|
16
|
-
return { ...session, final: true };
|
|
19
|
+
return { ...session, turnFinal: true, final: true };
|
|
17
20
|
}
|
|
18
21
|
|
|
19
22
|
export async function publishCapturedResponse(input: {
|