@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.
- package/dist/cli-adapter-types.d.ts +1 -0
- package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +154 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +75 -74
- package/dist/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +6 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2624 -1946
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2627 -1954
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +54 -0
- package/dist/mesh/mesh-active-work.d.ts +7 -1
- package/dist/mesh/mesh-events.d.ts +10 -4
- package/dist/mesh/mesh-ledger.d.ts +21 -1
- package/dist/mesh/mesh-refine-status.d.ts +2 -3
- package/dist/mesh/mesh-work-queue.d.ts +17 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +2 -4
- package/dist/providers/approval-utils.d.ts +9 -0
- package/dist/repo-mesh-types.d.ts +5 -0
- package/package.json +1 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +1 -0
- package/src/cli-adapters/cli-script-runner.ts +145 -0
- package/src/cli-adapters/cli-state-engine.ts +957 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +377 -1387
- package/src/cli-adapters/provider-cli-parse.ts +6 -0
- package/src/cli-adapters/provider-cli-shared.ts +6 -0
- package/src/commands/chat-commands.ts +23 -1
- package/src/commands/cli-manager.ts +3 -1
- package/src/commands/router.ts +8 -0
- package/src/config/chat-history.ts +7 -3
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +3 -2
- package/src/mesh/beads-db.ts +305 -2
- package/src/mesh/coordinator-prompt.ts +12 -17
- package/src/mesh/mesh-active-work.ts +162 -59
- package/src/mesh/mesh-events.ts +198 -53
- package/src/mesh/mesh-ledger.ts +321 -105
- package/src/mesh/mesh-refine-status.ts +2 -3
- package/src/mesh/mesh-work-queue.ts +116 -120
- package/src/mesh/worktree-bootstrap-config.ts +17 -4
- package/src/providers/approval-utils.ts +27 -0
- package/src/providers/cli-provider-instance.ts +26 -2
- package/src/providers/provider-schema.ts +2 -0
- 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
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
218
|
-
|
|
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
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
|
239
|
-
|
|
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
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
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
|
|
269
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
|
346
|
-
const
|
|
347
|
-
const
|
|
348
|
-
const
|
|
349
|
-
const
|
|
350
|
-
const
|
|
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:
|
|
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
|
-
|
|
362
|
-
|
|
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
|
-
|
|
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
|
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -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
|
+
}
|