mixdog 0.9.67 → 0.9.69

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 (73) hide show
  1. package/package.json +6 -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/session-lifecycle.mjs +3 -0
  11. package/src/runtime/agent/orchestrator/session/manager.mjs +0 -11
  12. package/src/runtime/agent/orchestrator/session/store/listing.mjs +613 -0
  13. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +1 -0
  14. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +10 -2
  15. package/src/runtime/agent/orchestrator/session/store.mjs +9 -575
  16. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +41 -0
  17. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-output.mjs +154 -0
  18. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +78 -18
  19. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +9 -144
  20. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +7 -3
  21. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +40 -3
  22. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +24 -8
  23. package/src/runtime/channels/lib/config.mjs +6 -5
  24. package/src/runtime/channels/lib/owned-runtime.mjs +65 -5
  25. package/src/runtime/channels/lib/scheduler.mjs +7 -15
  26. package/src/runtime/channels/lib/tool-dispatch.mjs +6 -1
  27. package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +6 -2
  28. package/src/runtime/channels/lib/webhook.mjs +36 -46
  29. package/src/runtime/channels/lib/worker-main.mjs +45 -92
  30. package/src/runtime/shared/automation-attachments.mjs +72 -0
  31. package/src/runtime/shared/automation-workflow.mjs +41 -0
  32. package/src/runtime/shared/schedule-session-run.mjs +10 -1
  33. package/src/runtime/shared/schedules-db.mjs +18 -3
  34. package/src/runtime/shared/webhook-session-run.mjs +57 -0
  35. package/src/runtime/shared/webhooks-db.mjs +19 -3
  36. package/src/session-runtime/channel-config-api.mjs +8 -1
  37. package/src/session-runtime/lifecycle-api.mjs +7 -0
  38. package/src/session-runtime/memory-daemon-probe.mjs +61 -0
  39. package/src/session-runtime/model-capabilities.mjs +6 -4
  40. package/src/session-runtime/notification-bus.mjs +82 -0
  41. package/src/session-runtime/provider-auth-api.mjs +16 -1
  42. package/src/session-runtime/provider-usage.mjs +3 -0
  43. package/src/session-runtime/remote-control.mjs +166 -0
  44. package/src/session-runtime/remote-transcript.mjs +126 -0
  45. package/src/session-runtime/remote-transition-queue.mjs +14 -0
  46. package/src/session-runtime/runtime-core.mjs +184 -660
  47. package/src/session-runtime/runtime-tunables.mjs +57 -0
  48. package/src/session-runtime/self-update.mjs +129 -0
  49. package/src/session-runtime/skills-api.mjs +77 -0
  50. package/src/session-runtime/tool-surface.mjs +83 -0
  51. package/src/session-runtime/workflow-agents-api.mjs +206 -3
  52. package/src/session-runtime/workflow.mjs +84 -17
  53. package/src/standalone/agent-tool/worker-index.mjs +287 -0
  54. package/src/standalone/agent-tool.mjs +22 -346
  55. package/src/standalone/agent-watchdog-registry.mjs +101 -0
  56. package/src/standalone/channel-admin.mjs +36 -1
  57. package/src/standalone/channel-daemon-client.mjs +5 -1
  58. package/src/standalone/channel-daemon-transport.mjs +112 -20
  59. package/src/standalone/channel-daemon.mjs +6 -4
  60. package/src/standalone/channel-worker.mjs +7 -13
  61. package/src/tui/App.jsx +2 -32
  62. package/src/tui/app/channel-pickers.mjs +2 -143
  63. package/src/tui/app/slash-commands.mjs +1 -3
  64. package/src/tui/app/slash-dispatch.mjs +3 -3
  65. package/src/tui/components/StatusLine.jsx +6 -5
  66. package/src/tui/dist/index.mjs +162 -259
  67. package/src/tui/engine/labels.mjs +0 -8
  68. package/src/tui/engine/session-api-ext.mjs +45 -10
  69. package/src/tui/engine/turn-watchdog.mjs +92 -0
  70. package/src/tui/engine/turn.mjs +18 -70
  71. package/src/tui/engine.mjs +14 -1
  72. package/src/workflows/default/WORKFLOW.md +1 -1
  73. 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
+ }
@@ -38,6 +38,7 @@ import {
38
38
  deleteEndpoint as dbDeleteEndpoint,
39
39
  setEndpointEnabled as dbSetEndpointEnabled,
40
40
  } from '../runtime/shared/webhooks-db.mjs';
41
+ import { normalizeAutomationAttachments } from '../runtime/shared/automation-attachments.mjs';
41
42
  import { readHookPublicBase } from '../runtime/channels/lib/webhook/relay-tunnel.mjs';
42
43
 
43
44
  const NAME_RE = /^[a-z0-9][a-z0-9-]{0,63}$/;
