mixdog 0.9.68 → 0.9.70

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 (64) hide show
  1. package/package.json +8 -4
  2. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +1 -6
  3. package/src/runtime/agent/orchestrator/context/collect.mjs +42 -27
  4. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +1 -1
  5. package/src/runtime/agent/orchestrator/providers/gemini.mjs +0 -6
  6. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +161 -0
  7. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +8 -204
  8. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +26 -0
  9. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +25 -13
  10. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -11
  11. package/src/runtime/agent/orchestrator/session/store/listing.mjs +613 -0
  12. package/src/runtime/agent/orchestrator/session/store.mjs +9 -575
  13. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +41 -0
  14. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +154 -0
  15. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +78 -18
  16. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +9 -144
  17. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +7 -3
  18. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -3
  19. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +24 -8
  20. package/src/runtime/channels/lib/owned-runtime.mjs +8 -1
  21. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
  22. package/src/runtime/channels/lib/webhook.mjs +13 -1
  23. package/src/runtime/media/adapters/codex-image.mjs +109 -0
  24. package/src/runtime/media/adapters/gemini-image.mjs +68 -0
  25. package/src/runtime/media/adapters/gemini-video.mjs +119 -0
  26. package/src/runtime/media/adapters/xai-media.mjs +116 -0
  27. package/src/runtime/media/auth.mjs +51 -0
  28. package/src/runtime/media/index.mjs +17 -0
  29. package/src/runtime/media/jobs.mjs +174 -0
  30. package/src/runtime/media/lanes.mjs +230 -0
  31. package/src/runtime/media/store.mjs +164 -0
  32. package/src/runtime/media/upstream-error.mjs +39 -0
  33. package/src/session-runtime/channel-config-api.mjs +12 -1
  34. package/src/session-runtime/media-api.mjs +47 -0
  35. package/src/session-runtime/memory-daemon-probe.mjs +61 -0
  36. package/src/session-runtime/model-capabilities.mjs +6 -4
  37. package/src/session-runtime/model-route-api.mjs +4 -1
  38. package/src/session-runtime/notification-bus.mjs +82 -0
  39. package/src/session-runtime/provider-auth-api.mjs +16 -1
  40. package/src/session-runtime/provider-usage.mjs +3 -0
  41. package/src/session-runtime/remote-control.mjs +166 -0
  42. package/src/session-runtime/remote-transcript.mjs +126 -0
  43. package/src/session-runtime/remote-transition-queue.mjs +14 -0
  44. package/src/session-runtime/runtime-core.mjs +160 -706
  45. package/src/session-runtime/runtime-tunables.mjs +57 -0
  46. package/src/session-runtime/self-update.mjs +129 -0
  47. package/src/session-runtime/skills-api.mjs +77 -0
  48. package/src/session-runtime/tool-surface.mjs +83 -0
  49. package/src/session-runtime/warmup-schedulers.mjs +14 -1
  50. package/src/session-runtime/workflow-agents-api.mjs +235 -7
  51. package/src/session-runtime/workflow.mjs +84 -17
  52. package/src/standalone/agent-tool/worker-index.mjs +287 -0
  53. package/src/standalone/agent-tool.mjs +22 -346
  54. package/src/standalone/agent-watchdog-registry.mjs +101 -0
  55. package/src/standalone/channel-worker.mjs +7 -13
  56. package/src/tui/components/StatusLine.jsx +6 -5
  57. package/src/tui/dist/index.mjs +144 -95
  58. package/src/tui/engine/labels.mjs +0 -8
  59. package/src/tui/engine/session-api-ext.mjs +31 -8
  60. package/src/tui/engine/turn-watchdog.mjs +92 -0
  61. package/src/tui/engine/turn.mjs +18 -70
  62. package/src/tui/engine.mjs +39 -1
  63. package/src/workflows/default/WORKFLOW.md +1 -1
  64. package/src/workflows/solo/WORKFLOW.md +1 -1
@@ -1,4 +1,3 @@
1
- import { readFileSync, statSync } from 'node:fs';
2
1
  import { dirname, resolve } from 'node:path';
