@adhdev/daemon-core 0.9.82-rc.135 → 0.9.82-rc.137

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 (48) hide show
  1. package/dist/cli-adapter-types.d.ts +1 -0
  2. package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
  3. package/dist/cli-adapters/cli-state-engine.d.ts +154 -0
  4. package/dist/cli-adapters/provider-cli-adapter.d.ts +75 -74
  5. package/dist/cli-adapters/provider-cli-parse.d.ts +2 -0
  6. package/dist/cli-adapters/provider-cli-shared.d.ts +6 -0
  7. package/dist/config/chat-history.d.ts +1 -0
  8. package/dist/index.d.ts +3 -3
  9. package/dist/index.js +2624 -1946
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +2627 -1954
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/mesh/beads-db.d.ts +54 -0
  14. package/dist/mesh/mesh-active-work.d.ts +7 -1
  15. package/dist/mesh/mesh-events.d.ts +10 -4
  16. package/dist/mesh/mesh-ledger.d.ts +21 -1
  17. package/dist/mesh/mesh-refine-status.d.ts +2 -3
  18. package/dist/mesh/mesh-work-queue.d.ts +17 -0
  19. package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
  20. package/dist/providers/approval-utils.d.ts +9 -0
  21. package/dist/repo-mesh-types.d.ts +5 -0
  22. package/package.json +1 -1
  23. package/src/cli-adapter-types.d.ts +1 -0
  24. package/src/cli-adapter-types.ts +1 -0
  25. package/src/cli-adapters/cli-script-runner.ts +145 -0
  26. package/src/cli-adapters/cli-state-engine.ts +957 -0
  27. package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
  28. package/src/cli-adapters/provider-cli-adapter.ts +377 -1387
  29. package/src/cli-adapters/provider-cli-parse.ts +6 -0
  30. package/src/cli-adapters/provider-cli-shared.ts +6 -0
  31. package/src/commands/chat-commands.ts +23 -1
  32. package/src/commands/cli-manager.ts +3 -1
  33. package/src/commands/router.ts +8 -0
  34. package/src/config/chat-history.ts +7 -3
  35. package/src/git/git-worktree.ts +8 -1
  36. package/src/index.ts +3 -2
  37. package/src/mesh/beads-db.ts +305 -2
  38. package/src/mesh/coordinator-prompt.ts +12 -17
  39. package/src/mesh/mesh-active-work.ts +162 -59
  40. package/src/mesh/mesh-events.ts +198 -53
  41. package/src/mesh/mesh-ledger.ts +321 -105
  42. package/src/mesh/mesh-refine-status.ts +2 -3
  43. package/src/mesh/mesh-work-queue.ts +116 -120
  44. package/src/mesh/worktree-bootstrap-config.ts +17 -4
  45. package/src/providers/approval-utils.ts +27 -0
  46. package/src/providers/cli-provider-instance.ts +26 -2
  47. package/src/providers/provider-schema.ts +2 -0
  48. package/src/repo-mesh-types.ts +10 -0
@@ -21,10 +21,12 @@ export interface MeshTaskModeValidationResult {
21
21
 
22
22
  const LIVE_DEBUG_READONLY_FORBIDDEN: Array<{ label: string; pattern: RegExp }> = [
23
23
  { label: 'source_edit', pattern: /\b(edit|modify|patch|apply\s+patch|write\s+(?:to\s+)?(?:file|source)|overwrite|delete\s+file|remove\s+file|create\s+file|touch\s+file)\b/i },
24
- { label: 'git_mutation', pattern: /\b(?:git\s+(?:add|commit|push|reset|rebase|clean|checkout|switch|merge|tag|restore|rm|mv)|push\b)/i },
24
+ { label: 'git_mutation', pattern: /\b(?:git\s+(?:add|commit|push|reset|rebase|clean|checkout|switch|merge|tag|restore|rm|mv|stash|worktree\s+(?:add|remove|move))|push\b)/i },
25
25
  { label: 'checkpoint', pattern: /\b(checkpoint|mesh_checkpoint)\b/i },
26
- { label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release)\b/i },
27
- { label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i)\b/i },
26
+ { label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release|npm\s+publish|yarn\s+publish|pnpm\s+publish)\b/i },
27
+ { label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i|shred\b)\b/i },
28
+ { label: 'package_install', pattern: /\b(npm\s+(?:install|i|add|link|uninstall|remove)|yarn\s+(?:add|remove|link)|pnpm\s+(?:add|remove|link)|pip\s+install|brew\s+install|apt\s+install|cargo\s+install)\b/i },
29
+ { label: 'container_mutation', pattern: /\b(docker\s+(?:build|run|exec|push|tag|rmi|rm|create|start|stop|kill)|kubectl\s+(?:apply|delete|patch|replace|create|scale))\b/i },
28
30
  ];
