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.
@@ -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 { deriveTaskSummary } from "../src/core/normalize-hook-payload.mjs";
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") {