@sema-agent/core 7.40.0 → 7.41.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +218 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +10 -2
  3. package/dist/agents/agent-transcript-tool.js +52 -26
  4. package/dist/agents/list-agents-tool.d.ts +17 -2
  5. package/dist/agents/list-agents-tool.js +28 -16
  6. package/dist/agents/peer-session-drain.d.ts +3 -1
  7. package/dist/agents/peer-session-drain.js +1 -0
  8. package/dist/agents/subagent-steps.d.ts +12 -2
  9. package/dist/agents/subagent-steps.js +8 -4
  10. package/dist/agents/subagent.d.ts +1 -17
  11. package/dist/agents/subagent.js +12 -24
  12. package/dist/core/agent-definition.d.ts +19 -40
  13. package/dist/core/agent-state.d.ts +91 -0
  14. package/dist/core/agent-state.js +79 -0
  15. package/dist/core/ask-origin.d.ts +28 -6
  16. package/dist/core/ask-origin.js +9 -5
  17. package/dist/core/ask-unresolvable-notice.d.ts +27 -4
  18. package/dist/core/ask-unresolvable-notice.js +8 -0
  19. package/dist/core/auto-promote.d.ts +14 -27
  20. package/dist/core/auto-promote.js +5 -5
  21. package/dist/core/checkpoint-store.d.ts +19 -20
  22. package/dist/core/decision-receipt.d.ts +5 -2
  23. package/dist/core/decision-receipt.js +1 -1
  24. package/dist/core/engine-notice.d.ts +7 -1
  25. package/dist/core/gate-crash-trace.d.ts +1 -1
  26. package/dist/core/gate-fold.js +7 -3
  27. package/dist/core/gate-lanes.js +22 -3
  28. package/dist/core/governance-codes.d.ts +3 -1
  29. package/dist/core/governance-codes.js +3 -0
  30. package/dist/core/hooks.d.ts +20 -6
  31. package/dist/core/hooks.js +28 -28
  32. package/dist/core/mcp-tool-meta.d.ts +24 -0
  33. package/dist/core/mcp-tool-meta.js +17 -0
  34. package/dist/core/mcp.d.ts +1 -6
  35. package/dist/core/mcp.js +6 -7
  36. package/dist/core/mode-policy.d.ts +25 -0
  37. package/dist/core/mode-policy.js +8 -0
  38. package/dist/core/protocol-table.d.ts +3 -0
  39. package/dist/core/protocol-table.js +3 -0
  40. package/dist/core/reminder-mint.d.ts +21 -1
  41. package/dist/core/reminder-mint.js +20 -0
  42. package/dist/core/retired-keys.d.ts +2 -1
  43. package/dist/core/retired-keys.js +13 -2
  44. package/dist/core/rule-denial-sentence.d.ts +2 -2
  45. package/dist/core/runner/call-hook-stations.d.ts +106 -0
  46. package/dist/core/runner/call-hook-stations.js +118 -0
  47. package/dist/core/runner/contracts.d.ts +35 -25
  48. package/dist/core/runner/gate-exit.d.ts +5 -1
  49. package/dist/core/runner/gate-exit.js +10 -6
  50. package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
  51. package/dist/core/runner/prepare-delegation-surface.js +4 -4
  52. package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
  53. package/dist/core/runner/prepare-gate-stations.js +13 -70
  54. package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
  55. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  56. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  57. package/dist/core/runner/prepare-park-ask.js +4 -1
  58. package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
  59. package/dist/core/runner/prepare-policy-chain.js +14 -25
  60. package/dist/core/runner/prepare-project-context.d.ts +15 -16
  61. package/dist/core/runner/prepare-project-context.js +15 -11
  62. package/dist/core/runner/prepare-run-refs.d.ts +8 -8
  63. package/dist/core/runner/prepare-run-refs.js +3 -3
  64. package/dist/core/runner/prepare-safety-scan.js +3 -3
  65. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
  66. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  67. package/dist/core/runner/prepare-task.js +14 -12
  68. package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
  69. package/dist/core/runner/prepare-turn-wiring.js +16 -13
  70. package/dist/core/runner/resume-admission.js +2 -1
  71. package/dist/core/runner/resume-apply.js +15 -26
  72. package/dist/core/runner/resume-checkpoint-screen.js +3 -0
  73. package/dist/core/runner/resume-preflight.js +3 -3
  74. package/dist/core/runner/run-leg.js +61 -47
  75. package/dist/core/runner/runtask.d.ts +1 -1
  76. package/dist/core/runner/session-rule-policy.d.ts +7 -19
  77. package/dist/core/runner/session-rule-policy.js +1 -2
  78. package/dist/core/runner/skill-base-dir.d.ts +10 -0
  79. package/dist/core/runner/skill-base-dir.js +4 -0
  80. package/dist/core/runner/synthetic-tools.d.ts +11 -17
  81. package/dist/core/runner/synthetic-tools.js +7 -8
  82. package/dist/core/runner-deps.d.ts +3 -1
  83. package/dist/core/safety-axis-vocab.d.ts +2 -2
  84. package/dist/core/safety-axis-vocab.js +2 -0
  85. package/dist/core/session-policy-store.d.ts +63 -11
  86. package/dist/core/session-policy-store.js +52 -6
  87. package/dist/core/skill-grant.d.ts +239 -0
  88. package/dist/core/skill-grant.js +313 -0
  89. package/dist/core/skills-directory.d.ts +5 -34
  90. package/dist/core/skills-directory.js +9 -58
  91. package/dist/core/task-registry-agent.d.ts +14 -2
  92. package/dist/core/task-registry-agent.js +23 -8
  93. package/dist/core/task-registry-shared.d.ts +6 -0
  94. package/dist/core/task-registry.d.ts +16 -0
  95. package/dist/core/task-registry.js +19 -6
  96. package/dist/core/task-spec.d.ts +10 -0
  97. package/dist/core/tool-face.d.ts +3 -2
  98. package/dist/core/tool-name-aliases.d.ts +38 -12
  99. package/dist/core/tool-name-aliases.js +16 -0
  100. package/dist/core/tool-name-readings.d.ts +6 -2
  101. package/dist/core/tool-name-readings.js +6 -4
  102. package/dist/core/tool-policy.d.ts +2 -2
  103. package/dist/core/tool-registry.d.ts +1 -4
  104. package/dist/core/tool-registry.js +0 -3
  105. package/dist/core/tool-spec.d.ts +11 -13
  106. package/dist/core/tools.d.ts +1 -1
  107. package/dist/core/trace.d.ts +15 -0
  108. package/dist/core/types.d.ts +1 -1
  109. package/dist/engine/harness/agent-harness.d.ts +12 -1
  110. package/dist/engine/harness/agent-harness.js +17 -13
  111. package/dist/engine/loop/agent-loop.d.ts +16 -1
  112. package/dist/engine/loop/agent-loop.js +9 -15
  113. package/dist/engine/loop/types.d.ts +2 -0
  114. package/dist/index.d.ts +9 -4
  115. package/dist/index.js +4 -2
  116. package/dist/stores/file/adoption/adopt.js +23 -16
  117. package/dist/stores/file/index.d.ts +8 -2
  118. package/dist/stores/file/index.js +2 -2
  119. package/dist/stores/file/mailbox-store.js +2 -2
  120. package/dist/stores/file/permission-rule-adopt.js +10 -1
  121. package/dist/stores/file/permission-rule-store.d.ts +6 -4
  122. package/dist/stores/file/permission-rule-store.js +127 -59
  123. package/dist/stores/file/session-policy-store.d.ts +86 -14
  124. package/dist/stores/file/session-policy-store.js +135 -118
  125. package/dist/stores/file/task-list-store.d.ts +5 -0
  126. package/dist/stores/file/task-list-store.js +18 -4
  127. package/dist/stores/file/usage-window-store.d.ts +9 -1
  128. package/dist/stores/file/usage-window-store.js +47 -33
  129. package/dist/tools/fs/safety.d.ts +1 -1
  130. package/package.json +1 -1
  131. package/test/export-surface.snapshot.json +45 -5
  132. package/wire-changes/core@7.40.1.json +39 -0
  133. package/wire-changes/core@7.41.0.json +1865 -0
  134. package/dist/core/runner/active-skill-scope.d.ts +0 -58
  135. package/dist/core/runner/active-skill-scope.js +0 -133
  136. package/dist/core/skill-tool-specifier.d.ts +0 -80
  137. package/dist/core/skill-tool-specifier.js +0 -57