29
31
 
30
32
  export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
@@ -122,35 +124,26 @@ export function enqueueTask(
122
124
  if (!modeValidation.valid) {
123
125
  throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
124
126
  }
125
- return withQueueLock(meshId, () => {
126
- const queue = readQueue(meshId);
127
- const entry: MeshWorkQueueEntry = {
128
- id: randomUUID(),
129
- meshId,
130
- message,
131
- status: 'pending',
132
- taskMode: modeValidation.taskMode,
133
- targetNodeId: opts?.targetNodeId,
134
- targetSessionId: opts?.targetSessionId,
135
- createdAt: new Date().toISOString(),
136
- updatedAt: new Date().toISOString(),
137
- };
138
- queue.push(entry);
139
- writeQueue(meshId, queue);
140
- return entry;
141
- });
127
+ const entry: MeshWorkQueueEntry = {
128
+ id: randomUUID(),
129
+ meshId,
130
+ message,
131
+ status: 'pending',
132
+ taskMode: modeValidation.taskMode,
133
+ targetNodeId: opts?.targetNodeId,
134
+ targetSessionId: opts?.targetSessionId,
135
+ createdAt: new Date().toISOString(),
136
+ updatedAt: new Date().toISOString(),
137
+ };
138
+ BeadsDB.getInstance().insertQueueEntry(entry);
139
+ return entry;
142
140
  }
143
141
 
144
142
  /**
145
143
  * Get all tasks in the queue, optionally filtered by status.
146
144
  */
147
145
  export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }): MeshWorkQueueEntry[] {
148
- let queue = readQueue(meshId);
149
- if (opts?.status?.length) {
150
- const statuses = new Set(opts.status);
151
- queue = queue.filter(q => statuses.has(q.status));
152
- }
153
- return queue;
146
+ return BeadsDB.getInstance().getQueueEntries(meshId, opts?.status?.length ? opts.status : undefined);
154
147
  }
155
148
 
156
149
  export function getMeshQueueRevision(meshId: string): string {
@@ -161,29 +154,7 @@ export function getMeshQueueRevision(meshId: string): string {
161
154
  * Find the next pending task that this node is allowed to claim, and mark it as assigned.
162
155
  */
163
156
  export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
164
- return withQueueLock(meshId, () => {
165
- const queue = readQueue(meshId);
166
- const hasActiveAssignment = queue.some(q => q.status === 'assigned' && (
167
- q.assignedSessionId === sessionId || q.assignedNodeId === nodeId
168
- ));
169
- if (hasActiveAssignment) return null;
170
- let targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetSessionId === sessionId);
171
- if (targetIdx === -1) {
172
- targetIdx = queue.findIndex(q => q.status === 'pending' && q.targetNodeId === nodeId && !q.targetSessionId);
173
- }
174
- if (targetIdx === -1) {
175
- targetIdx = queue.findIndex(q => q.status === 'pending' && !q.targetNodeId && !q.targetSessionId);
176
- }
177
- if (targetIdx === -1) return null;
178
- const entry = queue[targetIdx];
179
- entry.status = 'assigned';
180
- entry.assignedNodeId = nodeId;
181
- entry.assignedSessionId = sessionId;
182
- entry.dispatchTimestamp = new Date().toISOString();
183
- entry.updatedAt = new Date().toISOString();
184
- writeQueue(meshId, queue);
185
- return entry;
186
- });
157
+ return BeadsDB.getInstance().claimNextQueueTask(meshId, nodeId, sessionId);
187
158
  }
