@adhdev/daemon-core 0.9.82-rc.57 → 0.9.82-rc.59

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.
@@ -63,6 +63,44 @@ export function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind
63
63
  || payload.source === 'mesh_remove_node');
64
64
  }
65
65
 
66
+ export type MeshWorkerResultStatus = 'completed' | 'failed' | 'blocked' | 'partial' | 'unknown';
67
+ export type MeshProcessArtifactKind = 'process' | 'log' | 'port' | 'window' | 'session' | 'file' | 'url' | 'other';
68
+
69
+ export interface MeshValidationResultArtifact {
70
+ command?: string;
71
+ status: 'passed' | 'failed' | 'skipped' | 'unknown';
72
+ durationMs?: number;
73
+ outputPath?: string;
74
+ summary?: string;
75
+ }
76
+
77
+ export interface MeshProcessArtifact {
78
+ kind: MeshProcessArtifactKind;
79
+ id?: string;
80
+ label?: string;
81
+ locator?: string;
82
+ pid?: number;
83
+ port?: number;
84
+ url?: string;
85
+ path?: string;
86
+ sessionId?: string;
87
+ keepRunning?: boolean;
88
+ metadata?: Record<string, unknown>;
89
+ }
90
+
91
+ export interface MeshWorkerResultArtifact {
92
+ status: MeshWorkerResultStatus;
93
+ classification?: string;
94
+ changedFiles: string[];
95
+ validationResults: MeshValidationResultArtifact[];
96
+ gitStatus?: Record<string, unknown>;
97
+ processArtifacts: MeshProcessArtifact[];
98
+ errors: string[];
99
+ nextAction?: string;
100
+ requiresUserAction: boolean;
101
+ source: 'explicit_metadata' | 'final_summary_json' | 'default';
102
+ }
103
+
66
104
  export interface MeshTaskCompletionEvidence {
67
105
  source: 'agent_status_event';
68
106
  event: 'agent:generating_completed' | 'agent:ready';
@@ -76,6 +114,7 @@ export interface MeshTaskCompletionEvidence {
76
114
  providerSessionId?: string;
77
115
  finalSummaryAvailable: boolean;
78
116
  };
117
+ workerResult: MeshWorkerResultArtifact;
79
118
  git: {
80
119
  status: 'deferred';
81
120
  reason: string;
@@ -98,6 +137,7 @@ export interface BuildTaskCompletionEvidenceOptions {
98
137
  providerType?: string;
99
138
  providerSessionId?: string;
100
139
  finalSummary?: string;
140
+ workerResult?: Record<string, unknown>;
101
141
  completedAt?: string;
102
142
  }
103
143
 
@@ -190,6 +230,100 @@ function getRotatedPath(meshId: string, index: number): string {
190
230
 
191
231
  // ─── Core API ───────────────────────────────────
192
232
 
233
+ function readNonEmptyString(value: unknown): string | undefined {
234
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
235
+ }
236
+
237
+ function readStringArray(value: unknown): string[] {
238
+ if (!Array.isArray(value)) return [];
239
+ return value.map(item => readNonEmptyString(item)).filter(Boolean) as string[];
240
+ }
241
+
242
+ function extractJsonObjectFromSummary(summary?: string): Record<string, unknown> | undefined {
243
+ const text = readNonEmptyString(summary);
244
+ if (!text) return undefined;
245
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
246
+ const candidates = [fenced?.[1], text].filter(Boolean) as string[];
247
+ for (const candidate of candidates) {
248
+ const trimmed = candidate.trim();
249
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
250
+ try {
251
+ const parsed = JSON.parse(trimmed);
252
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed;
253
+ } catch { /* try next candidate */ }
254
+ }
255
+ return undefined;
256
+ }
257
+
258
+ function normalizeValidationResults(value: unknown): MeshValidationResultArtifact[] {
259
+ if (!Array.isArray(value)) return [];
260
+ return value
261
+ .filter(item => item && typeof item === 'object' && !Array.isArray(item))
262
+ .map((item: any) => {
263
+ const status = ['passed', 'failed', 'skipped', 'unknown'].includes(item.status) ? item.status : 'unknown';
264
+ return {
265
+ ...(readNonEmptyString(item.command) ? { command: readNonEmptyString(item.command) } : {}),
266
+ status,
267
+ ...(Number.isFinite(Number(item.durationMs)) ? { durationMs: Number(item.durationMs) } : {}),
268
+ ...(readNonEmptyString(item.outputPath) ? { outputPath: readNonEmptyString(item.outputPath) } : {}),
269
+ ...(readNonEmptyString(item.summary) ? { summary: readNonEmptyString(item.summary) } : {}),
270
+ };
271
+ });
272
+ }
273
+
274
+ function normalizeProcessArtifacts(value: unknown): MeshProcessArtifact[] {
275
+ if (!Array.isArray(value)) return [];
276
+ const kinds = new Set(['process', 'log', 'port', 'window', 'session', 'file', 'url', 'other']);
277
+ return value
278
+ .filter(item => item && typeof item === 'object' && !Array.isArray(item))
279
+ .map((item: any) => ({
280
+ kind: kinds.has(item.kind) ? item.kind : 'other',
281
+ ...(readNonEmptyString(item.id) ? { id: readNonEmptyString(item.id) } : {}),
282
+ ...(readNonEmptyString(item.label) ? { label: readNonEmptyString(item.label) } : {}),
283
+ ...(readNonEmptyString(item.locator) ? { locator: readNonEmptyString(item.locator) } : {}),
284
+ ...(Number.isFinite(Number(item.pid)) ? { pid: Number(item.pid) } : {}),
285
+ ...(Number.isFinite(Number(item.port)) ? { port: Number(item.port) } : {}),
286
+ ...(readNonEmptyString(item.url) ? { url: readNonEmptyString(item.url) } : {}),
287
+ ...(readNonEmptyString(item.path) ? { path: readNonEmptyString(item.path) } : {}),
288
+ ...(readNonEmptyString(item.sessionId) ? { sessionId: readNonEmptyString(item.sessionId) } : {}),
289
+ ...(typeof item.keepRunning === 'boolean' ? { keepRunning: item.keepRunning } : {}),
290
+ ...(item.metadata && typeof item.metadata === 'object' && !Array.isArray(item.metadata) ? { metadata: item.metadata as Record<string, unknown> } : {}),
291
+ }));
292
+ }
293
+
294
+ export function normalizeMeshWorkerResult(input?: Record<string, unknown>, source: MeshWorkerResultArtifact['source'] = 'explicit_metadata'): MeshWorkerResultArtifact {
295
+ const raw = input && typeof input === 'object' ? input : {};
296
+ const status = ['completed', 'failed', 'blocked', 'partial', 'unknown'].includes(String(raw.status))
297
+ ? raw.status as MeshWorkerResultStatus
298
+ : 'unknown';
299
+ const gitStatus = raw.gitStatus && typeof raw.gitStatus === 'object' && !Array.isArray(raw.gitStatus)
300
+ ? raw.gitStatus as Record<string, unknown>
301
+ : undefined;
302
+ return {
303
+ status,
304
+ ...(readNonEmptyString(raw.classification) ? { classification: readNonEmptyString(raw.classification) } : {}),
305
+ changedFiles: readStringArray(raw.changedFiles),
306
+ validationResults: normalizeValidationResults(raw.validationResults),
307
+ ...(gitStatus ? { gitStatus } : {}),
308
+ processArtifacts: normalizeProcessArtifacts(raw.processArtifacts),
309
+ errors: readStringArray(raw.errors),
310
+ ...(readNonEmptyString(raw.nextAction) ? { nextAction: readNonEmptyString(raw.nextAction) } : {}),
311
+ requiresUserAction: raw.requiresUserAction === true,
312
+ source,
313
+ };
314
+ }
315
+
316
+ function resolveWorkerResult(opts: BuildTaskCompletionEvidenceOptions): MeshWorkerResultArtifact {
317
+ if (opts.workerResult && typeof opts.workerResult === 'object') {
318
+ return normalizeMeshWorkerResult(opts.workerResult, 'explicit_metadata');
319
+ }
320
+ const parsed = extractJsonObjectFromSummary(opts.finalSummary);
321
+ if (parsed) {
322
+ return normalizeMeshWorkerResult(parsed, 'final_summary_json');
323
+ }
324
+ return normalizeMeshWorkerResult(undefined, 'default');
325
+ }
326
+
193
327
  export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOptions): MeshTaskCompletionEvidence {
194
328
  const providerSessionId = opts.providerSessionId?.trim() || undefined;
195
329
  const providerType = opts.providerType?.trim() || undefined;
@@ -206,6 +340,7 @@ export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOpt
206
340
  providerSessionId,
207
341
  finalSummaryAvailable: typeof opts.finalSummary === 'string' && opts.finalSummary.trim().length > 0,
208
342
  },
343
+ workerResult: resolveWorkerResult(opts),
209
344
  git: {
210
345
  status: 'deferred',
211
346
  reason: 'ordinary_completion_git_status_not_checked',
@@ -8,15 +8,63 @@ import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
8
8
  export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
9
9
  export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
10
10
  export type MeshHistoricalTaskStatus = Extract<MeshTaskStatus, 'completed' | 'failed' | 'cancelled'>;
11
+ export type MeshTaskMode = 'code_change' | 'validation' | 'live_debug_readonly' | 'launch_app' | 'convergence';
11
12
 
12
13
  export const ACTIVE_MESH_QUEUE_STATUSES: MeshActiveTaskStatus[] = ['pending', 'assigned'];
13
14
  export const HISTORICAL_MESH_QUEUE_STATUSES: MeshHistoricalTaskStatus[] = ['completed', 'failed', 'cancelled'];
15
+ export const MESH_TASK_MODES: MeshTaskMode[] = ['code_change', 'validation', 'live_debug_readonly', 'launch_app', 'convergence'];
16
+
17
+ export interface MeshTaskModeValidationResult {
18
+ valid: boolean;
19
+ taskMode?: MeshTaskMode;
20
+ violations: string[];
21
+ allowedOperations?: string[];
22
+ }
23
+
24
+ const LIVE_DEBUG_READONLY_FORBIDDEN: Array<{ label: string; pattern: RegExp }> = [
25
+ { 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 },
26
+ { label: 'git_mutation', pattern: /\b(?:git\s+(?:add|commit|push|reset|rebase|clean|checkout|switch|merge|tag|restore|rm|mv)|push\b)/i },
27
+ { label: 'checkpoint', pattern: /\b(checkpoint|mesh_checkpoint)\b/i },
28
+ { label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release)\b/i },
29
+ { label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i)\b/i },
30
+ ];
31
+
32
+ export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
33
+ if (typeof value !== 'string') return undefined;
34
+ const normalized = value.trim() as MeshTaskMode;
35
+ return (MESH_TASK_MODES as string[]).includes(normalized) ? normalized : undefined;
36
+ }
37
+
38
+ export function validateMeshTaskModeRequest(mode: unknown, message: string): MeshTaskModeValidationResult {
39
+ const taskMode = normalizeMeshTaskMode(mode);
40
+ if (!taskMode) {
41
+ return { valid: true, violations: [] };
42
+ }
43
+ if (taskMode !== 'live_debug_readonly') {
44
+ return { valid: true, taskMode, violations: [] };
45
+ }
46
+ const violations = LIVE_DEBUG_READONLY_FORBIDDEN
47
+ .filter(rule => rule.pattern.test(message || ''))
48
+ .map(rule => rule.label);
49
+ return {
50
+ valid: violations.length === 0,
51
+ taskMode,
52
+ violations,
53
+ allowedOperations: [
54
+ 'process/log/window/port/session inspection',
55
+ 'read-only filesystem listing/reading',
56
+ 'status probes and keep-running handle reporting',
57
+ 'diagnostic summaries without source edits, commits, checkpoints, pushes, deploys, resets, rebases, or destructive cleanups',
58
+ ],
59
+ };
60
+ }
14
61
 
