@adhdev/daemon-core 0.9.82-rc.422 → 0.9.82-rc.424

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.
@@ -58,7 +58,7 @@ export interface MeshWorkerResultArtifact {
58
58
  errors: string[];
59
59
  nextAction?: string;
60
60
  requiresUserAction: boolean;
61
- source: 'explicit_metadata' | 'final_summary_json' | 'default';
61
+ source: 'explicit_metadata' | 'final_summary_json' | 'parseable_answer' | 'default';
62
62
  }
63
63
  export interface MeshTaskCompletionEvidence {
64
64
  source: 'agent_status_event';
@@ -16,6 +16,19 @@ type PersistableCliHistoryMessage = {
16
16
  senderName?: string;
17
17
  receivedAt?: number;
18
18
  };
19
+ /**
20
+ * NOTIF Defect-2a: the REPORTED short-generating duration, anchored on the IMMUTABLE turn
21
+ * start. generatingStartedAt is reset to 0 on every mid-turn waiting_approval/idle blip and
22
+ * re-armed on the next →generating, so a long turn that blips would otherwise measure only the
23
+ * final 1.5-2.5s sliver. engine.currentTurnStartedAt (set once at onTurnStarted, surviving
24
+ * mid-turn blips until the next turn starts) is preferred; generatingStartedAt is the fallback
25
+ * for turns that never recorded an engine turn start. Returns 0 when neither anchor is set.
26
+ * Pure / unit-testable.
27
+ */
28
+ export declare function computeTurnAnchoredDurationMs(engineTurnStartedAt: number | undefined, generatingStartedAt: number, now: number): {
29
+ durationMs: number;
30
+ anchor: 'turn-start' | 'generatingStartedAt' | 'none';
31
+ };
19
32
  export declare function buildCliStructuredInputPrompt(input: InputEnvelope, options?: {
20
33
  materializeDir?: string;
21
34
  }): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.422",
3
+ "version": "0.9.82-rc.424",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.422",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.424",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -1174,6 +1174,11 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
1174
1174
  : undefined,
1175
1175
  evidence: completionEvidence,
1176
1176
  // B2: evidenceLevel lets coordinator know when completion evidence is insufficient.
1177
+ // NOTIF Defect-2b: ONLY source==='default' (no parseable answer at all) is
1178
+ // 'insufficient'. 'parseable_answer' (a real JSON answer that just isn't
1179
+ // worker-result-shaped, e.g. a MAGI envelope) is concrete evidence and must
1180
+ // resolve to 'sufficient' — resolveWorkerResult now upgrades that case so a
1181
+ // complete, valid answer is no longer mislabelled insufficient/reviewRecommended.
1177
1182
  ...(completionEvidence
1178
1183
  ? completionEvidence.workerResult.source === 'default'
1179
1184
  ? { evidenceLevel: 'insufficient', reviewRecommended: true }
@@ -126,7 +126,14 @@ export interface MeshWorkerResultArtifact {
126
126
  errors: string[];
127
127
  nextAction?: string;
128
128
  requiresUserAction: boolean;
129
- source: 'explicit_metadata' | 'final_summary_json' | 'default';
129
+ // NOTIF Defect-2b: `parseable_answer` = the final summary held a parseable JSON
130
+ // ANSWER (e.g. a MAGI claim_audit / rca envelope) that is NOT worker-result-shaped
131
+ // (no status + changedFiles/errors/…). It is still concrete evidence that the worker
132
+ // produced a real, parseable answer — so it must NOT be labelled evidenceLevel
133
+ // 'insufficient' — but it is NOT a self-attributing worker result, so it is deliberately
134
+ // distinct from 'final_summary_json' and stays subject to the direct-dispatch grace gate
135
+ // in mesh-events-stale.ts (which keys on `!== 'final_summary_json'`).
136
+ source: 'explicit_metadata' | 'final_summary_json' | 'parseable_answer' | 'default';
130
137
  }
131
138
 
132
139
  export interface MeshTaskCompletionEvidence {
@@ -508,6 +515,35 @@ export function normalizeMeshWorkerResult(input?: Record<string, unknown>, sourc
508
515
  };
509
516
  }
510
517
 
518
+ /**
519
+ * NOTIF Defect-2b: does the summary contain ANY parseable JSON object answer (not just a
520
+ * worker-result-shaped one)? Some providers (and every MAGI replica) emit a complete, valid
521
+ * answer as a JSON envelope that has no `status`/`changedFiles` worker-result fields, so
522
+ * extractJsonObjectFromSummary returns undefined and the completion is mislabelled
523
+ * source='default' → evidenceLevel='insufficient' even though a real answer was produced.
524
+ * This is a conservative existence check: it only returns true when a JSON object actually
525
+ * parses out of the summary (raw or fenced), so an empty / prose-only / unparseable summary
526
+ * still resolves to 'default'.
527
+ */
528
+ function summaryHasParseableJsonAnswer(summary?: string): boolean {
529
+ const text = readNonEmptyString(summary);
530
+ if (!text) return false;
531
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
532
+ const candidates = [fenced?.[1], text].filter(Boolean) as string[];
533
+ for (const candidate of candidates) {
534
+ const trimmed = candidate.trim();
535
+ if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
536
+ try {
537
+ const parsed = JSON.parse(trimmed);
538
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)
539
+ && Object.keys(parsed).length > 0) {
540
+ return true;
541
+ }
542
+ } catch { /* try next candidate */ }
543
+ }
544
+ return false;
545
+ }
546
+
511
547
  function resolveWorkerResult(opts: BuildTaskCompletionEvidenceOptions): MeshWorkerResultArtifact {
512
548
  if (opts.workerResult && typeof opts.workerResult === 'object') {
513
549
  return normalizeMeshWorkerResult(opts.workerResult, 'explicit_metadata');
@@ -516,6 +552,13 @@ function resolveWorkerResult(opts: BuildTaskCompletionEvidenceOptions): MeshWork
516
552
  if (parsed) {
517
553
  return normalizeMeshWorkerResult(parsed, 'final_summary_json');
518
554
  }
555
+ // NOTIF Defect-2b: no worker-result-shaped JSON, but a parseable JSON answer IS present
556
+ // (the common MAGI / answer-only case). Treat it as concrete evidence so the completion is
557
+ // not labelled 'insufficient', while keeping the worker-result fields empty (status stays
558
+ // 'unknown') — we only know an answer parsed, not its task outcome.
559
+ if (summaryHasParseableJsonAnswer(opts.finalSummary)) {
560
+ return normalizeMeshWorkerResult(undefined, 'parseable_answer');
561
+ }
519
562
  return normalizeMeshWorkerResult(undefined, 'default');
520
563
  }
521
564
 
@@ -249,6 +249,28 @@ function isCliGeneratingLikeStatus(status: unknown): boolean {
249
249
  return status === 'generating' || status === 'streaming' || status === 'no_progress' || status === 'long_generating' || status === 'starting';
250
250
  }
251
251
 
252
+ /**
253
+ * NOTIF Defect-2a: the REPORTED short-generating duration, anchored on the IMMUTABLE turn
254
+ * start. generatingStartedAt is reset to 0 on every mid-turn waiting_approval/idle blip and
255
+ * re-armed on the next →generating, so a long turn that blips would otherwise measure only the
256
+ * final 1.5-2.5s sliver. engine.currentTurnStartedAt (set once at onTurnStarted, surviving
257
+ * mid-turn blips until the next turn starts) is preferred; generatingStartedAt is the fallback
258
+ * for turns that never recorded an engine turn start. Returns 0 when neither anchor is set.
259
+ * Pure / unit-testable.
260
+ */
261
+ export function computeTurnAnchoredDurationMs(
262
+ engineTurnStartedAt: number | undefined,
263
+ generatingStartedAt: number,
264
+ now: number,
265
+ ): { durationMs: number; anchor: 'turn-start' | 'generatingStartedAt' | 'none' } {
266
+ const engineStart = typeof engineTurnStartedAt === 'number' && Number.isFinite(engineTurnStartedAt)
267
+ ? engineTurnStartedAt
268
+ : 0;
269
+ if (engineStart > 0) return { durationMs: now - engineStart, anchor: 'turn-start' };
270
+ if (generatingStartedAt > 0) return { durationMs: now - generatingStartedAt, anchor: 'generatingStartedAt' };
271
+ return { durationMs: 0, anchor: 'none' };
272
+ }
273
+
252
274
  export function buildCliStructuredInputPrompt(
253
275
  input: InputEnvelope,
254
276
  options: { materializeDir?: string } = {},
@@ -2375,8 +2397,21 @@ export class CliProviderInstance implements ProviderInstance {
2375
2397
  // Still emit agent:generating_completed so mesh orchestration can record
2376
2398
  // task_completed for direct dispatches that complete faster than the debounce.
2377
2399
  if (this.generatingDebouncePending) {
2378
- const shortDurationMs = this.generatingStartedAt ? now - this.generatingStartedAt : 0;
2379
- LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms)`);
2400
+ // NOTIF Defect-2a: shortDurationMs is the REPORTED turn duration, so it must be
2401
+ // measured from the IMMUTABLE turn start — not generatingStartedAt, which is
2402
+ // reset to 0 on every mid-turn waiting_approval/idle blip (see :1864/1885/2397/
2403
+ // 2503/2521) and re-armed on the next →generating, so a long turn that blipped
2404
+ // would measure only the final 1.5-2.5s sliver. engine.currentTurnStartedAt is
2405
+ // stamped once at onTurnStarted and persists past mid-turn blips until the next
2406
+ // turn starts, so it captures the true turn length. generatingStartedAt remains
2407
+ // the fallback (and the debounce itself stays a pure UI-suppression signal,
2408
+ // decoupled from the reported duration).
2409
+ const { durationMs: shortDurationMs, anchor: durationAnchor } = computeTurnAnchoredDurationMs(
2410
+ (this.adapter as any)?.currentTurnStartedAt,
2411
+ this.generatingStartedAt,
2412
+ now,
2413
+ );
2414
+ LOG.info('CLI', `[${this.type}] suppressed short generating (${shortDurationMs}ms, anchor=${durationAnchor})`);
2380
2415
  if (this.generatingDebounceTimer) { clearTimeout(this.generatingDebounceTimer); this.generatingDebounceTimer = null; }
2381
2416
  // Emit completion for mesh task association even though the UI generating
2382
2417
  // started/completed pair is suppressed (too short for visible UI update).