@@ -3,6 +3,8 @@ import type { TaskAccess } from "../core/task-registry.js";
3
3
  import { type RosterEntry } from "./roster-store.js";
4
4
  import { type PeerDirectory, type PeerPresence, type PeerSessionRecord } from "./peer-directory.js";
5
5
  import type { SubagentTranscriptTier } from "../core/wiring-manifest.js";
6
+ import { formatAgo, type AgentState, type AgentStateSource } from "../core/agent-state.js";
7
+ import type { CheckpointSeat } from "../core/checkpoint-seat.js";
6
8
  export declare const LIST_AGENTS_TOOL_NAME = "ListAgents";
7
9
  export declare const LIST_AGENTS_TOOL_ALIAS = "ListPeers";
8
10
  /** CC `maxResultSizeChars` for this tool. */
@@ -16,8 +18,8 @@ export declare const PEER_LISTING_FRESH_WINDOW_MS: number;
16
18
  * addressable by name. Dead and stale rows are what `includeAll` adds.
17
19
  */
18
20
  export declare function isFreshPeerRow(r: Pick<PeerSessionRecord, "liveness" | "tempo" | "updatedAt">, now: number, windowMs?: number): boolean;
19
- /** `Ns` / `Nm` / `Nh` / `Nd` — a bounded age word for a listing row; a future stamp reads as `0s`. */
20
- export declare function formatAgo(ms: number): string;
21
+ /** `Ns` / `Nm` / `Nh` / `Nd` — a bounded age word for a listing row (one definition, shared with the agent-state clauses). */
22
+ export { formatAgo };
21
23
  /** The last two path segments of a working directory (what a person recognizes a project by). */
22
24
  export declare function cwdTail(cwd: string): string;
23
25
  export interface ListAgentsToolOptions {
@@ -42,6 +44,9 @@ export interface ListAgentsToolOptions {
42
44
  name?: string;
43
45
  status: string;
44
46
  } | undefined;
47
+ /** The live background-agent row's state facts ({@link import("../core/task-registry.js").TaskRegistry.agentStateSource}).
48
+ * Absent on a registry that does not offer it ⇒ agent rows carry no `agentState` and no state columns. */
49
+ agentStateSource?(id: string, access: TaskAccess): AgentStateSource | undefined;
45
50
  };
46
51
  access?: TaskAccess;
47
52
  /** design/147 S3a parity — a delegated child's TRUSTED parent view (owner = the parent's task id,
@@ -62,9 +67,14 @@ export interface ListAgentsToolOptions {
62
67
  * the conversation does not; `none` = only the recorded name. Absent (a direct mount that did not say) ⇒
63
68
  * the `none` wording — a label never promises more than the mount can show. */
64
69
  continuationTier?: SubagentTranscriptTier;
70
+ /** The run's checkpoint seat — where a PARKED agent row's approval card is read from (the same seat the
71
+ * Agent tool parks children into). Absent ⇒ the card reads `unreadable`. */
72
+ checkpointSeat?: CheckpointSeat;
65
73
  /** The clock (tests). */
66
74
  now?: () => number;
67
75
  }
76
+ /** The store seats the listing reads agent rows' facts through, as one bundle a mount helper passes on unchanged. */
77
+ export type ListAgentsReadSeats = Pick<ListAgentsToolOptions, "checkpointSeat">;
68
78
  /** One rendered row. `kind` names the lane; `address` is what SendMessage accepts. */