188
159
 
189
160
  /**
@@ -198,13 +169,11 @@ export function updateTaskStatus(
198
169
  ): MeshWorkQueueEntry | null {
199
170
  requireMeshHostQueueOwner(opts);
200
171
  return withQueueLock(meshId, () => {
201
- const queue = readQueue(meshId);
202
- const idx = queue.findIndex(q => q.id === taskId);
203
- if (idx === -1) return null;
204
- queue[idx].status = status;
205
- queue[idx].updatedAt = new Date().toISOString();
206
- writeQueue(meshId, queue);
207
- return queue[idx];
172
+ const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
173
+ if (!entry) return null;
174
+ entry.status = status;
175
+ BeadsDB.getInstance().updateQueueEntry(entry);
176
+ return entry;
208
177
  });
209
178
  }
210
179
 
@@ -214,14 +183,12 @@ export function recordTaskAutoLaunch(
214
183
  autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
215
184
  ): MeshWorkQueueEntry | null {
216
185
  return withQueueLock(meshId, () => {
217
- const queue = readQueue(meshId);
218
- const idx = queue.findIndex(q => q.id === taskId);
219
- if (idx === -1) return null;
186
+ const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
187
+ if (!entry) return null;
220
188
  const now = new Date().toISOString();
221
- queue[idx].autoLaunch = { ...autoLaunch, updatedAt: now };
222
- queue[idx].updatedAt = now;
223
- writeQueue(meshId, queue);
224
- return queue[idx];
189
+ entry.autoLaunch = { ...autoLaunch, updatedAt: now };
190
+ BeadsDB.getInstance().updateQueueEntry(entry);
191
+ return entry;
225
192
  });
226
193
  }
227
194
 
@@ -235,16 +202,14 @@ export function cancelTask(
235
202
  ): MeshWorkQueueEntry | null {
236
203
  requireMeshHostQueueOwner(opts);
237
204
  return withQueueLock(meshId, () => {
238
- const queue = readQueue(meshId);
239
- const idx = queue.findIndex(q => q.id === taskId);
240
- if (idx === -1) return null;
205
+ const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
206
+ if (!entry) return null;
241
207
  const now = new Date().toISOString();
242
- queue[idx].status = 'cancelled';
243
- queue[idx].updatedAt = now;
244
- queue[idx].cancelledAt = now;
245
- if (opts?.reason) queue[idx].cancelReason = opts.reason;
246
- writeQueue(meshId, queue);
247
- return queue[idx];
208
+ entry.status = 'cancelled';
209
+ entry.cancelledAt = now;
210
+ if (opts?.reason) entry.cancelReason = opts.reason;
211
+ BeadsDB.getInstance().updateQueueEntry(entry);
212
+ return entry;
248
213
  });
249
214
  }
250
215
 
@@ -265,11 +230,8 @@ export function requeueTask(
265
230
  ): MeshWorkQueueEntry | null {
266
231
  requireMeshHostQueueOwner(opts);
267
232
  return withQueueLock(meshId, () => {
268
- const queue = readQueue(meshId);
269
- const idx = queue.findIndex(q => q.id === taskId);
270
- if (idx === -1) return null;
271
- const entry = queue[idx];
272
- const now = new Date().toISOString();
233
+ const entry = BeadsDB.getInstance().findQueueEntryById(meshId, taskId);
234
+ if (!entry) return null;
273
235
  entry.status = 'pending';
274
236
  delete entry.assignedNodeId;
275
237
  delete entry.assignedSessionId;
@@ -279,11 +241,10 @@ export function requeueTask(
279
241
  if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
280
242
  if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
281
243
  if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
282
- entry.updatedAt = now;
283
- entry.requeuedAt = now;
244
+ entry.requeuedAt = new Date().toISOString();
284
245
  entry.requeueCount = (entry.requeueCount || 0) + 1;
285
246
  if (opts?.reason) entry.requeueReason = opts.reason;
286
- writeQueue(meshId, queue);
247
+ BeadsDB.getInstance().updateQueueEntry(entry);
287
248
  return entry;
288
249
  });
289
250
  }
@@ -298,22 +259,12 @@ export function updateSessionTaskStatus(
298
259
  opts?: { occurredAt?: string },
299
260
  ): MeshWorkQueueEntry | null {
300
261
  return withQueueLock(meshId, () => {
301
- const queue = readQueue(meshId);
302
- const occurredAtTime = opts?.occurredAt ? new Date(opts.occurredAt).getTime() : Number.NaN;
303
- const hasOccurredAt = Number.isFinite(occurredAtTime);
304
- let bestIdx = -1;
305
- let bestTime = 0;
306
- for (let i = queue.length - 1; i >= 0; i--) {
307
- if (queue[i].assignedSessionId !== sessionId || queue[i].status !== 'assigned') continue;
308
- const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
309
- if (hasOccurredAt && Number.isFinite(time) && time > occurredAtTime) continue;
310
- if (time > bestTime) { bestTime = time; bestIdx = i; }
311
- }
312
- if (bestIdx === -1) return null;
313
- queue[bestIdx].status = status;
314
- queue[bestIdx].updatedAt = new Date().toISOString();
315
- writeQueue(meshId, queue);
316
- return queue[bestIdx];
262
+ const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
263
+ const entry = BeadsDB.getInstance().findAssignedBySession(meshId, sessionId, occurredAtIso);
264
+ if (!entry) return null;
265
+ entry.status = status;
266
+ BeadsDB.getInstance().updateQueueEntry(entry);
267
+ return entry;
317
268
  });
318
269
  }
319
270
 
@@ -342,14 +293,16 @@ export interface MeshWorkQueueStats {
342
293
  * Return aggregate queue statistics for the given mesh.
343
294
  */