3
2
  import { fileURLToPath } from 'node:url';
4
3
  import {
@@ -12,20 +11,16 @@ import {
12
11
  taskIdFromArgs,
13
12
  } from '../runtime/shared/background-tasks.mjs';
14
13
  import { presentErrorText, errorLine } from '../runtime/shared/err-text.mjs';
15
- import { updateJsonAtomicSync } from '../runtime/shared/atomic-file.mjs';
16
14
  import { normalizeAgentPermission } from '../runtime/shared/markdown-frontmatter.mjs';
17
15
  import { ensureProcessListenerHeadroom } from '../runtime/shared/process-listener-headroom.mjs';
18
16
  import { resourceAdmission } from '../runtime/shared/resource-admission.mjs';
19
17
  import { prepareAgentSession } from '../runtime/agent/orchestrator/agent-runtime/session-builder.mjs';
20
18
  import {
21
- abortAgentProgressWatchdog,
22
- agentWatchdogPolicyActive,
23
- evaluateAgentWatchdogAbort,
24
19
  resolveAgentWatchdogPolicy,
25
20
  resolveHandoffMessageStartIndex,
26
21
  watchdogPartialHandoffFromError,
27
- AgentStallAbortError,
28
22
  } from '../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
23
+ import { createProgressWatchdogRegistry } from './agent-watchdog-registry.mjs';
29
24
  import { buildAgentTaskProgressFields } from './agent-task-status.mjs';
30
25
  import { AGENT_OWNER } from '../runtime/agent/orchestrator/agent-owner.mjs';
31
26
  import {
@@ -57,15 +52,13 @@ import { createNotify } from './agent-tool/notify.mjs';
57
52
  import { resolveAgentSpawnPreset } from './agent-tool/spawn-preset.mjs';
58
53
  import {
59
54
  TAG_TOMBSTONE_TTL_MS,
60
- applyWorkerRowUpsert,
61
55
  isTerminalWorkerStatus,
62
- normalizeTagTombstones,
63
56
  tagTombstoneKey,
64
- workerRowKey,
65
57
  workerRowTime,
66
58
  workerRowToSession,
67
59
  } from './agent-tool/worker-rows.mjs';
68
60
  import { resolveAgentTerminalReapMs } from '../session-runtime/config-helpers.mjs';
61
+ import { createWorkerIndex } from './agent-tool/worker-index.mjs';
69
62
  // Re-export the static tool descriptor so importers of this facade keep the
70
63
  // identical public surface (`import { AGENT_TOOL } from './agent-tool.mjs'`).
71
64
  export { AGENT_TOOL };
@@ -102,253 +95,20 @@ export function createStandaloneAgent({
102
95
  const tagAgents = new Map();
103
96
  const tagCwds = new Map();
104
97
  const reapTimers = new Map();
105
- const workerIndexMutators = [];
106
- let workerIndexFlushTimer = null;
107
- function workerIndexPath() {
108
- return dataDir ? resolve(dataDir, WORKER_INDEX_FILE) : null;
109
- }
110
-
111
- function keepWorkerRow(row = {}) {
112
- if (!clean(row.tag) || !clean(row.sessionId)) return false;
113
- const t = workerRowTime(row);
114
- if (!t) return true;
115
- if (!isTerminalWorkerStatus(row.status || row.stage)) return true;
116
- const reapMs = resolveAgentTerminalReapMs(cfgMod.loadConfig(), row.provider);
117
- return reapMs == null || Date.now() - t < reapMs;
118
- }
119
-
120
- function normalizeWorkerRows(value) {
121
- const source = Array.isArray(value?.workers)
122
- ? value.workers
123
- : (value?.workers && typeof value.workers === 'object'
124
- ? Object.values(value.workers)
125
- : (Array.isArray(value) ? value : []));
126
- return source
127
- .filter((row) => row && typeof row === 'object')
128
- .map((row) => ({
129
- tag: clean(row.tag),
130
- sessionId: clean(row.sessionId),
131
- agent: clean(row.agent) || null,
132
- provider: clean(row.provider) || null,
133
- model: clean(row.model) || null,
134
- preset: clean(row.preset) || null,
135
- effort: clean(row.effort) || null,
136
- fast: row.fast === true ? true : (row.fast === false ? false : null),
137
- status: clean(row.status) || 'idle',
138
- stage: clean(row.stage) || clean(row.status) || 'idle',
139
- createdAt: clean(row.createdAt) || null,
140
- updatedAt: clean(row.updatedAt) || null,
141
- lastUsedAt: clean(row.lastUsedAt) || null,
142
- finishedAt: clean(row.finishedAt) || null,
143
- clientHostPid: positiveInt(row.clientHostPid),
144
- cwd: clean(row.cwd) || null,
145
- task_id: clean(row.task_id || row.taskId) || null,
146
- error: clean(row.error) || null,
147
- permission: clean(row.permission) || null,
148
- toolPermission: clean(row.toolPermission) || null,
149
- messages: positiveInt(row.messages) || 0,
150
- tools: positiveInt(row.tools) || 0,
151
- }))
152
- .filter(keepWorkerRow);
153
- }
154
-
155
- // Mtime-keyed parse cache for the worker index. A single spawn calls
156
- // refreshTagsFromSessions()/resolveTag()/nextTag() which each re-read and
157
- // re-JSON.parse this file; across a parallel fanout that is O(spawns^2)
158
- // synchronous reads of the same bytes on the event loop. Cache the parsed,
159
- // normalized rows and reuse them while the file mtime+size is unchanged.
160
- // Writes bump _workerRowsCacheDirty so the very next read re-parses.
161
- let _workerRowsCache = null; // { mtimeMs, size, rows, tombstones }
162
- let _workerRowsCacheDirty = true;
163
- function invalidateWorkerRowsCache() {
164
- _workerRowsCacheDirty = true;
165
- }
166
- function readAllWorkerRows() {
167
- const file = workerIndexPath();
168
- if (!file) return [];
169
- let st = null;
170
- try { st = statSync(file); } catch { _workerRowsCache = null; return []; }
171
- if (!_workerRowsCacheDirty
172
- && _workerRowsCache
173
- && _workerRowsCache.mtimeMs === st.mtimeMs
174
- && _workerRowsCache.size === st.size) {
175
- return _workerRowsCache.rows;
176
- }
177
- let rows = [];
178
- let tombstones = [];
179
- try {
180
- const parsed = JSON.parse(readFileSync(file, 'utf8'));
181
- rows = normalizeWorkerRows(parsed);
182
- tombstones = normalizeTagTombstones(parsed);
183
- } catch {
184
- rows = [];
185
- tombstones = [];
186
- }
187
- _workerRowsCache = { mtimeMs: st.mtimeMs, size: st.size, rows, tombstones };
188
- _workerRowsCacheDirty = false;
189
- return rows;
190
- }
191
- function readAllTagTombstones() {
192
- readAllWorkerRows();
193
- return _workerRowsCache?.tombstones || [];
194
- }
195
- function readTagTombstones(context = {}) {
196
- return readAllTagTombstones().filter((row) => rowMatchesContext(row, context));
197
- }
198
- function readWorkerRows(context = {}) {
199
- const rows = readAllWorkerRows();
200
- if (rows.length === 0) return rows;
201
- return rows.filter((row) => rowMatchesContext(row, context));
202
- }
203
-
204
- function workerRowsForUpdate(cur) {
205
- return normalizeWorkerRows(cur);
206
- }
207
-
208
- function writeWorkerRows(mutator) {
209
- const file = workerIndexPath();
210
- if (!file || typeof mutator !== 'function') return null;
211
- try {
212
- const result = updateJsonAtomicSync(file, (cur) => {
213
- const byKey = new Map();
214
- for (const row of workerRowsForUpdate(cur)) {
215
- const key = workerRowKey(row);
216
- if (key) byKey.set(key, row);
217
- }
218
- const tombstonesByKey = new Map();
219
- for (const row of normalizeTagTombstones(cur, { cap: false })) {
220
- tombstonesByKey.set(tagTombstoneKey(row), row);
221
- }
222
- const priorityTombstoneKeys = new Set();
223
- mutator(byKey, tombstonesByKey, priorityTombstoneKeys);
224
- const workers = {};
225
- for (const row of [...byKey.values()].filter(keepWorkerRow)) {
226
- const key = workerRowKey(row);
227
- if (key) workers[key] = row;
228
- }
229
- const tombstones = {};
230
- for (const row of normalizeTagTombstones(
231
- { tombstones: [...tombstonesByKey.values()] },
232
- { priorityKeys: priorityTombstoneKeys },
233
- )) {
234
- tombstones[tagTombstoneKey(row)] = row;
235
- }
236
- return { version: 2, updatedAt: new Date().toISOString(), workers, tombstones };
237
- }, { lock: true });
238
- // This process just rewrote the index; force the next read to re-parse
239
- // even if the new mtime/size happen to collide with the cached stat.
240
- invalidateWorkerRowsCache();
241
- return result;
242
- } catch {
243
- return null;
244
- }
245
- }
246
-
247
- function flushWorkerIndexMutations() {
248
- if (workerIndexFlushTimer) {
249
- try { clearImmediate(workerIndexFlushTimer); } catch {}
250
- workerIndexFlushTimer = null;
251
- }
252
- if (workerIndexMutators.length === 0) return;
253
- const batch = workerIndexMutators.splice(0, workerIndexMutators.length);
254
- writeWorkerRows((byKey) => {
255
- for (const mutator of batch) {
256
- try { mutator(byKey); } catch {}
257
- }
258
- });
259
- }
260
-
261
- function queueWorkerIndexMutation(mutator) {
262
- if (typeof mutator !== 'function') return false;
263
- if (!workerIndexPath()) return false;
264
- workerIndexMutators.push(mutator);
265
- if (!workerIndexFlushTimer) {
266
- workerIndexFlushTimer = setImmediate(flushWorkerIndexMutations);
267
- workerIndexFlushTimer.unref?.();
268
- }
269
- return true;
270
- }
271
-
272
- function workerRowFromSession(session, fallbackTag = '', extra = {}) {
273
- const tag = agentTagOf(session) || clean(fallbackTag) || clean(extra.tag);
274
- const sessionId = clean(session?.id || extra.sessionId);
275
- if (!tag || !sessionId) return null;
276
- const runtime = mgr.getSessionRuntime?.(sessionId);
277
- const status = clean(extra.status) || (session?.closed === true ? 'closed' : clean(session?.status) || 'idle');
278
- const stage = clean(extra.stage) || clean(runtime?.stage) || status;
279
- const nowIso = new Date().toISOString();
280
- return {
281
- tag,
282
- sessionId,
283
- agent: clean(extra.agent) || clean(session?.agent) || null,
284
- provider: clean(extra.provider) || clean(session?.provider) || null,
285
- model: clean(extra.model) || clean(session?.model) || null,
286
- preset: clean(extra.preset) || clean(session?.presetName) || null,
287
- effort: clean(extra.effort) || clean(session?.effort) || null,
288
- fast: extra.fast === true || extra.fast === false ? extra.fast : (session?.fast === true ? true : null),
289
- status,
290
- stage,
291
- createdAt: clean(session?.createdAt) || clean(extra.createdAt) || nowIso,
292
- updatedAt: clean(extra.updatedAt) || nowIso,
293
- lastUsedAt: clean(session?.lastUsedAt) || null,
294
- finishedAt: clean(extra.finishedAt) || null,
295
- clientHostPid: positiveInt(extra.clientHostPid) || positiveInt(session?.clientHostPid),
296
- cwd: clean(session?.cwd) || clean(extra.cwd) || null,
297
- task_id: clean(extra.task_id || extra.taskId) || null,
298
- error: clean(extra.error) || null,
299
- permission: clean(session?.permission) || null,
300
- toolPermission: clean(session?.toolPermission) || null,
301
- messages: Array.isArray(session?.messages) ? session.messages.length : 0,
302
- tools: Array.isArray(session?.tools) ? session.tools.length : 0,
303
- };
304
- }
305
-
306
- function upsertWorkerRow(row, { defer = false } = {}) {
307
- const normalized = normalizeWorkerRows({ workers: [row] })[0];
308
- if (!normalized) return false;
309
- tags.set(normalized.tag, normalized.sessionId);
310
- if (normalized.agent) tagAgents.set(normalized.tag, normalized.agent);
311
- if (normalized.cwd) tagCwds.set(normalized.tag, normalized.cwd);
312
- if (defer) {
313
- return queueWorkerIndexMutation((byKey) => applyWorkerRowUpsert(byKey, normalized));
314
- }
315
- writeWorkerRows((byKey) => {
316
- applyWorkerRowUpsert(byKey, normalized);
317
- });
318
- return true;
319
- }
320
-
321
- function upsertWorkerSession(session, fallbackTag = '', extra = {}) {
322
- return upsertWorkerRow(workerRowFromSession(session, fallbackTag, extra));
323
- }
324
-
325
- function upsertWorkerSessionDeferred(session, fallbackTag = '', extra = {}) {
326
- return upsertWorkerRow(workerRowFromSession(session, fallbackTag, extra), { defer: true });
327
- }
328
-
329
- function removeWorkerRow({ tag = '', sessionId = '' } = {}) {
330
- const targetTag = clean(tag);
331
- const targetSessionId = clean(sessionId);
332
- flushWorkerIndexMutations();
333
- writeWorkerRows((byKey) => {
334
- for (const [key, row] of [...byKey.entries()]) {
335
- if ((targetSessionId && row.sessionId === targetSessionId) || (targetTag && row.tag === targetTag)) {
336
- byKey.delete(key);
337
- }
338
- }
339
- });
340
- }
341
-
342
- function refreshTagsFromIndex(context = {}) {
343
- const rows = readWorkerRows(context);
344
- for (const row of rows) {
345
- if (!row.tag || !row.sessionId) continue;
346
- tags.set(row.tag, row.sessionId);
347
- if (row.agent) tagAgents.set(row.tag, row.agent);
348
- if (row.cwd) tagCwds.set(row.tag, row.cwd);
349
- }
350
- return rows;
351
- }
98
+ // Worker-index persistence (row store, parse cache, batched atomic writer,
99
+ // tag projection) lives in agent-tool/worker-index.mjs; the tag maps are
100
+ // shared by reference so both sides see the same live state.
101
+ const {
102
+ readAllTagTombstones,
103
+ readTagTombstones,
104
+ readWorkerRows,
105
+ writeWorkerRows,
106
+ flushWorkerIndexMutations,
107
+ upsertWorkerSession,
108
+ upsertWorkerSessionDeferred,
109
+ removeWorkerRow,
110
+ refreshTagsFromIndex,
111
+ } = createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCwds });
352
112
 
353
113
  function wantsSessionScan(args = {}) {
354
114
  return args.recover === true || args.scanSessions === true || args.scan_sessions === true;
@@ -1025,95 +785,11 @@ export function createStandaloneAgent({
1025
785
  }, notifyContext);
1026
786
  }
1027
787
 
1028
- const progressWatchdogs = new Map();
1029
- let progressWatchdogTimer = null;
1030
-
1031
- function stopProgressWatchdogTimerIfIdle() {
1032
- if (progressWatchdogs.size > 0 || !progressWatchdogTimer) return;
1033
- try { clearInterval(progressWatchdogTimer); } catch {}
1034
- progressWatchdogTimer = null;
1035
- }
1036
-
1037
- function checkProgressIdleWatchdog(state) {
1038
- const {
1039
- sessionId,
1040
- watchdogPolicy,
1041
- agent,
1042
- controller,
1043
- anchorTs,
1044
- } = state;
1045
- if (controller.signal?.aborted) {
1046
- progressWatchdogs.delete(sessionId);
1047
- stopProgressWatchdogTimerIfIdle();
1048
- return;
1049
- }
1050
- const now = Date.now();
1051
- const snapshot = typeof mgr.getSessionProgressSnapshot === 'function'
1052
- ? mgr.getSessionProgressSnapshot(sessionId)
1053
- : null;
1054
- const abortErr = snapshot
1055
- ? evaluateAgentWatchdogAbort(snapshot, now, watchdogPolicy)
1056
- : null;
1057
- const sess = typeof mgr.getSession === 'function' ? mgr.getSession(sessionId) : null;
1058
- const iteration = typeof sess?.lastIterationIndex === 'number' ? sess.lastIterationIndex : null;
1059
- if (!abortErr && !snapshot) {
1060
- const reported = mgr.getSessionLastProgressAt(sessionId);
1061
- const last = reported || anchorTs;
1062
- if (watchdogPolicy.idleStaleMs > 0 && now - last > watchdogPolicy.idleStaleMs) {
1063
- abortAgentProgressWatchdog(controller, {
1064
- sessionId,
1065
- agent,
1066
- error: new AgentStallAbortError(`agent task stale (${watchdogPolicy.idleStaleMs}ms without progress)`),
1067
- policy: watchdogPolicy,
1068
- now,
1069
- anchorTs,
1070
- lastProgressAt: reported,
1071
- iteration,
1072
- });
1073
- }
1074
- return;
1075
- }
1076
- if (abortErr) {
1077
- abortAgentProgressWatchdog(controller, {
1078
- sessionId,
1079
- agent,
1080
- error: abortErr,
1081
- snapshot,
1082
- policy: watchdogPolicy,
1083
- now,
1084
- anchorTs,
1085
- iteration,
1086
- });
1087
- }
1088
- }
1089
-
1090
- function ensureProgressWatchdogTimer() {
1091
- if (progressWatchdogTimer || progressWatchdogs.size === 0) return;
1092
- progressWatchdogTimer = setInterval(() => {
1093
- for (const state of [...progressWatchdogs.values()]) {
1094
- checkProgressIdleWatchdog(state);
1095
- }
1096
- }, 1000);
1097
- progressWatchdogTimer.unref?.();
1098
- }
1099
-
1100
- function startProgressIdleWatchdog(sessionId, watchdogPolicy, agent = null) {
1101
- if (!sessionId || !agentWatchdogPolicyActive(watchdogPolicy)) return null;
1102
- if (typeof mgr.getSessionProgressSnapshot !== 'function' && typeof mgr.getSessionLastProgressAt !== 'function') return null;
1103
- if (typeof mgr.linkParentSignalToSession !== 'function') return null;
1104
- const controller = new AbortController();
1105
- const anchorTs = Date.now();
1106
- try { mgr.linkParentSignalToSession(sessionId, controller.signal); } catch { return null; }
1107
- const state = { sessionId, watchdogPolicy, agent, controller, anchorTs };
1108
- progressWatchdogs.set(sessionId, state);
1109
- ensureProgressWatchdogTimer();
1110
- return {
1111
- stop: () => {
1112
- if (progressWatchdogs.get(sessionId) === state) progressWatchdogs.delete(sessionId);
1113
- stopProgressWatchdogTimerIfIdle();
1114
- },
1115
- };
1116
- }
788
+ // Idle-progress watchdogs (shared sweep timer) live in
789
+ // agent-watchdog-registry.mjs.
790
+ const progressWatchdogs = createProgressWatchdogRegistry({ mgr });
791
+ const startProgressIdleWatchdog = (sessionId, watchdogPolicy, agent = null) =>
792
+ progressWatchdogs.start(sessionId, watchdogPolicy, agent);
1117
793
 
1118
794
  async function prepareSpawn(args, callerCwd = null, context = {}, prepState = null) {
1119
795
  refreshTagsFromSessions({ context });
@@ -1824,4 +1500,4 @@ export function createStandaloneAgent({
1824
1500
  },
1825
1501
  closeAll,
1826
1502
  };
1827
- }
1503
+ }
@@ -0,0 +1,101 @@
1
+ // Idle-progress watchdogs for running agent sessions, extracted from
2
+ // agent-tool.mjs. One shared 1s timer sweeps every watched session, so a burst
3
+ // of agents costs a single interval; the timer stops as soon as the last
4
+ // watchdog is released. Abort policy itself lives in the runtime module.
5
+ import {
6
+ abortAgentProgressWatchdog,
7
+ agentWatchdogPolicyActive,
8
+ evaluateAgentWatchdogAbort,
9
+ AgentStallAbortError,
10
+ } from '../runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs';
11
+
12
+ const WATCHDOG_SWEEP_INTERVAL_MS = 1000;
13
+
14
+ export function createProgressWatchdogRegistry({ mgr }) {
15
+ const watched = new Map();
16
+ let timer = null;
17
+
18
+ function stopTimerIfIdle() {
19
+ if (watched.size > 0 || !timer) return;
20
+ try { clearInterval(timer); } catch {}
21
+ timer = null;
22
+ }
23
+
24
+ function check(state) {
25
+ const { sessionId, watchdogPolicy, agent, controller, anchorTs } = state;
26
+ if (controller.signal?.aborted) {
27
+ watched.delete(sessionId);
28
+ stopTimerIfIdle();
29
+ return;
30
+ }
31
+ const now = Date.now();
32
+ const snapshot = typeof mgr.getSessionProgressSnapshot === 'function'
33
+ ? mgr.getSessionProgressSnapshot(sessionId)
34
+ : null;
35
+ const abortErr = snapshot ? evaluateAgentWatchdogAbort(snapshot, now, watchdogPolicy) : null;
36
+ const sess = typeof mgr.getSession === 'function' ? mgr.getSession(sessionId) : null;
37
+ const iteration = typeof sess?.lastIterationIndex === 'number' ? sess.lastIterationIndex : null;
38
+ // No progress snapshot: fall back to the coarse last-progress timestamp so a
39
+ // session that never reports structured progress can still stall out.
40
+ if (!abortErr && !snapshot) {
41
+ const reported = mgr.getSessionLastProgressAt(sessionId);
42
+ const last = reported || anchorTs;
43
+ if (watchdogPolicy.idleStaleMs > 0 && now - last > watchdogPolicy.idleStaleMs) {
44
+ abortAgentProgressWatchdog(controller, {
45
+ sessionId,
46
+ agent,
47
+ error: new AgentStallAbortError(`agent task stale (${watchdogPolicy.idleStaleMs}ms without progress)`),
48
+ policy: watchdogPolicy,
49
+ now,
50
+ anchorTs,
51
+ lastProgressAt: reported,
52
+ iteration,
53
+ });
54
+ }
55
+ return;
56
+ }
57
+ if (abortErr) {
58
+ abortAgentProgressWatchdog(controller, {
59
+ sessionId,
60
+ agent,
61
+ error: abortErr,
62
+ snapshot,
63
+ policy: watchdogPolicy,
64
+ now,
65
+ anchorTs,
66
+ iteration,
67
+ });
68
+ }
69
+ }
70
+
71
+ function ensureTimer() {
72
+ if (timer || watched.size === 0) return;
73
+ timer = setInterval(() => {
74
+ for (const state of [...watched.values()]) check(state);
75
+ }, WATCHDOG_SWEEP_INTERVAL_MS);
76
+ timer.unref?.();
77
+ }
78
+
79
+ return {
80
+ /** Watch a session until the returned handle is stopped; null when the
81
+ * policy is off or the manager cannot link an abort signal. */
82
+ start(sessionId, watchdogPolicy, agent = null) {
83
+ if (!sessionId || !agentWatchdogPolicyActive(watchdogPolicy)) return null;
84
+ if (typeof mgr.getSessionProgressSnapshot !== 'function'
85
+ && typeof mgr.getSessionLastProgressAt !== 'function') return null;
86
+ if (typeof mgr.linkParentSignalToSession !== 'function') return null;
87
+ const controller = new AbortController();
88
+ const anchorTs = Date.now();
89
+ try { mgr.linkParentSignalToSession(sessionId, controller.signal); } catch { return null; }
90
+ const state = { sessionId, watchdogPolicy, agent, controller, anchorTs };
91
+ watched.set(sessionId, state);
92
+ ensureTimer();
93
+ return {
94
+ stop: () => {
95
+ if (watched.get(sessionId) === state) watched.delete(sessionId);
96
+ stopTimerIfIdle();
97
+ },
98
+ };
99
+ },
100
+ };
101
+ }
@@ -253,21 +253,12 @@ export function createStandaloneChannelWorker({
253
253
  }
254
254
 
255
255
  function start() {
256
+ if (stopPromise) return stopPromise.then(() => start());
256
257
  if (!useProcessWorker) return startInProcess();
257
258
  // Daemon mode (default): spawn-or-attach to the machine-global channels
258
259
  // daemon. The fork code below is unreachable while useProcessWorker is true.
259
- return ensureDaemonAttached().then(() => status());
260
- if (stopPromise) {
261
- return stopPromise.then(() => start());
262
- }
263
- if (child && child.exitCode == null && !child.killed) return readyPromise || Promise.resolve(status());
264
260
  stopRequested = false;
265
- readyPromise = new Promise((resolve, reject) => {
266
- readyResolve = resolve;
267
- readyReject = reject;
268
- });
269
- spawnWorkerChild(1);
270
- return readyPromise;
261
+ return ensureDaemonAttached().then(() => status());
271
262
  }
272
263
 
273
264
  function spawnWorkerChild(attempt) {
@@ -745,10 +736,13 @@ export function createStandaloneChannelWorker({
745
736
  daemonClient = null;
746
737
  attachPromise = null;
747
738
  daemonPid = null;
748
- return Promise.all([
739
+ stopPromise = Promise.all([
749
740
  client ? client.close(reason).then(() => true).catch(() => true) : Promise.resolve(false),
750
741
  Promise.resolve(inFlightAttach).catch(() => null),
751
- ]).then(([detached]) => detached);
742
+ ]).then(([detached]) => detached).finally(() => {
743
+ stopPromise = null;
744
+ });
745
+ return stopPromise;
752
746
  }
753
747
  if (!child) {
754
748
  return Promise.resolve(false);
@@ -682,16 +682,17 @@ function StatusLineView({ sessionId, clientHostPid, provider, model, effort, fas
682
682
 
683
683
  const lines = line ? line.split('\n').slice(0, 2) : [' ', ' '];
684
684
  const workflowLabel = workflowModeLabel(workflow, remoteEnabled);
685
- // Footer footprint stays 3 rows total (L1 + L2 + one spacer). Keep L1/L2 in
686
- // the TOP two rows so the statusline is visually attached to the prompt box;
687
- // the leftover breathing row sits below L2 at the terminal bottom.
685
+ // Footer footprint is exactly 2 rows (L1 + L2) so the statusline sits tight
686
+ // under the prompt box the old third "breathing" row left a dead band at
687
+ // the terminal bottom. Left/right insets match the prompt box's text column
688
+ // (1 border + 1 padding) so both edges line up vertically with the input.
688
689
  return (
689
- <Box flexDirection="column" width="100%" height={3} overflow="hidden" justifyContent="flex-start" paddingLeft={2} backgroundColor={surfaceBackground()}>
690
+ <Box flexDirection="column" width="100%" height={2} overflow="hidden" justifyContent="flex-start" paddingLeft={2} paddingRight={2} backgroundColor={surfaceBackground()}>
690
691
  <Box flexDirection="row" width="100%" overflow="hidden">
691
692
  <Box flexGrow={1} flexShrink={1} flexBasis={0} overflow="hidden">
692
693
  <Text wrap="truncate">{lines[0] || ' '}</Text>
693
694
  </Box>
694
- <Box flexShrink={0} marginLeft={1} marginRight={1}>
695
+ <Box flexShrink={0} marginLeft={1}>
695
696
  <Text color={theme.statusText} wrap="truncate">{workflowLabel}</Text>
696
697
  </Box>
697
698
  </Box>