69
79
  export interface ListAgentsRow {
70
80
  kind: "peer_session" | "background_agent" | "roster";
@@ -88,6 +98,11 @@ export interface ListAgentsRow {
88
98
  * — on the peer lane the common case, since no host writes the activity word. `finished` means
89
99
  * COMPLETED and nothing else: a model applies the continuable rule to that word. */
90
100
  state: "busy" | "idle" | "offline" | "running" | "parked" | "finished" | "failed" | "killed" | "cancelled" | "unknown";
101
+ /** Agent lanes with a live registry row: the agent's current state
102
+ * ({@link import("../core/agent-state.js").readAgentState}, the one reading AgentTranscript and TaskOutput
103
+ * render too); {@link state} is this status word through the lane table. Absent on peer rows and on a
104
+ * roster row with no live row. */
105
+ agentState?: AgentState;
91
106
  }
92
107
  export declare function createListAgentsTool(opts: ListAgentsToolOptions): import("../internal/harness-types.js").AgentTool<Type.TObject<{
93
108
  includeAll: Type.TOptional<Type.TBoolean>;
@@ -8,6 +8,7 @@ import { PEER_LANE_DEFAULTS } from "./peer-admission.js";
8
8
  import { toolFace } from "../core/tool-catalog-entries.js";
9
9
  import { thrownText } from "../core/thrown-value.js";
10
10
  import { assertNoRetiredKeys } from "../core/retired-keys.js";
11
+ import { agentActivityClause, agentApprovalClause, formatAgo, readAgentState } from "../core/agent-state.js";
11
12
  export const LIST_AGENTS_TOOL_NAME = "ListAgents";
12
13
  export const LIST_AGENTS_TOOL_ALIAS = "ListPeers";
13
14
  export const LIST_AGENTS_MAX_RESULT_CHARS = 10_000;
@@ -19,18 +20,7 @@ export function isFreshPeerRow(r, now, windowMs = PEER_LISTING_FRESH_WINDOW_MS)
19
20
  return true;
20
21
  return now - r.updatedAt <= windowMs;
21
22
  }
22
- export function formatAgo(ms) {
23
- const sec = Math.max(0, Math.floor(ms / 1000));
24
- if (sec < 60)
25
- return `${sec}s`;
26
- const min = Math.floor(sec / 60);
27
- if (min < 60)
28
- return `${min}m`;
29
- const h = Math.floor(min / 60);
30
- if (h < 48)
31
- return `${h}h`;
32
- return `${Math.floor(h / 24)}d`;
33
- }
23
+ export { formatAgo };
34
24
  export function cwdTail(cwd) {
35
25
  const parts = cwd.split(/[\\/]+/).filter((p) => p !== "");
36
26
  return parts.slice(-2).join("/");
@@ -80,7 +70,7 @@ export function createListAgentsTool(opts) {
80
70
  `pass includeAll: true to also list stale and offline ones. A peer that is offline or unlisted still accepts messages — they are parked ` +
81
71
  `durably in its session box and delivered when it next runs. `
82
72
  : `Peer sessions of this engine are not addressable on this mount (no session directory). `) +
83
- `Background agents you spawned are listed with their task_id. ` +
73
+ `Background agents you spawned are listed with their task_id, their status, when they last did anything, and — for one parked on a durable approval — that approval's card id, state and deadline. ` +
84
74
  `A ref is only valid against the listing (or disambiguation error) you read it from — do not invent one.`,
85
75
  parameters: Type.Object({
86
76
  includeAll: Type.Optional(Type.Boolean({ description: "Also list stale (no activity within the freshness window) and offline peer sessions. Default false." })),
@@ -120,6 +110,10 @@ export function createListAgentsTool(opts) {
120
110
  });
121
111
  }
122
112
  let registryCapped = false;
113
+ const agentStateOf = (id, view) => {
114
+ const src = opts.registry?.agentStateSource?.(id, view);
115
+ return src === undefined ? undefined : readAgentState(src, opts.checkpointSeat, opts.now?.() ?? Date.now());
116
+ };
123
117
  const views = [opts.access, opts.parentAccess].filter((a) => a !== undefined);
124
118
  const seenIds = new Set();
125
119
  if (opts.registry !== undefined) {
@@ -131,7 +125,15 @@ export function createListAgentsTool(opts) {
131
125
  continue;
132
126
  seenIds.add(t.task_id);
133
127
  const full = opts.registry.getAccessibleTask(t.task_id, view);
134
- rows.push({ kind: "background_agent", name: full?.name ?? t.description ?? t.task_id, address: t.task_id, state: agentRowState(t.status) });
128
+ const stateRead = agentStateOf(t.task_id, view);
129
+ const agentState = stateRead instanceof Promise ? await stateRead : stateRead;
130
+ rows.push({
131
+ kind: "background_agent",
132
+ name: full?.name ?? t.description ?? t.task_id,
133
+ address: t.task_id,
134
+ state: agentRowState(agentState?.status ?? t.status),
135
+ ...(agentState !== undefined ? { agentState } : {}),
136
+ });
135
137
  }
136
138
  }
137
139
  }
@@ -143,7 +145,15 @@ export function createListAgentsTool(opts) {
143
145
  continue;
144
146
  seenIds.add(e.agentId);
145
147
  const live = opts.registry?.getAccessibleTask(e.agentId, view);
146
- rows.push({ kind: "roster", name: e.name, address: e.agentId, state: live === undefined ? "unknown" : agentRowState(live.status) });
148
+ const stateRead = live === undefined ? undefined : agentStateOf(e.agentId, view);
149
+ const agentState = stateRead instanceof Promise ? await stateRead : stateRead;
150
+ rows.push({
151
+ kind: "roster",
152
+ name: e.name,
153
+ address: e.agentId,
154
+ state: live === undefined ? "unknown" : agentRowState(agentState?.status ?? live.status),
155
+ ...(agentState !== undefined ? { agentState } : {}),
156
+ });
147
157
  }
148
158
  }
