@adhdev/daemon-core 0.9.82-rc.136 → 0.9.82-rc.138
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/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/cli-adapters/cli-script-runner.d.ts +45 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +169 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +72 -74
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +5 -0
- package/dist/config/chat-history.d.ts +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +3507 -2288
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3515 -2301
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/beads-db.d.ts +54 -0
- package/dist/mesh/contracts.d.ts +164 -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/contracts.d.ts +19 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/repo-mesh-types.d.ts +5 -0
- package/dist/shared-types.d.ts +7 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +1 -1
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +9 -0
- package/src/cli-adapters/cli-script-runner.ts +145 -0
- package/src/cli-adapters/cli-state-engine.ts +1054 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +0 -1
- package/src/cli-adapters/provider-cli-adapter.ts +413 -1399
- package/src/cli-adapters/provider-cli-parse.ts +3 -0
- package/src/cli-adapters/provider-cli-shared.ts +17 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +715 -368
- package/src/commands/router.ts +22 -2
- package/src/config/chat-history.ts +43 -16
- 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/contracts.ts +329 -0
- 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/contracts.ts +19 -0
- package/src/providers/provider-loader.ts +21 -7
- package/src/providers/provider-schema.ts +12 -0
- package/src/providers/read-chat-contract.ts +74 -14
- package/src/providers/transcript-v2.ts +567 -0
- package/src/repo-mesh-types.ts +10 -0
- package/src/shared-types.ts +7 -0
- package/src/status/snapshot.ts +35 -11
- package/src/types.ts +5 -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;
|
|
@@ -15,6 +15,14 @@ import type { ChatMessageKind } from './chat-message-normalization.js';
|
|
|
15
15
|
export type ReadChatTurnStatus = 'open' | 'waiting_approval' | 'complete' | 'error';
|
|
16
16
|
|
|
17
17
|
export interface ReadChatResult {
|
|
18
|
+
/**
|
|
19
|
+
* Declared chat contract version. Absent or `'1.0'` → legacy v1 payload
|
|
20
|
+
* (current shape). `'2.0'` → v2 payload conforming to transcript-v2.ts
|
|
21
|
+
* (ReadChatResultV2). Validators in read-chat-contract.ts route on this
|
|
22
|
+
* field. A1 only adds the field; A2 will make v2 the daemon-internal
|
|
23
|
+
* canonical form and reject unrecognised versions at provider load time.
|
|
24
|
+
*/
|
|
25
|
+
contractVersion?: import('./transcript-v2.js').ChatContractVersion;
|
|
18
26
|
messages: ChatMessage[];
|
|
19
27
|
status: AgentStatus;
|
|
20
28
|
activeModal?: ModalInfo | null;
|
|
@@ -702,6 +710,17 @@ export interface ProviderCanonicalHistoryConfig {
|
|
|
702
710
|
* Omitted mode defaults to 'native-source'.
|
|
703
711
|
*/
|
|
704
712
|
mode?: 'native-source' | 'materialized-mirror' | 'disabled';
|
|
713
|
+
/**
|
|
714
|
+
* Chat transcript contract version this provider's read_chat output
|
|
715
|
+
* conforms to. See transcript-v2.ts for the v2 invariants. Absent or `'1.0'`
|
|
716
|
+
* → legacy v1 payload (current behaviour). `'2.0'` → strict v2 payload
|
|
717
|
+
* (stable providerUnitKey/bubbleId/sequence, strict enums, honest coverage).
|
|
718
|
+
*
|
|
719
|
+
* A1 only surfaces this field; validators in read-chat-contract.ts route
|
|
720
|
+
* on it. A2 makes v2 the daemon-internal canonical form and rejects
|
|
721
|
+
* unrecognised values at provider load time.
|
|
722
|
+
*/
|
|
723
|
+
contractVersion?: import('./transcript-v2.js').ChatContractVersion;
|
|
705
724
|
}
|
|
706
725
|
|
|
707
726
|
/**
|
|
@@ -1378,11 +1378,15 @@ export class ProviderLoader {
|
|
|
1378
1378
|
return args ? args.map((arg) => /\s/.test(arg) ? JSON.stringify(arg) : arg).join(' ') : '';
|
|
1379
1379
|
}
|
|
1380
1380
|
const schemaDef = this.getSettingsSchema(providerType)[key];
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1381
|
+
// (fix) Previously this hard-coded `autoApprove` boolean default to `true`,
|
|
1382
|
+
// overriding whatever schemaDef.default the provider.json declared. That
|
|
1383
|
+
// surfaced as soon as a provider added an `autoApprove` schema entry with
|
|
1384
|
+
// default=false: the user had never opted in but the daemon treated the
|
|
1385
|
+
// session as auto-approve, which then triggered recordAutoApproval every
|
|
1386
|
+
// time the CLI showed an approval modal — producing a flood of system
|
|
1387
|
+
// "Auto-approved: ..." messages and keeping the session pinned to
|
|
1388
|
+
// generating while modals cycled in and out. Trust the schemaDef.default.
|
|
1389
|
+
const defaultVal = schemaDef ? schemaDef.default : undefined;
|
|
1386
1390
|
|
|
1387
1391
|
const config = this.readConfig();
|
|
1388
1392
|
const userVal = config?.providerSettings?.[providerType]?.[key];
|
|
@@ -1496,10 +1500,17 @@ export class ProviderLoader {
|
|
|
1496
1500
|
...this.getSyntheticSettings(type, provider),
|
|
1497
1501
|
...(provider.settings || {}),
|
|
1498
1502
|
};
|
|
1503
|
+
// (fix) Previously this clause forced `autoApprove.default = true` for any
|
|
1504
|
+
// boolean autoApprove schema, even when the provider.json explicitly set
|
|
1505
|
+
// `default: false`. Combined with the synthetic-settings fallback at
|
|
1506
|
+
// getSyntheticSettings (which also defaults autoApprove to true when the
|
|
1507
|
+
// provider doesn't supply one), that meant CLI providers silently turned on
|
|
1508
|
+
// auto-approval, producing a flood of "Auto-approved: ..." system messages
|
|
1509
|
+
// every time an approval modal appeared and pinning the session to
|
|
1510
|
+
// generating while modals cycled. Trust the provider's declared default.
|
|
1499
1511
|
if (result.autoApprove?.type === 'boolean') {
|
|
1500
1512
|
result.autoApprove = {
|
|
1501
1513
|
...result.autoApprove,
|
|
1502
|
-
default: true,
|
|
1503
1514
|
public: true,
|
|
1504
1515
|
label: result.autoApprove.label || 'Auto Approve',
|
|
1505
1516
|
description: result.autoApprove.description || 'Automatically approve actionable prompts without sending approval alerts.',
|
|
@@ -1524,7 +1535,10 @@ export class ProviderLoader {
|
|
|
1524
1535
|
if (!provider.settings?.autoApprove) {
|
|
1525
1536
|
result.autoApprove = {
|
|
1526
1537
|
type: 'boolean',
|
|
1527
|
-
default
|
|
1538
|
+
// (fix) Safe default is *off*. Auto-approving every modal without the
|
|
1539
|
+
// user opting in produced silent-bash-execution surprises and the
|
|
1540
|
+
// "Auto-approved: ..." system-message flood seen on AGY/Codex.
|
|
1541
|
+
default: false,
|
|
1528
1542
|
public: true,
|
|
1529
1543
|
label: 'Auto Approve',
|
|
1530
1544
|
description: 'Automatically approve actionable prompts without sending approval alerts.',
|
|
@@ -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
|
])
|
|
@@ -251,6 +253,16 @@ function validateCanonicalHistory(raw: unknown, errors: string[]): void {
|
|
|
251
253
|
errors.push('canonicalHistory.mode must be one of: native-source, materialized-mirror, disabled')
|
|
252
254
|
}
|
|
253
255
|
|
|
256
|
+
// Chat transcript contract version (transcript-v2.ts). Absent → treated as
|
|
257
|
+
// v1 by readDeclaredChatContractVersion(). A2 will reject unrecognised
|
|
258
|
+
// values at load time; A1 only validates the field shape.
|
|
259
|
+
const chatContractVersion = canonicalHistory.contractVersion
|
|
260
|
+
if (chatContractVersion !== undefined
|
|
261
|
+
&& chatContractVersion !== '1.0'
|
|
262
|
+
&& chatContractVersion !== '2.0') {
|
|
263
|
+
errors.push(`canonicalHistory.contractVersion must be '1.0' or '2.0' when provided (got ${JSON.stringify(chatContractVersion)})`)
|
|
264
|
+
}
|
|
265
|
+
|
|
254
266
|
const scripts = canonicalHistory.scripts
|
|
255
267
|
if (scripts === undefined) return
|
|
256
268
|
if (!scripts || typeof scripts !== 'object' || Array.isArray(scripts)) {
|
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { MessagePart, ModalInfo, ReadChatResult } from './contracts.js'
|
|
2
2
|
import { normalizeMessageParts } from './contracts.js'
|
|
3
3
|
import type { ChatBubbleState, ChatMessage } from '../types.js'
|
|
4
|
+
import {
|
|
5
|
+
CHAT_CONTRACT_VERSION_V1,
|
|
6
|
+
CHAT_CONTRACT_VERSION_V2,
|
|
7
|
+
assertReadChatResultV2Payload,
|
|
8
|
+
isSupportedChatContractVersion,
|
|
9
|
+
type ChatContractVersion,
|
|
10
|
+
type ReadChatResultV2,
|
|
11
|
+
} from './transcript-v2.js'
|
|
4
12
|
|
|
5
13
|
const VALID_STATUSES = ['idle', 'generating', 'waiting_approval', 'error', 'panel_hidden', 'starting', 'streaming', 'long_generating'] as const
|
|
6
14
|
const VALID_ROLES = ['user', 'assistant', 'system', 'human'] as const
|
|
@@ -63,7 +71,7 @@ function validateMessage(message: unknown, source: string, index: number): ChatM
|
|
|
63
71
|
content: validateMessageContent(message.content, source, index),
|
|
64
72
|
}
|
|
65
73
|
|
|
66
|
-
if (typeof message.kind === 'string') normalized.kind = message.kind as
|
|
74
|
+
if (typeof message.kind === 'string') normalized.kind = message.kind as ChatMessage['kind']
|
|
67
75
|
if (typeof message.id === 'string') normalized.id = message.id
|
|
68
76
|
if (typeof message.bubbleId === 'string') normalized.bubbleId = message.bubbleId
|
|
69
77
|
if (typeof message.providerUnitKey === 'string') normalized.providerUnitKey = message.providerUnitKey
|
|
@@ -71,20 +79,23 @@ function validateMessage(message: unknown, source: string, index: number): ChatM
|
|
|
71
79
|
if (isFiniteNumber(message.index)) normalized.index = message.index
|
|
72
80
|
if (isFiniteNumber(message.timestamp)) normalized.timestamp = message.timestamp
|
|
73
81
|
if (isFiniteNumber(message.receivedAt)) normalized.receivedAt = message.receivedAt
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
if (
|
|
82
|
+
// (A2.3) sequence is the monotonic ordering key consumed by ChatSourceMachine.
|
|
83
|
+
// v1 producers omit it; the daemon derives it in normalizeNativeHistoryMessages.
|
|
84
|
+
if (isFiniteNumber(message.sequence)) normalized.sequence = message.sequence
|
|
85
|
+
if (typeof message._turnKey === 'string') normalized._turnKey = message._turnKey
|
|
86
|
+
if (Array.isArray(message.toolCalls)) normalized.toolCalls = message.toolCalls as ChatMessage['toolCalls']
|
|
87
|
+
if (isPlainObject(message.meta)) normalized.meta = message.meta as ChatMessage['meta']
|
|
77
88
|
if (typeof message.senderName === 'string') normalized.senderName = message.senderName
|
|
78
|
-
if (typeof
|
|
79
|
-
if (typeof
|
|
80
|
-
if (typeof
|
|
81
|
-
if (typeof
|
|
82
|
-
if (typeof
|
|
83
|
-
if (typeof
|
|
84
|
-
if (typeof
|
|
85
|
-
if (typeof
|
|
86
|
-
if (typeof
|
|
87
|
-
if (typeof
|
|
89
|
+
if (typeof message._type === 'string') normalized._type = message._type
|
|
90
|
+
if (typeof message._sub === 'string') normalized._sub = message._sub
|
|
91
|
+
if (typeof message.visibility === 'string') normalized.visibility = message.visibility
|
|
92
|
+
if (typeof message.transcriptVisibility === 'string') normalized.transcriptVisibility = message.transcriptVisibility
|
|
93
|
+
if (typeof message.audience === 'string') normalized.audience = message.audience
|
|
94
|
+
if (typeof message.source === 'string') normalized.source = message.source
|
|
95
|
+
if (typeof message.userFacing === 'boolean') normalized.userFacing = message.userFacing
|
|
96
|
+
if (typeof message.internal === 'boolean') normalized.internal = message.internal
|
|
97
|
+
if (typeof message.isInternal === 'boolean') normalized.isInternal = message.isInternal
|
|
98
|
+
if (typeof message.debug === 'boolean') normalized.debug = message.debug
|
|
88
99
|
|
|
89
100
|
return normalized
|
|
90
101
|
}
|
|
@@ -129,6 +140,55 @@ function validateControlValues(controlValues: unknown, source: string): Record<s
|
|
|
129
140
|
return normalized
|
|
130
141
|
}
|
|
131
142
|
|
|
143
|
+
/**
|
|
144
|
+
* Read the producer-declared contract version from a raw read_chat payload.
|
|
145
|
+
* Returns v1 when absent or unrecognised, so legacy producers keep working
|
|
146
|
+
* through A1. A2 will tighten this to throw when an unsupported version is
|
|
147
|
+
* declared.
|
|
148
|
+
*/
|
|
149
|
+
export function readPayloadContractVersion(raw: unknown): ChatContractVersion {
|
|
150
|
+
if (!isPlainObject(raw)) return CHAT_CONTRACT_VERSION_V1
|
|
151
|
+
const declared = (raw as Record<string, unknown>).contractVersion
|
|
152
|
+
if (isSupportedChatContractVersion(declared)) return declared
|
|
153
|
+
return CHAT_CONTRACT_VERSION_V1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Validate a v2 payload. Thin wrapper around assertReadChatResultV2Payload
|
|
158
|
+
* that prefixes the contract violation with the caller's source label.
|
|
159
|
+
*/
|
|
160
|
+
export function validateReadChatResultV2Payload(raw: unknown, source = 'read_chat'): ReadChatResultV2 {
|
|
161
|
+
try {
|
|
162
|
+
return assertReadChatResultV2Payload(raw)
|
|
163
|
+
} catch (err) {
|
|
164
|
+
if (err instanceof Error) {
|
|
165
|
+
err.message = `${source}: ${err.message}`
|
|
166
|
+
}
|
|
167
|
+
throw err
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Versioned entry point. Routes on the producer-declared contractVersion:
|
|
173
|
+
* - v2 → strict v2 validation (transcript-v2.ts invariants)
|
|
174
|
+
* - v1 (or absent) → legacy permissive validation
|
|
175
|
+
*
|
|
176
|
+
* Callers that have not yet been audited to handle v2 outputs should use
|
|
177
|
+
* validateReadChatResultPayload directly; that path stays bound to v1 shape
|
|
178
|
+
* during the A1 transition.
|
|
179
|
+
*/
|
|
180
|
+
export function validateReadChatResultPayloadVersioned(
|
|
181
|
+
raw: unknown,
|
|
182
|
+
source = 'read_chat',
|
|
183
|
+
): { version: typeof CHAT_CONTRACT_VERSION_V1; payload: ReadChatResult & Record<string, unknown> }
|
|
184
|
+
| { version: typeof CHAT_CONTRACT_VERSION_V2; payload: ReadChatResultV2 } {
|
|
185
|
+
const version = readPayloadContractVersion(raw)
|
|
186
|
+
if (version === CHAT_CONTRACT_VERSION_V2) {
|
|
187
|
+
return { version, payload: validateReadChatResultV2Payload(raw, source) }
|
|
188
|
+
}
|
|
189
|
+
return { version: CHAT_CONTRACT_VERSION_V1, payload: validateReadChatResultPayload(raw, source) }
|
|
190
|
+
}
|
|
191
|
+
|
|
132
192
|
export function validateReadChatResultPayload(raw: unknown, source = 'read_chat'): ReadChatResult & Record<string, unknown> {
|
|
133
193
|
if (!isPlainObject(raw)) {
|
|
134
194
|
throw new Error(`${source}: payload must be an object`)
|