codex-agent-view 0.5.0 → 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,11 @@ 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";
27
+ const AMBIENT_BROWSER_CONTEXT_OPEN =
28
+ '<in-app-browser-context source="ambient-ui-state">';
29
+ const AMBIENT_BROWSER_CONTEXT_CLOSE = "</in-app-browser-context>";
30
+ const USER_REQUEST_DELIMITER = "## My request for Codex:";
26
31
  const CONTROL_CHARACTERS = /[\u0000-\u001f\u007f-\u009f]/;
27
32
  const CONTROL_CHARACTERS_GLOBAL = /[\u0000-\u001f\u007f-\u009f]/g;
28
33
 
@@ -43,6 +48,8 @@ const UNC_ABSOLUTE_PATH =
43
48
  /(^|[\s("'`=:[{])\\\\[^\s<>"'`)\]},;]+(?:\\[^\s<>"'`)\]},;]+)+/gu;
44
49
  const POSIX_ABSOLUTE_PATH =
45
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;
46
53
 
47
54
  function isObject(value) {
48
55
  return value !== null && typeof value === "object" && !Array.isArray(value);
@@ -120,6 +127,35 @@ function replaceAbsolutePaths(value) {
120
127
  .replace(POSIX_ABSOLUTE_PATH, (_match, prefix) => `${prefix}[path]`);
121
128
  }
122
129
 
130
+ function stripLeadingAmbientBrowserContext(value) {
131
+ const withoutLeadingWhitespace = value.trimStart();
132
+ if (!withoutLeadingWhitespace.startsWith(AMBIENT_BROWSER_CONTEXT_OPEN)) {
133
+ return value;
134
+ }
135
+
136
+ const closeAt = withoutLeadingWhitespace.indexOf(
137
+ AMBIENT_BROWSER_CONTEXT_CLOSE,
138
+ AMBIENT_BROWSER_CONTEXT_OPEN.length,
139
+ );
140
+ if (closeAt === -1) {
141
+ return null;
142
+ }
143
+
144
+ const remainder = withoutLeadingWhitespace.slice(
145
+ closeAt + AMBIENT_BROWSER_CONTEXT_CLOSE.length,
146
+ );
147
+ const delimiterCandidate = remainder.trimStart();
148
+ if (!delimiterCandidate.startsWith(USER_REQUEST_DELIMITER)) {
149
+ return remainder;
150
+ }
151
+
152
+ const afterDelimiter = delimiterCandidate.slice(USER_REQUEST_DELIMITER.length);
153
+ if (afterDelimiter.length > 0 && !/^[\r\n]/u.test(afterDelimiter)) {
154
+ return remainder;
155
+ }
156
+ return afterDelimiter;
157
+ }
158
+
123
159
  /**
124
160
  * Derive a short, display-safe hint from an untrusted UserPromptSubmit prompt.
125
161
  * The caller must discard the raw prompt after this synchronous derivation.
@@ -129,8 +165,14 @@ export function deriveTaskSummary(value) {
129
165
  return null;
130
166
  }
131
167
 
132
- let summary = value
133
- .slice(0, MAX_PROMPT_INSPECTION_LENGTH)
168
+ const summaryCandidate = stripLeadingAmbientBrowserContext(
169
+ value.slice(0, MAX_PROMPT_INSPECTION_LENGTH),
170
+ );
171
+ if (summaryCandidate === null) {
172
+ return null;
173
+ }
174
+
175
+ let summary = summaryCandidate
134
176
  .replace(PRIVATE_KEY_BLOCK, "[credential]")
135
177
  .replace(CONTROL_CHARACTERS_GLOBAL, " ")
136
178
  .replace(URL, "[link]")
@@ -159,6 +201,55 @@ export function deriveTaskSummary(value) {
159
201
  return `${readableBoundary ? bounded.slice(0, lastSpace) : bounded}…`;
160
202
  }
161
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
+
162
253
  /**
163
254
  * Validate an untrusted Codex hook payload and retain only monitor-safe fields.
164
255
  * Raw prompts, tool input/output, paths, and assistant messages are never copied.
@@ -246,6 +337,29 @@ export function normalizeHookPayload(payload, options = {}) {
246
337
 
247
338
  event.tool_name = payload.tool_name;
248
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
+ }
249
363
  }
250
364
 
251
365
  if (type === "permission_requested") {