149
159
  catch {
@@ -159,7 +169,9 @@ export function createListAgentsTool(opts) {
159
169
  const lines = rows.map((r) => {
160
170
  const label = r.kind === "peer_session" ? formatPeerNameRef(inlineUntrusted(r.name, 64), r.ref) : inlineUntrusted(r.name, 64);
161
171
  const age = r.kind !== "peer_session" ? undefined : r.lastActiveAt !== undefined ? `active ${formatAgo(now - r.lastActiveAt)} ago` : r.offlineAt !== undefined ? `offline ${formatAgo(now - r.offlineAt)} ago` : "offline";
162
- const cols = [label, r.state, age, r.cwd !== undefined ? inlineUntrusted(r.cwd, 64) : undefined, r.presence !== undefined ? renderPresence(r.presence) : undefined, r.kind === "peer_session" ? "peer session" : r.kind === "roster" ? rosterLabel : "agent", r.address];
172
+ const activity = r.agentState !== undefined ? agentActivityClause(r.agentState) : undefined;
173
+ const approval = r.agentState?.approval !== undefined ? agentApprovalClause(r.agentState.approval, r.agentState.readAt) : undefined;
174
+ const cols = [label, r.state, age, activity, approval, r.cwd !== undefined ? inlineUntrusted(r.cwd, 64) : undefined, r.presence !== undefined ? renderPresence(r.presence) : undefined, r.kind === "peer_session" ? "peer session" : r.kind === "roster" ? rosterLabel : "agent", r.address];
163
175
  return cols.filter((c) => c !== undefined).join(" · ");
164
176
  });
165
177
  const finishedCount = rows.filter((r) => r.state === "finished").length;
@@ -6,7 +6,7 @@ import { type PermissionModeClass } from "./cross-session-envelope.js";
6
6
  import { type CrossSessionInboundSettingLayers } from "./cross-session-judge.js";
7
7
  import { type PeerLaneConfig } from "./peer-admission.js";
8
8
  import { type PeerDirectory } from "./peer-directory.js";
9
- import { createListAgentsTool } from "./list-agents-tool.js";
9
+ import { createListAgentsTool, type ListAgentsReadSeats } from "./list-agents-tool.js";
10
10
  import { foldPermissionModeClass } from "./cross-session-judge.js";
11
11
  import type { AskEffective } from "../core/wiring-manifest.js";
12
12
  import type { SendMessageToolOptions } from "./send-message-tool.js";
@@ -248,6 +248,8 @@ export declare function mountListAgents(args: {
248
248
  aliases?: readonly string[];
249
249
  }>;
250
250
  toolEffects: Map<string, "read" | "write" | "idempotent">;
251
+ /** The read faces' store seats, handed to the tool unchanged (see {@link ListAgentsReadSeats}). */
252
+ readSeats?: ListAgentsReadSeats;
251
253
  }): ReturnType<typeof createListAgentsTool> | undefined;
252
254
  type ListAgentsToolRegistry = NonNullable<Parameters<typeof createListAgentsTool>[0]["registry"]>;
253
255
  /** The harness surface the binding needs (structural — the machine itself never sees the harness). */
@@ -702,6 +702,7 @@ export function mountListAgents(args) {
702
702
  ...(args.parentTaskId !== undefined ? { parentAccess: { owner: args.parentTaskId, scope: args.scope, ...(args.parentSessionId !== undefined ? { sessionId: args.parentSessionId } : {}) } } : {}),
703
703
  ...(args.roster !== undefined ? { roster: args.roster } : {}),
704
704
  ...(args.continuationTier !== undefined ? { continuationTier: args.continuationTier } : {}),
705
+ ...(args.readSeats ?? {}),
705
706
  });
706
707
  }
707
708
  export function bindPeerSessionDrain(harness, opts) {
@@ -31,7 +31,8 @@ export interface SubagentStep {
31
31
  tool: string;
32
32
  /** The step's primary argument, one line: file path / command / pattern. May be "". */
33
33
  target: string;
34
- /** Result first line — or `error: …` when the tool failed. May be "". */
34
+ /** Result first line — or `error: …` when the tool failed. May be "" (a result that was only an engine
35
+ * reminder block has no line of its own). The engine's marked reminder blocks are not part of a result. */
35
36
  outcome: string;
36
37
  }
37
38
  /** A file the child mutated, with how many times (Write/Edit/MultiEdit/NotebookEdit). */
@@ -104,10 +105,19 @@ export declare class SubagentStepRecorder {
104
105
  target?: string;
105
106
  } | undefined;
106
107
  }
108
+ /** One step read back from a session's persisted messages: a {@link SubagentStep} plus WHEN. */
109
+ export interface TranscriptStep extends SubagentStep {
110
+ /** Epoch ms: the result message's timestamp when the call has a result, the calling message's when it
111
+ * has none. Absent when that message carries no finite timestamp. */
112
+ at?: number;
113
+ /** Present (`true`) exactly when the transcript holds the call but no result for it — the call is still
114
+ * in flight, or the run stopped before its result was recorded. */
115
+ inFlight?: true;
116
+ }
107
117
  /**
108
118
  * Residual lane C — derive the last `lastN` tool steps from a session's PERSISTED messages (the pull-model
109
119
  * `AgentTranscript` read face). Mirrors {@link SubagentStepRecorder} but sources from committed history
110
120
  * instead of the live event stream, so a parent can retrospectively read a child's tail on demand. Walks
111
121
  * assistant `toolCall`s paired with the following `toolResult`, oldest→newest, then keeps the last N.
112
122
  */
113
- export declare function stepsFromMessages(messages: readonly unknown[], lastN: number): SubagentStep[];
123
+ export declare function stepsFromMessages(messages: readonly unknown[], lastN: number): TranscriptStep[];
@@ -1,3 +1,4 @@
1
+ import { stripMarkedReminderBlocks } from "../core/reminder-mint.js";
1
2
  import { redactSecrets } from "../core/untrusted-egress.js";
2
3
  import { isProtectedWrite } from "../core/tool-registry.js";
3
4
  import { writeTargetPath } from "../tools/fs/safety.js";
@@ -32,7 +33,7 @@ export function redactThenCut(s, max) {
32
33
  return cutAt(redactSecrets(s), max);
33
34
  }
34
35
  function clip(s) {
35
- return redactThenCut(firstLine(s).trim(), FIELD_MAX);
36
+ return redactThenCut(firstLine(stripMarkedReminderBlocks(s)).trim(), FIELD_MAX);
36
37
  }