15
62
  export interface MeshWorkQueueEntry {
16
63
  id: string;
17
64
  meshId: string;
18
65
  message: string;
19
66
  status: MeshTaskStatus;
67
+ taskMode?: MeshTaskMode;
20
68
  /** If specified, only this node can claim the task (used by legacy mesh_send_task) */
21
69
  targetNodeId?: string;
22
70
  /** If specified, only this runtime session can claim the task */
@@ -103,9 +151,13 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
103
151
  export function enqueueTask(
104
152
  meshId: string,
105
153
  message: string,
106
- opts?: { targetNodeId?: string; targetSessionId?: string } & MeshQueueMutationOptions,
154
+ opts?: { targetNodeId?: string; targetSessionId?: string; taskMode?: MeshTaskMode | string } & MeshQueueMutationOptions,
107
155
  ): MeshWorkQueueEntry {
108
156
  requireMeshHostQueueOwner(opts);
157
+ const modeValidation = validateMeshTaskModeRequest(opts?.taskMode, message);
158
+ if (!modeValidation.valid) {
159
+ throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
160
+ }
109
161
  return withQueueLock(meshId, () => {
110
162
  const queue = readQueue(meshId);
111
163
  const entry: MeshWorkQueueEntry = {
@@ -113,6 +165,7 @@ export function enqueueTask(
113
165
  meshId,
114
166
  message,
115
167
  status: 'pending',
168
+ taskMode: modeValidation.taskMode,
116
169
  targetNodeId: opts?.targetNodeId,
117
170
  targetSessionId: opts?.targetSessionId,
118
171
  createdAt: new Date().toISOString(),
@@ -743,6 +743,55 @@ export class CliProviderInstance implements ProviderInstance {
743
743
  return role === 'assistant' && !!content;
744
744
  }
745
745
 
746
+ private buildCompletedFinalizationDiagnostic(args: {
747
+ blockReason: string;
748
+ latestStatus?: any;
749
+ latestVisibleStatus: string;
750
+ waitedMs: number;
751
+ pending: CompletedDebouncePending;
752
+ emittedAfterFinalizationTimeout: boolean;
753
+ }): Record<string, unknown> {
754
+ let parsed: any = null;
755
+ let parseError: string | undefined;
756
+ try {
757
+ parsed = this.adapter.getScriptParsedStatus();
758
+ } catch (error: any) {
759
+ parseError = error?.message || String(error);
760
+ }
761
+
762
+ const visibleMessages = (Array.isArray(parsed?.messages) ? parsed.messages : [])
763
+ .filter((message: any) => isUserFacingChatMessage(message as ChatMessage));
764
+ const lastVisible = visibleMessages[visibleMessages.length - 1] as ChatMessage | undefined;
765
+ const lastVisibleRole = typeof lastVisible?.role === 'string' ? lastVisible.role.trim().toLowerCase() : null;
766
+ const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
767
+ const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
768
+
769
+ return {
770
+ providerType: this.type,
771
+ sessionId: this.instanceId,
772
+ providerSessionId: this.providerSessionId || null,
773
+ workspace: this.workingDir,
774
+ blockReason: args.blockReason,
775
+ emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
776
+ waitedMs: args.waitedMs,
777
+ maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
778
+ adapterStatus: typeof args.latestStatus?.status === 'string' ? args.latestStatus.status : null,
779
+ latestVisibleStatus: args.latestVisibleStatus,
780
+ parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
781
+ parseError: parseError || undefined,
782
+ finalAssistantPresent: this.completionHasFinalAssistantMessage(parsed?.messages),
783
+ visibleMessageCount: visibleMessages.length,
784
+ lastVisibleRole,
785
+ lastVisibleKind,
786
+ lastVisibleContentLength,
787
+ pendingStartedAt: this.generatingStartedAt || null,
788
+ pendingFirstObservedAt: args.pending.firstObservedAt,
789
+ pendingTimestamp: args.pending.timestamp,
790
+ pendingDurationSec: args.pending.duration,
791
+ previousBlockReason: args.pending.loggedBlockReason || null,
792
+ };
793
+ }
794
+
746
795
  private hasAdapterPendingResponse(): boolean {
747
796
  const adapterAny = this.adapter as any;
748
797
  if (adapterAny?.isWaitingForResponse === true) return true;
@@ -828,7 +877,23 @@ export class CliProviderInstance implements ProviderInstance {
828
877
  this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
829
878
  return;
830
879
  }
831
- LOG.warn('CLI', `[${this.type}] suppressed completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
880
+ const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
881
+ blockReason,
882
+ latestStatus,
883
+ latestVisibleStatus,
884
+ waitedMs,
885
+ pending,
886
+ emittedAfterFinalizationTimeout: true,
887
+ });
888
+ LOG.warn('CLI', `[${this.type}] emitting completed event after ${waitedMs}ms without finalized assistant turn (${blockReason})`);
889
+ this.pushEvent({
890
+ event: 'agent:generating_completed',
891
+ chatTitle: pending.chatTitle,
892
+ duration: pending.duration,
893
+ timestamp: pending.timestamp,
894
+ finalSummary: extractFinalSummaryFromMessages(this.adapter?.getScriptParsedStatus()?.messages),
895
+ completionDiagnostic,
896
+ });
832
897
  this.completedDebouncePending = null;
833
898
  this.completedDebounceTimer = null;
834
899
  this.generatingStartedAt = 0;