mixdog 0.9.87 → 0.9.89

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 (167) hide show
  1. package/package.json +13 -7
  2. package/scripts/tool-failures.mjs +60 -24
  3. package/src/agents/heavy-worker/AGENT.md +3 -4
  4. package/src/agents/worker/AGENT.md +1 -2
  5. package/src/cli.mjs +8 -0
  6. package/src/defaults/cycle3-review-prompt.md +4 -4
  7. package/src/rules/agent/00-core.md +3 -0
  8. package/src/rules/agent/42-cycle3-agent.md +5 -6
  9. package/src/rules/lead/01-general.md +2 -0
  10. package/src/rules/shared/01-tool.md +29 -35
  11. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
  12. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
  13. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
  14. package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
  16. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
  17. package/src/runtime/agent/orchestrator/config.mjs +29 -11
  18. package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
  21. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
  23. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
  24. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
  25. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
  26. package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
  28. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
  29. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
  30. package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
  31. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
  32. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
  33. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
  34. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
  35. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
  36. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
  37. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
  38. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
  40. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
  41. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
  42. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
  43. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
  44. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
  45. package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
  46. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
  47. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
  48. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
  49. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
  50. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
  51. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
  52. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
  53. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
  54. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
  55. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
  56. package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
  57. package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
  58. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
  59. package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
  60. package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
  61. package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
  62. package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
  63. package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
  64. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
  65. package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
  66. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
  67. package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
  68. package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
  69. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
  70. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
  71. package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
  72. package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
  73. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
  74. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
  75. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
  76. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
  77. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
  78. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
  79. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
  81. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
  83. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
  84. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
  85. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
  86. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
  87. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
  88. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
  89. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
  90. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
  91. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
  92. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
  93. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  94. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
  95. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
  96. package/src/runtime/media/lanes.mjs +2 -2
  97. package/src/runtime/media/renditions.mjs +101 -17
  98. package/src/runtime/media/renditions.test.mjs +54 -0
  99. package/src/runtime/media/store.mjs +111 -43
  100. package/src/runtime/media/store.test.mjs +53 -11
  101. package/src/runtime/memory/index.mjs +26 -50
  102. package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
  103. package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
  104. package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
  105. package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
  106. package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
  107. package/src/runtime/memory/lib/http-router.mjs +4 -0
  108. package/src/runtime/memory/lib/ko-morph.mjs +49 -6
  109. package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
  110. package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
  111. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
  112. package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
  113. package/src/runtime/memory/lib/query-handlers.mjs +26 -28
  114. package/src/runtime/memory/tool-defs.mjs +3 -3
  115. package/src/runtime/shared/atomic-file.mjs +5 -1
  116. package/src/runtime/shared/child-guardian.mjs +73 -2
  117. package/src/runtime/shared/provider-api-key.mjs +22 -5
  118. package/src/runtime/shared/tool-execution-contract.mjs +53 -0
  119. package/src/session-runtime/config-helpers.mjs +6 -5
  120. package/src/session-runtime/lifecycle-api.mjs +16 -4
  121. package/src/session-runtime/media-api.mjs +31 -16
  122. package/src/session-runtime/prewarm.mjs +2 -24
  123. package/src/session-runtime/runtime-core.mjs +33 -15
  124. package/src/session-runtime/runtime-tunables.mjs +0 -3
  125. package/src/session-runtime/session-lifecycle.mjs +0 -6
  126. package/src/session-runtime/session-title.mjs +228 -0
  127. package/src/session-runtime/session-turn-api.mjs +11 -7
  128. package/src/session-runtime/workflow-agents-api.mjs +16 -0
  129. package/src/session-runtime/workflow.mjs +7 -5
  130. package/src/standalone/agent-tool/job-views.mjs +90 -36
  131. package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
  132. package/src/standalone/agent-tool/worker-index.mjs +8 -0
  133. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  134. package/src/standalone/agent-tool.mjs +17 -17
  135. package/src/standalone/agent-watchdog-registry.mjs +19 -2
  136. package/src/standalone/channel-daemon.mjs +8 -0
  137. package/src/standalone/explore-tool.mjs +1 -1
  138. package/src/standalone/memory-runtime-proxy.mjs +4 -7
  139. package/src/standalone/projects.mjs +4 -1
  140. package/src/standalone/usage-dashboard.mjs +25 -3
  141. package/src/tui/App.jsx +5 -3
  142. package/src/tui/app/resume-picker.mjs +7 -2
  143. package/src/tui/app/route-pickers.mjs +11 -117
  144. package/src/tui/app/slash-dispatch.mjs +13 -3
  145. package/src/tui/app/transcript-row-estimate.mjs +1 -1
  146. package/src/tui/app/transcript-window.mjs +17 -93
  147. package/src/tui/app/use-transcript-scroll.mjs +32 -2
  148. package/src/tui/app/use-transcript-window.mjs +64 -114
  149. package/src/tui/components/Markdown.jsx +19 -2
  150. package/src/tui/components/Message.jsx +8 -3
  151. package/src/tui/components/ToolExecution.jsx +7 -3
  152. package/src/tui/dist/index.mjs +243 -223
  153. package/src/tui/engine/live-share.mjs +15 -5
  154. package/src/tui/engine/render-timing.mjs +2 -2
  155. package/src/tui/engine/session-api-ext.mjs +74 -4
  156. package/src/tui/engine/session-flow.mjs +3 -1
  157. package/src/tui/engine.mjs +2 -2
  158. package/src/tui/hooks/useSharedTick.mjs +0 -2
  159. package/src/tui/index.jsx +5 -5
  160. package/src/ui/statusline-agents.mjs +44 -11
  161. package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
  162. package/src/workflows/default/WORKFLOW.md +3 -3
  163. package/src/workflows/solo/WORKFLOW.md +5 -3
  164. package/src/runtime/media/index.mjs +0 -18
  165. package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
  166. package/src/standalone/agent-shard/shard-child.mjs +0 -300
  167. package/src/standalone/agent-shard/shard-pool.mjs +0 -443