37
38
  export function extractTarget(args) {
38
39
  if (args === null || typeof args !== "object")
@@ -51,7 +52,7 @@ export function extractTarget(args) {
51
52
  if (primary !== undefined)
52
53
  return clip(primary);
53
54
  try {
54
- return clip(JSON.stringify(a));
55
+ return clip(JSON.stringify(a, (_key, v) => (typeof v === "string" ? stripMarkedReminderBlocks(v) : v)));
55
56
  }
56
57
  catch {
57
58
  return "";
@@ -162,12 +163,13 @@ export class SubagentStepRecorder {
162
163
  return this.actionTool === undefined ? undefined : { toolName: this.actionTool, ...(this.actionTarget !== undefined ? { target: this.actionTarget } : {}) };
163
164
  }
164
165
  }
166
+ const stampOf = (m) => (typeof m.timestamp === "number" && Number.isFinite(m.timestamp) ? m.timestamp : undefined);
165
167
  export function stepsFromMessages(messages, lastN) {
166
168
  const results = new Map();
167
169
  for (const m of messages) {
168
170
  const msg = m;
169
171
  if (msg.role === "toolResult" && typeof msg.toolCallId === "string") {
170
- results.set(msg.toolCallId, { isError: msg.isError === true, body: outputFirstLineFromContent(msg.content) });
172
+ results.set(msg.toolCallId, { isError: msg.isError === true, body: outputFirstLineFromContent(msg.content), at: stampOf(msg) });
171
173
  }
172
174
  }
173
175
  const steps = [];
@@ -175,13 +177,15 @@ export function stepsFromMessages(messages, lastN) {
175
177
  const msg = m;
176
178
  if (msg.role !== "assistant" || !Array.isArray(msg.content))
177
179
  continue;
180
+ const calledAt = stampOf(msg);
178
181
  for (const block of msg.content) {
179
182
  const b = block;
180
183
  if (b.type !== "toolCall" || typeof b.name !== "string")
181
184
  continue;
182
185
  const res = typeof b.id === "string" ? results.get(b.id) : undefined;
183
186
  const outcome = res ? (res.isError ? cutAt(`error: ${res.body}`, FIELD_MAX) : res.body) : "";
184
- steps.push({ tool: b.name, target: extractTarget(b.arguments), outcome });
187
+ const at = res ? res.at : calledAt;
188
+ steps.push({ tool: b.name, target: extractTarget(b.arguments), outcome, ...(at !== undefined ? { at } : {}), ...(res === undefined ? { inFlight: true } : {}) });
185
189
  }
186
190
  }
187
191
  const cap = Math.max(0, Math.min(lastN, steps.length));
@@ -1,4 +1,4 @@
1
- import type { RunInternals, RunnerSelfSeat } from "../core/runner/contracts.js";
1
+ import type { RunnerSelfSeat } from "../core/runner/contracts.js";
2
2
  import type { AgentDefinition, Model, ModelRef, TaskLimits, TerminalCause, ToolSpec } from "../core/types.js";
3
3
  export { childThinkingSeat, deploymentSubagentThinking, deploymentSubagentTierModel } from "./child-model-seat.js";
4
4
  import { type ExecutionEnv } from "../internal/harness.js";
@@ -74,22 +74,6 @@ interface ReviewSample {
74
74
  /** Layered head/middle/tail sample of `text` under `budget`, with each skipped span marked in band so the
75
75
  * reader can see that the material is discontinuous. Exported for tests (window determinism). */
76
76
  export declare function layeredReviewSample(text: string, budget?: number, windows?: number): ReviewSample;
77
- /**
78
- * 🔴 design/77 §3 / §7 — skill→subagent manifest-scope PROPAGATION (fail-closed). Given the parent task's
79
- * live skill-manifest snapshot (from `ctx.activeSkillScope()`), build the inherited-scope frames to thread
80
- * into a spawned child via the trusted {@link RunInternals.inheritedManifestScope} channel, so the child's
81
- * tool gate inherits the parent skill's deny-narrowing (a child of a manifested skill is at most as capable
82
- * as the manifest — MONOTONIC).
83
- *
84
- * - Parent has NO active manifest (empty/absent snapshot) → return `undefined` (no inheritance; the child
85
- * behaves exactly as today, backward-compatible).
86
- * - Parent HAS active manifest frames → return them verbatim (the child seeds them as the LIFO base; its
87
- * own skill loads can only intersect/narrow further).
88
- * - Fail-closed: the snapshot is non-empty but its shape is not a recognizable frame array (a future/older
89
- * Runner, a tampered accessor) → return a SINGLE DENY-ALL `unresolved` frame, so the child runs under a
90
- * deny-all inherited scope rather than UNMANIFESTED. The safe path is the default.
91
- */
92
- export declare function inheritedManifestScopeFor(snapshot: readonly unknown[] | undefined): RunInternals["inheritedManifestScope"];
93
77
  /** Default model-facing name of the delegation tool (CC 2.1.187 `Agent`; legacy alias `Task`). */
94
78
  export declare const DEFAULT_SUBAGENT_TOOL_NAME = "Agent";
95
79
  /**
@@ -31,6 +31,7 @@ import { shellQuote } from "../tools/fs/search.js";
31
31
  import { BG_AGENT_REAP_STOP_ERROR } from "../core/task-registry.js";
32
32
  import { readDurableOrgAdmission } from "../core/memory-admission.js";
33
33
  import { governanceRetryClass } from "../core/governance-codes.js";
34
+ import { readSkillGrantCapture } from "../core/skill-grant.js";
34
35
  import { terminalFrameFacets } from "../core/task-notification.js";
35
36
  import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX, RUNNING_AGENT_OBSERVE_EVERY_BEATS, DELEGATION_MAX_CONCURRENT_DEFAULT, DELEGATION_MAX_PER_SESSION_DEFAULT } from "../config/defaults.js";
36
37
  function rollupDelegatedCost(stats, nested) {
@@ -187,28 +188,6 @@ function withHandbackWarning(warning, result) {
187
188
  return result;
188
189
  return result ? `${warning}\n\n${result}` : warning;
189
190
  }
190
- export function inheritedManifestScopeFor(snapshot) {
191
- if (!snapshot || snapshot.length === 0)
192
- return undefined;
193
- const frames = [];
194
- for (const f of snapshot) {
195
- if (f && typeof f === "object" && "kind" in f) {
196
- const kind = f.kind;
197
- if (kind === "manifest" || kind === "unresolved") {
198
- frames.push(f);
199
- continue;
200
- }
201
- }
202
- return [
203
- {
204
- kind: "unresolved",
205
- lineageId: "inherited-parent-manifest",
206
- reason: "parent skill manifest was active at spawn but its scope could not be propagated to the subagent — fail-closed deny-all",
207
- },
208
- ];
209
- }
210
- return frames;
211
- }
212
191
  export const DEFAULT_SUBAGENT_TOOL_NAME = "Agent";
213
192
  export const EXTRA_TOOLS_MAX_FACTORY_CALLS_PER_TREE = 64;
214
193
  export const DEFAULT_SUBAGENT_MAX_DEPTH = 3;
@@ -1724,6 +1703,14 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1724
1703
  })()
1725
1704
  : seatModel?.id;
1726
1705
  const rowModelKey = perCallModel !== undefined ? requestedModel ?? perCallModel.id : typeof seatModel === "string" ? seatModel : seatModel?.id;
1706
+ const inheritedSkillGrants = readSkillGrantCapture(ctx.activeSkillGrants?.());
1707
+ if (inheritedSkillGrants === undefined) {
1708
+ return {
1709
+ isError: true,
1710
+ content: "Sub-agent not started: the parent's skill grants could not be read for the child (an unrecognizable capture); a child started without them would lose the parent's disallowed-tools refusals.",
1711
+ details: { error: "skill_grants.unreadable" },
1712
+ };
1713
+ }
1727
1714
  let worktreeDir;
1728
1715
  if (reviveClaim === undefined && (a.isolation ?? def?.isolation) === "worktree") {
1729
1716
  const iso = ctx.worktreeIsolation;
@@ -1844,7 +1831,6 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1844
1831
  if (depth + 1 < maxDepth && defAllowsDelegation) {
1845
1832
  childTools.push(createSubagentToolNode(opts, depth + 1, nextExcluded, treeBudget));
1846
1833
  }
1847
- const inheritedManifestScope = inheritedManifestScopeFor(ctx.activeSkillScope?.());
1848
1834
  const childAgentName = (typeof a.description === "string" && a.description.trim() ? a.description.trim() : undefined) ?? def?.name;
1849
1835
  const observerArm = ctx.observersAllowed === true && def !== undefined
1850
1836
  ? resolveObserverDeclaration({
@@ -2100,7 +2086,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2100
2086
  : undefined;
2101
2087
  const childInternals = {
2102
2088
  ...(childPick.modelRef !== undefined ? { inheritedModelRef: childPick.modelRef } : {}),
2103
- ...(inheritedManifestScope ? { inheritedManifestScope } : {}),
2089
+ ...(inheritedSkillGrants.length > 0 ? { inheritedSkillGrants } : {}),
2104
2090
  ...(childProvenanceRef !== undefined && provenanceRequest !== undefined ? { delegationProvenance: { ref: childProvenanceRef, contentSafety: provenanceRequest } } : {}),
2105
2091
  ...(treeScope !== undefined ? { registryScope: treeScope } : {}),
2106
2092
  ...(ctx.inheritedGateForChildren ? { inheritedGate: ctx.inheritedGateForChildren() } : {}),
@@ -2668,6 +2654,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2668
2654
  }
2669
2655
  catch {
2670
2656
  }
2657
+ bgForkActivityBeat(e);
2671
2658
  },
2672
2659
  };
2673
2660
  void opts.runner
@@ -3305,6 +3292,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
3305
3292
  }
3306
3293
  catch {
3307
3294
  }
3295
+ bgActivityBeat(e);
3308
3296
  },
3309
3297
  };
3310
3298
  const driveParkedResume = async () => {
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * A delegated AGENT's definition and the skills it may mount: `AgentDefinition` (the agent a subagent
3
- * tool spawns), `SkillManifest` (a skill's on-disk front matter) and `SkillSpec` (the wired form the
4
- * runner mounts). Layer 0 vocabulary; `types.ts` re-exports the three, so no consumer's import changes.
3
+ * tool spawns) and `SkillSpec` (the wired form the runner mounts). Layer 0 vocabulary; `types.ts`
4
+ * re-exports the two, so no consumer's import changes.
5
5
  */
6
6
  import type { ThinkingLevel } from "../internal/harness.js";
7
7
  import type { ModelRef } from "./model-seat.js";
@@ -101,49 +101,28 @@ export interface AgentDefinition {
101
101
  */
102
102
  observerMessage?: string;
103
103
  }
104
- /**
105
- * A per-skill runtime capability manifest (design/77 §3 Gate-3). When present, loading the skill
106
- * SCOPES the rest of the invocation to ONLY these tools/paths via runtime deny-narrowing: a tool call
107
- * not in `allowTools` (intersected across nested skill frames) is DENIED, and a write whose canonical
108
- * target falls outside `allowPaths` is DENIED — regardless of how permissive the task policy is.
109
- *
110
- * 🔴 **Safe-by-construction:** a manifest can only SUBTRACT capability. The narrowing composes via the
111
- * `deny > ask > allow` fold (`combinePolicies`) where `deny` short-circuits, so a manifest can never
112
- * *grant* a tool the task policy withheld — whoever sets a manifest can only restrict further.
113
- * Worst case (the model never pops the frame) = no narrowing, never widening — still ⊆ task policy.
114
- *
115
- * The manifest is CALLER-provided here (same trust level as the rest of `SkillSpec`); deriving it
116
- * automatically (the auto-promote pipeline) is a separate, deferred concern (design/77 §5).
117
- */
118
- export interface SkillManifest {
119
- /** The ONLY tool names the invocation may call while this skill's frame is active. */
120
- allowTools: string[];
121
- /**
122
- * Optional path allowlist for WRITE tools (`write_file`/`edit_file`): a write whose canonicalized
123
- * target is not inside one of these (path-segment-aware) prefixes is DENIED. Omit = no path narrowing
124
- * (only `allowTools` applies). Reads are never path-narrowed (this gate restricts capability, not visibility).
125
- *
126
- * 🔴 **Fail-closed (MAJOR-2):** `allowPaths` can only confine tools whose write target is a parseable `path`
127
- * argument (`write_file`/`edit_file`). While an `allowPaths` constraint is active, any OTHER mutating
128
- * (effect:write/idempotent) allowlisted tool — notably `bash`, whose command can write anywhere — is DENIED
129
- * outright, because its target cannot be path-confined. So a manifest that lists `bash` together with
130
- * `allowPaths` does NOT let bash write/egress anywhere: bash is denied while the path constraint is active.
131
- * Confine writes to specific paths via `write_file`/`edit_file` only.
132
- */
133
- allowPaths?: string[];
134
- /** Reserved (design/77 §3): whether egress is permitted under this manifest. Carried through; v1 does
135
- * not act on it beyond the allowTools intersection (egress tools must also be in `allowTools`). */
136
- egress?: boolean;
137
- /** Stable lineage id of the artifact this manifest came from — used in deny reasons + as the scope key. */
138
- lineageId: string;
139
- }
140
104
  /** A skill made available to this task (passed as an object; not loaded from disk). */
141
105
  export interface SkillSpec {
142
106
  name: string;
143
107
  description: string;
144
108
  content: string;
145
- /** Optional Gate-3 runtime capability manifest (design/77 §3) — see {@link SkillManifest}. */
146
- manifest?: SkillManifest;
109
+ /**
110
+ * The tools this skill lets run WITHOUT ASKING for the rest of the run it is activated in — the frontmatter
111
+ * `allowed-tools` list, spelled as a permission rule is spelled: a tool name (`Read`, `mcp__srv__get_issue`), a
112
+ * protocol namespace (`mcp__srv` / `mcp__srv__*` / `mcp__srv__get_*`), a shell command rule (`Bash(git:*)`) or the Read
113
+ * directory form (`Read(//abs/dir/**)`). The list is an allow-rule source, not a fence: an unlisted tool is judged as
114
+ * it always is, and a listed one is released only from the questions an allow rule may clear (a deny or ask rule, the
115
+ * plan mode, the organization ceiling and every mandated question still stand). Activating a skill that declares
116
+ * either list asks once (`Execute skill: <name>`, the card listing what it releases); a skill the person invoked by
117
+ * name ({@link import("./task-spec.js").TaskSpec.userInvokedSkills}) grants without asking. The grant holds for that
118
+ * run (a steer it takes in does not end it) and the next runTask starts without it. An entry this grammar reads as nothing — `*` (it voids an allow list; on a deny list it is inert and the rest of the
119
+ * list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`skills.allowed_tool_not_granted`).
120
+ */
121
+ allowedTools?: string[];
122
+ /** The tools refused for the rest of the run this skill is activated in — the frontmatter `disallowed-tools` list,
123
+ * in the {@link allowedTools} grammar (a path tool also takes a path pattern here); read wherever a person's deny
124
+ * rows are read, ahead of every allow. */
125
+ disallowedTools?: string[];
147
126
  /**
148
127
  * Optional attachments bundled with the skill (additive) — the CC bundled-skill shape (a SKILL.md whose
149
128
  * body references sibling files by relative path) translated to sema's brain-outside posture, where the
@@ -0,0 +1,91 @@
1
+ /**
2
+ * A background agent's CURRENT state — the one reading every read face renders.
3
+ *
4
+ * A parent model answers "how far along is it?" from what its read faces show. Three faces read a
5
+ * background agent: AgentTranscript (its recent steps), ListAgents (the roster) and TaskOutput (the poll).
6
+ * Each one projects {@link readAgentState}'s result and renders it with {@link agentStateSummary} /
7
+ * {@link agentActivityClause} / {@link agentApprovalClause}; none derives status, activity time or approval
8
+ * facts on its own, so the three cannot disagree about one agent at one instant.
9
+ *
10
+ * Sources:
11
+ * - the registry row — the live handle this engine process runs ({@link agentStateSourceOfHandle}), or the
12
+ * durable record when no live handle answers ({@link agentStateSourceOfRecord});
13
+ * - for a row whose status is `parked` only, the approval checkpoint the row names, read through the run's
14
+ * checkpoint seat (the same seat the Agent tool parks children into). The checkpoint token is the resume
15
+ * capability and never leaves this module: a face renders the checkpoint's non-secret `checkpointId`.
16
+ *
17
+ * Absence is stated, never left silent: a missing activity time renders as "last activity unknown (…why)",
18
+ * and a parked row whose card cannot be read carries `approval.state` `missing` / `unreadable`. A face that
19
+ * goes quiet on a fact invites the reader to fill the gap with a guess.
20
+ */
21
+ import type { SemaTaskStatus } from "./task-registry-shared.js";
22
+ import { type CheckpointSeat } from "./checkpoint-seat.js";
23
+ /** The approval card's state: the checkpoint row's own `status`, plus `missing` (the row names no card, or
24
+ * the store holds none under its token) and `unreadable` (no checkpoint seat on this face, or the store
25
+ * threw). The same five answers the parked-row reconciliation sweep gives the same question. */
26
+ export type AgentApprovalState = "pending" | "resolved" | "expired" | "missing" | "unreadable";
27
+ /** The approval checkpoint a parked agent waits on. */
28
+ export interface AgentApproval {
29
+ state: AgentApprovalState;
30
+ /** The checkpoint's non-secret identity (the card id a person sees in the approval inbox). Present when
31
+ * the card was read and carries one. */
32
+ checkpointId?: string;
33
+ /** The card's SLA / abandonment deadline, epoch ms. Present when the card was read and carries one. */
34
+ deadline?: number;
35
+ }
36
+ /** Where the row was read from: the live handle this engine process runs, or the durable record (the
37
+ * process that wrote it may be another one, or gone). */
38
+ export type AgentStateRow = "live_row" | "durable_row";
39
+ /** One agent's state at {@link readAt}. Times are epoch ms; relative ages appear only in rendered text. */
40
+ export interface AgentState {
41
+ /** The registry row's own status word. A word outside {@link SemaTaskStatus} reads `unknown`. */
42
+ status: SemaTaskStatus | "unknown";
43
+ /** The instant this state was read (epoch ms) — the base every rendered age is computed from. */
44
+ readAt: number;
45
+ row: AgentStateRow;
46
+ /** The row's last recorded change (epoch ms). Live row: every state transition and every tool start /
47
+ * end of the direct child moves it. Durable row: when it parked (`parked`) or settled (terminal).
48
+ * ABSENT when the source holds no such time — a durable row still marked running (activity in another
49
+ * process is not observable here), or a row that never recorded one. */
50
+ lastActivityAt?: number;
51
+ /** Present exactly when {@link status} is `parked`. */
52
+ approval?: AgentApproval;
53
+ }
54
+ /** The row facts {@link readAgentState} reads. Built by the two projections below — never by a face. */
55
+ export interface AgentStateSource {
56
+ status: string;
57
+ row: AgentStateRow;
58
+ lastActivityAt?: number;
59
+ parkedCheckpointToken?: string;
60
+ }
61
+ /** The live handle's facts: `updatedAt` is moved by every state transition and by each direct-child tool
62
+ * lifecycle beat, so it is the row's last activity. */
63
+ export declare function agentStateSourceOfHandle(h: {
64
+ status: string;
65
+ updatedAt: number;
66
+ parkedCheckpointToken?: string;
67
+ }): AgentStateSource;
68
+ /** The durable record's facts. Its `updatedAt` is the writer's liveness lease renewal, not activity, so it
69
+ * is never read as such: a parked record answers with `parkedAt`, a terminal one with `settledAt`, and a
70
+ * record still marked running answers nothing. */
71
+ export declare function agentStateSourceOfRecord(r: {
72
+ status: string;
73
+ parkedAt?: number;
74
+ settledAt?: number;
75
+ parkedCheckpointToken?: string;
76
+ }): AgentStateSource;
77
+ /**
78
+ * THE state of one background agent at `now`. Synchronous unless the row is parked on a card the seat can
79
+ * read — only that row takes an async frame (the store read); every other row is answered in place.
80
+ * The caller must already have passed the row through its face's access check: this reads the card the
81
+ * row names and authorizes nothing.
82
+ */
83
+ export declare function readAgentState(src: AgentStateSource, seat: CheckpointSeat | undefined, now: number): AgentState | Promise<AgentState>;
84
+ /** `Ns` / `Nm` / `Nh` / `Nd` — a bounded age word; a negative span reads as `0s`. */
85
+ export declare function formatAgo(ms: number): string;
86
+ /** `last activity <age> ago`, or the reason the time is unknown. */
87
+ export declare function agentActivityClause(s: AgentState): string;
88
+ /** `approval <card id> <state>[ (deadline in <age> | deadline passed <age> ago)]` — facts only. */
89
+ export declare function agentApprovalClause(a: AgentApproval, readAt: number): string;
90
+ /** The one-line state sentence: `<status> · <activity clause>[ · <approval clause>]`. */
91
+ export declare function agentStateSummary(s: AgentState): string;
@@ -0,0 +1,79 @@
1
+ import { checkpointStoreOfSeat } from "./checkpoint-seat.js";
2
+ import { inlineUntrusted } from "./untrusted-text.js";
3
+ const STATUS_WORDS = { pending: true, running: true, parked: true, completed: true, failed: true, killed: true, cancelled: true };
4
+ export function agentStateSourceOfHandle(h) {
5
+ return {
6
+ status: h.status,
7
+ row: "live_row",
8
+ lastActivityAt: h.updatedAt,
9
+ ...(h.parkedCheckpointToken !== undefined ? { parkedCheckpointToken: h.parkedCheckpointToken } : {}),
10
+ };
11
+ }
12
+ export function agentStateSourceOfRecord(r) {
13
+ const at = r.status === "parked" ? r.parkedAt : r.status === "running" ? undefined : r.settledAt;
14
+ return {
15
+ status: r.status,
16
+ row: "durable_row",
17
+ ...(at !== undefined ? { lastActivityAt: at } : {}),
18
+ ...(r.parkedCheckpointToken !== undefined ? { parkedCheckpointToken: r.parkedCheckpointToken } : {}),
19
+ };
20
+ }
21
+ const finite = (n) => typeof n === "number" && Number.isFinite(n);
22
+ async function readApprovalCard(store, token) {
23
+ let cp;
24
+ try {
25
+ cp = (await store.get(token));
26
+ }
27
+ catch {
28
+ return { state: "unreadable" };
29
+ }
30
+ if (cp === null || cp === undefined)
31
+ return { state: "missing" };
32
+ const state = cp.status === "pending" || cp.status === "resolved" || cp.status === "expired" ? cp.status : "unreadable";
33
+ return {
34
+ state,
35
+ ...(typeof cp.checkpointId === "string" && cp.checkpointId !== "" ? { checkpointId: cp.checkpointId } : {}),
36
+ ...(finite(cp.deadline) ? { deadline: cp.deadline } : {}),
37
+ };
38
+ }
39
+ export function readAgentState(src, seat, now) {
40
+ const base = {
41
+ status: Object.hasOwn(STATUS_WORDS, src.status) ? src.status : "unknown",
42
+ readAt: now,
43
+ row: src.row,
44
+ ...(finite(src.lastActivityAt) ? { lastActivityAt: src.lastActivityAt } : {}),
45
+ };
46
+ if (base.status !== "parked")
47
+ return base;
48
+ if (src.parkedCheckpointToken === undefined)
49
+ return { ...base, approval: { state: "missing" } };
50
+ const store = checkpointStoreOfSeat(seat);
51
+ if (store === undefined)
52
+ return { ...base, approval: { state: "unreadable" } };
53
+ return readApprovalCard(store, src.parkedCheckpointToken).then((approval) => ({ ...base, approval }));
54
+ }
55
+ export function formatAgo(ms) {
56
+ const sec = Math.max(0, Math.floor(ms / 1000));
57
+ if (sec < 60)
58
+ return `${sec}s`;
59
+ const min = Math.floor(sec / 60);
60
+ if (min < 60)
61
+ return `${min}m`;
62
+ const h = Math.floor(min / 60);
63
+ if (h < 48)
64
+ return `${h}h`;
65
+ return `${Math.floor(h / 24)}d`;
66
+ }
67
+ export function agentActivityClause(s) {
68
+ if (s.lastActivityAt !== undefined)
69
+ return `last activity ${formatAgo(s.readAt - s.lastActivityAt)} ago`;
70
+ return s.row === "durable_row" && s.status === "running" ? "last activity unknown (recorded as running by another engine process)" : "last activity unknown (not recorded)";
71
+ }
72
+ export function agentApprovalClause(a, readAt) {
73
+ const id = a.checkpointId !== undefined ? ` ${inlineUntrusted(a.checkpointId, 64)}` : "";
74
+ const deadline = a.deadline === undefined ? "" : a.deadline > readAt ? ` (deadline in ${formatAgo(a.deadline - readAt)})` : ` (deadline passed ${formatAgo(readAt - a.deadline)} ago)`;
75
+ return `approval${id} ${a.state}${deadline}`;
76
+ }
77
+ export function agentStateSummary(s) {
78
+ return [s.status, agentActivityClause(s), ...(s.approval !== undefined ? [agentApprovalClause(s.approval, s.readAt)] : [])].join(" · ");
79
+ }