@slock-ai/daemon 0.55.2-alpha.0 → 0.55.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,7 @@ import {
5
5
  executeJsonRequest,
6
6
  executeResponseRequest,
7
7
  logger
8
- } from "./chunk-VOZJ2ELH.js";
8
+ } from "./chunk-M2KQBJR3.js";
9
9
 
10
10
  // src/core.ts
11
11
  import path16 from "path";
@@ -38,6 +38,244 @@ var CHANNEL_MESSAGE_RE = new RegExp(
38
38
  "iu"
39
39
  );
40
40
 
41
+ // ../shared/src/producerFactLineage.ts
42
+ var PRODUCER_FACT_TEXT_LABEL = "producerFactId";
43
+ function formatProducerFactLineageBracket(producerFactId) {
44
+ const id = normalizeProducerFactId(producerFactId);
45
+ return id ? ` [${PRODUCER_FACT_TEXT_LABEL}=${id}]` : "";
46
+ }
47
+ function normalizeProducerFactId(producerFactId) {
48
+ return typeof producerFactId === "string" ? producerFactId.trim() : "";
49
+ }
50
+
51
+ // ../shared/src/apmHeldFreshness.ts
52
+ function buildApmFreshnessDecisionProducerFactId(agentId, input) {
53
+ const stableInput = {
54
+ agentId,
55
+ action: input.action,
56
+ decision: input.decision,
57
+ target: input.target ?? null,
58
+ reason: input.reason,
59
+ pendingMaxSeq: input.pendingMaxSeq ?? null,
60
+ modelSeenSeq: input.modelSeenSeq ?? null,
61
+ heldMessageCount: input.heldMessageCount ?? null,
62
+ omittedMessageCount: input.omittedMessageCount ?? null
63
+ };
64
+ return `freshness_decision_fact:${hashApmHeldFreshnessStable(stableInput)}`;
65
+ }
66
+ function projectApmHeldFreshnessEnvelope(input) {
67
+ const body = {
68
+ state: "held",
69
+ outcome: "held",
70
+ subtype: "freshness",
71
+ reason: "newer_messages_available",
72
+ producerFactId: input.producerFactId,
73
+ available_actions: apmHeldFreshnessAvailableActions(input.action),
74
+ heldMessages: input.heldMessages,
75
+ newMessageCount: input.newMessageCount,
76
+ shownMessageCount: input.heldMessages.length,
77
+ omittedMessageCount: input.omittedMessageCount,
78
+ seenUpToSeq: input.seenUpToSeq
79
+ };
80
+ return {
81
+ clauseId: "SMR-006",
82
+ projector: "held-envelope",
83
+ surface: "agent-api-held-response",
84
+ producerFactId: input.producerFactId,
85
+ body
86
+ };
87
+ }
88
+ function projectApmHeldFreshnessActivity(input) {
89
+ const title = input.action === "send" ? "Send held by freshness check" : input.action === "task_claim" ? "Task claim held by freshness check" : "Task update held by freshness check";
90
+ const text = [
91
+ input.target ? `target: ${input.target}` : null,
92
+ `new messages: ${input.messageCount} newer message${input.messageCount === 1 ? "" : "s"}`,
93
+ `decision: ${input.decision === "syncing_hold" ? "syncing hold" : "local hold"}; review the newer context before retrying`
94
+ ].filter((line) => Boolean(line)).join("\n");
95
+ return {
96
+ clauseId: "SMR-006",
97
+ projector: "held-envelope",
98
+ surface: "agent:activity",
99
+ producerFactId: input.producerFactId,
100
+ entry: {
101
+ kind: "slock_action",
102
+ producerFactId: input.producerFactId,
103
+ title,
104
+ text
105
+ }
106
+ };
107
+ }
108
+ function projectApmFreshnessDecisionTrace(input) {
109
+ return {
110
+ clauseId: "SMR-006",
111
+ projector: "held-envelope",
112
+ surface: "daemon-trace",
113
+ producerFactId: input.producerFactId,
114
+ attrs: {
115
+ producer_fact_id: input.producerFactId,
116
+ action: input.decision.action,
117
+ decision: input.decision.decision,
118
+ target: input.decision.target,
119
+ inbox_trust_state: input.decision.inboxTrustState,
120
+ reason: input.decision.reason,
121
+ pending_count: input.decision.pendingCount,
122
+ pending_max_seq: input.decision.pendingMaxSeq,
123
+ model_seen_seq: input.decision.modelSeenSeq,
124
+ held_message_count: input.decision.heldMessageCount,
125
+ omitted_message_count: input.decision.omittedMessageCount
126
+ }
127
+ };
128
+ }
129
+ function apmHeldFreshnessAvailableActions(action) {
130
+ return action === "send" ? ["check_messages", "send_draft", "send_anyway"] : ["check_messages", "retry_action"];
131
+ }
132
+ function hashApmHeldFreshnessStable(value) {
133
+ return sha256HexUtf8(stableStringifyApmHeldFreshness(value));
134
+ }
135
+ function stableStringifyApmHeldFreshness(value) {
136
+ return JSON.stringify(stableNormalizeApmHeldFreshness(value));
137
+ }
138
+ function stableNormalizeApmHeldFreshness(value) {
139
+ if (Array.isArray(value)) return value.map((item) => stableNormalizeApmHeldFreshness(item));
140
+ if (!value || typeof value !== "object") return value;
141
+ const record = value;
142
+ const normalized = {};
143
+ for (const key of Object.keys(record).sort()) {
144
+ const child = record[key];
145
+ if (child === void 0) continue;
146
+ normalized[key] = stableNormalizeApmHeldFreshness(child);
147
+ }
148
+ return normalized;
149
+ }
150
+ var SHA256_INITIAL_STATE = [
151
+ 1779033703,
152
+ 3144134277,
153
+ 1013904242,
154
+ 2773480762,
155
+ 1359893119,
156
+ 2600822924,
157
+ 528734635,
158
+ 1541459225
159
+ ];
160
+ var SHA256_K = [
161
+ 1116352408,
162
+ 1899447441,
163
+ 3049323471,
164
+ 3921009573,
165
+ 961987163,
166
+ 1508970993,
167
+ 2453635748,
168
+ 2870763221,
169
+ 3624381080,
170
+ 310598401,
171
+ 607225278,
172
+ 1426881987,
173
+ 1925078388,
174
+ 2162078206,
175
+ 2614888103,
176
+ 3248222580,
177
+ 3835390401,
178
+ 4022224774,
179
+ 264347078,
180
+ 604807628,
181
+ 770255983,
182
+ 1249150122,
183
+ 1555081692,
184
+ 1996064986,
185
+ 2554220882,
186
+ 2821834349,
187
+ 2952996808,
188
+ 3210313671,
189
+ 3336571891,
190
+ 3584528711,
191
+ 113926993,
192
+ 338241895,
193
+ 666307205,
194
+ 773529912,
195
+ 1294757372,
196
+ 1396182291,
197
+ 1695183700,
198
+ 1986661051,
199
+ 2177026350,
200
+ 2456956037,
201
+ 2730485921,
202
+ 2820302411,
203
+ 3259730800,
204
+ 3345764771,
205
+ 3516065817,
206
+ 3600352804,
207
+ 4094571909,
208
+ 275423344,
209
+ 430227734,
210
+ 506948616,
211
+ 659060556,
212
+ 883997877,
213
+ 958139571,
214
+ 1322822218,
215
+ 1537002063,
216
+ 1747873779,
217
+ 1955562222,
218
+ 2024104815,
219
+ 2227730452,
220
+ 2361852424,
221
+ 2428436474,
222
+ 2756734187,
223
+ 3204031479,
224
+ 3329325298
225
+ ];
226
+ function sha256HexUtf8(value) {
227
+ const bytes = new TextEncoder().encode(value);
228
+ const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
229
+ const padded = new Uint8Array(paddedLength);
230
+ padded.set(bytes);
231
+ padded[bytes.length] = 128;
232
+ const bitLength = bytes.length * 8;
233
+ const view = new DataView(padded.buffer);
234
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
235
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
236
+ const h = [...SHA256_INITIAL_STATE];
237
+ const words = new Array(64);
238
+ for (let offset = 0; offset < paddedLength; offset += 64) {
239
+ for (let i = 0; i < 16; i += 1) {
240
+ words[i] = view.getUint32(offset + i * 4);
241
+ }
242
+ for (let i = 16; i < 64; i += 1) {
243
+ const s0 = rotateRight(words[i - 15], 7) ^ rotateRight(words[i - 15], 18) ^ words[i - 15] >>> 3;
244
+ const s1 = rotateRight(words[i - 2], 17) ^ rotateRight(words[i - 2], 19) ^ words[i - 2] >>> 10;
245
+ words[i] = words[i - 16] + s0 + words[i - 7] + s1 >>> 0;
246
+ }
247
+ let [a, b, c, d, e, f, g, hh] = h;
248
+ for (let i = 0; i < 64; i += 1) {
249
+ const s1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
250
+ const ch = e & f ^ ~e & g;
251
+ const temp1 = hh + s1 + ch + SHA256_K[i] + words[i] >>> 0;
252
+ const s0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
253
+ const maj = a & b ^ a & c ^ b & c;
254
+ const temp2 = s0 + maj >>> 0;
255
+ hh = g;
256
+ g = f;
257
+ f = e;
258
+ e = d + temp1 >>> 0;
259
+ d = c;
260
+ c = b;
261
+ b = a;
262
+ a = temp1 + temp2 >>> 0;
263
+ }
264
+ h[0] = h[0] + a >>> 0;
265
+ h[1] = h[1] + b >>> 0;
266
+ h[2] = h[2] + c >>> 0;
267
+ h[3] = h[3] + d >>> 0;
268
+ h[4] = h[4] + e >>> 0;
269
+ h[5] = h[5] + f >>> 0;
270
+ h[6] = h[6] + g >>> 0;
271
+ h[7] = h[7] + hh >>> 0;
272
+ }
273
+ return h.map((part) => part.toString(16).padStart(8, "0")).join("");
274
+ }
275
+ function rotateRight(value, bits) {
276
+ return value >>> bits | value << 32 - bits;
277
+ }
278
+
41
279
  // ../shared/src/tracing/index.ts
42
280
  var DEFAULT_TRACE_FLAGS = "00";
43
281
  var TRACEPARENT_VERSION = "00";