@@ -358,6 +359,11 @@ function scheduleToDisplay(s) {
358
359
  channel: s.channelId || undefined,
359
360
  model: s.model || undefined,
360
361
  cwd: s.cwd || undefined,
362
+ workflow: s.workflow || undefined,
363
+ attachments: s.attachments || undefined,
364
+ // Delivery mode: legacy channel-target rows (pre-delivery) behaved as
365
+ // relay+visible, which maps to 'both'.
366
+ delivery: s.delivery || (s.target === 'channel' ? 'both' : 'app'),
361
367
  enabled: s.enabled !== false,
362
368
  instructions: s.prompt,
363
369
  route: s.target === 'channel' ? `channel:${s.channelId}` : 'session',
@@ -383,6 +389,9 @@ export async function saveSchedule({
383
389
  channel,
384
390
  model,
385
391
  cwd,
392
+ workflow,
393
+ attachments,
394
+ delivery,
386
395
  enabled,
387
396
  instructions,
388
397
  overwrite = false,
@@ -391,6 +400,11 @@ export async function saveSchedule({
391
400
  const body = String(instructions || '').trim();
392
401
  if (!body) throw new Error('schedule instructions are required');
393
402
  if (channel && !model) throw new Error('model is required when channel is set');
403
+ // 'app' → session-only; 'channel'/'both' → the run result relays to the
404
+ // main channel (target 'channel', channelId resolved at fire time).
405
+ const mode = ['app', 'channel', 'both'].includes(String(delivery || '').trim())
406
+ ? String(delivery).trim()
407
+ : (channel ? 'both' : 'app');
394
408
  const hasTime = time != null && String(time).trim() !== '';
395
409
  const hasAt = at != null && String(at).trim() !== '';
396
410
  if (hasTime && hasAt) throw new Error('provide either `time` (recurring) or `at` (one-shot), not both');
@@ -406,10 +420,13 @@ export async function saveSchedule({
406
420
  whenCron,
407
421
  whenAt,
408
422
  timezone: timezone ? String(timezone).trim() : null,
409
- target: channel ? 'channel' : 'session',
423
+ target: mode === 'app' ? 'session' : 'channel',
410
424
  channelId: channel ? String(channel).trim() : null,
411
425
  model: model ? String(model).trim() : null,
412
426
  cwd: cwd ? String(cwd).trim() : null,
427
+ workflow: workflow ? String(workflow).trim() : null,
428
+ attachments: normalizeAutomationAttachments(attachments),
429
+ delivery: mode,
413
430
  prompt: body,
414
431
  enabled: enabled !== false,
415
432
  });
@@ -441,6 +458,10 @@ async function listWebhooks() {
441
458
  parser: ep.parser || 'github',
442
459
  ...(ep.channelId ? { channel: ep.channelId } : {}),
443
460
  ...(ep.model ? { model: ep.model } : {}),
461
+ ...(ep.cwd ? { cwd: ep.cwd } : {}),
462
+ ...(ep.workflow ? { workflow: ep.workflow } : {}),
463
+ ...(ep.attachments ? { attachments: ep.attachments } : {}),
464
+ delivery: ep.delivery || 'app',
444
465
  enabled: ep.enabled,
445
466
  // The store never projects the plaintext secret through list paths; it
446
467
  // exposes a presence flag (secretSet) instead.
@@ -458,6 +479,10 @@ export async function saveWebhook({
458
479
  secret,
459
480
  channel,
460
481
  model,
482
+ cwd,
483
+ workflow,
484
+ attachments,
485
+ delivery,
461
486
  enabled,
462
487
  instructions,
463
488
  overwrite = false,
@@ -486,6 +511,12 @@ export async function saveWebhook({
486
511
  parser: nextParser,
487
512
  channelId: channel ? String(channel).trim() : null,
488
513
  model: model ? String(model).trim() : null,
514
+ cwd: cwd ? String(cwd).trim() : null,
515
+ workflow: workflow ? String(workflow).trim() : null,
516
+ attachments: normalizeAutomationAttachments(attachments),
517
+ delivery: ['app', 'channel', 'both'].includes(String(delivery || '').trim())
518
+ ? String(delivery).trim()
519
+ : 'app',
489
520
  secret: secretValue,
490
521
  instructions: body,
491
522
  enabled: enabled !== false,
@@ -496,6 +527,10 @@ export async function saveWebhook({
496
527
  parser: saved.parser,
497
528
  ...(saved.channelId ? { channel: saved.channelId } : {}),
498
529
  ...(saved.model ? { model: saved.model } : {}),
530
+ ...(saved.cwd ? { cwd: saved.cwd } : {}),
531
+ ...(saved.workflow ? { workflow: saved.workflow } : {}),
532
+ ...(saved.attachments ? { attachments: saved.attachments } : {}),
533
+ delivery: saved.delivery || 'app',
499
534
  ...(enabled === false ? { enabled: false } : {}),
500
535
  secret: secretValue,
501
536
  instructions: body,
@@ -147,7 +147,11 @@ export async function attachToDaemon({
147
147
  token: serverToken,
148
148
  method: 'POST',
149
149
  path: '/client/register',
150
- body: { leadPid, cwd },
150
+ // Initial attachment is transport-only. Mark it with the legacy
151
+ // non-stealing registration intent as well as relying on the current
152
+ // daemon contract, so a newly updated desktop cannot supersede a
153
+ // terminal still served by an older machine-global daemon.
154
+ body: { leadPid, cwd, reattach: true },
151
155
  timeoutMs: 3000,
152
156
  });
153
157
  } catch (err) {