344
295
  export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
345
- const queue = readQueue(meshId);
346
- const pending = queue.filter(q => q.status === 'pending').length;
347
- const assigned = queue.filter(q => q.status === 'assigned').length;
348
- const completed = queue.filter(q => q.status === 'completed').length;
349
- const failed = queue.filter(q => q.status === 'failed').length;
350
- const cancelled = queue.filter(q => q.status === 'cancelled').length;
296
+ const rows = BeadsDB.getInstance().getQueueStatsByStatus(meshId);
297
+ const counts: Record<string, number> = {};
298
+ for (const r of rows) counts[r.status] = r.count;
299
+ const pending = counts['pending'] ?? 0;
300
+ const assigned = counts['assigned'] ?? 0;
301
+ const completed = counts['completed'] ?? 0;
302
+ const failed = counts['failed'] ?? 0;
303
+ const cancelled = counts['cancelled'] ?? 0;
351
304
  return {
352
- total: queue.length,
305
+ total: pending + assigned + completed + failed + cancelled,
353
306
  active: pending + assigned,
354
307
  historical: completed + failed + cancelled,
355
308
  pending,
@@ -357,23 +310,9 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
357
310
  completed,
358
311
  failed,
359
312
  cancelled,
360
- activeCounts: {
361
- pending,
362
- assigned,
363
- },
364
- historicalCounts: {
365
- completed,
366
- failed,
367
- cancelled,
368
- },
369
- activeAssignments: queue
370
- .filter(q => q.status === 'assigned')
371
- .map(q => ({
372
- id: q.id,
373
- nodeId: q.assignedNodeId,
374
- sessionId: q.assignedSessionId,
375
- message: q.message,
376
- })),
313
+ activeCounts: { pending, assigned },
314
+ historicalCounts: { completed, failed, cancelled },
315
+ activeAssignments: BeadsDB.getInstance().getActiveAssignmentDetails(meshId),
377
316
  };
378
317
  }
379
318
 