@@ -954,7 +1192,7 @@ var DISPLAY_PLAN_CONFIG = {
954
1192
  // src/agentProcessManager.ts
955
1193
  import { mkdirSync as mkdirSync4, readdirSync, statSync, writeFileSync as writeFileSync7 } from "fs";
956
1194
  import { mkdir, writeFile, access, readdir as readdir2, stat as stat2, readFile, rm as rm2 } from "fs/promises";
957
- import { createHash as createHash2 } from "crypto";
1195
+ import { createHash as createHash3 } from "crypto";
958
1196
  import path12 from "path";
959
1197
  import os5 from "os";
960
1198
 
@@ -1059,8 +1297,8 @@ Use the \`slock\` CLI for chat / task / attachment operations. The daemon inject
1059
1297
  17. **\`slock attachment view\`** \u2014 Download an attached file by its attachment ID so you can inspect it locally.
1060
1298
  18. **\`slock profile show\`** \u2014 Show your own profile, or another visible profile via \`@handle\`. Mirrors the canonical Slock profile view.
1061
1299
  19. **\`slock profile update\`** \u2014 Update your own profile. Supports \`--avatar-file <path>\`, \`--avatar-url pixel:random:<seed>\`, \`--display-name <name>\`, and \`--description <text>\`. Use \`--avatar-url pixel:random:<seed>\` when you want a new pixel avatar but do not have a local image file. Values must be non-empty. Provide at least one flag per call; multiple flags can be combined.
1062
- 20. **\`slock integration list\`** \u2014 List registered third-party services and this agent's active Slock Agent Logins.
1063
- 21. **\`slock integration login\`** \u2014 Provision or reuse this agent's login for a registered third-party service.
1300
+ 20. **\`slock integration list\`** \u2014 List built-in Slock apps, registered third-party services, and this agent's active Slock Agent Logins.
1301
+ 21. **\`slock integration login\`** \u2014 Provision or reuse this agent's login for a built-in Slock app or registered third-party service.
1064
1302
  22. **\`slock integration env\`** \u2014 Print per-agent local CLI environment for a manifest-backed service that requires isolated HOME/XDG state.
1065
1303
  23. **\`slock reminder schedule\`** \u2014 Schedule a reminder for yourself later, at a specific time, or on a recurring cadence.
1066
1304
  24. **\`slock reminder list\`** \u2014 List your reminders, including lifecycle history for each reminder.
@@ -1174,9 +1412,9 @@ Each channel has a **name** and optionally a **description** that define its pur
1174
1412
  - If unsure where something belongs, call ${serverInfoCmd} to review channel descriptions.`;
1175
1413
  const thirdPartyIntegrationsSection = isCli ? `### Third-party integrations
1176
1414
 
1177
- If a registered third-party service requires login, use Slock Agent Login through the CLI instead of asking the human to copy tokens or complete human OAuth for you. If a human asks you to sign into, open, use, or fetch identity from a third-party app, first run \`slock integration list\` and match the app to a registered service before browsing the app. Use \`slock integration login --service <service>\` to provision or reuse your agent login for that service. If the service exposes an agent behavior manifest and you need to run its local CLI, run \`slock integration env --service <service>\` before invoking that CLI; if it prints exports, apply them first so service credentials stay under a per-agent profile HOME/XDG tree instead of the host user's global HOME. If it reports that no local env is required, do not invent HOME/XDG overrides. If it fails, do not run that local CLI with the host user's HOME; report that the service manifest is unsupported. Slock does not execute commands from remote manifests automatically. If the CLI reports that the \`integration\` command is unknown, the local daemon/CLI is too old for Slock Agent Login; report that the machine must be upgraded/restarted instead of calling internal HTTP endpoints yourself. When the command returns \`Agent login ready\` or \`Already logged in\`, the agent-side login is ready. If the output includes an app URL, open that URL as the service-provided third-party app surface; it should look like the service's normal Login with Slock callback and not require you to understand Slock's internal grant/request protocol. Do not crawl third-party routes looking for a session before trying the registered-service login path. Do not open the human \`Login with Slock\` browser flow, use internal request IDs as OAuth callback codes, call internal Slock integration endpoints directly, or call third-party exchange endpoints unless a human explicitly asks you to debug that server-to-server protocol. If the service or human asks for your Slock Agent identity card, use \`slock profile show\`. Third-party pages may show \`Login with Slock\`; for agent-facing access, prefer the registered service / Slock Agent Login path.` : `### Third-party integrations
1415
+ If a built-in Slock app or registered third-party service requires login, use Slock Agent Login through the CLI instead of asking the human to copy tokens or complete human OAuth for you. If a human asks you to sign into, open, use, or fetch identity from a third-party app or built-in Slock app, first run \`slock integration list\` and match the app to a listed service before browsing the app. Use \`slock integration login --service <service>\` to provision or reuse your agent login for that service. If the service exposes an agent behavior manifest and you need to run its local CLI, run \`slock integration env --service <service>\` before invoking that CLI; if it prints exports, apply them first so service credentials stay under a per-agent profile HOME/XDG tree instead of the host user's global HOME. If it reports that no local env is required, do not invent HOME/XDG overrides. If it fails, do not run that local CLI with the host user's HOME; report that the service manifest is unsupported. Slock does not execute commands from remote manifests automatically. If the CLI reports that the \`integration\` command is unknown, the local daemon/CLI is too old for Slock Agent Login; report that the machine must be upgraded/restarted instead of calling internal HTTP endpoints yourself. When the command returns \`Agent login ready\` or \`Already logged in\`, the agent-side login is ready. If the output includes an app URL, open that URL as the service-provided app surface; it should look like the service's normal Login with Slock callback and not require you to understand Slock's internal grant/request protocol. Do not crawl third-party routes looking for a session before trying the registered-service login path. Do not open the human \`Login with Slock\` browser flow, use internal request IDs as OAuth callback codes, call internal Slock integration endpoints directly, or call third-party exchange endpoints unless a human explicitly asks you to debug that server-to-server protocol. If the service or human asks for your Slock Agent identity card, use \`slock profile show\`. Third-party pages may show \`Login with Slock\`; for agent-facing access, prefer the listed service / Slock Agent Login path.` : `### Third-party integrations
1178
1416
 
1179
- If a registered third-party service requires login, use Slock Agent Login through the available registered-service interface instead of asking the human to copy tokens or complete human OAuth for you. If a human asks you to sign into, open, use, or fetch identity from a third-party app, first inspect the registered-service interface and match the app to a registered service before browsing the app. Once the registered-service interface reports the agent login is ready, the agent-side login is ready. If that interface provides an app URL, use it as the service-provided third-party app surface; it should look like the service's normal Login with Slock callback and not require you to understand Slock's internal grant/request protocol. Do not crawl third-party routes looking for a session before trying the registered-service login path. Do not open the human \`Login with Slock\` browser flow or treat internal request IDs as OAuth callback codes unless a human explicitly asks you to debug that server-to-server protocol. If the service or human asks for your Slock Agent identity card, use your Slock profile view. Third-party pages may show \`Login with Slock\`; for agent-facing access, prefer the registered service / Slock Agent Login path.`;
1417
+ If a built-in Slock app or registered third-party service requires login, use Slock Agent Login through the available registered-service interface instead of asking the human to copy tokens or complete human OAuth for you. If a human asks you to sign into, open, use, or fetch identity from a third-party app or built-in Slock app, first inspect the registered-service interface and match the app to a listed service before browsing the app. Once the registered-service interface reports the agent login is ready, the agent-side login is ready. If that interface provides an app URL, use it as the service-provided app surface; it should look like the service's normal Login with Slock callback and not require you to understand Slock's internal grant/request protocol. Do not crawl third-party routes looking for a session before trying the registered-service login path. Do not open the human \`Login with Slock\` browser flow or treat internal request IDs as OAuth callback codes unless a human explicitly asks you to debug that server-to-server protocol. If the service or human asks for your Slock Agent identity card, use your Slock profile view. Third-party pages may show \`Login with Slock\`; for agent-facing access, prefer the listed service / Slock Agent Login path.`;
1180
1418
  const readingHistorySection = isCli ? `### Reading history
1181
1419
 
1182
1420
  \`slock message read --channel "#channel-name"\` or \`slock message read --channel dm:@peer-name\` or \`slock message read --channel "#channel:shortid"\`
@@ -1544,6 +1782,258 @@ function listLegacySlockStatePaths(slockHome = resolveSlockHome(), homeDir = os.
1544
1782
  import { randomBytes } from "crypto";
1545
1783
  import http from "http";
1546
1784
  import { URL as URL2 } from "url";
1785
+
1786
+ // src/apmStateMachine.ts
1787
+ import { createHash } from "crypto";
1788
+ var MAX_APM_GATED_STEERING_EVENTS = 12;
1789
+ function createInitialApmGatedSteeringState() {
1790
+ return {
1791
+ isIdle: false,
1792
+ expectedTerminationReason: null,
1793
+ phase: "idle",
1794
+ outstandingToolUses: 0,
1795
+ compacting: false,
1796
+ toolBoundaryFlushDisabled: false,
1797
+ lastFlushReason: null,
1798
+ recentEvents: []
1799
+ };
1800
+ }
1801
+ function reduceApmIdleState(state, input) {
1802
+ return {
1803
+ nextState: {
1804
+ ...state,
1805
+ isIdle: input.isIdle
1806
+ }
1807
+ };
1808
+ }
1809
+ function reduceApmGatedToolUse(state, input) {
1810
+ if (input.kind === "tool_call") {
1811
+ return {
1812
+ nextState: {
1813
+ isIdle: false,
1814
+ expectedTerminationReason: state.expectedTerminationReason,
1815
+ phase: "tool_wait",
1816
+ outstandingToolUses: state.outstandingToolUses + 1,
1817
+ compacting: state.compacting,
1818
+ toolBoundaryFlushDisabled: state.toolBoundaryFlushDisabled,
1819
+ lastFlushReason: state.lastFlushReason,
1820
+ recentEvents: state.recentEvents
1821
+ },
1822
+ hadOutstandingToolUse: state.outstandingToolUses > 0,
1823
+ shouldFlushToolBatch: false
1824
+ };
1825
+ }
1826
+ const hadOutstandingToolUse = state.outstandingToolUses > 0;
1827
+ const outstandingToolUses = Math.max(0, state.outstandingToolUses - 1);
1828
+ return {
1829
+ nextState: {
1830
+ isIdle: false,
1831
+ expectedTerminationReason: state.expectedTerminationReason,
1832
+ phase: "tool_boundary",
1833
+ outstandingToolUses,
1834
+ compacting: state.compacting,
1835
+ toolBoundaryFlushDisabled: state.toolBoundaryFlushDisabled,
1836
+ lastFlushReason: state.lastFlushReason,
1837
+ recentEvents: state.recentEvents
1838
+ },
1839
+ hadOutstandingToolUse,
1840
+ shouldFlushToolBatch: hadOutstandingToolUse && outstandingToolUses === 0
1841
+ };
1842
+ }
1843
+ function reduceApmGatedCompaction(state, input) {
1844
+ if (input.kind === "compaction_started") {
1845
+ return {
1846
+ nextState: {
1847
+ isIdle: false,
1848
+ expectedTerminationReason: state.expectedTerminationReason,
1849
+ phase: "compacting",
1850
+ outstandingToolUses: state.outstandingToolUses,
1851
+ compacting: true,
1852
+ toolBoundaryFlushDisabled: state.toolBoundaryFlushDisabled,
1853
+ lastFlushReason: state.lastFlushReason,
1854
+ recentEvents: state.recentEvents
1855
+ }
1856
+ };
1857
+ }
1858
+ if (input.kind === "compaction_interrupted") {
1859
+ return {
1860
+ nextState: {
1861
+ isIdle: false,
1862
+ expectedTerminationReason: state.expectedTerminationReason,
1863
+ phase: state.phase,
1864
+ outstandingToolUses: state.outstandingToolUses,
1865
+ compacting: false,
1866
+ toolBoundaryFlushDisabled: state.toolBoundaryFlushDisabled,
1867
+ lastFlushReason: state.lastFlushReason,
1868
+ recentEvents: state.recentEvents
1869
+ }
1870
+ };
1871
+ }
1872
+ return {
1873
+ nextState: {
1874
+ isIdle: false,
1875
+ expectedTerminationReason: state.expectedTerminationReason,
1876
+ phase: "assistant_continuation",
1877
+ outstandingToolUses: state.outstandingToolUses,
1878
+ compacting: false,
1879
+ toolBoundaryFlushDisabled: state.toolBoundaryFlushDisabled,
1880
+ lastFlushReason: state.lastFlushReason,
1881
+ recentEvents: state.recentEvents
1882
+ }
1883
+ };
1884
+ }
1885
+ function reduceApmGatedCompactionBoundaryFlush(_state, input) {
1886
+ if (!input.hasSession || !input.supportsStdinNotification || input.inboxLength === 0) {
1887
+ return { effects: [] };
1888
+ }
1889
+ if (input.pendingNotificationCount === 0) return { effects: [] };
1890
+ return {
1891
+ effects: [{
1892
+ kind: "notify_stdin",
1893
+ reason: "compaction_finished",
1894
+ stdinMode: "busy",
1895
+ clauseId: "SMR-002"
1896
+ }]
1897
+ };
1898
+ }
1899
+ function reduceApmGatedTurnEnd(_state, input = {}) {
1900
+ const shouldDeliverQueuedMessages = Boolean(
1901
+ input.inboxLength && input.inboxLength > 0 && input.supportsStdinNotification && input.hasSession
1902
+ );
1903
+ return {
1904
+ nextState: {
1905
+ isIdle: !shouldDeliverQueuedMessages,
1906
+ expectedTerminationReason: input.terminateProcessOnTurnEnd === true ? "turn_end" : _state.expectedTerminationReason,
1907
+ phase: "idle",
1908
+ outstandingToolUses: 0,
1909
+ compacting: false,
1910
+ toolBoundaryFlushDisabled: _state.toolBoundaryFlushDisabled,
1911
+ lastFlushReason: _state.lastFlushReason,
1912
+ recentEvents: _state.recentEvents
1913
+ },
1914
+ effects: shouldDeliverQueuedMessages ? [{
1915
+ kind: "deliver_stdin",
1916
+ reason: "turn_end",
1917
+ stdinMode: "idle",
1918
+ clauseId: "SMR-002"
1919
+ }] : []
1920
+ };
1921
+ }
1922
+ function reduceApmGatedError(state, input = {}) {
1923
+ const shouldDisableToolBoundaryFlush = input.disableToolBoundaryFlush === true;
1924
+ return {
1925
+ nextState: {
1926
+ isIdle: input.terminalWakeable === true,
1927
+ expectedTerminationReason: state.expectedTerminationReason,
1928
+ phase: "error",
1929
+ outstandingToolUses: state.outstandingToolUses,
1930
+ compacting: false,
1931
+ toolBoundaryFlushDisabled: state.toolBoundaryFlushDisabled || shouldDisableToolBoundaryFlush,
1932
+ lastFlushReason: state.lastFlushReason,
1933
+ recentEvents: state.recentEvents
1934
+ },
1935
+ shouldDisableToolBoundaryFlush
1936
+ };
1937
+ }
1938
+ function reduceApmGatedAssistantContinuation(state) {
1939
+ return {
1940
+ nextState: {
1941
+ isIdle: false,
1942
+ expectedTerminationReason: state.expectedTerminationReason,
1943
+ phase: "assistant_continuation",
1944
+ outstandingToolUses: state.outstandingToolUses,
1945
+ compacting: state.compacting,
1946
+ toolBoundaryFlushDisabled: state.toolBoundaryFlushDisabled,
1947
+ lastFlushReason: state.lastFlushReason,
1948
+ recentEvents: state.recentEvents
1949
+ }
1950
+ };
1951
+ }
1952
+ function reduceApmStalledRecoveryTermination(state, input) {
1953
+ if (input.inboxLength === 0) {
1954
+ return { nextState: state, shouldTerminate: false, alreadyRecovering: false, blockedReason: "empty_inbox" };
1955
+ }
1956
+ if (state.expectedTerminationReason === "stalled_recovery") {
1957
+ return { nextState: state, shouldTerminate: false, alreadyRecovering: true, blockedReason: null };
1958
+ }
1959
+ const directStdinRuntime = input.supportsStdinNotification && input.busyDeliveryMode === "direct";
1960
+ const canRestartDirectStdinProcess = directStdinRuntime && input.hasSession && (state.outstandingToolUses === 0 || input.hasDirectStdinRecoveryEvidence);
1961
+ const canRestartStalledProcess = !input.supportsStdinNotification || canRestartDirectStdinProcess;
1962
+ if (!canRestartStalledProcess) {
1963
+ return { nextState: state, shouldTerminate: false, alreadyRecovering: false, blockedReason: "runtime_not_restartable" };
1964
+ }
1965
+ if (input.staleForMs < input.staleThresholdMs && !input.runtimeProgressIsStale) {
1966
+ return { nextState: state, shouldTerminate: false, alreadyRecovering: false, blockedReason: "runtime_progress_recent" };
1967
+ }
1968
+ return {
1969
+ nextState: {
1970
+ ...state,
1971
+ expectedTerminationReason: "stalled_recovery"
1972
+ },
1973
+ shouldTerminate: true,
1974
+ alreadyRecovering: false,
1975
+ blockedReason: null
1976
+ };
1977
+ }
1978
+ function reduceApmStartupTimeoutTermination(state, input) {
1979
+ if (input.hasRuntimeProgressEvent) {
1980
+ return {
1981
+ nextState: state,
1982
+ shouldTerminate: false,
1983
+ blockedReason: "runtime_progress_started"
1984
+ };
1985
+ }
1986
+ return {
1987
+ nextState: {
1988
+ ...state,
1989
+ isIdle: false,
1990
+ expectedTerminationReason: "startup_timeout"
1991
+ },
1992
+ shouldTerminate: true,
1993
+ blockedReason: null
1994
+ };
1995
+ }
1996
+ function reduceApmGatedFlush(state, input) {
1997
+ return {
1998
+ nextState: {
1999
+ ...state,
2000
+ lastFlushReason: input.reason
2001
+ }
2002
+ };
2003
+ }
2004
+ function reduceApmGatedRecentEvent(state, input) {
2005
+ const summary = `${input.event}:${state.phase}:tools=${state.outstandingToolUses}:compact=${state.compacting}`;
2006
+ return {
2007
+ nextState: {
2008
+ ...state,
2009
+ recentEvents: [...state.recentEvents, summary].slice(-MAX_APM_GATED_STEERING_EVENTS)
2010
+ }
2011
+ };
2012
+ }
2013
+ function reduceApmGatedFlushReadiness(state, input) {
2014
+ if (!input.isGated) return { shouldNotify: false, blockedReason: "non_gated", effects: [] };
2015
+ if (!input.hasSession) return { shouldNotify: false, blockedReason: "missing_session", effects: [] };
2016
+ if (input.inboxLength === 0) return { shouldNotify: false, blockedReason: "empty_inbox", effects: [] };
2017
+ if (state.toolBoundaryFlushDisabled) {
2018
+ return { shouldNotify: false, blockedReason: "tool_boundary_flush_disabled", effects: [] };
2019
+ }
2020
+ if (state.compacting) return { shouldNotify: false, blockedReason: "compacting", effects: [] };
2021
+ if (state.outstandingToolUses > 0) {
2022
+ return { shouldNotify: false, blockedReason: "outstanding_tool_uses", effects: [] };
2023
+ }
2024
+ return {
2025
+ shouldNotify: true,
2026
+ blockedReason: null,
2027
+ effects: [{
2028
+ kind: "notify_stdin",
2029
+ reason: input.reason,
2030
+ stdinMode: "busy",
2031
+ clauseId: "SMR-002"
2032
+ }]
2033
+ };
2034
+ }
2035
+
2036
+ // src/agentCredentialProxy.ts
1547
2037
  var registrations = /* @__PURE__ */ new Map();
1548
2038
  var proxyServerState = null;
1549
2039
  var proxyServerStartPromise = null;
@@ -1895,6 +2385,10 @@ function maxMessageSeq(messages) {
1895
2385
  }
1896
2386
  return maxSeq > 0 ? maxSeq : void 0;
1897
2387
  }
2388
+ function resolveFreshnessBoundary(messages) {
2389
+ const seenUpToSeq = maxMessageSeq(messages);
2390
+ return typeof seenUpToSeq === "number" ? { ok: true, seenUpToSeq } : { ok: false, reason: "missing_seq_boundary" };
2391
+ }
1898
2392
  function sortBySeq(messages) {
1899
2393
  return [...messages].sort((a, b) => messageSeq(a) - messageSeq(b));
1900
2394
  }
@@ -1963,35 +2457,29 @@ function localAgentApiEventsResponse(registration, target) {
1963
2457
  }
1964
2458
  };
1965
2459
  }
1966
- function heldAvailableActions(action) {
1967
- return action === "send" ? ["check_messages", "send_draft", "send_anyway"] : ["check_messages", "retry_action"];
1968
- }
1969
- function localHeldResponse(input) {
1970
- if (input.messages.length === 0) return void 0;
2460
+ function localHeldContext(input) {
2461
+ if (input.messages.length === 0) return { ok: false, reason: "empty_context" };
1971
2462
  const normalized = sortBySeq(normalizeVisibleMessages(input.messages, input.target));
1972
2463
  const heldMessages = latestVisibleMessages(normalized, LOCAL_HELD_CONTEXT_LIMIT);
1973
2464
  const omittedMessageCount = Math.max(0, normalized.length - heldMessages.length);
1974
- const seenUpToSeq = maxMessageSeq(normalized);
1975
- if (seenUpToSeq === void 0) return void 0;
2465
+ const boundary = resolveFreshnessBoundary(normalized);
2466
+ if (!boundary.ok) return boundary;
1976
2467
  input.coordinator.consumeVisibleMessages({
1977
2468
  target: input.target,
1978
2469
  messages: heldMessages,
1979
- boundarySeq: seenUpToSeq,
2470
+ boundarySeq: boundary.seenUpToSeq,
1980
2471
  source: input.source
1981
2472
  });
1982
- const response = {
1983
- state: "held",
1984
- outcome: "held",
1985
- subtype: "freshness",
1986
- reason: "newer_messages_available",
1987
- available_actions: heldAvailableActions(input.action),
1988
- heldMessages,
1989
- newMessageCount: normalized.length,
1990
- shownMessageCount: heldMessages.length,
1991
- omittedMessageCount
2473
+ return {
2474
+ ok: true,
2475
+ context: {
2476
+ heldMessages,
2477
+ newMessageCount: normalized.length,
2478
+ shownMessageCount: heldMessages.length,
2479
+ omittedMessageCount,
2480
+ seenUpToSeq: boundary.seenUpToSeq
2481
+ }
1992
2482
  };
1993
- response.seenUpToSeq = seenUpToSeq;
1994
- return response;
1995
2483
  }
1996
2484
  function recordFreshnessDecision(coordinator, decision) {
1997
2485
  coordinator?.recordFreshnessDecision?.(decision);
@@ -2086,31 +2574,46 @@ async function prepareAgentApiSideEffectForward(registration, headers, rawBody,
2086
2574
  }
2087
2575
  const pending = coordinator.getPendingMessages(target);
2088
2576
  if (pending.length > 0) {
2089
- const localResponse = localHeldResponse({
2090
- action,
2577
+ const modelSeenSeq = coordinator.getBoundary(target);
2578
+ const contextResult = localHeldContext({
2091
2579
  target,
2092
2580
  messages: pending,
2093
2581
  coordinator,
2094
2582
  source: "side_effect_preflight_context"
2095
2583
  });
2096
- if (localResponse) {
2097
- recordFreshnessDecision(coordinator, {
2584
+ if (contextResult.ok) {
2585
+ const { context } = contextResult;
2586
+ const decision = {
2098
2587
  action,
2099
2588
  decision: "local_hold",
2100
2589
  target,
2101
2590
  inboxTrustState: "trusted",
2102
2591
  reason: "exact_target_pending",
2103
2592
  pendingCount: pending.length,
2104
- pendingMaxSeq: maxMessageSeq(pending),
2105
- modelSeenSeq: coordinator.getBoundary(target),
2106
- heldMessageCount: typeof localResponse.shownMessageCount === "number" ? localResponse.shownMessageCount : void 0,
2107
- omittedMessageCount: typeof localResponse.omittedMessageCount === "number" ? localResponse.omittedMessageCount : void 0
2108
- });
2593
+ pendingMaxSeq: context.seenUpToSeq,
2594
+ modelSeenSeq,
2595
+ heldMessageCount: context.shownMessageCount,
2596
+ omittedMessageCount: context.omittedMessageCount
2597
+ };
2598
+ const producerFactId = buildApmFreshnessDecisionProducerFactId(registration.agentId, decision);
2599
+ const localResponse = projectApmHeldFreshnessEnvelope({
2600
+ producerFactId,
2601
+ action,
2602
+ heldMessages: context.heldMessages,
2603
+ newMessageCount: context.newMessageCount,
2604
+ omittedMessageCount: context.omittedMessageCount,
2605
+ seenUpToSeq: context.seenUpToSeq
2606
+ }).body;
2607
+ recordFreshnessDecision(coordinator, { ...decision, producerFactId });
2608
+ return {
2609
+ bodyText: JSON.stringify(body),
2610
+ target,
2611
+ localResponse
2612
+ };
2109
2613
  }
2110
2614
  return {
2111
2615
  bodyText: JSON.stringify(body),
2112
- target,
2113
- localResponse
2616
+ target
2114
2617
  };
2115
2618
  }
2116
2619
  const existingBoundary = typeof body.seenUpToSeq === "number" && Number.isFinite(body.seenUpToSeq) ? Math.max(0, Math.floor(body.seenUpToSeq)) : void 0;
@@ -2131,9 +2634,22 @@ async function prepareAgentApiSideEffectForward(registration, headers, rawBody,
2131
2634
  }
2132
2635
  const recent = await loadRecentTargetMessages(registration, headers, target);
2133
2636
  if (recent.length > 0) {
2134
- const seenUpToSeq = maxMessageSeq(recent);
2637
+ const boundary2 = resolveFreshnessBoundary(recent);
2638
+ if (!boundary2.ok) {
2639
+ recordFreshnessDecision(coordinator, {
2640
+ action,
2641
+ decision: "forward",
2642
+ target,
2643
+ inboxTrustState: "untrusted",
2644
+ reason: "target_first_touch_recent_context_without_seq_boundary",
2645
+ pendingCount: 0,
2646
+ modelSeenSeq: 0
2647
+ });
2648
+ return { bodyText: JSON.stringify(body), target };
2649
+ }
2650
+ const { seenUpToSeq } = boundary2;
2135
2651
  coordinator.consumeVisibleMessages({ target, messages: recent, boundarySeq: seenUpToSeq, source: "side_effect_preflight_context" });
2136
- recordFreshnessDecision(coordinator, {
2652
+ const decision = {
2137
2653
  action,
2138
2654
  decision: "syncing_hold",
2139
2655
  target,
@@ -2144,22 +2660,20 @@ async function prepareAgentApiSideEffectForward(registration, headers, rawBody,
2144
2660
  modelSeenSeq: 0,
2145
2661
  heldMessageCount: recent.length,
2146
2662
  omittedMessageCount: 0
2147
- });
2663
+ };
2664
+ const producerFactId = buildApmFreshnessDecisionProducerFactId(registration.agentId, decision);
2665
+ recordFreshnessDecision(coordinator, { ...decision, producerFactId });
2148
2666
  return {
2149
2667
  bodyText: JSON.stringify(body),
2150
2668
  target,
2151
- localResponse: {
2152
- state: "held",
2153
- outcome: "held",
2154
- subtype: "freshness",
2155
- reason: "newer_messages_available",
2156
- available_actions: heldAvailableActions(action),
2157
- seenUpToSeq,
2669
+ localResponse: projectApmHeldFreshnessEnvelope({
2670
+ producerFactId,
2671
+ action,
2158
2672
  heldMessages: recent,
2159
2673
  newMessageCount: recent.length,
2160
- shownMessageCount: recent.length,
2161
- omittedMessageCount: 0
2162
- }
2674
+ omittedMessageCount: 0,
2675
+ seenUpToSeq
2676
+ }).body
2163
2677
  };
2164
2678
  }
2165
2679
  recordFreshnessDecision(coordinator, {
@@ -5356,7 +5870,7 @@ async function deleteWorkspaceDirectory(dataDir, directoryName) {
5356
5870
  }
5357
5871
 
5358
5872
  // src/runtimeErrorDiagnostics.ts
5359
- import { createHash } from "crypto";
5873
+ import { createHash as createHash2 } from "crypto";
5360
5874
  var MAX_RUNTIME_ERROR_MESSAGE_EXCERPT_CHARS = 4096;
5361
5875
  var RUNTIME_AUTH_ACTION_REQUIRED_PATTERNS = [
5362
5876
  /access token could not be refreshed/i,
@@ -5506,7 +6020,7 @@ function runtimeDisplayName(runtimeId) {
5506
6020
  }
5507
6021
  function fingerprintRuntimeError(value) {
5508
6022
  const normalized = value.toLowerCase().replace(/[0-9a-f]{12,}/g, "<hex>").replace(/\b\d+\b/g, "<num>").replace(/\s+/g, " ").trim();
5509
- return createHash("sha256").update(normalized).digest("hex").slice(0, 16);
6023
+ return createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
5510
6024
  }
5511
6025
  function bucketLength(length) {
5512
6026
  if (length === 0) return "0";
@@ -5612,6 +6126,9 @@ var RuntimeNotificationState = class {
5612
6126
  var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
5613
6127
  var DEFAULT_AGENT_START_INTERVAL_MS = 500;
5614
6128
  var RUNNER_CREDENTIAL_MINT_MAX_ATTEMPTS = 3;
6129
+ function assertNeverApmEffect(effect) {
6130
+ throw new Error(`Unhandled APM gated steering effect: ${String(effect)}`);
6131
+ }
5615
6132
  var RUNNER_CREDENTIAL_MINT_RETRY_DELAY_MS = 250;
5616
6133
  var WORKSPACE_TEXT_FILE_MAX_BYTES = 1048576;
5617
6134
  var WORKSPACE_IMAGE_PREVIEW_MAX_BYTES = 5 * 1024 * 1024;
@@ -5917,7 +6434,8 @@ function formatIncomingMessage(message, driver) {
5917
6434
  const senderType = formatVisibleActorType(message.sender_type);
5918
6435
  const attachSuffix = message.attachments?.length ? ` [${message.attachments.length} attachment${message.attachments.length > 1 ? "s" : ""}: ${message.attachments.map((a) => `${a.filename} (id:${a.id})`).join(", ")} \u2014 use view_file to download]` : "";
5919
6436
  const taskSuffix = message.task_status ? ` [task #${message.task_number} status=${message.task_status}${message.task_assignee_id ? ` assignee=${formatTaskAssigneeType(message.task_assignee_type)}:${message.task_assignee_id}` : ""}]` : "";
5920
- const body = `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(message)}: ${message.content}${attachSuffix}${taskSuffix}`;
6437
+ const lineageSuffix = formatProducerFactLineageBracket(message.producerFactId);
6438
+ const body = `[target=${target} msg=${msgId} time=${time}${senderType}] ${formatSenderHandle(message)}: ${message.content}${attachSuffix}${taskSuffix}${lineageSuffix}`;
5921
6439
  return threadJoinPrefix ? `${threadJoinPrefix}
5922
6440
  ${body}` : body;
5923
6441
  }
@@ -5959,6 +6477,8 @@ function buildUnreadSummary(messages, excludeChannel) {
5959
6477
  var MAX_TRAJECTORY_TEXT = 2e3;
5960
6478
  var TRAJECTORY_COALESCE_MS = 350;
5961
6479
  var ACTIVITY_HEARTBEAT_MS = 6e4;
6480
+ var STDIN_NOTIFICATION_INITIAL_DELAY_MS = 3e3;
6481
+ var STDIN_NOTIFICATION_RETRY_DELAY_MS = 15e3;
5962
6482
  var COMPACTION_STALE_MS = 5 * 6e4;
5963
6483
  var RUNTIME_PROGRESS_STALE_MS = 15 * 6e4;
5964
6484
  var DEFAULT_RUNTIME_START_TIMEOUT_MS = 2 * 6e4;
@@ -5967,7 +6487,6 @@ var MAX_STDOUT_LINES = 8;
5967
6487
  var MAX_STDOUT_LINE_LENGTH = 240;
5968
6488
  var MAX_STDERR_LINES = 8;
5969
6489
  var MAX_STDERR_LINE_LENGTH = 240;
5970
- var MAX_GATED_STEERING_EVENTS = 12;
5971
6490
  var ONBOARDING_MEMORY_SEED_ENV = "SLOCK_ONBOARDING_MEMORY_SEED";
5972
6491
  var FIRST_CINDY_SEED_MODE = "first-cindy";
5973
6492
  function getOnboardingSeedMode(config) {
@@ -6383,13 +6902,8 @@ function stripManagedRunnerCredential(config) {
6383
6902
  }
6384
6903
  function createGatedSteeringState() {
6385
6904
  return {
6386
- phase: "idle",
6387
- outstandingToolUses: 0,
6388
- compacting: false,
6389
- toolBoundaryFlushDisabled: process.env.SLOCK_CLAUDE_GATED_STEERING_TOOL_BOUNDARY === "0",
6390
- lastFlushReason: null,
6391
- recentEvents: [],
6392
- inFlightBatch: null
6905
+ ...createInitialApmGatedSteeringState(),
6906
+ toolBoundaryFlushDisabled: process.env.SLOCK_CLAUDE_GATED_STEERING_TOOL_BOUNDARY === "0"
6393
6907
  };
6394
6908
  }
6395
6909
  var RUNTIME_PROFILE_MIGRATION_MESSAGE_PREFIX = "runtime-profile-migration-";
@@ -6408,7 +6922,7 @@ function runtimeProfileNotificationTitle(kind) {
6408
6922
  }
6409
6923
  function hashRuntimeProfileKey(key) {
6410
6924
  if (!key) return void 0;
6411
- return createHash2("sha256").update(key).digest("hex").slice(0, 16);
6925
+ return createHash3("sha256").update(key).digest("hex").slice(0, 16);
6412
6926
  }
6413
6927
  function runtimeProfileTurnControl(kind, key, source) {
6414
6928
  return {
@@ -6680,6 +7194,19 @@ function summarizeMessageInputBytes(messages) {
6680
7194
  runtime_input_thread_context_content_bytes_bucket: bucketBytes(threadContextContentBytes)
6681
7195
  };
6682
7196
  }
7197
+ function messageProducerFactTraceAttrs(messages) {
7198
+ if (!messages || messages.length === 0) return {};
7199
+ const producerFactIds = /* @__PURE__ */ new Set();
7200
+ for (const message of messages) {
7201
+ const producerFactId = typeof message.producerFactId === "string" ? message.producerFactId.trim() : "";
7202
+ if (producerFactId) producerFactIds.add(producerFactId);
7203
+ }
7204
+ if (producerFactIds.size === 0) return {};
7205
+ return {
7206
+ message_producer_fact_count: producerFactIds.size,
7207
+ ...producerFactIds.size === 1 ? { message_producer_fact_id: [...producerFactIds][0] } : {}
7208
+ };
7209
+ }
6683
7210
  function buildRuntimeInputTraceAttrs(opts) {
6684
7211
  return {
6685
7212
  runtime_input_source: opts.source,
@@ -6748,6 +7275,7 @@ var AgentProcessManager = class _AgentProcessManager {
6748
7275
  driverResolver;
6749
7276
  defaultAgentEnvVarsProvider;
6750
7277
  tracer;
7278
+ stdinNotificationRetryMs;
6751
7279
  cliTransportTraceDir = null;
6752
7280
  deliveryTraceContexts = /* @__PURE__ */ new WeakMap();
6753
7281
  processExitTraceAttrs = /* @__PURE__ */ new WeakMap();
@@ -6763,6 +7291,10 @@ var AgentProcessManager = class _AgentProcessManager {
6763
7291
  this.driverResolver = opts.driverResolver || getDriver;
6764
7292
  this.defaultAgentEnvVarsProvider = opts.defaultAgentEnvVarsProvider || null;
6765
7293
  this.tracer = opts.tracer ?? noopTracer;
7294
+ this.stdinNotificationRetryMs = Math.max(
7295
+ 0,
7296
+ Math.floor(opts.stdinNotificationRetryMs ?? STDIN_NOTIFICATION_RETRY_DELAY_MS)
7297
+ );
6766
7298
  this.maxConcurrentAgentStarts = Math.max(
6767
7299
  1,
6768
7300
  Math.floor(
@@ -6793,6 +7325,11 @@ var AgentProcessManager = class _AgentProcessManager {
6793
7325
  getVisibleBoundary(agentId, target) {
6794
7326
  return this.agentVisibleBoundaries.get(agentId)?.get(target);
6795
7327
  }
7328
+ scheduleStdinNotification(agentId, ap, delayMs) {
7329
+ return ap.notifications.schedule(() => {
7330
+ this.sendStdinNotification(agentId);
7331
+ }, delayMs);
7332
+ }
6796
7333
  allPendingVisibleMessages(agentId) {
6797
7334
  const collect = (messages) => (messages ?? []).filter((message) => typeof message.seq === "number" && message.seq > 0);
6798
7335
  return [
@@ -6888,20 +7425,16 @@ var AgentProcessManager = class _AgentProcessManager {
6888
7425
  recordProxyFailure: (input) => this.recordAgentProxyFailure(agentId, input),
6889
7426
  recordTransportNormalizedError: (input) => this.recordAgentProxyTransportNormalizedError(agentId, input),
6890
7427
  recordFreshnessDecision: (input) => {
7428
+ const producerFactId = input.producerFactId ?? buildApmFreshnessDecisionProducerFactId(agentId, input);
7429
+ const trace = projectApmFreshnessDecisionTrace({
7430
+ producerFactId,
7431
+ decision: input
7432
+ });
6891
7433
  this.recordDaemonTrace("daemon.agent.inbox.freshness_decision", {
6892
7434
  agentId,
6893
- action: input.action,
6894
- decision: input.decision,
6895
- target: input.target,
6896
- inbox_trust_state: input.inboxTrustState,
6897
- reason: input.reason,
6898
- pending_count: input.pendingCount,
6899
- pending_max_seq: input.pendingMaxSeq,
6900
- model_seen_seq: input.modelSeenSeq,
6901
- held_message_count: input.heldMessageCount,
6902
- omitted_message_count: input.omittedMessageCount
7435
+ ...trace.attrs
6903
7436
  });
6904
- this.recordFreshnessDecisionActivity(agentId, input);
7437
+ this.recordFreshnessDecisionActivity(agentId, input, producerFactId);
6905
7438
  }
6906
7439
  };
6907
7440
  }
@@ -6931,20 +7464,17 @@ var AgentProcessManager = class _AgentProcessManager {
6931
7464
  upstream: input.upstream
6932
7465
  }, "error");
6933
7466
  }
6934
- recordFreshnessDecisionActivity(agentId, input) {
7467
+ recordFreshnessDecisionActivity(agentId, input, producerFactId) {
6935
7468
  if (input.decision !== "local_hold" && input.decision !== "syncing_hold") return;
6936
7469
  const ap = this.agents.get(agentId);
6937
7470
  const messageCount = input.pendingCount ?? input.heldMessageCount ?? 0;
6938
- const title = input.action === "send" ? "Send held by freshness check" : input.action === "task_claim" ? "Task claim held by freshness check" : "Task update held by freshness check";
6939
- const entry = {
6940
- kind: "slock_action",
6941
- title,
6942
- text: [
6943
- input.target ? `target: ${input.target}` : null,
6944
- `new messages: ${messageCount} newer message${messageCount === 1 ? "" : "s"}`,
6945
- `decision: ${input.decision === "syncing_hold" ? "syncing hold" : "local hold"}; review the newer context before retrying`
6946
- ].filter((line) => Boolean(line)).join("\n")
6947
- };
7471
+ const entry = projectApmHeldFreshnessActivity({
7472
+ producerFactId,
7473
+ action: input.action,
7474
+ decision: input.decision,
7475
+ target: input.target,
7476
+ messageCount
7477
+ }).entry;
6948
7478
  if (ap) ap.activityClientSeq += 1;
6949
7479
  this.sendToServer({
6950
7480
  type: "agent:activity",
@@ -7353,7 +7883,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7353
7883
  notifications: new RuntimeNotificationState(),
7354
7884
  activityHeartbeat: null,
7355
7885
  startupTimeoutTimer: null,
7356
- startupTimedOut: false,
7357
7886
  compactionWatchdog: null,
7358
7887
  compactionStartedAt: null,
7359
7888
  runtimeProgress: new RuntimeProgressState(Date.now()),
@@ -7485,7 +8014,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7485
8014
  this.clearStalledRecoverySigtermWatchdog(ap);
7486
8015
  const finalCode = ap.exitCode ?? code;
7487
8016
  const finalSignal = ap.exitSignal ?? signal;
7488
- const expectedTermination = Boolean(ap.expectedTerminationReason) || ap.startupTimedOut;
8017
+ const startupTimeoutTermination = ap.expectedTerminationReason === "startup_timeout";
8018
+ const expectedTermination = Boolean(ap.expectedTerminationReason);
7489
8019
  const processEndedCleanly = finalCode === 0 || expectedTermination && !ap.lastRuntimeError;
7490
8020
  const terminalFailureDetail = processEndedCleanly ? null : classifyTerminalFailure(ap);
7491
8021
  const resumeRecoveryReason = resumeSessionRecoveryReason(ap);
@@ -7589,7 +8119,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7589
8119
  });
7590
8120
  logger.warn(`[Agent ${agentId}] Recoverable provider stream failure (${reason}) \u2014 keeping agent wakeable`);
7591
8121
  this.sendAgentStatus(agentId, "active", ap.launchId);
7592
- } else if (ap.startupTimedOut) {
8122
+ } else if (startupTimeoutTermination) {
7593
8123
  logger.warn(`[Agent ${agentId}] Startup timeout cleanup completed (${reason})`);
7594
8124
  } else {
7595
8125
  this.idleAgentConfigs.delete(agentId);
@@ -7597,7 +8127,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7597
8127
  this.sendAgentStatus(agentId, "inactive", ap.launchId);
7598
8128
  }
7599
8129
  if (terminalFailureDetail) {
7600
- if (!ap.startupTimedOut) {
8130
+ if (!startupTimeoutTermination) {
7601
8131
  this.broadcastActivity(
7602
8132
  agentId,
7603
8133
  "error",
@@ -7795,9 +8325,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7795
8325
  pendingMessages: ap.inbox.length
7796
8326
  });
7797
8327
  } else if (!ap.notifications.hasTimer) {
7798
- ap.notifications.schedule(() => {
7799
- this.sendStdinNotification(agentId);
7800
- }, 3e3);
8328
+ this.scheduleStdinNotification(agentId, ap, STDIN_NOTIFICATION_INITIAL_DELAY_MS);
7801
8329
  }
7802
8330
  }
7803
8331
  this.recordDaemonTrace("daemon.agent.runtime_profile.routed", {
@@ -7975,7 +8503,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7975
8503
  if (ap.isIdle && ap.driver.supportsStdinNotification && ap.sessionId) {
7976
8504
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
7977
8505
  nextMessages.push(message);
7978
- ap.isIdle = false;
8506
+ this.commitApmIdleState(agentId, ap, false);
7979
8507
  this.startRuntimeTrace(agentId, ap, "stdin-idle-delivery", nextMessages);
7980
8508
  this.broadcastActivity(agentId, "working", "Message received");
7981
8509
  const stdinAccepted = this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle");
@@ -8059,9 +8587,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8059
8587
  if (ap.driver.busyDeliveryMode === "gated") {
8060
8588
  ap.notifications.add();
8061
8589
  if (!ap.notifications.hasTimer) {
8062
- ap.notifications.schedule(() => {
8063
- this.sendStdinNotification(agentId);
8064
- }, 3e3);
8590
+ this.scheduleStdinNotification(agentId, ap, STDIN_NOTIFICATION_INITIAL_DELAY_MS);
8065
8591
  }
8066
8592
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
8067
8593
  reason: "busy_message",
@@ -8082,9 +8608,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8082
8608
  }
8083
8609
  ap.notifications.add();
8084
8610
  if (!ap.notifications.hasTimer) {
8085
- ap.notifications.schedule(() => {
8086
- this.sendStdinNotification(agentId);
8087
- }, 3e3);
8611
+ this.scheduleStdinNotification(agentId, ap, STDIN_NOTIFICATION_INITIAL_DELAY_MS);
8088
8612
  }
8089
8613
  this.recordDaemonTrace("daemon.agent.delivery.routed", this.deliveryTraceAttrs(agentId, message, {
8090
8614
  outcome: "queued_busy_notification",
@@ -8258,7 +8782,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8258
8782
  return true;
8259
8783
  }
8260
8784
  if (ap?.sessionId && ap.driver.supportsStdinNotification && ap.isIdle) {
8261
- ap.isIdle = false;
8785
+ this.commitApmIdleState(agentId, ap, false);
8262
8786
  this.startRuntimeTrace(agentId, ap, "runtime-profile", [message]);
8263
8787
  const written = this.deliverMessagesViaStdin(agentId, ap, [message], "idle");
8264
8788
  span.end(written ? "ok" : "error", {
@@ -8768,18 +9292,22 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8768
9292
  if (!ap || !ap.compactionStartedAt) return;
8769
9293
  this.clearCompactionWatchdog(ap);
8770
9294
  this.broadcastActivity(agentId, "working", detail, [{ kind: "compaction_finished" }]);
8771
- ap.gatedSteering.compacting = false;
8772
- this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "compaction_finished", inferred: true });
9295
+ const reduction = reduceApmGatedCompaction(ap.gatedSteering, { kind: "compaction_finished" });
9296
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, {
9297
+ event: "compaction_finished",
9298
+ inferred: true
9299
+ });
8773
9300
  if (options.flushBoundaryMessages ?? true) {
8774
9301
  this.flushCompactionBoundaryMessages(agentId, ap);
8775
9302
  }
8776
- ap.isIdle = false;
9303
+ this.commitApmIdleState(agentId, ap, false);
8777
9304
  }
8778
9305
  interruptCompactionIfActive(agentId) {
8779
9306
  const ap = this.agents.get(agentId);
8780
9307
  if (!ap || !ap.compactionStartedAt && !ap.gatedSteering.compacting) return;
8781
9308
  this.clearCompactionWatchdog(ap);
8782
- ap.gatedSteering.compacting = false;
9309
+ const reduction = reduceApmGatedCompaction(ap.gatedSteering, { kind: "compaction_interrupted" });
9310
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState);
8783
9311
  }
8784
9312
  messagesTraceAttrs(messages) {
8785
9313
  if (!messages || messages.length === 0) return {};
@@ -8792,6 +9320,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8792
9320
  message_id_present: Boolean(first.message_id),
8793
9321
  deliveryId: context.deliveryId,
8794
9322
  delivery_correlation_id: context.deliveryId,
9323
+ ...messageProducerFactTraceAttrs(messages),
8795
9324
  control_kind: notification.kind,
8796
9325
  key_present: Boolean(notification.key),
8797
9326
  runtime_profile_control_kind: notification.kind,
@@ -8804,7 +9333,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8804
9333
  messageId: first.message_id,
8805
9334
  message_id_present: Boolean(first.message_id),
8806
9335
  deliveryId: context.deliveryId,
8807
- delivery_correlation_id: context.deliveryId ?? first.message_id
9336
+ delivery_correlation_id: context.deliveryId ?? first.message_id,
9337
+ ...messageProducerFactTraceAttrs(messages)
8808
9338
  };
8809
9339
  }
8810
9340
  runtimeProfileTurnControlTraceAttrs(control) {
@@ -8893,10 +9423,11 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8893
9423
  }
8894
9424
  recordGatedSteeringEvent(agentId, ap, event, attrs = {}) {
8895
9425
  if (ap.driver.busyDeliveryMode !== "gated") return;
8896
- const summary = `${event}:${ap.gatedSteering.phase}:tools=${ap.gatedSteering.outstandingToolUses}:compact=${ap.gatedSteering.compacting}`;
8897
- ap.gatedSteering.recentEvents.push(summary);
8898
- ap.gatedSteering.recentEvents = ap.gatedSteering.recentEvents.slice(-MAX_GATED_STEERING_EVENTS);
9426
+ const reduction = reduceApmGatedRecentEvent(ap.gatedSteering, { event });
9427
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState);
8899
9428
  this.recordRuntimeTraceEvent(agentId, ap, `runtime.gated_steering.${event}`, {
9429
+ isIdle: ap.gatedSteering.isIdle,
9430
+ expectedTerminationReason: ap.gatedSteering.expectedTerminationReason,
8900
9431
  phase: ap.gatedSteering.phase,
8901
9432
  outstandingToolUses: ap.gatedSteering.outstandingToolUses,
8902
9433
  compacting: ap.gatedSteering.compacting,
@@ -8905,63 +9436,110 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8905
9436
  ...attrs
8906
9437
  });
8907
9438
  }
8908
- setGatedSteeringPhase(agentId, ap, phase, attrs = {}) {
8909
- if (!ap || ap.driver.busyDeliveryMode !== "gated") return;
8910
- ap.gatedSteering.phase = phase;
8911
- this.recordGatedSteeringEvent(agentId, ap, "phase", attrs);
8912
- }
8913
- tryFlushGatedSteering(agentId, ap, reason) {
8914
- if (ap.driver.busyDeliveryMode !== "gated") return false;
8915
- if (!ap.sessionId || ap.inbox.length === 0) return false;
8916
- if (reason !== "turn_end") {
8917
- if (ap.gatedSteering.toolBoundaryFlushDisabled) return false;
8918
- if (ap.gatedSteering.compacting || ap.gatedSteering.outstandingToolUses > 0) return false;
8919
- this.recordGatedSteeringEvent(agentId, ap, "notify", { reason, pendingMessages: ap.inbox.length });
8920
- return this.sendStdinNotification(agentId);
8921
- }
8922
- const pendingMessages = ap.inbox.length;
8923
- const pendingNotificationCount = ap.notifications.pendingCount;
8924
- const nextMessages = ap.inbox.splice(0, ap.inbox.length);
8925
- ap.notifications.clear();
8926
- ap.gatedSteering.lastFlushReason = reason;
8927
- if (reason !== "turn_end") {
8928
- ap.gatedSteering.inFlightBatch = {
8929
- reason,
8930
- messages: nextMessages
8931
- };
8932
- } else {
8933
- ap.gatedSteering.inFlightBatch = null;
8934
- }
8935
- this.recordGatedSteeringEvent(agentId, ap, "flush", { reason, messageCount: nextMessages.length });
8936
- logger.info(
8937
- `[Agent ${agentId}] Claude gated steering flush reason=${reason} messages=${nextMessages.length}`
8938
- );
8939
- if (reason === "turn_end") {
8940
- this.broadcastActivity(agentId, "working", "Message received");
8941
- }
8942
- if (this.deliverMessagesViaStdin(agentId, ap, nextMessages, reason === "turn_end" ? "idle" : "busy")) {
8943
- return true;
9439
+ commitGatedSteeringDecisionState(agentId, ap, nextState, phaseEventAttrs) {
9440
+ ap.gatedSteering.phase = nextState.phase;
9441
+ ap.gatedSteering.outstandingToolUses = nextState.outstandingToolUses;
9442
+ ap.gatedSteering.compacting = nextState.compacting;
9443
+ ap.gatedSteering.toolBoundaryFlushDisabled = nextState.toolBoundaryFlushDisabled;
9444
+ ap.gatedSteering.lastFlushReason = nextState.lastFlushReason;
9445
+ ap.gatedSteering.recentEvents = nextState.recentEvents;
9446
+ ap.gatedSteering.isIdle = nextState.isIdle;
9447
+ ap.gatedSteering.expectedTerminationReason = nextState.expectedTerminationReason;
9448
+ ap.isIdle = nextState.isIdle;
9449
+ ap.expectedTerminationReason = nextState.expectedTerminationReason;
9450
+ if (phaseEventAttrs) {
9451
+ this.recordGatedSteeringEvent(agentId, ap, "phase", phaseEventAttrs);
9452
+ }
9453
+ }
9454
+ commitApmIdleState(agentId, ap, isIdle) {
9455
+ const reduction = reduceApmIdleState(ap.gatedSteering, { isIdle });
9456
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState);
9457
+ }
9458
+ notifyGatedSteeringBoundary(agentId, ap, reason) {
9459
+ const readiness = reduceApmGatedFlushReadiness(ap.gatedSteering, {
9460
+ isGated: ap.driver.busyDeliveryMode === "gated",
9461
+ hasSession: Boolean(ap.sessionId),
9462
+ inboxLength: ap.inbox.length,
9463
+ reason
9464
+ });
9465
+ if (readiness.effects.length === 0) return false;
9466
+ let didExecute = false;
9467
+ for (const effect of readiness.effects) {
9468
+ didExecute = this.executeApmGatedSteeringEffect(agentId, ap, effect) || didExecute;
8944
9469
  }
8945
- if (reason !== "turn_end") {
8946
- ap.gatedSteering.inFlightBatch = null;
9470
+ return didExecute;
9471
+ }
9472
+ recordApmGatedSteeringEffectTrace(agentId, ap, effect, attrs) {
9473
+ this.recordDaemonTrace("daemon.apm.gated_effect", {
9474
+ agentId,
9475
+ launchId: ap.launchId || void 0,
9476
+ runtime: ap.config.runtime,
9477
+ effect_kind: effect.kind,
9478
+ reason: effect.reason,
9479
+ target: "runtime-stdin",
9480
+ stdin_mode: effect.stdinMode,
9481
+ clause_id: effect.clauseId,
9482
+ pending_messages: ap.inbox.length,
9483
+ ...attrs
9484
+ });
9485
+ }
9486
+ executeApmGatedSteeringEffect(agentId, ap, effect) {
9487
+ switch (effect.kind) {
9488
+ case "notify_stdin":
9489
+ this.recordGatedSteeringEvent(agentId, ap, "notify", {
9490
+ reason: effect.reason,
9491
+ pendingMessages: ap.inbox.length
9492
+ });
9493
+ {
9494
+ const written = this.sendStdinNotification(agentId);
9495
+ this.recordApmGatedSteeringEffectTrace(agentId, ap, effect, {
9496
+ outcome: written ? "written" : "not_written"
9497
+ });
9498
+ return written;
9499
+ }
9500
+ case "deliver_stdin": {
9501
+ const messages = ap.inbox.splice(0, ap.inbox.length);
9502
+ ap.notifications.clear();
9503
+ if (messages.length === 0) {
9504
+ this.recordApmGatedSteeringEffectTrace(agentId, ap, effect, {
9505
+ outcome: "empty",
9506
+ delivered_messages_count: 0
9507
+ });
9508
+ return true;
9509
+ }
9510
+ if (ap.driver.busyDeliveryMode === "gated") {
9511
+ const flushReduction = reduceApmGatedFlush(ap.gatedSteering, { reason: effect.reason });
9512
+ this.commitGatedSteeringDecisionState(agentId, ap, flushReduction.nextState);
9513
+ this.recordGatedSteeringEvent(agentId, ap, "flush", {
9514
+ reason: effect.reason,
9515
+ messageCount: messages.length
9516
+ });
9517
+ }
9518
+ this.broadcastActivity(agentId, "working", "Message received");
9519
+ const accepted = this.deliverMessagesViaStdin(agentId, ap, messages, effect.stdinMode);
9520
+ this.recordApmGatedSteeringEffectTrace(agentId, ap, effect, {
9521
+ outcome: accepted ? "written" : "not_written",
9522
+ delivered_messages_count: messages.length
9523
+ });
9524
+ return accepted;
9525
+ }
9526
+ default:
9527
+ return assertNeverApmEffect(effect);
8947
9528
  }
8948
- ap.notifications.add(pendingNotificationCount || pendingMessages);
8949
- return false;
8950
9529
  }
8951
9530
  flushCompactionBoundaryMessages(agentId, ap) {
8952
- if (!ap.sessionId || !ap.driver.supportsStdinNotification || ap.inbox.length === 0) return false;
8953
- if (ap.notifications.pendingCount > 0) {
8954
- ap.notifications.clearTimer();
8955
- this.sendStdinNotification(agentId);
8956
- return true;
9531
+ const reduction = reduceApmGatedCompactionBoundaryFlush(ap.gatedSteering, {
9532
+ hasSession: Boolean(ap.sessionId),
9533
+ supportsStdinNotification: ap.driver.supportsStdinNotification,
9534
+ inboxLength: ap.inbox.length,
9535
+ pendingNotificationCount: ap.notifications.pendingCount
9536
+ });
9537
+ if (reduction.effects.length === 0) return false;
9538
+ ap.notifications.clearTimer();
9539
+ for (const effect of reduction.effects) {
9540
+ this.executeApmGatedSteeringEffect(agentId, ap, effect);
8957
9541
  }
8958
- return false;
8959
- }
8960
- clearGatedInFlightBatch(agentId, ap, reason) {
8961
- if (ap.driver.busyDeliveryMode !== "gated" || !ap.gatedSteering.inFlightBatch) return;
8962
- const messageCount = ap.gatedSteering.inFlightBatch.messages.length;
8963
- ap.gatedSteering.inFlightBatch = null;
8964
- this.recordGatedSteeringEvent(agentId, ap, "ack", { reason, messageCount });
9542
+ return true;
8965
9543
  }
8966
9544
  startRuntimeStartupTimeout(agentId, ap) {
8967
9545
  const timeoutMs = runtimeStartTimeoutMs();
@@ -8979,14 +9557,17 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8979
9557
  handleRuntimeStartupTimeout(agentId, ap, timeoutMs) {
8980
9558
  const current = this.agents.get(agentId);
8981
9559
  if (current !== ap) return;
8982
- if (ap.runtimeProgress.lastEventKind) {
9560
+ const reduction = reduceApmStartupTimeoutTermination(ap.gatedSteering, {
9561
+ hasRuntimeProgressEvent: Boolean(ap.runtimeProgress.lastEventKind)
9562
+ });
9563
+ if (!reduction.shouldTerminate) {
8983
9564
  this.clearRuntimeStartupTimeout(ap);
8984
9565
  return;
8985
9566
  }
8986
9567
  this.clearRuntimeStartupTimeout(ap);
9568
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState);
8987
9569
  const terminalFailureDetail = classifyTerminalFailure(ap);
8988
9570
  const detail = terminalFailureDetail?.detail ?? formatRuntimeStartTimeoutMessage(ap.driver.id);
8989
- ap.startupTimedOut = true;
8990
9571
  ap.lastRuntimeError = detail;
8991
9572
  ap.runtimeProgress.markStale();
8992
9573
  const staleForMs = Math.max(timeoutMs, ap.runtimeProgress.ageMs());
@@ -9015,6 +9596,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9015
9596
  try {
9016
9597
  this.processExitTraceAttrs.set(ap.process, {
9017
9598
  stop_source: "startup_timeout",
9599
+ expectedTerminationReason: "startup_timeout",
9018
9600
  timeout_ms: timeoutMs
9019
9601
  });
9020
9602
  ap.process.kill("SIGTERM");
@@ -9023,18 +9605,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9023
9605
  logger.warn(`[Agent ${agentId}] Failed to terminate startup-timed-out ${ap.driver.id} process: ${reason}`);
9024
9606
  }
9025
9607
  }
9026
- requeueGatedInFlightBatch(agentId, ap, reason) {
9027
- if (ap.driver.busyDeliveryMode !== "gated" || !ap.gatedSteering.inFlightBatch) return;
9028
- const batch = ap.gatedSteering.inFlightBatch;
9029
- ap.gatedSteering.inFlightBatch = null;
9030
- ap.inbox.unshift(...batch.messages);
9031
- ap.notifications.add(batch.messages.length);
9032
- this.recordGatedSteeringEvent(agentId, ap, "requeue", {
9033
- reason,
9034
- originalFlushReason: batch.reason,
9035
- messageCount: batch.messages.length
9036
- });
9037
- }
9038
9608
  isThinkingBlockMutationError(message) {
9039
9609
  return /thinking.*redacted_thinking|redacted_thinking.*thinking/i.test(message) && /cannot be modified/i.test(message);
9040
9610
  }
@@ -9068,16 +9638,22 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9068
9638
  return true;
9069
9639
  }
9070
9640
  recoverStaleProcessForQueuedMessageIfNeeded(agentId, ap) {
9071
- if (ap.inbox.length === 0) return false;
9072
- if (ap.expectedTerminationReason === "stalled_recovery") {
9641
+ const staleForMs = ap.runtimeProgress.ageMs();
9642
+ const reduction = reduceApmStalledRecoveryTermination(ap.gatedSteering, {
9643
+ inboxLength: ap.inbox.length,
9644
+ supportsStdinNotification: ap.driver.supportsStdinNotification,
9645
+ busyDeliveryMode: ap.driver.busyDeliveryMode,
9646
+ hasSession: Boolean(ap.sessionId),
9647
+ hasDirectStdinRecoveryEvidence: hasDirectStdinRecoveryEvidence(ap),
9648
+ runtimeProgressIsStale: ap.runtimeProgress.isStale,
9649
+ staleForMs,
9650
+ staleThresholdMs: RUNTIME_PROGRESS_STALE_MS
9651
+ });
9652
+ if (reduction.alreadyRecovering) {
9073
9653
  return true;
9074
9654
  }
9075
- const directStdinRuntime = ap.driver.supportsStdinNotification && ap.driver.busyDeliveryMode === "direct";
9076
- const canRestartDirectStdinProcess = directStdinRuntime && Boolean(ap.sessionId) && (ap.gatedSteering.outstandingToolUses === 0 || hasDirectStdinRecoveryEvidence(ap));
9077
- const canRestartStalledProcess = !ap.driver.supportsStdinNotification || canRestartDirectStdinProcess;
9078
- if (!canRestartStalledProcess) return false;
9079
- const staleForMs = ap.runtimeProgress.ageMs();
9080
- if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgress.isStale) return false;
9655
+ if (!reduction.shouldTerminate) return false;
9656
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState);
9081
9657
  const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
9082
9658
  ap.runtimeProgress.markStale();
9083
9659
  const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
@@ -9103,7 +9679,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9103
9679
  ...runtimeTraceCounterAttrs(ap),
9104
9680
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_stalled")
9105
9681
  });
9106
- ap.expectedTerminationReason = "stalled_recovery";
9107
9682
  const runtimeLabel = ap.driver.id === "opencode" ? "OpenCode" : ap.driver.id;
9108
9683
  logger.warn(
9109
9684
  `[Agent ${agentId}] ${runtimeLabel} process stalled for ${staleForMinutes}m with ${ap.inbox.length} queued message(s); terminating for restart`
@@ -9169,31 +9744,32 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9169
9744
  this.sendRuntimeProfileReport(agentId);
9170
9745
  break;
9171
9746
  case "thinking": {
9172
- if (ap) this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
9173
9747
  this.completeCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
9174
9748
  this.queueTrajectoryText(agentId, "thinking", event.text);
9175
- if (ap) ap.isIdle = false;
9176
- this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "thinking" });
9749
+ if (ap) {
9750
+ const reduction = reduceApmGatedAssistantContinuation(ap.gatedSteering);
9751
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, { event: "thinking" });
9752
+ }
9177
9753
  break;
9178
9754
  }
9179
9755
  case "text": {
9180
- if (ap) this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
9181
9756
  this.completeCompactionIfActive(agentId, "Context compaction finished (inferred from resumed output)");
9182
9757
  this.queueTrajectoryText(agentId, "text", event.text);
9183
- if (ap) ap.isIdle = false;
9184
- this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "text" });
9758
+ if (ap) {
9759
+ const reduction = reduceApmGatedAssistantContinuation(ap.gatedSteering);
9760
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, { event: "text" });
9761
+ }
9185
9762
  break;
9186
9763
  }
9187
9764
  case "tool_call": {
9188
- if (ap) this.clearGatedInFlightBatch(agentId, ap, "non_error_progress");
9189
9765
  this.completeCompactionIfActive(agentId, "Context compaction finished (inferred from resumed tool use)");
9190
9766
  this.flushPendingTrajectory(agentId);
9191
9767
  const invocation = normalizeToolDisplayInvocation(event.name, event.input);
9192
9768
  if (ap) {
9193
- ap.gatedSteering.outstandingToolUses++;
9769
+ const reduction = reduceApmGatedToolUse(ap.gatedSteering, { kind: "tool_call" });
9194
9770
  this.noteRuntimeProfileToolCall(agentId, ap, invocation.toolName);
9195
9771
  this.recordRuntimeTraceEvent(agentId, ap, "tool.call.started", { tool: invocation.toolName });
9196
- this.setGatedSteeringPhase(agentId, ap, "tool_wait", {
9772
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, {
9197
9773
  event: "tool_call",
9198
9774
  tool: invocation.toolName
9199
9775
  });
@@ -9205,23 +9781,20 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9205
9781
  toolName: invocation.toolName,
9206
9782
  toolInput: inputSummary
9207
9783
  }]);
9208
- if (ap) ap.isIdle = false;
9209
9784
  break;
9210
9785
  }
9211
9786
  case "tool_output": {
9212
9787
  const invocation = normalizeToolDisplayInvocation(event.name, {});
9213
9788
  if (ap) {
9214
- const hadOutstandingToolUse = ap.gatedSteering.outstandingToolUses > 0;
9215
- ap.gatedSteering.outstandingToolUses = Math.max(0, ap.gatedSteering.outstandingToolUses - 1);
9789
+ const reduction = reduceApmGatedToolUse(ap.gatedSteering, { kind: "tool_output" });
9216
9790
  this.recordRuntimeTraceEvent(agentId, ap, "tool.output.observed", { tool: invocation.toolName });
9217
9791
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.continuation.expected");
9218
- this.setGatedSteeringPhase(agentId, ap, "tool_boundary", {
9792
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, {
9219
9793
  event: "tool_output",
9220
9794
  tool: invocation.toolName
9221
9795
  });
9222
- ap.isIdle = false;
9223
- if (hadOutstandingToolUse && ap.gatedSteering.outstandingToolUses === 0) {
9224
- this.tryFlushGatedSteering(agentId, ap, "tool_batch_complete");
9796
+ if (reduction.shouldFlushToolBatch) {
9797
+ this.notifyGatedSteeringBoundary(agentId, ap, "tool_batch_complete");
9225
9798
  }
9226
9799
  }
9227
9800
  break;
@@ -9232,9 +9805,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9232
9805
  if (ap) this.startCompactionWatchdog(agentId, ap);
9233
9806
  this.broadcastActivity(agentId, "working", "Compacting context", [{ kind: "compaction_started" }]);
9234
9807
  if (ap) {
9235
- ap.gatedSteering.compacting = true;
9236
- this.setGatedSteeringPhase(agentId, ap, "compacting", { event: "compaction_started" });
9237
- ap.isIdle = false;
9808
+ const reduction = reduceApmGatedCompaction(ap.gatedSteering, { kind: "compaction_started" });
9809
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, { event: "compaction_started" });
9238
9810
  }
9239
9811
  break;
9240
9812
  case "compaction_finished":
@@ -9243,10 +9815,9 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9243
9815
  if (ap) this.clearCompactionWatchdog(ap);
9244
9816
  this.broadcastActivity(agentId, "working", "Context compaction finished", [{ kind: "compaction_finished" }]);
9245
9817
  if (ap) {
9246
- ap.gatedSteering.compacting = false;
9247
- this.setGatedSteeringPhase(agentId, ap, "assistant_continuation", { event: "compaction_finished" });
9818
+ const reduction = reduceApmGatedCompaction(ap.gatedSteering, { kind: "compaction_finished" });
9819
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, { event: "compaction_finished" });
9248
9820
  this.flushCompactionBoundaryMessages(agentId, ap);
9249
- ap.isIdle = false;
9250
9821
  }
9251
9822
  break;
9252
9823
  case "turn_end":
@@ -9256,28 +9827,21 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9256
9827
  });
9257
9828
  this.flushPendingTrajectory(agentId);
9258
9829
  if (ap) {
9259
- this.clearGatedInFlightBatch(agentId, ap, "turn_end");
9260
9830
  if (event.sessionId) ap.sessionId = event.sessionId;
9261
- ap.gatedSteering.outstandingToolUses = 0;
9262
- ap.gatedSteering.compacting = false;
9263
- this.setGatedSteeringPhase(agentId, ap, "idle", { event: "turn_end" });
9264
- if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
9265
- const nextMessages = ap.inbox.splice(0, ap.inbox.length);
9266
- ap.notifications.clear();
9267
- if (ap.driver.busyDeliveryMode === "gated") {
9268
- ap.gatedSteering.lastFlushReason = "turn_end";
9269
- this.recordGatedSteeringEvent(agentId, ap, "flush", {
9270
- reason: "turn_end",
9271
- messageCount: nextMessages.length
9272
- });
9273
- }
9274
- this.broadcastActivity(agentId, "working", "Message received");
9275
- if (!this.deliverMessagesViaStdin(agentId, ap, nextMessages, "idle")) {
9276
- ap.isIdle = true;
9831
+ const reduction = reduceApmGatedTurnEnd(ap.gatedSteering, {
9832
+ inboxLength: ap.inbox.length,
9833
+ supportsStdinNotification: ap.driver.supportsStdinNotification,
9834
+ hasSession: Boolean(ap.sessionId),
9835
+ terminateProcessOnTurnEnd: ap.driver.terminateProcessOnTurnEnd === true
9836
+ });
9837
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, { event: "turn_end" });
9838
+ const deliverStdinEffect = reduction.effects.find((effect) => effect.kind === "deliver_stdin");
9839
+ if (deliverStdinEffect) {
9840
+ if (!this.executeApmGatedSteeringEffect(agentId, ap, deliverStdinEffect)) {
9841
+ this.commitApmIdleState(agentId, ap, true);
9277
9842
  this.broadcastActivity(agentId, "online", "Idle");
9278
9843
  }
9279
9844
  } else {
9280
- ap.isIdle = true;
9281
9845
  if (ap.lastRuntimeError) {
9282
9846
  this.broadcastActivity(agentId, "error", ap.lastRuntimeError);
9283
9847
  } else {
@@ -9290,7 +9854,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9290
9854
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "turn_end")
9291
9855
  });
9292
9856
  if (ap.driver.terminateProcessOnTurnEnd) {
9293
- ap.expectedTerminationReason = "turn_end";
9294
9857
  logger.info(`[Agent ${agentId}] Turn completed; terminating ${ap.driver.id} process`);
9295
9858
  try {
9296
9859
  this.processExitTraceAttrs.set(ap.process, {
@@ -9319,10 +9882,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9319
9882
  if (runtimeErrorDiagnostics.spanAttrs.runtime_error_action_required === true) {
9320
9883
  visibleErrorMessage = formatRuntimeLoginRequiredMessage(ap.driver.id);
9321
9884
  }
9322
- this.setGatedSteeringPhase(agentId, ap, "error", { event: "error" });
9323
- if (ap.driver.busyDeliveryMode === "gated" && this.isThinkingBlockMutationError(event.message)) {
9324
- this.requeueGatedInFlightBatch(agentId, ap, "thinking_block_mutation_error");
9325
- ap.gatedSteering.toolBoundaryFlushDisabled = true;
9885
+ const shouldDisableToolBoundaryFlush = ap.driver.busyDeliveryMode === "gated" && this.isThinkingBlockMutationError(event.message);
9886
+ const terminalFailure = classifyTerminalFailure(ap);
9887
+ const reduction = reduceApmGatedError(ap.gatedSteering, {
9888
+ disableToolBoundaryFlush: shouldDisableToolBoundaryFlush,
9889
+ terminalWakeable: Boolean(ap.driver.supportsStdinNotification && terminalFailure && !terminalFailure.actionRequired)
9890
+ });
9891
+ this.commitGatedSteeringDecisionState(agentId, ap, reduction.nextState, { event: "error" });
9892
+ if (reduction.shouldDisableToolBoundaryFlush) {
9326
9893
  this.recordGatedSteeringEvent(agentId, ap, "disabled", {
9327
9894
  reason: "thinking_block_mutation_error",
9328
9895
  lastFlushReason: ap.gatedSteering.lastFlushReason,
@@ -9341,7 +9908,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9341
9908
  ...runtimeTraceCounterAttrs(ap),
9342
9909
  ...this.finalizeRuntimeProfileTurnControl(agentId, ap, "runtime_error")
9343
9910
  });
9344
- const terminalFailure = classifyTerminalFailure(ap);
9345
9911
  if (ap.driver.supportsStdinNotification && terminalFailure) {
9346
9912
  if (terminalFailure.actionRequired) {
9347
9913
  logger.warn(`[Agent ${agentId}] ${ap.driver.id} auth requires user action; terminating runtime process`);
@@ -9356,7 +9922,6 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9356
9922
  logger.warn(`[Agent ${agentId}] Failed to terminate ${ap.driver.id} after auth error: ${reason}`);
9357
9923
  }
9358
9924
  } else {
9359
- ap.isIdle = true;
9360
9925
  ap.notifications.clear();
9361
9926
  logger.info(`[Agent ${agentId}] Marked ${ap.driver.id} wakeable after terminal runtime error`);
9362
9927
  }
@@ -9471,6 +10036,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9471
10036
  return true;
9472
10037
  } else {
9473
10038
  ap.notifications.add(count);
10039
+ const retryScheduled = ap.driver.busyDeliveryMode === "direct" ? this.scheduleStdinNotification(agentId, ap, this.stdinNotificationRetryMs) : false;
9474
10040
  this.recordDaemonTrace("daemon.agent.stdin_notification", {
9475
10041
  agentId,
9476
10042
  runtime: ap.config.runtime,
@@ -9479,6 +10045,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9479
10045
  outcome: "encode_failed",
9480
10046
  mode: "busy",
9481
10047
  pending_notification_count: count,
10048
+ retry_scheduled: retryScheduled,
10049
+ notification_timer_present: ap.notifications.hasTimer,
9482
10050
  inbox_count: inboxCount,
9483
10051
  session_id_present: true
9484
10052
  }, "error");
@@ -9507,7 +10075,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
9507
10075
  });
9508
10076
  if (messages.length === 0) {
9509
10077
  if (mode === "idle") {
9510
- ap.isIdle = true;
10078
+ this.commitApmIdleState(agentId, ap, true);
9511
10079
  }
9512
10080
  return true;
9513
10081
  }
@@ -9549,7 +10117,7 @@ ${RESPONSE_TARGET_HINT}`);
9549
10117
  if (!encoded) {
9550
10118
  ap.inbox.unshift(...messages);
9551
10119
  if (mode === "idle") {
9552
- ap.isIdle = true;
10120
+ this.commitApmIdleState(agentId, ap, true);
9553
10121
  }
9554
10122
  logger.warn(
9555
10123
  `[Agent ${agentId}] Failed to encode ${mode} stdin delivery; re-queued ${messages.length === 1 ? "message" : `${messages.length} messages`}`
@@ -9901,7 +10469,7 @@ var ReminderCache = class {
9901
10469
  };
9902
10470
 
9903
10471
  // src/machineLock.ts
9904
- import { createHash as createHash3, randomUUID as randomUUID3 } from "crypto";
10472
+ import { createHash as createHash4, randomUUID as randomUUID3 } from "crypto";
9905
10473
  import { mkdirSync as mkdirSync5, readFileSync as readFileSync5, rmSync as rmSync3, statSync as statSync2, writeFileSync as writeFileSync8 } from "fs";
9906
10474
  import os6 from "os";
9907
10475
  import path13 from "path";
@@ -9917,7 +10485,7 @@ var DaemonMachineLockConflictError = class extends Error {
9917
10485
  }
9918
10486
  };
9919
10487
  function apiKeyFingerprint(apiKey) {
9920
- return createHash3("sha256").update(apiKey).digest("hex");
10488
+ return createHash4("sha256").update(apiKey).digest("hex");
9921
10489
  }
9922
10490
  function getDaemonMachineLockId(apiKey) {
9923
10491
  return `machine-${apiKeyFingerprint(apiKey).slice(0, 16)}`;
@@ -10180,7 +10748,7 @@ function isDiagnosticErrorAttr(key) {
10180
10748
  }
10181
10749
 
10182
10750
  // src/traceBundleUpload.ts
10183
- import { createHash as createHash5, randomUUID as randomUUID4 } from "crypto";
10751
+ import { createHash as createHash6, randomUUID as randomUUID4 } from "crypto";
10184
10752
  import { gzipSync } from "zlib";
10185
10753
  import { mkdir as mkdir2, readFile as readFile2, readdir as readdir3, stat as stat3, writeFile as writeFile2 } from "fs/promises";
10186
10754
  import path15 from "path";
@@ -10310,12 +10878,12 @@ async function uploadWithSignedCapability({
10310
10878
  }
10311
10879
 
10312
10880
  // src/traceJitter.ts
10313
- import { createHash as createHash4 } from "crypto";
10881
+ import { createHash as createHash5 } from "crypto";
10314
10882
  var INITIAL_UPLOAD_DELAY_SPAN_MS = 3e4;
10315
10883
  var UPLOAD_INTERVAL_JITTER_SPAN_MS = 6e4;
10316
10884
  var MAX_FILE_AGE_JITTER_SPAN_MS = 6e4;
10317
10885
  function computeTraceJitter(lockId) {
10318
- const seed = createHash4("sha256").update(lockId).digest();
10886
+ const seed = createHash5("sha256").update(lockId).digest();
10319
10887
  return {
10320
10888
  initialUploadDelayMs: seed.readUInt32BE(0) % INITIAL_UPLOAD_DELAY_SPAN_MS,
10321
10889
  uploadIntervalJitterMs: seed.readUInt32BE(4) % UPLOAD_INTERVAL_JITTER_SPAN_MS,
@@ -10516,7 +11084,7 @@ var DaemonTraceBundleUploader = class {
10516
11084
  }
10517
11085
  };
10518
11086
  function sha256Hex(body) {
10519
- return createHash5("sha256").update(body).digest("hex");
11087
+ return createHash6("sha256").update(body).digest("hex");
10520
11088
  }
10521
11089
  function readPositiveIntegerEnv2(name, fallback) {
10522
11090
  const value = process.env[name];