codex-agent-view 0.5.1 → 0.5.2
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/.codex-plugin/plugin.json +2 -3
- package/README.ko.md +53 -257
- package/README.md +48 -180
- package/package.json +1 -1
- package/public/app.js +123 -7
- package/public/index.html +1 -1
- package/public/styles.css +31 -0
- package/scripts/send-hook.mjs +13 -1
- package/src/core/monitor-store.mjs +207 -0
- package/src/core/normalize-hook-payload.mjs +75 -0
package/scripts/send-hook.mjs
CHANGED
|
@@ -5,7 +5,10 @@ import { basename } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
|
|
7
7
|
import { minimizePayload } from "./capture-hook.mjs";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
deriveSpawnAssignmentSummary,
|
|
10
|
+
deriveTaskSummary,
|
|
11
|
+
} from "../src/core/normalize-hook-payload.mjs";
|
|
9
12
|
import { readRuntimeInfo } from "../src/runtime/config.mjs";
|
|
10
13
|
|
|
11
14
|
const MAX_STDIN_BYTES = 2 * 1024 * 1024;
|
|
@@ -13,6 +16,7 @@ const SEND_TIMEOUT_MS = 500;
|
|
|
13
16
|
const AUTO_START_WAIT_MS = 1_600;
|
|
14
17
|
const AUTO_START_POLL_MS = 40;
|
|
15
18
|
const MAX_WORKSPACE_LABEL_LENGTH = 120;
|
|
19
|
+
const OBSERVED_SPAWN_AGENT_TOOL_NAME = "collaborationspawn_agent";
|
|
16
20
|
const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/g;
|
|
17
21
|
|
|
18
22
|
async function readStdin() {
|
|
@@ -59,10 +63,18 @@ function monitorEnvelope(payload) {
|
|
|
59
63
|
payload.hook_event_name === "UserPromptSubmit"
|
|
60
64
|
? deriveTaskSummary(payload.prompt)
|
|
61
65
|
: null;
|
|
66
|
+
const spawnAssignmentObserved =
|
|
67
|
+
payload.hook_event_name === "PreToolUse" &&
|
|
68
|
+
payload.tool_name === OBSERVED_SPAWN_AGENT_TOOL_NAME;
|
|
69
|
+
const assignmentSummary = spawnAssignmentObserved
|
|
70
|
+
? deriveSpawnAssignmentSummary(payload.tool_input)
|
|
71
|
+
: null;
|
|
62
72
|
return {
|
|
63
73
|
...minimized,
|
|
64
74
|
...(workspaceLabel ? { workspace_label: workspaceLabel } : {}),
|
|
65
75
|
...(taskSummary ? { task_summary: taskSummary } : {}),
|
|
76
|
+
...(spawnAssignmentObserved ? { spawn_assignment_observed: true } : {}),
|
|
77
|
+
...(assignmentSummary ? { assignment_summary: assignmentSummary } : {}),
|
|
66
78
|
};
|
|
67
79
|
}
|
|
68
80
|
|
|
@@ -8,6 +8,10 @@ const DEFAULT_LIMITS = Object.freeze({
|
|
|
8
8
|
staleAfterMs: 5 * 60 * 1000,
|
|
9
9
|
});
|
|
10
10
|
|
|
11
|
+
const OBSERVED_SPAWN_AGENT_TOOL_NAME = "collaborationspawn_agent";
|
|
12
|
+
const SPAWN_ASSIGNMENT_MATCH_WINDOW_MS = 15_000;
|
|
13
|
+
const PENDING_SPAWN_PRE_TTL_MS = 30_000;
|
|
14
|
+
|
|
11
15
|
function positiveInteger(value, name) {
|
|
12
16
|
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
13
17
|
throw new TypeError(`${name} must be a positive safe integer`);
|
|
@@ -26,6 +30,8 @@ function createSession(event) {
|
|
|
26
30
|
last_seen_at_ms: event.received_at_ms,
|
|
27
31
|
agents: new Map(),
|
|
28
32
|
tools: new Map(),
|
|
33
|
+
pending_spawn_assignments: new Map(),
|
|
34
|
+
pending_spawn_assignment_overflowed: false,
|
|
29
35
|
lifecycle: {
|
|
30
36
|
start_observed: false,
|
|
31
37
|
end_observed: false,
|
|
@@ -119,6 +125,7 @@ function settleRunningState(session, status, options = {}) {
|
|
|
119
125
|
const turnId = options.turnId;
|
|
120
126
|
const settledAgentIds = new Set();
|
|
121
127
|
const settledToolUseIds = new Set();
|
|
128
|
+
const settledTurnIds = new Set();
|
|
122
129
|
for (const agent of session.agents.values()) {
|
|
123
130
|
if (
|
|
124
131
|
agent.status === "running" &&
|
|
@@ -135,6 +142,7 @@ function settleRunningState(session, status, options = {}) {
|
|
|
135
142
|
) {
|
|
136
143
|
tool.status = status;
|
|
137
144
|
settledToolUseIds.add(tool.tool_use_id);
|
|
145
|
+
settledTurnIds.add(tool.turn_id);
|
|
138
146
|
}
|
|
139
147
|
}
|
|
140
148
|
for (const activity of session.recent_activities) {
|
|
@@ -148,6 +156,9 @@ function settleRunningState(session, status, options = {}) {
|
|
|
148
156
|
activity.status = status;
|
|
149
157
|
}
|
|
150
158
|
}
|
|
159
|
+
for (const settledTurnId of settledTurnIds) {
|
|
160
|
+
syncAgentCurrentToolForTurn(session, settledTurnId);
|
|
161
|
+
}
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
function clearPermission(session, activityStatus) {
|
|
@@ -185,6 +196,8 @@ function settleRootTurnActivity(session, status) {
|
|
|
185
196
|
function resetTransientState(session) {
|
|
186
197
|
session.agents.clear();
|
|
187
198
|
session.tools.clear();
|
|
199
|
+
session.pending_spawn_assignments.clear();
|
|
200
|
+
session.pending_spawn_assignment_overflowed = false;
|
|
188
201
|
session.root_turn = {
|
|
189
202
|
status: "idle",
|
|
190
203
|
turn_id: null,
|
|
@@ -206,6 +219,171 @@ function trimMap(map, limit) {
|
|
|
206
219
|
}
|
|
207
220
|
}
|
|
208
221
|
|
|
222
|
+
function prunePendingSpawnAssignments(session, observedAtMs) {
|
|
223
|
+
for (const [toolUseId, candidate] of session.pending_spawn_assignments) {
|
|
224
|
+
const expiresAtMs =
|
|
225
|
+
candidate.post_observed_at_ms === null
|
|
226
|
+
? candidate.pre_observed_at_ms + PENDING_SPAWN_PRE_TTL_MS
|
|
227
|
+
: candidate.post_observed_at_ms + SPAWN_ASSIGNMENT_MATCH_WINDOW_MS;
|
|
228
|
+
if (observedAtMs > expiresAtMs) {
|
|
229
|
+
session.pending_spawn_assignments.delete(toolUseId);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function observeSpawnAssignmentToolEvent(session, event, limit) {
|
|
235
|
+
if (
|
|
236
|
+
event.tool_name !== OBSERVED_SPAWN_AGENT_TOOL_NAME ||
|
|
237
|
+
session.pending_spawn_assignment_overflowed
|
|
238
|
+
) {
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (event.type === "tool_started") {
|
|
243
|
+
if (!("spawn_assignment_observed" in event)) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (
|
|
247
|
+
!session.pending_spawn_assignments.has(event.tool_use_id) &&
|
|
248
|
+
session.pending_spawn_assignments.size >= limit
|
|
249
|
+
) {
|
|
250
|
+
session.pending_spawn_assignments.clear();
|
|
251
|
+
session.pending_spawn_assignment_overflowed = true;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
touchMapEntry(session.pending_spawn_assignments, event.tool_use_id, {
|
|
255
|
+
tool_use_id: event.tool_use_id,
|
|
256
|
+
turn_id: event.turn_id,
|
|
257
|
+
assignment_summary: event.assignment_summary ?? null,
|
|
258
|
+
pre_observed_at_ms: event.received_at_ms,
|
|
259
|
+
post_observed_at_ms: null,
|
|
260
|
+
});
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const candidate = session.pending_spawn_assignments.get(event.tool_use_id);
|
|
265
|
+
if (!candidate) {
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (candidate.turn_id !== event.turn_id) {
|
|
269
|
+
session.pending_spawn_assignments.delete(event.tool_use_id);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
candidate.post_observed_at_ms = event.received_at_ms;
|
|
273
|
+
touchMapEntry(session.pending_spawn_assignments, event.tool_use_id, candidate);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function attachSingletonSpawnAssignment(session, agent) {
|
|
277
|
+
if (
|
|
278
|
+
session.pending_spawn_assignment_overflowed ||
|
|
279
|
+
!agent.start_observed ||
|
|
280
|
+
agent.stop_observed ||
|
|
281
|
+
agent.has_out_of_order_events ||
|
|
282
|
+
"assignment_match" in agent
|
|
283
|
+
) {
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const pendingCandidates = [...session.pending_spawn_assignments.values()];
|
|
288
|
+
if (pendingCandidates.length !== 1) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const [candidate] = pendingCandidates;
|
|
293
|
+
if (
|
|
294
|
+
candidate.post_observed_at_ms === null ||
|
|
295
|
+
candidate.post_observed_at_ms > agent.started_at_ms ||
|
|
296
|
+
agent.started_at_ms - candidate.post_observed_at_ms >
|
|
297
|
+
SPAWN_ASSIGNMENT_MATCH_WINDOW_MS ||
|
|
298
|
+
candidate.assignment_summary === null
|
|
299
|
+
) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const eligibleUnmatchedAgents = [...session.agents.values()].filter(
|
|
303
|
+
(candidateAgent) =>
|
|
304
|
+
candidateAgent.start_observed &&
|
|
305
|
+
!("assignment_match" in candidateAgent) &&
|
|
306
|
+
candidateAgent.started_at_ms >= candidate.post_observed_at_ms &&
|
|
307
|
+
candidateAgent.started_at_ms - candidate.post_observed_at_ms <=
|
|
308
|
+
SPAWN_ASSIGNMENT_MATCH_WINDOW_MS,
|
|
309
|
+
);
|
|
310
|
+
if (
|
|
311
|
+
eligibleUnmatchedAgents.length !== 1 ||
|
|
312
|
+
eligibleUnmatchedAgents[0].agent_id !== agent.agent_id
|
|
313
|
+
) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
agent.assignment_summary = candidate.assignment_summary;
|
|
318
|
+
agent.assignment_match = "best_effort_singleton";
|
|
319
|
+
session.pending_spawn_assignments.delete(candidate.tool_use_id);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function clearAgentCurrentTool(agent) {
|
|
323
|
+
delete agent.current_tool_name;
|
|
324
|
+
delete agent.current_tool_status;
|
|
325
|
+
delete agent.current_tool_observed_at_ms;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function syncAgentCurrentToolForTurn(session, turnId) {
|
|
329
|
+
const matchingAgents = [...session.agents.values()].filter(
|
|
330
|
+
(agent) => agent.turn_id === turnId,
|
|
331
|
+
);
|
|
332
|
+
|
|
333
|
+
if (matchingAgents.length !== 1) {
|
|
334
|
+
for (const agent of matchingAgents) {
|
|
335
|
+
clearAgentCurrentTool(agent);
|
|
336
|
+
}
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const matchingTools = [...session.tools.values()].filter(
|
|
341
|
+
(tool) => tool.turn_id === turnId,
|
|
342
|
+
);
|
|
343
|
+
const runningTools = matchingTools.filter((tool) => tool.status === "running");
|
|
344
|
+
const currentToolCandidates =
|
|
345
|
+
runningTools.length > 0 ? runningTools : matchingTools;
|
|
346
|
+
const latestObservedAtMs = currentToolCandidates.reduce(
|
|
347
|
+
(latest, tool) => Math.max(latest, tool.last_seen_at_ms),
|
|
348
|
+
-1,
|
|
349
|
+
);
|
|
350
|
+
const latestTools = currentToolCandidates.filter(
|
|
351
|
+
(tool) => tool.last_seen_at_ms === latestObservedAtMs,
|
|
352
|
+
);
|
|
353
|
+
const [agent] = matchingAgents;
|
|
354
|
+
if (latestTools.length !== 1) {
|
|
355
|
+
clearAgentCurrentTool(agent);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const [latestTool] = latestTools;
|
|
360
|
+
agent.current_tool_name = latestTool.tool_name;
|
|
361
|
+
agent.current_tool_status = latestTool.status;
|
|
362
|
+
agent.current_tool_observed_at_ms = latestTool.last_seen_at_ms;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function settleRunningToolsForExactAgentTurn(session, turnId) {
|
|
366
|
+
const matchingAgents = [...session.agents.values()].filter(
|
|
367
|
+
(agent) => agent.turn_id === turnId,
|
|
368
|
+
);
|
|
369
|
+
if (matchingAgents.length !== 1) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
for (const tool of session.tools.values()) {
|
|
374
|
+
if (tool.turn_id !== turnId || tool.status !== "running") {
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
tool.status = "completion_not_observed";
|
|
378
|
+
refineUnresolvedStartActivity(session, {
|
|
379
|
+
type: "tool_started",
|
|
380
|
+
idField: "tool_use_id",
|
|
381
|
+
id: tool.tool_use_id,
|
|
382
|
+
status: "completion_not_observed",
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
209
387
|
function addActivity(session, event, status, limit) {
|
|
210
388
|
const activity = {
|
|
211
389
|
type: event.type,
|
|
@@ -301,6 +479,8 @@ function applySessionEvent(session, event, limits) {
|
|
|
301
479
|
session.root_turn.stopped_at_ms ??= event.received_at_ms;
|
|
302
480
|
clearPermission(session, "interrupted");
|
|
303
481
|
settleRunningState(session, "interrupted");
|
|
482
|
+
session.pending_spawn_assignments.clear();
|
|
483
|
+
session.pending_spawn_assignment_overflowed = false;
|
|
304
484
|
addActivity(session, event, "completed", limits.maxActivitiesPerSession);
|
|
305
485
|
return "applied";
|
|
306
486
|
}
|
|
@@ -312,6 +492,8 @@ function applyTurnEvent(session, event, limits) {
|
|
|
312
492
|
return turn.status === "running" ? "duplicate" : "stale";
|
|
313
493
|
}
|
|
314
494
|
settleRunningState(session, "completion_not_observed");
|
|
495
|
+
session.pending_spawn_assignments.clear();
|
|
496
|
+
session.pending_spawn_assignment_overflowed = false;
|
|
315
497
|
session.root_turn = {
|
|
316
498
|
status: "running",
|
|
317
499
|
turn_id: event.turn_id,
|
|
@@ -352,6 +534,11 @@ function applyTurnEvent(session, event, limits) {
|
|
|
352
534
|
settleRunningState(session, "completion_not_observed", {
|
|
353
535
|
turnId: event.turn_id,
|
|
354
536
|
});
|
|
537
|
+
for (const [toolUseId, candidate] of session.pending_spawn_assignments) {
|
|
538
|
+
if (candidate.turn_id === event.turn_id) {
|
|
539
|
+
session.pending_spawn_assignments.delete(toolUseId);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
355
542
|
addActivity(
|
|
356
543
|
session,
|
|
357
544
|
event,
|
|
@@ -413,6 +600,13 @@ function applySubagentEvent(session, event, limits) {
|
|
|
413
600
|
agent.last_seen_at_ms = Math.max(agent.last_seen_at_ms, event.received_at_ms);
|
|
414
601
|
touchMapEntry(session.agents, event.agent_id, agent);
|
|
415
602
|
trimMap(session.agents, limits.maxAgentsPerSession);
|
|
603
|
+
if (event.type === "subagent_started") {
|
|
604
|
+
attachSingletonSpawnAssignment(session, agent);
|
|
605
|
+
}
|
|
606
|
+
if (event.type === "subagent_stopped") {
|
|
607
|
+
settleRunningToolsForExactAgentTurn(session, event.turn_id);
|
|
608
|
+
}
|
|
609
|
+
syncAgentCurrentToolForTurn(session, event.turn_id);
|
|
416
610
|
addActivity(session, event, agent.status, limits.maxActivitiesPerSession);
|
|
417
611
|
return "applied";
|
|
418
612
|
}
|
|
@@ -484,6 +678,12 @@ function applyToolEvent(session, event, limits) {
|
|
|
484
678
|
tool.last_seen_at_ms = Math.max(tool.last_seen_at_ms, event.received_at_ms);
|
|
485
679
|
touchMapEntry(session.tools, event.tool_use_id, tool);
|
|
486
680
|
trimMap(session.tools, limits.maxActivitiesPerSession);
|
|
681
|
+
observeSpawnAssignmentToolEvent(
|
|
682
|
+
session,
|
|
683
|
+
event,
|
|
684
|
+
limits.maxAgentsPerSession,
|
|
685
|
+
);
|
|
686
|
+
syncAgentCurrentToolForTurn(session, event.turn_id);
|
|
487
687
|
addActivity(session, event, activityStatus, limits.maxActivitiesPerSession);
|
|
488
688
|
return "applied";
|
|
489
689
|
}
|
|
@@ -521,6 +721,10 @@ function applyPermissionEvent(session, event, limits) {
|
|
|
521
721
|
}
|
|
522
722
|
|
|
523
723
|
function applyEvent(session, event, limits) {
|
|
724
|
+
prunePendingSpawnAssignments(
|
|
725
|
+
session,
|
|
726
|
+
Math.max(session.last_seen_at_ms, event.received_at_ms),
|
|
727
|
+
);
|
|
524
728
|
if (event.type === "session_started") {
|
|
525
729
|
return applySessionEvent(session, event, limits);
|
|
526
730
|
}
|
|
@@ -571,6 +775,9 @@ function snapshotSession(session, nowMs, staleAfterMs) {
|
|
|
571
775
|
...(staleActive && agent.status === "running"
|
|
572
776
|
? { status: "completion_not_observed" }
|
|
573
777
|
: {}),
|
|
778
|
+
...(staleActive && agent.current_tool_status === "running"
|
|
779
|
+
? { current_tool_status: "completion_not_observed" }
|
|
780
|
+
: {}),
|
|
574
781
|
}))
|
|
575
782
|
.sort((left, right) => right.last_seen_at_ms - left.last_seen_at_ms),
|
|
576
783
|
tools: [...session.tools.values()]
|
|
@@ -23,6 +23,7 @@ const MAX_LABEL_LENGTH = 256;
|
|
|
23
23
|
const MAX_WORKSPACE_LABEL_LENGTH = 120;
|
|
24
24
|
const MAX_PROMPT_INSPECTION_LENGTH = 4_096;
|
|
25
25
|
const MAX_TASK_SUMMARY_LENGTH = 180;
|
|
26
|
+
const OBSERVED_SPAWN_AGENT_TOOL_NAME = "collaborationspawn_agent";
|
|
26
27
|
const AMBIENT_BROWSER_CONTEXT_OPEN =
|
|
27
28
|
'<in-app-browser-context source="ambient-ui-state">';
|
|
28
29
|
const AMBIENT_BROWSER_CONTEXT_CLOSE = "</in-app-browser-context>";
|
|
@@ -47,6 +48,8 @@ const UNC_ABSOLUTE_PATH =
|
|
|
47
48
|
/(^|[\s("'`=:[{])\\\\[^\s<>"'`)\]},;]+(?:\\[^\s<>"'`)\]},;]+)+/gu;
|
|
48
49
|
const POSIX_ABSOLUTE_PATH =
|
|
49
50
|
/(^|[\s("'`=:[{])\/(?!\/)[^\s<>"'`)\]},;]*(?:\/[^\s<>"'`)\]},;]+)*/gu;
|
|
51
|
+
const OPAQUE_SINGLE_TOKEN = /^[A-Za-z0-9+/_=.:-]+$/u;
|
|
52
|
+
const LONG_HEXADECIMAL_TOKEN = /^[A-F0-9]{40,}$/iu;
|
|
50
53
|
|
|
51
54
|
function isObject(value) {
|
|
52
55
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
@@ -198,6 +201,55 @@ export function deriveTaskSummary(value) {
|
|
|
198
201
|
return `${readableBoundary ? bounded.slice(0, lastSpace) : bounded}…`;
|
|
199
202
|
}
|
|
200
203
|
|
|
204
|
+
function isOpaqueSummaryValue(value) {
|
|
205
|
+
const candidate = typeof value === "string" ? value.trim() : "";
|
|
206
|
+
if (!candidate || /\s/u.test(candidate)) {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
if (/^gAAAA[A-Za-z0-9_-]+={0,2}$/u.test(candidate)) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
return (
|
|
213
|
+
(candidate.length >= 64 && OPAQUE_SINGLE_TOKEN.test(candidate)) ||
|
|
214
|
+
LONG_HEXADECIMAL_TOKEN.test(candidate)
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Sanitize a potential assignment label and reject opaque machine values. */
|
|
219
|
+
export function deriveAssignmentSummary(value) {
|
|
220
|
+
if (isOpaqueSummaryValue(value)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
const summary = deriveTaskSummary(value);
|
|
224
|
+
return summary && !isOpaqueSummaryValue(summary) ? summary : null;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function deriveSpawnTaskName(value) {
|
|
228
|
+
if (!isBoundedString(value, MAX_LABEL_LENGTH) || isOpaqueSummaryValue(value)) {
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
const sanitized = deriveAssignmentSummary(value);
|
|
232
|
+
if (!sanitized) {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
const humanized = sanitized.replace(/[_-]+/gu, " ").replace(/\s+/gu, " ").trim();
|
|
236
|
+
return humanized && !isOpaqueSummaryValue(humanized) ? humanized : null;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** Prefer a readable spawn message, then fall back to a safe task_name label. */
|
|
240
|
+
export function deriveSpawnAssignmentSummary(toolInput) {
|
|
241
|
+
if (
|
|
242
|
+
!isObject(toolInput) ||
|
|
243
|
+
!isBoundedString(toolInput.task_name, MAX_LABEL_LENGTH) ||
|
|
244
|
+
typeof toolInput.message !== "string"
|
|
245
|
+
) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const taskName = deriveSpawnTaskName(toolInput.task_name);
|
|
249
|
+
const message = deriveAssignmentSummary(toolInput.message);
|
|
250
|
+
return message ?? taskName;
|
|
251
|
+
}
|
|
252
|
+
|
|
201
253
|
/**
|
|
202
254
|
* Validate an untrusted Codex hook payload and retain only monitor-safe fields.
|
|
203
255
|
* Raw prompts, tool input/output, paths, and assistant messages are never copied.
|
|
@@ -285,6 +337,29 @@ export function normalizeHookPayload(payload, options = {}) {
|
|
|
285
337
|
|
|
286
338
|
event.tool_name = payload.tool_name;
|
|
287
339
|
event.tool_use_id = payload.tool_use_id;
|
|
340
|
+
|
|
341
|
+
if (
|
|
342
|
+
type === "tool_started" &&
|
|
343
|
+
payload.tool_name === OBSERVED_SPAWN_AGENT_TOOL_NAME
|
|
344
|
+
) {
|
|
345
|
+
event.spawn_assignment_observed = true;
|
|
346
|
+
let assignmentSummary = null;
|
|
347
|
+
if (
|
|
348
|
+
isObject(payload.tool_input) &&
|
|
349
|
+
isBoundedString(payload.tool_input.task_name, MAX_LABEL_LENGTH) &&
|
|
350
|
+
typeof payload.tool_input.message === "string"
|
|
351
|
+
) {
|
|
352
|
+
assignmentSummary = deriveSpawnAssignmentSummary(payload.tool_input);
|
|
353
|
+
} else if (
|
|
354
|
+
payload.spawn_assignment_observed === true &&
|
|
355
|
+
typeof payload.assignment_summary === "string"
|
|
356
|
+
) {
|
|
357
|
+
assignmentSummary = deriveAssignmentSummary(payload.assignment_summary);
|
|
358
|
+
}
|
|
359
|
+
if (assignmentSummary) {
|
|
360
|
+
event.assignment_summary = assignmentSummary;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
288
363
|
}
|
|
289
364
|
|
|
290
365
|
if (type === "permission_requested") {
|