@@ -9,6 +9,45 @@ import { buildAgentTaskProgressFields } from '../agent-task-status.mjs';
9
9
  import { clean, normalizeAgentName, positiveInt, presetKey, terminalPidForContext } from './helpers.mjs';
10
10
  import { getBackgroundTask, listBackgroundTasks, sanitizeTaskMeta, taskIdFromArgs } from '../../runtime/shared/background-tasks.mjs';
11
11
  import { resolveAgentSpawnPreset } from './spawn-preset.mjs';
12
+ import { isTerminalWorkerStatus } from './worker-rows.mjs';
13
+
14
+ // Job statuses that can never progress again (mirrors TERMINAL_STATUSES in
15
+ // runtime/shared/background-tasks.mjs; 'canceled' accepted defensively).
16
+ const TERMINAL_JOB_STATUSES = new Set(['completed', 'failed', 'cancelled', 'canceled']);
17
+
18
+ export function isTerminalJobStatus(status) {
19
+ return TERMINAL_JOB_STATUSES.has(String(status ?? '').trim().toLowerCase());
20
+ }
21
+
22
+ // A finished job is frozen history. Tags are reused, so its meta.sessionId can
23
+ // point at a session that is NOW running a *different* job; reading live
24
+ // progress for it made every old completed row inherit the current worker's
25
+ // stage ("20 completed rows all reporting streaming/model active"). Terminal
26
+ // rows therefore report only their own recorded status: no live worker
27
+ // snapshot, no silent_for/watchdog/queued_followups.
28
+ export function terminalJobFrozenFields(status, now = Date.now()) {
29
+ const stage = String(status ?? '').trim().toLowerCase() || 'unknown';
30
+ return {
31
+ workerStatus: stage,
32
+ stage,
33
+ clientHostPid: null,
34
+ lastStreamDeltaAt: null,
35
+ ...buildAgentTaskProgressFields({ now, runtimeStage: stage, taskStatus: stage }),
36
+ };
37
+ }
38
+
39
+ // Default /agents worker section = workers that can still do work. Idle,
40
+ // closed, errored and reaped/unknown rows are history and stay reachable
41
+ // through the task section (status/read keep their terminal result).
42
+ export function isActiveWorkerRow(row = {}) {
43
+ const stage = String(row.stage ?? '').trim().toLowerCase();
44
+ const status = String(row.status ?? '').trim().toLowerCase();
45
+ if (ACTIVE_STAGES.has(stage) || ACTIVE_STAGES.has(status)) return true;
46
+ const effective = stage || status;
47
+ if (!effective || effective === 'unknown') return false;
48
+ return !isTerminalWorkerStatus(effective);
49
+ }
50
+
12
51
  export function createJobViews({
13
52
  mgr,
14
53
  getLiveSession,
@@ -32,7 +71,7 @@ export function createJobViews({
32
71
  // chain-gate defaults to the spawn-prep cap (see provider-init.mjs comments).
33
72
  const { ensureProvider } = createProviderInit(reg, DEFAULT_SPAWN_PREP_TIMEOUT_MS);
34
73
 
35
- function list({ scanSessions = false, context = {} } = {}) {
74
+ function list({ scanSessions = false, context = {}, includeTerminal = false } = {}) {
36
75
  refreshTagsFromSessions({ scanSessions, context });
37
76
  const now = Date.now();
38
77
  const rows = [];
@@ -70,7 +109,7 @@ export function createJobViews({
70
109
  tools: Array.isArray(session.tools) ? session.tools.length : Math.max(0, Number(session.toolCount || 0)),
71
110
  });
72
111
  }
73
- return rows;
112
+ return includeTerminal ? rows : rows.filter((row) => isActiveWorkerRow(row));
74
113
  }
75
114
 
76
115
  function sessionProgressExtras(sessionId, role, now = Date.now(), taskStatus = null) {
@@ -115,25 +154,32 @@ export function createJobViews({
115
154
 
116
155
  function listJobs(context = {}) {
117
156
  const wantedPid = terminalPidForContext(context);
118
- const rows = listBackgroundTasks({ surface: 'agent', context }).map((task) => ({
119
- task_id: task.task_id,
120
- type: task.operation,
121
- status: task.status,
122
- tag: task.tag || null,
123
- sessionId: task.sessionId || null,
124
- agent: task.agent || null,
125
- preset: task.preset || null,
126
- provider: task.provider || null,
127
- model: task.model || null,
128
- effort: task.effort || null,
129
- fast: task.fast === true || task.fast === false ? task.fast : null,
130
- maxLoopIterations: task.maxLoopIterations || null,
131
- startedAt: task.startedAt,
132
- finishedAt: task.finishedAt || null,
133
- error: task.error || null,
134
- ...jobWorkerSnapshot(task.sessionId),
135
- ...sessionProgressExtras(task.sessionId, task.agent || null, Date.now(), task.status),
136
- }));
157
+ const now = Date.now();
158
+ const rows = listBackgroundTasks({ surface: 'agent', context }).map((task) => {
159
+ const head = {
160
+ task_id: task.task_id,
161
+ type: task.operation,
162
+ status: task.status,
163
+ tag: task.tag || null,
164
+ sessionId: task.sessionId || null,
165
+ agent: task.agent || null,
166
+ preset: task.preset || null,
167
+ provider: task.provider || null,
168
+ model: task.model || null,
169
+ effort: task.effort || null,
170
+ fast: task.fast === true || task.fast === false ? task.fast : null,
171
+ maxLoopIterations: task.maxLoopIterations || null,
172
+ startedAt: task.startedAt,
173
+ finishedAt: task.finishedAt || null,
174
+ error: task.error || null,
175
+ };
176
+ if (isTerminalJobStatus(task.status)) return { ...head, ...terminalJobFrozenFields(task.status, now) };
177
+ return {
178
+ ...head,
179
+ ...jobWorkerSnapshot(task.sessionId),
180
+ ...sessionProgressExtras(task.sessionId, task.agent || null, now, task.status),
181
+ };
182
+ });
137
183
  return wantedPid
138
184
  ? rows.filter((row) => {
139
185
  const pid = positiveInt(row.clientHostPid);
@@ -221,20 +267,7 @@ export function createJobViews({
221
267
 
222
268
  function renderJob(job, includeResult = false) {
223
269
  const meta = job.meta || {};
224
- let progress = sessionProgressExtras(meta.sessionId, meta.agent || null, Date.now(), job.status);
225
- // Spawn is deferred: before the worker session exists, sessionProgressExtras
226
- // returns {} and the status card would show only "status: running" with no
227
- // stage/progress. Fill a minimal stage so the caller can tell the job is
228
- // still spinning up rather than silently stalled.
229
- if (!meta.sessionId && (!progress || Object.keys(progress).length === 0)) {
230
- const spawning = job.status === 'running';
231
- progress = {
232
- worker_stage: spawning ? 'spawning' : (job.status || 'unknown'),
233
- last_progress: spawning ? 'spawning worker session' : (job.status || 'unknown'),
234
- diagnostic: spawning ? 'worker session not started yet' : (job.status || 'unknown'),
235
- };
236
- }
237
- return {
270
+ const head = {
238
271
  task_id: job.taskId,
239
272
  type: job.operation,
240
273
  status: job.status,
@@ -251,9 +284,30 @@ export function createJobViews({
251
284
  startedAt: job.startedAt,
252
285
  finishedAt: job.finishedAt || null,
253
286
  error: job.error || null,
287
+ };
288
+ const tail = includeResult && job.result !== undefined ? { result: job.result } : {};
289
+ // Frozen terminal view: never re-read the (possibly reused) live session.
290
+ if (isTerminalJobStatus(job.status)) {
291
+ return { ...head, ...terminalJobFrozenFields(job.status), ...tail };
292
+ }
293
+ let progress = sessionProgressExtras(meta.sessionId, meta.agent || null, Date.now(), job.status);
294
+ // Spawn is deferred: before the worker session exists, sessionProgressExtras
295
+ // returns {} and the status card would show only "status: running" with no
296
+ // stage/progress. Fill a minimal stage so the caller can tell the job is
297
+ // still spinning up rather than silently stalled.
298
+ if (!meta.sessionId && (!progress || Object.keys(progress).length === 0)) {
299
+ const spawning = job.status === 'running';
300
+ progress = {
301
+ worker_stage: spawning ? 'spawning' : (job.status || 'unknown'),
302
+ last_progress: spawning ? 'spawning worker session' : (job.status || 'unknown'),
303
+ diagnostic: spawning ? 'worker session not started yet' : (job.status || 'unknown'),
304
+ };
305
+ }
306
+ return {
307
+ ...head,
254
308
  ...jobWorkerSnapshot(meta.sessionId),
255
309
  ...progress,
256
- ...(includeResult && job.result !== undefined ? { result: job.result } : {}),
310
+ ...tail,
257
311
  };
258
312
  }
259
313
 
@@ -1,15 +1,13 @@
1
1
  // Spawn/job lifecycle, extracted from agent-tool.mjs: job start (foreground
2
2
  // deferral, admission control), the progress-idle watchdogs, spawn prep
3
- // (shard enqueue + in-process fallback paths), and the full runSpawn
3
+ // (provider/session preparation), and the full runSpawn
4
4
  // execution with turn-review collection and terminal accounting.
5
- import { clean, clearAgentStatuslineRoute, nonNegativeInt, normalizeAgentName, positiveInt, presetKey, readAgentFrontmatterPermission, resolvePrompt, rowMatchesContext, terminalPidForContext, withCwdHeader, writeAgentStatuslineRoute } from './helpers.mjs';
5
+ import { clean, clearAgentStatuslineRoute, nonNegativeInt, normalizeAgentName, positiveInt, presetKey, readAgentFrontmatterPermission, resolvePrompt, terminalPidForContext, withCwdHeader, writeAgentStatuslineRoute } from './helpers.mjs';
6
6
  import { createNotify } from './notify.mjs';
7
7
  import { reconcileBackgroundTask, sanitizeTaskMeta, startBackgroundTask } from '../../runtime/shared/background-tasks.mjs';
8
8
  import { abnormalEmptyFinishError, renderResult } from './render.mjs';
9
- import { agentShardEnabled, shardRequest } from '../agent-shard/shard-pool.mjs';
10
9
  import { resourceAdmission } from '../../runtime/shared/resource-admission.mjs';
11
10
  import { createProgressWatchdogRegistry } from '../agent-watchdog-registry.mjs';
12
- import { isTerminalWorkerStatus } from './worker-rows.mjs';
13
11
  import { resolve } from 'node:path';
14
12
  import { normalizeAgentPermission } from '../../runtime/shared/markdown-frontmatter.mjs';
15
13
  import { resolveAgentSpawnPreset } from './spawn-preset.mjs';
@@ -25,7 +23,6 @@ export function createSpawnFlow({
25
23
  preparedSpawnMeta,
26
24
  upsertWorkerSessionDeferred,
27
25
  refreshTagsFromSessions,
28
- readWorkerRows,
29
26
  defaultCwd,
30
27
  nextTag,
31
28
  cancelReap,
@@ -74,17 +71,6 @@ export function createSpawnFlow({
74
71
  if (currentMeta?.sessionId) {
75
72
  try { mgr.closeSession(currentMeta.sessionId, 'agent-task-cancel'); } catch {}
76
73
  }
77
- // Sharded spawn: the live session exists only in the shard child —
78
- // forward the cancel by sessionId (known after the `spawned` progress
79
- // event) or tag.
80
- if (agentShardEnabled() && (currentMeta?.tag || currentMeta?.sessionId)) {
81
- try {
82
- void shardRequest('cancel', {
83
- tag: currentMeta?.tag || null,
84
- sessionId: currentMeta?.sessionId || null,
85
- }).catch(() => {});
86
- } catch { /* best-effort */ }
87
- }
88
74
  },
89
75
  run: async () => {
90
76
  const lease = await resourceAdmission.acquire('agent', {
@@ -109,13 +95,6 @@ export function createSpawnFlow({
109
95
  function startDeferredSpawnJob(args, callerCwd, context, notifyContext, extras = {}) {
110
96
  return startJob('spawn', pendingSpawnMeta(args, extras), async (job) => {
111
97
  if (job?.status === 'cancelled') return null;
112
- // P1 agent sharding (MIXDOG_AGENT_SHARD=1, default off): run the worker
113
- // session in a shard child process instead of this event loop. Prompt
114
- // resolution stays here (cheap); prep + the full agent loop run in the
115
- // shard. Tag/index/watchdog parity lands in P2 (docs/agent-shard-design.md).
116
- if (agentShardEnabled()) {
117
- return await runShardSpawn(args, callerCwd, context, job);
118
- }
119
98
  // prepareSpawn (ensureProvider/prepareAgentSession) runs before runSpawn
120
99
  // installs its progress watchdog, so guard prep with an internal env-
121
100
  // backed cap rather than exposing per-call timeout knobs on the agent
@@ -158,6 +137,7 @@ export function createSpawnFlow({
158
137
  stage: 'running',
159
138
  task_id: job.taskId,
160
139
  startedAt: job.startedAt,
140
+ turnStartedAt: new Date().toISOString(),
161
141
  });
162
142
  if (job?.status === 'cancelled') {
163
143
  closePreparedSpawn(prepared);
@@ -170,202 +150,26 @@ export function createSpawnFlow({
170
150
  // Idle-progress watchdogs (shared sweep timer) live in
171
151
  // agent-watchdog-registry.mjs.
172
152
  const progressWatchdogs = createProgressWatchdogRegistry({ mgr });
173
- const startProgressIdleWatchdog = (sessionId, watchdogPolicy, agent = null) =>
174
- progressWatchdogs.start(sessionId, watchdogPolicy, agent);
153
+ const startProgressIdleWatchdog = (sessionId, watchdogPolicy, agent = null, options = {}) =>
154
+ progressWatchdogs.start(sessionId, watchdogPolicy, agent, options);
155
+
156
+ // Queued `agent type=send` prompts run as follow-up turns INSIDE one
157
+ // askSession call, so the dispatch stamp alone would keep the panel elapsed
158
+ // growing across them. The watchdog sweep reports each turn's start; re-stamp
159
+ // the worker-index row so elapsed resets at the queued-turn boundary.
160
+ const turnStartStamper = (session, tag) => (turnStartedAtMs) => {
161
+ upsertWorkerSessionDeferred(session, tag, {
162
+ status: 'running',
163
+ stage: 'running',
164
+ turnStartedAt: new Date(turnStartedAtMs).toISOString(),
165
+ });
166
+ };
175
167
 
176
168
  async function prepareSpawn(args, callerCwd = null, context = {}, prepState = null) {
177
- // (sharded spawns never reach here — see startDeferredSpawnJob)
178
169
  refreshTagsFromSessions({ context });
179
170
  return prepareSpawnInProcess(args, callerCwd, context, prepState);
180
171
  }
181
172
 
182
- // Tag-reuse routing for sharded workers (P2): a same-tag spawn/send while
183
- // the shard runs the session mirrors the in-process busy-session enqueue.
184
- // Discriminator: a non-terminal worker-index row whose session has no live
185
- // runtime in THIS process. (mgr.getSession must not be used here — it
186
- // falls back to the on-disk session file the shard child persists, which
187
- // makes a shard-owned session look lead-live.) Returns the rendered queued
188
- // result, or null to fall through to the normal path (unknown tag,
189
- // terminal row, lead-owned runtime, or the shard no longer owns the
190
- // session — e.g. after a shard crash, where respawn is the correct
191
- // recovery).
192
- async function tryShardEnqueue(target, args, context, extras = {}) {
193
- if (!agentShardEnabled()) return null;
194
- const value = clean(target);
195
- if (!value) return null;
196
- const row = readWorkerRows(context).find((r) => (
197
- (clean(r.tag) === value || clean(r.sessionId) === value)
198
- && rowMatchesContext(r, context)
199
- && !isTerminalWorkerStatus(r.status || r.stage)
200
- ));
201
- if (!row) return null;
202
- const rowSessionId = clean(row.sessionId);
203
- const runtime = rowSessionId ? mgr.getSessionRuntime?.(rowSessionId) : null;
204
- if (runtime?.controller?.signal && !runtime.controller.signal.aborted) return null;
205
- const prompt = await resolvePrompt(args, clean(row.cwd) || defaultCwd || process.cwd());
206
- if (!clean(prompt)) return null;
207
- let queued = null;
208
- try {
209
- queued = await shardRequest('enqueue', {
210
- tag: clean(row.tag) || null,
211
- sessionId: rowSessionId || null,
212
- prompt,
213
- });
214
- } catch { return null; }
215
- if (queued?.queued !== true) return null;
216
- return renderResult({
217
- queued: true,
218
- ...extras,
219
- tag: clean(row.tag) || null,
220
- sessionId: rowSessionId || null,
221
- agent: clean(row.agent) || null,
222
- queueDepth: queued.queueDepth || 1,
223
- });
224
- }
225
-
226
- async function runShardSpawn(args, callerCwd, context, job) {
227
- const agent = normalizeAgentName(args.agent);
228
- if (!agent) throw new Error('agent spawn: agent is required');
229
- const tag = clean(args.tag) || nextTag(agent, context);
230
- const baseCwd = resolve(callerCwd || defaultCwd || process.cwd());
231
- const workerCwd = clean(args.cwd) ? resolve(baseCwd, args.cwd) : baseCwd;
232
- const prompt = withCwdHeader(await resolvePrompt(args, workerCwd), workerCwd);
233
- const clientHostPid = terminalPidForContext(context) || null;
234
- // P2 progress parity: the shard reports the bound session the moment it
235
- // exists (`spawned`), so the lead mirrors its in-process spawn
236
- // bookkeeping — tag maps, worker-index row, statusline route, job meta,
237
- // SubagentStart — while the worker loop runs in the child.
238
- let spawned = null;
239
- const onSpawned = (payload = {}) => {
240
- if (spawned || !payload.sessionId) return;
241
- spawned = payload;
242
- const sessionLike = {
243
- id: payload.sessionId,
244
- agent,
245
- cwd: payload.cwd || workerCwd,
246
- provider: payload.provider || null,
247
- model: payload.model || null,
248
- presetName: payload.presetName || null,
249
- effort: payload.effort || null,
250
- fast: payload.fast === true,
251
- clientHostPid,
252
- status: 'running',
253
- };
254
- cancelReap(payload.sessionId);
255
- writeAgentStatuslineRoute(payload.sessionId, {
256
- provider: payload.provider || null,
257
- model: payload.model || null,
258
- effort: payload.effort || null,
259
- fast: payload.fast === true,
260
- });
261
- bindTag(tag, sessionLike, {
262
- agent,
263
- preset: payload.presetName || null,
264
- provider: payload.provider || null,
265
- model: payload.model || null,
266
- effort: payload.effort || null,
267
- fast: payload.fast === true,
268
- status: 'running',
269
- stage: 'running',
270
- });
271
- if (job) {
272
- mergeJobMeta(job, {
273
- tag,
274
- sessionId: payload.sessionId,
275
- agent,
276
- preset: payload.presetName || null,
277
- provider: payload.provider || null,
278
- model: payload.model || null,
279
- effort: payload.effort || null,
280
- fast: payload.fast === true,
281
- });
282
- }
283
- emitSubagentEvent('start', agent, { session_id: payload.sessionId, tag });
284
- // Cancel raced the shard prep: the task is already terminal on the lead
285
- // but the child just started its loop — abort it now that the sessionId
286
- // is known, instead of letting it burn tokens to completion.
287
- if (job?.status === 'cancelled') {
288
- try { void shardRequest('cancel', { tag, sessionId: payload.sessionId }).catch(() => {}); } catch { /* best-effort */ }
289
- }
290
- };
291
- let finalStatus = 'error';
292
- try {
293
- const response = await shardRequest('spawn', {
294
- args: {
295
- agent,
296
- provider: args.provider,
297
- model: args.model,
298
- effort: args.effort,
299
- fast: args.fast,
300
- preset: args.preset,
301
- },
302
- tag,
303
- cwd: workerCwd,
304
- prompt,
305
- context: args.context || null,
306
- parentSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
307
- ownerSessionId: clean(context?.callerSessionId || context?.sessionId) || null,
308
- clientHostPid,
309
- }, {
310
- onProgress: (event, payload) => { if (event === 'spawned') onSpawned(payload); },
311
- });
312
- // Same abnormal-empty promotion as the in-process completionValue; a
313
- // watchdog partial handoff (stallAbort) is a valid completion and is
314
- // exempt, matching runSpawn's catch path.
315
- const abnormalError = response?.stallAbort === true
316
- ? null
317
- : abnormalEmptyFinishError(response, agent);
318
- const value = {
319
- tag,
320
- sessionId: response.sessionId,
321
- agent,
322
- preset: response.presetName || null,
323
- provider: response.provider || null,
324
- model: response.model || null,
325
- effort: response.effort || null,
326
- fast: response.fast === true,
327
- content: response.content || '',
328
- ...(response.stallAbort === true ? { stallAbort: true } : {}),
329
- ...(abnormalError ? { error: abnormalError } : {}),
330
- };
331
- if (job) job._terminalResultValue = value;
332
- if (value.error) throw new Error(value.error);
333
- finalStatus = 'idle';
334
- return value;
335
- } finally {
336
- // Terminal bookkeeping parity with runSpawn's finally: worker-index row
337
- // transitions to idle/error, the terminal-reap timer starts (tombstone +
338
- // statusline cleanup), and SubagentStop fires. Only after `spawned` —
339
- // a prep failure inside the shard bound nothing on the lead.
340
- if (spawned?.sessionId) {
341
- upsertWorkerSessionDeferred({
342
- id: spawned.sessionId,
343
- agent,
344
- cwd: spawned.cwd || workerCwd,
345
- provider: spawned.provider || null,
346
- model: spawned.model || null,
347
- presetName: spawned.presetName || null,
348
- effort: spawned.effort || null,
349
- fast: spawned.fast === true,
350
- clientHostPid,
351
- status: finalStatus,
352
- }, tag, {
353
- agent,
354
- preset: spawned.presetName || null,
355
- provider: spawned.provider || null,
356
- model: spawned.model || null,
357
- effort: spawned.effort || null,
358
- fast: spawned.fast === true,
359
- status: finalStatus,
360
- stage: finalStatus,
361
- finishedAt: new Date().toISOString(),
362
- });
363
- scheduleReap(spawned.sessionId, spawned.provider || null);
364
- emitSubagentEvent('stop', agent, { session_id: spawned.sessionId, tag, status: finalStatus });
365
- }
366
- }
367
- }
368
-
369
173
  async function prepareSpawnInProcess(args, callerCwd = null, context = {}, prepState = null) {
370
174
  const config = cfgMod.loadConfig();
371
175
  const agent = normalizeAgentName(args.agent);
@@ -442,7 +246,9 @@ export function createSpawnFlow({
442
246
 
443
247
  async function runSpawn(prepared, notifyContext = null, job = null) {
444
248
  const { args, tag, session, agent, preset, presetName, workerCwd, prompt, watchdogPolicy } = prepared;
445
- const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy, agent);
249
+ const watchdog = startProgressIdleWatchdog(session.id, watchdogPolicy, agent, {
250
+ onTurnStart: turnStartStamper(session, tag),
251
+ });
446
252
  const turnReview = createTurnReviewCollector(session, tag, agent, notifyContext || {});
447
253
  let finalStatus = 'idle';
448
254
  // SubagentStart: a worker session is about to run its first turn.
@@ -456,6 +262,7 @@ export function createSpawnFlow({
456
262
  fast: preset.fast === true,
457
263
  status: 'running',
458
264
  stage: 'running',
265
+ turnStartedAt: new Date().toISOString(),
459
266
  });
460
267
  let handoffMsgStart = 0;
461
268
  try {
@@ -609,6 +416,9 @@ export function createSpawnFlow({
609
416
  scheduleReap(session.id);
610
417
  // SubagentStop: worker finished (terminal), regardless of outcome.
611
418
  emitSubagentEvent('stop', agent, { session_id: session.id, tag, status: finalStatus });
419
+ // Keep the durable session/tag for same-tag follow-ups, but release every
420
+ // process-local provider, shell, watchdog and tool cache immediately.
421
+ try { mgr.unloadSessionRuntime?.(session.id, 'agent-turn-complete'); } catch {}
612
422
  }
613
423
  }
614
424
 
@@ -620,9 +430,8 @@ export function createSpawnFlow({
620
430
  startDeferredSpawnJob,
621
431
  progressWatchdogs,
622
432
  startProgressIdleWatchdog,
433
+ turnStartStamper,
623
434
  prepareSpawn,
624
- tryShardEnqueue,
625
- runShardSpawn,
626
435
  prepareSpawnInProcess,
627
436
  runSpawn,
628
437
  };
@@ -54,6 +54,7 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
54
54
  .map((row) => ({
55
55
  tag: clean(row.tag),
56
56
  sessionId: clean(row.sessionId),
57
+ ownerSessionId: clean(row.ownerSessionId || row.parentSessionId) || null,
57
58
  agent: clean(row.agent) || null,
58
59
  provider: clean(row.provider) || null,
59
60
  model: clean(row.model) || null,
@@ -66,6 +67,7 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
66
67
  updatedAt: clean(row.updatedAt) || null,
67
68
  lastUsedAt: clean(row.lastUsedAt) || null,
68
69
  finishedAt: clean(row.finishedAt) || null,
70
+ turnStartedAt: clean(row.turnStartedAt) || null,
69
71
  clientHostPid: positiveInt(row.clientHostPid),
70
72
  cwd: clean(row.cwd) || null,
71
73
  task_id: clean(row.task_id || row.taskId) || null,
@@ -199,6 +201,9 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
199
201
  return {
200
202
  tag,
201
203
  sessionId,
204
+ ownerSessionId: clean(extra.ownerSessionId || extra.parentSessionId)
205
+ || clean(session?.ownerSessionId || session?.parentSessionId)
206
+ || null,
202
207
  agent: clean(extra.agent) || clean(session?.agent) || null,
203
208
  provider: clean(extra.provider) || clean(session?.provider) || null,
204
209
  model: clean(extra.model) || clean(session?.model) || null,
@@ -211,6 +216,9 @@ export function createWorkerIndex({ dataDir, cfgMod, mgr, tags, tagAgents, tagCw
211
216
  updatedAt: clean(extra.updatedAt) || nowIso,
212
217
  lastUsedAt: clean(session?.lastUsedAt) || null,
213
218
  finishedAt: clean(extra.finishedAt) || null,
219
+ // Turn dispatch stamps this; terminal upserts leave it null and the
220
+ // merge in applyWorkerRowUpsert preserves the running turn's value.
221
+ turnStartedAt: clean(extra.turnStartedAt) || null,
214
222
  clientHostPid: positiveInt(extra.clientHostPid) || positiveInt(session?.clientHostPid),
215
223
  cwd: clean(session?.cwd) || clean(extra.cwd) || null,
216
224
  task_id: clean(extra.task_id || extra.taskId) || null,
@@ -56,7 +56,7 @@ export function applyWorkerRowUpsert(byKey, normalized) {
56
56
  if (!key) return;
57
57
  const prev = byKey.get(key) || {};
58
58
  const merged = { ...prev, ...normalized };
59
- for (const field of ['agent', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'cwd', 'task_id', 'permission', 'toolPermission']) {
59
+ for (const field of ['ownerSessionId', 'agent', 'provider', 'model', 'preset', 'effort', 'fast', 'clientHostPid', 'cwd', 'task_id', 'permission', 'toolPermission', 'turnStartedAt']) {
60
60
  if ((merged[field] === null || merged[field] === '') && prev[field] != null && prev[field] !== '') {
61
61
  merged[field] = prev[field];
62
62
  }
@@ -71,6 +71,7 @@ export function applyWorkerRowUpsert(byKey, normalized) {
71
71
  export function workerRowToSession(row = {}) {
72
72
  return {
73
73
  id: row.sessionId,
74
+ ownerSessionId: row.ownerSessionId || null,
74
75
  agentTag: row.tag,
75
76
  agent: row.agent || null,
76
77
  provider: row.provider || null,
@@ -62,7 +62,6 @@ import {
62
62
  } from './agent-tool/worker-rows.mjs';
63
63
  import { resolveAgentTerminalReapMs } from '../session-runtime/config-helpers.mjs';
64
64
  import { createWorkerIndex } from './agent-tool/worker-index.mjs';
65
- import { agentShardEnabled, shardRequest } from './agent-shard/shard-pool.mjs';
66
65
  import {
67
66
  beginAgentTurnReview,
68
67
  completeAgentTurnReview,
@@ -192,9 +191,8 @@ export function createStandaloneAgent({
192
191
  startDeferredSpawnJob,
193
192
  progressWatchdogs,
194
193
  startProgressIdleWatchdog,
194
+ turnStartStamper,
195
195
  prepareSpawn,
196
- tryShardEnqueue,
197
- runShardSpawn,
198
196
  prepareSpawnInProcess,
199
197
  runSpawn,
200
198
  } = createSpawnFlow({
@@ -242,11 +240,19 @@ export function createStandaloneAgent({
242
240
  async function runSend(prepared, notifyContext = null, job = null) {
243
241
  const { args, session, sessionId, prompt } = prepared;
244
242
  const sendAgent = session.agent || normalizeAgentName(args.agent);
245
- const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendAgent), sendAgent);
246
243
  const tag = tagForSession(sessionId);
244
+ // Queued sends run as follow-up turns inside this same askSession call;
245
+ // the watchdog sweep re-stamps turnStartedAt at each turn boundary.
246
+ const watchdog = startProgressIdleWatchdog(sessionId, resolveAgentWatchdogPolicy(sendAgent), sendAgent, {
247
+ onTurnStart: turnStartStamper(session, tag),
248
+ });
247
249
  const turnReview = createTurnReviewCollector(session, tag, sendAgent, notifyContext || {});
248
250
  let finalStatus = 'idle';
249
- upsertWorkerSessionDeferred(session, tag, { status: 'running', stage: 'running' });
251
+ upsertWorkerSessionDeferred(session, tag, {
252
+ status: 'running',
253
+ stage: 'running',
254
+ turnStartedAt: new Date().toISOString(),
255
+ });
250
256
  let handoffMsgStart = 0;
251
257
  try {
252
258
  const completionValue = (result) => {
@@ -366,6 +372,9 @@ export function createStandaloneAgent({
366
372
  } catch {}
367
373
  }
368
374
  scheduleReap(sessionId);
375
+ // Same lifecycle as a fresh spawn: the transcript/tag stays resumable,
376
+ // while heavy process-local runtime state is reclaimed immediately.
377
+ try { mgr.unloadSessionRuntime?.(sessionId, 'agent-turn-complete'); } catch {}
369
378
  }
370
379
  }
371
380
 
@@ -458,7 +467,9 @@ export function createStandaloneAgent({
458
467
  tasksRemoved: cleaned.removed,
459
468
  tagsRemoved: beforeTags - tags.size,
460
469
  tasks: listJobs(scopedContext).length,
461
- workers: list({ scanSessions: wantsSessionScan(args), context: scopedContext }).length,
470
+ // Cleanup reports how many worker rows remain known (idle/terminal
471
+ // included); only the /agents worker section hides terminal rows.
472
+ workers: list({ scanSessions: wantsSessionScan(args), context: scopedContext, includeTerminal: true }).length,
462
473
  };
463
474
  }
464
475
 
@@ -537,11 +548,6 @@ export function createStandaloneAgent({
537
548
  if (type === 'cancel') return renderResult(close(args, scopedContext));
538
549
  if (type === 'close') return renderResult(close(args, scopedContext));
539
550
  if (type === 'send') {
540
- // Sharded busy worker: queue the follow-up into the shard-owned live
541
- // session (in-process busy sessions are handled below via
542
- // dispatchToExistingSession; their lead session resolves live).
543
- const shardQueued = await tryShardEnqueue(clean(args.tag || args.sessionId), args, scopedContext);
544
- if (shardQueued) return shardQueued;
545
551
  try {
546
552
  const prepared = await prepareSend(args, scopedContext);
547
553
  return dispatchToExistingSession(prepared, notifyContext);
@@ -614,12 +620,6 @@ export function createStandaloneAgent({
614
620
  let respawned = false;
615
621
  let spawnArgs = args;
616
622
  if (explicitTag) {
617
- // Sharded busy worker under this tag: queue into the shard-owned
618
- // live session BEFORE the live-reuse resolve — the lead's disk-read
619
- // session view would otherwise misroute this into an in-process
620
- // send against a shard-owned session file (dual writer).
621
- const shardQueued = await tryShardEnqueue(explicitTag, args, scopedContext, { reused: true });
622
- if (shardQueued) return shardQueued;
623
623
  // Resolve a LIVE same-tag session in this terminal (busy or idle).
624
624
  let liveSessionId = null;
625
625
  try {
@@ -32,6 +32,15 @@ export function createProgressWatchdogRegistry({ mgr }) {
32
32
  const snapshot = typeof mgr.getSessionProgressSnapshot === 'function'
33
33
  ? mgr.getSessionProgressSnapshot(sessionId)
34
34
  : null;
35
+ // Turn boundary: askStartedAt is re-stamped by markSessionAskStart for
36
+ // EVERY turn of one askSession call, including the follow-up turns drained
37
+ // from the pending-message queue. The sweep already reads this snapshot, so
38
+ // observing the change is the cheapest per-turn signal available here.
39
+ const askStartedAt = Number(snapshot?.askStartedAt || 0);
40
+ if (askStartedAt > 0 && askStartedAt !== state.lastAskStartedAt) {
41
+ state.lastAskStartedAt = askStartedAt;
42
+ try { state.onTurnStart?.(askStartedAt); } catch { /* bookkeeping only */ }
43
+ }
35
44
  const abortErr = snapshot ? evaluateAgentWatchdogAbort(snapshot, now, watchdogPolicy) : null;
36
45
  const sess = typeof mgr.getSession === 'function' ? mgr.getSession(sessionId) : null;
37
46
  const iteration = typeof sess?.lastIterationIndex === 'number' ? sess.lastIterationIndex : null;
@@ -79,7 +88,7 @@ export function createProgressWatchdogRegistry({ mgr }) {
79
88
  return {
80
89
  /** Watch a session until the returned handle is stopped; null when the
81
90
  * policy is off or the manager cannot link an abort signal. */
82
- start(sessionId, watchdogPolicy, agent = null) {
91
+ start(sessionId, watchdogPolicy, agent = null, { onTurnStart = null } = {}) {
83
92
  if (!sessionId || !agentWatchdogPolicyActive(watchdogPolicy)) return null;
84
93
  if (typeof mgr.getSessionProgressSnapshot !== 'function'
85
94
  && typeof mgr.getSessionLastProgressAt !== 'function') return null;
@@ -87,7 +96,15 @@ export function createProgressWatchdogRegistry({ mgr }) {
87
96
  const controller = new AbortController();
88
97
  const anchorTs = Date.now();
89
98
  try { mgr.linkParentSignalToSession(sessionId, controller.signal); } catch { return null; }
90
- const state = { sessionId, watchdogPolicy, agent, controller, anchorTs };
99
+ const state = {
100
+ sessionId,
101
+ watchdogPolicy,
102
+ agent,
103
+ controller,
104
+ anchorTs,
105
+ onTurnStart: typeof onTurnStart === 'function' ? onTurnStart : null,
106
+ lastAskStartedAt: 0,
107
+ };
91
108
  watched.set(sessionId, state);
92
109
  ensureTimer();
93
110
  return {