@@ -390,3 +329,60 @@ export function __clearMeshQueueForTests(meshId: string): void {
390
329
  export function __resetBeadsDBForTests(): void {
391
330
  BeadsDB.resetForTests();
392
331
  }
332
+
333
+ // ── Direct Dispatch Tracking ─────────────────────────────────────────────────
334
+ // Persists direct (non-queue) task dispatches so buildMeshActiveWork can read
335
+ // active work from BeadsDB instead of scanning ledger JSONL entries.
336
+
337
+ export type DirectDispatchRecord = ReturnType<BeadsDB['getActiveDirectDispatches']>[number];
338
+
339
+ export function insertDirectDispatch(
340
+ meshId: string,
341
+ data: {
342
+ taskId: string;
343
+ nodeId?: string;
344
+ sessionId?: string;
345
+ providerType?: string;
346
+ message: string;
347
+ taskMode?: string;
348
+ via: string;
349
+ dispatchedToIdleSession?: boolean;
350
+ dispatchedAt: string;
351
+ },
352
+ ): void {
353
+ try {
354
+ BeadsDB.getInstance().insertDirectDispatch({ ...data, meshId });
355
+ } catch (e: any) {
356
+ process.stderr.write(`[adhdev-mesh] insertDirectDispatch failed for task ${data.taskId}: ${e?.message || e}\n`);
357
+ }
358
+ }
359
+
360
+ export function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[] {
361
+ try {
362
+ return BeadsDB.getInstance().getActiveDirectDispatches(meshId);
363
+ } catch {
364
+ return [];
365
+ }
366
+ }
367
+
368
+ export function updateDirectDispatchStatus(
369
+ meshId: string,
370
+ sessionId: string,
371
+ status: 'acked' | 'completed' | 'failed' | 'stale',
372
+ ): void {
373
+ try {
374
+ BeadsDB.getInstance().updateDirectDispatchStatus(meshId, sessionId, status);
375
+ } catch { /* best-effort */ }
376
+ }
377
+
378
+ export function cleanupTerminalDirectDispatches(olderThanMs = 7 * 24 * 60 * 60_000): void {
379
+ try {
380
+ BeadsDB.getInstance().cleanupTerminalDirectDispatches(olderThanMs);
381
+ } catch { /* best-effort */ }
382
+ }
383
+
384
+ export function markStaleDirectDispatches(meshId: string, olderThanMs = 60 * 60_000): void {
385
+ try {
386
+ BeadsDB.getInstance().markStaleDirectDispatches(meshId, olderThanMs);
387
+ } catch { /* best-effort */ }
388
+ }
@@ -9,6 +9,7 @@ import {
9
9
  type MeshRefineValidationCommandPlan,
10
10
  type RepoMeshRefineValidationCommandConfig,
11
11
  } from './refine-config.js';
12
+ import type { MeshAsyncJobLifecycle } from '../repo-mesh-types.js';
12
13
 
13
14
  export type WorktreeBootstrapStatus = 'ready' | 'running' | 'failed' | 'not_configured' | 'disabled' | 'stale';
14
15
 
@@ -21,16 +22,13 @@ export interface RepoMeshWorktreeBootstrapConfig {
21
22
  staleInputs?: string[];
22
23
  }
23
24
 
24
- export interface WorktreeBootstrapState {
25
+ export interface WorktreeBootstrapState extends MeshAsyncJobLifecycle {
25
26
  status: WorktreeBootstrapStatus;
26
27
  required: boolean;
27
28
  configSource?: string;
28
29
  configSourceType?: 'repo_file' | 'mesh_policy' | 'unavailable' | 'invalid';
29
- startedAt?: string;
30
- completedAt?: string;
31
30
  lastCommand?: string;
32
31
  exitCode?: number | null;
33
- error?: string;
34
32
  commandsRun?: Array<Record<string, unknown>>;
35
33
  staleInputs?: string[];
36
34
  }
@@ -177,7 +175,22 @@ export async function runMeshWorktreeBootstrap(mesh: any, workspace: string): Pr
177
175
  commandsRun: [],
178
176
  staleInputs: loaded.config.staleInputs,
179
177
  };
178
+ const staleInputPaths = loaded.config.staleInputs ?? [];
179
+ // Snapshot which staleInputs files are absent at bootstrap start.
180
+ // Files already present before we start are not interference signals.
181
+ const initiallyAbsent = staleInputPaths.filter(p => !existsSync(join(workspace, p)));
180
182
  for (const command of validation.commands) {
183
+ // Check if any initially-absent staleInputs files appeared since bootstrap started
184
+ // (indicates another process modified the environment mid-run).
185
+ if (initiallyAbsent.length > 0) {
186
+ const appearedNow = initiallyAbsent.filter(p => existsSync(join(workspace, p)));
187
+ if (appearedNow.length > 0) {
188
+ state.status = 'stale';
189
+ state.completedAt = new Date().toISOString();
190
+ state.error = `Bootstrap interrupted: staleInputs files appeared during run: ${appearedNow.join(', ')}`;
191
+ return state;
192
+ }
193
+ }
181
194
  const cwd = command.cwd ? pathResolve(workspace, command.cwd) : workspace;
182
195
  const startedAt = Date.now();
183
196
  state.lastCommand = command.displayCommand;
@@ -72,3 +72,30 @@ export function formatAutoApprovalMessage(modalMessage?: string, buttonLabel?: s
72
72
  if (cleanMessage) lines.push(cleanMessage);
73
73
  return lines.join('\n');
74
74
  }
75
+
76
+ /**
77
+ * Returns true when the given text (e.g. last assistant message content, or
78
+ * the tail of the PTY screen) looks like an active approval/input prompt
79
+ * rather than a completed assistant response. Used to prevent false
80
+ * idle/completion events when Claude Code surfaces a "Do you want to proceed?"
81
+ * style prompt that the PTY parser captures as an assistant turn while the
82
+ * session is still awaiting user input.
83
+ */
84
+ export function looksLikeActiveApprovalPromptText(content: string): boolean {
85
+ const text = content.trim();
86
+ if (!text || text.length > 2000) return false;
87
+ const hasApprovalQuestion = /do you want to (?:proceed|allow|run|make this edit|create)/i.test(text)
88
+ || /this command requires approval/i.test(text)
89
+ || /quick safety check/i.test(text)
90
+ || /is this a project you trust/i.test(text);
91
+ const hasNumberedChoices = /^\s*[❯›>]?\s*1[.)]\s+(?:yes|allow|proceed|run)/im.test(text)
92
+ || /^\s*1[.)]\s+yes\b/im.test(text);
93
+ if (hasApprovalQuestion && hasNumberedChoices) return true;
94
+ const lastLines = text.split(/\r?\n/).slice(-12).join('\n');
95
+ const hasDontAskAgain = /yes.*don'?t ask again/i.test(lastLines)
96
+ || /yes.*always allow/i.test(lastLines);
97
+ const hasNoOption = /^\s*[❯›>]?\s*\d+[.)]\s+no\b/im.test(lastLines);
98
+ if (hasDontAskAgain && hasNoOption) return true;
99
+ if (/what do you want to do\?/i.test(text) && /^\s*\d+[.)]\s+\S/m.test(text)) return true;
100
+ return false;
101
+ }
@@ -21,7 +21,7 @@ import { ChatHistoryWriter, isNativeSourceCanonicalHistory, materializeProviderN
21
21
  import { LOG } from '../logging/logger.js';
22
22
  import type { ChatMessage } from '../types.js';
23
23
  import { buildPersistedProviderEffectMessage, normalizeProviderEffects } from './control-effects.js';
24
- import { formatAutoApprovalMessage, pickApprovalButton } from './approval-utils.js';
24
+ import { formatAutoApprovalMessage, pickApprovalButton, looksLikeActiveApprovalPromptText } from './approval-utils.js';
25
25
  import { getCliScriptCommand, parseCliScriptResult } from './cli-script-results.js';
26
26
  import { mergeProviderPatchState, resolveProviderStateSurface } from './provider-patch-state.js';
27
27
  import { normalizeProviderSessionId } from './provider-session-id.js';
@@ -48,6 +48,7 @@ function isIdleStatus(value: unknown): boolean {
48
48
  return !status || status === 'idle' || status === 'ready';
49
49
  }
50
50
 
51
+
51
52
  function getMessageTime(message: unknown): number {
52
53
  if (!message || typeof message !== 'object') return 0;
53
54
  const record = message as { receivedAt?: unknown; timestamp?: unknown };
@@ -389,6 +390,9 @@ export class CliProviderInstance implements ProviderInstance {
389
390
  this.launchMode = options?.launchMode || 'new';
390
391
  this.onProviderSessionResolved = options?.onProviderSessionResolved;
391
392
  this.adapter = new ProviderCliAdapter(provider as CliProviderModule, workingDir, cliArgs, options?.extraEnv || {}, transportFactory);
393
+ if (this.providerSessionId) {
394
+ this.adapter.updateRuntimeMeta({ providerSessionId: this.providerSessionId });
395
+ }
392
396
  this.monitor = new StatusMonitor();
393
397
  this.historyWriter = new ChatHistoryWriter();
394
398
  }
@@ -825,7 +829,11 @@ export class CliProviderInstance implements ProviderInstance {
825
829
  const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
826
830
  const role = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : '';
827
831
  const content = lastVisible ? flattenContent(lastVisible.content).trim() : '';
828
- return role === 'assistant' && !!content;
832
+ if (role !== 'assistant' || !content) return false;
833
+ // Guard: if the last assistant message looks like an active approval/input prompt,
834
+ // it is not a real completion — the session is still awaiting user input.
835
+ if (looksLikeActiveApprovalPromptText(content)) return false;
836
+ return true;
829
837
  }
830
838
 
831
839
  private buildCompletedFinalizationDiagnostic(args: {
@@ -940,6 +948,22 @@ export class CliProviderInstance implements ProviderInstance {
940
948
  if (parsed?.activeModal || parsed?.modal) return { reason: 'parsed_modal_active', terminal: true };
941
949
  if (!this.completionHasFinalAssistantMessage(parsed?.messages)) return { reason: 'missing_final_assistant' };
942
950
 
951
+ // Guard: if the screen still shows an approval/choice prompt as the last visible text,
952
+ // the turn is not complete even if the parsed status says idle and there is an assistant
953
+ // message. This catches the case where waiting_approval→idle transitions occur before
954
+ // the modal has been resolved (e.g. the PTY rendered the prompt but no button press fired).
955
+ try {
956
+ const screenText = typeof (this.adapter as any).getScreenText === 'function'
957
+ ? String((this.adapter as any).getScreenText() || '')
958
+ : '';
959
+ if (screenText) {
960
+ const tailLines = screenText.split(/\r?\n/).slice(-16).join('\n');
961
+ if (looksLikeActiveApprovalPromptText(tailLines)) {
962
+ return { reason: 'screen_shows_approval_prompt', terminal: false };
963
+ }
964
+ }
965
+ } catch { /* defensive: screen text read is best-effort */ }
966
+
943
967
  return null;
944
968
  }
945
969
 
@@ -64,6 +64,8 @@ const KNOWN_PROVIDER_FIELDS = new Set<string>([
64
64
  'sendKey',
65
65
  'submitStrategy',
66
66
  'requirePromptEchoBeforeSubmit',
67
+ 'requiresFinalAssistantBeforeIdle',
68
+ 'augmentStaleSnapshot',
67
69
  'timeouts',
68
70
  'disableUpstream',
69
71
  ])
@@ -472,3 +472,13 @@ export interface RepoMeshLedgerStatus {
472
472
  entries: RepoMeshLedgerEntryStatus[];
473
473
  summary: RepoMeshLedgerSummaryStatus;
474
474
  }
475
+
476
+ // ─── Async Job Lifecycle ─────────────────────────
477
+ // Shared base for all mesh async job types (refine jobs, bootstrap runs, etc.)
478
+ // Each concrete type adds its own status enum and domain-specific fields.
479
+
480
+ export interface MeshAsyncJobLifecycle {
481
+ startedAt?: string;
482
+ completedAt?: string;
483
+ error?: string;
484
+ }