@adhdev/daemon-core 1.0.18-rc.11 → 1.0.18-rc.13

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.
@@ -192,6 +192,7 @@ export declare class CliProviderInstance implements ProviderInstance {
192
192
  private meshStallEmittedForAnchor;
193
193
  private meshStallTurnActiveLast;
194
194
  private meshStallLastFiredAt;
195
+ private meshStallNativeTranscriptSample;
195
196
  private busyEpoch;
196
197
  private fastCollapseSynthesizedTaskId;
197
198
  private startupGraceCollapseAt;
@@ -424,6 +425,7 @@ export declare class CliProviderInstance implements ProviderInstance {
424
425
  * once at completion). Reset on the next turn's start.
425
426
  */
426
427
  private lastCompletionSummary;
428
+ private lastEmittedCompletion;
427
429
  private enforceFreshSessionLaunchIfNeeded;
428
430
  /**
429
431
  * BRAIN-ROUTING (runtime-control thinking axis): for a provider that selects
@@ -574,6 +576,49 @@ export declare class CliProviderInstance implements ProviderInstance {
574
576
  * starts with a clean slate (a restart is not throttled by a prior stall).
575
577
  */
576
578
  private resetMeshStallEpisode;
579
+ /**
580
+ * KIMI-MESH-COMPLETION-EMIT (axis 1). For a native-source provider
581
+ * (transcriptAuthority=provider — its authoritative history is an on-disk
582
+ * transcript file, e.g. kimi's wire.jsonl), sample the transcript's current
583
+ * progress fingerprint (record count + source-file mtime) WITHOUT parsing the
584
+ * PTY. Used by the stall watchdog to distinguish "PTY render is quiet but the
585
+ * worker is still doing long tool work (transcript growing)" from a genuine
586
+ * wedge. Returns null for a pure-PTY provider (no native source) or when the
587
+ * source cannot be resolved this tick — the caller then falls back to the
588
+ * unchanged lastOutputAt-only judgment.
589
+ *
590
+ * Generalized on the provider's native-source flag, NOT a hardcoded provider
591
+ * type, so every current and future pure-PTY long-tool native-source provider
592
+ * benefits. Cheap enough for the stall path: it runs only at the stall threshold
593
+ * (≥180s of PTY stasis), never on the routine 5s tick.
594
+ */
595
+ private sampleNativeTranscriptProgress;
596
+ /**
597
+ * KIMI-MESH-COMPLETION-EMIT (axis 2). Last-chance completion emit for a mesh
598
+ * DELEGATED worker whose PTY has already exited (e.g. killed by a false stall)
599
+ * and is about to be auto-cleaned (cli-manager.startCliExitMonitor →
600
+ * removeInstance closes the event-emit window forever). If the worker actually
601
+ * FINISHED its assigned turn — its authoritative native transcript holds a final
602
+ * assistant message for the injected task — but the completion event never fired
603
+ * (the stall-kill happened before the FSM's idle transition), emit it now so the
604
+ * coordinator learns the task completed instead of waiting ~180s for the reconcile
605
+ * transcript-poll to reclaim it.
606
+ *
607
+ * Scope & guards (must all hold to emit):
608
+ * • mesh worker session only — a normal standalone session's ordinary exit is
609
+ * never synthesized (isMeshWorkerSession()).
610
+ * • DOUBLE-EMIT guard — refuse if this turn's completion already fired
611
+ * (lastEmittedCompletion matches the current taskId). A worker that completed
612
+ * cleanly and is merely being cleaned up never double-emits.
613
+ * • evidence gate — reuse the same final-assistant/turn-scoped summary machinery
614
+ * as the normal completion path (completionFinalSummary over the native
615
+ * transcript). No in-turn assistant summary ⇒ no proof of completion ⇒ leave
616
+ * the reclaim path to handle a genuinely-unfinished worker.
617
+ *
618
+ * Returns true when a synthetic completion was emitted, false otherwise. Safe to
619
+ * call unconditionally from the cleanup path.
620
+ */
621
+ flushMeshCompletionBeforeCleanup(): boolean;
577
622
  /**
578
623
  * AUTOAPPROVE-FLAP-RECUR (Fix A+B): how long a busy blip / modal scroll-out may
579
624
  * persist before the in-progress settle gate is torn down. For a delegated
@@ -624,7 +624,7 @@ export interface DashboardBootstrapDaemonEntry extends Partial<CloudDaemonSummar
624
624
  p2p?: StatusReportPayload['p2p'];
625
625
  timestamp?: number;
626
626
  }
627
- export type DaemonStatusEventName = 'agent:generating_started' | 'agent:waiting_approval' | 'agent:generating_completed' | 'agent:stopped' | 'monitor:no_progress' | 'monitor:long_generating';
627
+ export type DaemonStatusEventName = 'agent:generating_started' | 'agent:waiting_approval' | 'agent:waiting_choice' | 'agent:generating_completed' | 'agent:stopped' | 'monitor:no_progress' | 'monitor:long_generating';
628
628
  /** Minimal daemon-originated event payload relayed through the server. */
629
629
  export interface DaemonStatusEventPayload {
630
630
  event: DaemonStatusEventName;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "1.0.18-rc.11",
3
+ "version": "1.0.18-rc.13",
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",
@@ -47,8 +47,8 @@
47
47
  "author": "vilmire",
48
48
  "license": "AGPL-3.0-or-later",
49
49
  "dependencies": {
50
- "@adhdev/mesh-shared": "1.0.18-rc.11",
51
- "@adhdev/session-host-core": "1.0.18-rc.11",
50
+ "@adhdev/mesh-shared": "1.0.18-rc.13",
51
+ "@adhdev/session-host-core": "1.0.18-rc.13",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -707,6 +707,24 @@ export class DaemonCliManager {
707
707
  clearInterval(checkStopped);
708
708
  setTimeout(() => {
709
709
  if (this.adapters.has(key)) {
710
+ // KIMI-MESH-COMPLETION-EMIT (axis 2): before removeInstance closes the
711
+ // event-emit window, give a mesh DELEGATED worker one last chance to emit
712
+ // its completion. A native-source worker (e.g. kimi) can have its PTY
713
+ // killed by a false stall AFTER it finished the task (transcript written)
714
+ // but BEFORE the FSM's idle→completed event fired — the instance is the
715
+ // only thing that can emit that event, and it is about to be removed. The
716
+ // instance-side method is a no-op for a non-mesh session or when the
717
+ // turn's completion already fired (double-emit guard) or when there is no
718
+ // transcript evidence of a finished turn. Best-effort — never blocks cleanup.
719
+ try {
720
+ const inst = instanceManager?.getInstance(key) as (ProviderInstance & { flushMeshCompletionBeforeCleanup?: () => boolean }) | undefined;
721
+ if (typeof inst?.flushMeshCompletionBeforeCleanup === 'function') {
722
+ const emitted = inst.flushMeshCompletionBeforeCleanup();
723
+ if (emitted) LOG.info('CLI', `Emitted pre-cleanup mesh completion for ${cliType} session ${key} before auto-clean`);
724
+ }
725
+ } catch (e) {
726
+ LOG.warn('CLI', `pre-cleanup mesh completion flush failed for ${key}: ${(e as Error)?.message || e}`);
727
+ }
710
728
  this.adapters.delete(key);
711
729
  this.deps.removeAgentTracking(key);
712
730
  sessionRegistry?.unregisterByInstanceKey(key);
@@ -332,6 +332,18 @@ export class CliProviderInstance implements ProviderInstance {
332
332
  // MESH_WORKER_STALL_REFIRE_COOLDOWN_MS between successive emissions across
333
333
  // anchor re-arms (the per-anchor guard only covers a single continuous stall).
334
334
  private meshStallLastFiredAt = -1;
335
+ // KIMI-MESH-COMPLETION-EMIT (axis 1): the native-source transcript progress
336
+ // fingerprint (record count + source mtime) observed the LAST time the stall
337
+ // watchdog checked a native-source provider (transcriptAuthority=provider — e.g.
338
+ // kimi wire.jsonl). A pure-PTY native-source worker running a long, screen-quiet
339
+ // tool (no viewport bytes for minutes) looks stalled by the lastOutputAt clock
340
+ // even though its transcript file is still growing. Before firing the stall, we
341
+ // compare the current transcript fingerprint against this snapshot; if the
342
+ // transcript advanced, the "stall" is a false positive of PTY-render stasis, so
343
+ // we re-arm the anchor instead of paging the coordinator (whose no_progress
344
+ // handling can lead to the worker being stopped mid-work → completion never
345
+ // emitted). null = not yet sampled for this session/anchor.
346
+ private meshStallNativeTranscriptSample: { msgCount: number; sourceMtimeMs: number } | null = null;
335
347
  // FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
336
348
  // phase (→generating or →waiting_approval). The completedDebouncePending snapshots
337
349
  // this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
@@ -1418,6 +1430,15 @@ export class CliProviderInstance implements ProviderInstance {
1418
1430
  */
1419
1431
  private lastCompletionSummary: { content: string; receivedAt: number; sourceTimestampMs?: number } | null = null;
1420
1432
 
1433
+ // KIMI-MESH-COMPLETION-EMIT (axis 2, double-emit guard): the (taskId, wall-clock)
1434
+ // of the most recent agent:generating_completed this instance emitted, stamped by
1435
+ // emitGeneratingCompleted. The pre-cleanup completion flush
1436
+ // (flushMeshCompletionBeforeCleanup, driven by cli-manager's exit monitor) reads
1437
+ // this to refuse a SECOND completion for a turn whose completion already fired —
1438
+ // so a worker that finished cleanly and is simply being auto-cleaned never emits a
1439
+ // duplicate. taskId '' covers an ad-hoc (no-task) turn. null = none emitted yet.
1440
+ private lastEmittedCompletion: { taskId: string; at: number } | null = null;
1441
+
1421
1442
  private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
1422
1443
  const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
1423
1444
  if (!scriptName) return;
@@ -2404,6 +2425,38 @@ export class CliProviderInstance implements ProviderInstance {
2404
2425
  const stalledMs = now - this.meshStallAnchorAt;
2405
2426
  if (stalledMs < threshold) return;
2406
2427
 
2428
+ // KIMI-MESH-COMPLETION-EMIT (axis 1): transcript-aware stall for native-source
2429
+ // providers. A pure-PTY native-source worker (transcriptAuthority=provider,
2430
+ // e.g. kimi) running a long, screen-quiet tool emits no viewport bytes for
2431
+ // minutes, so the lastOutputAt clock above reads it as stalled — but its
2432
+ // authoritative transcript (wire.jsonl) is still being appended. Firing
2433
+ // monitor:no_progress here surfaces a false stall to the coordinator whose
2434
+ // downstream handling can stop the worker mid-work, killing the emit window
2435
+ // before the real completion propagates. So before firing, sample the native
2436
+ // transcript's progress fingerprint: if it advanced (more records OR a fresher
2437
+ // source mtime) since the last sample, the worker is demonstrably alive — treat
2438
+ // it as progress. Re-arm the anchor to `now` and reset the episode so the clock
2439
+ // restarts from this proven-live moment; only a transcript that is ALSO static
2440
+ // falls through to the genuine-stall fire below. No-op for pure-PTY providers
2441
+ // (sample is null → unchanged behavior).
2442
+ const nativeSample = this.sampleNativeTranscriptProgress();
2443
+ if (nativeSample) {
2444
+ const prev = this.meshStallNativeTranscriptSample;
2445
+ const advanced = !prev
2446
+ || nativeSample.msgCount > prev.msgCount
2447
+ || nativeSample.sourceMtimeMs > prev.sourceMtimeMs;
2448
+ this.meshStallNativeTranscriptSample = nativeSample;
2449
+ if (advanced) {
2450
+ if (this.isMeshWorkerSession()) {
2451
+ traceMeshEventDrop('mesh_worker_stall_transcript_advancing', this.meshTraceCtx('monitor:no_progress'),
2452
+ `msgCount=${nativeSample.msgCount} sourceMtime=${nativeSample.sourceMtimeMs} (PTY quiet ${Math.round(stalledMs / 1000)}s but transcript advancing)`);
2453
+ }
2454
+ this.meshStallAnchorAt = now;
2455
+ this.meshStallEmittedForAnchor = false;
2456
+ return;
2457
+ }
2458
+ }
2459
+
2407
2460
  // FALSE-STALL-WATCHDOG-OVERFIRE (fix E): per-session refire cooldown. Mark
2408
2461
  // this anchor emitted regardless so a still-static anchor does not re-check
2409
2462
  // every tick, but suppress the actual coordinator notification (and the
@@ -2454,6 +2507,116 @@ export class CliProviderInstance implements ProviderInstance {
2454
2507
  this.meshStallEmittedForAnchor = false;
2455
2508
  this.meshStallTurnActiveLast = undefined;
2456
2509
  this.meshStallLastFiredAt = -1;
2510
+ this.meshStallNativeTranscriptSample = null;
2511
+ }
2512
+
2513
+ /**
2514
+ * KIMI-MESH-COMPLETION-EMIT (axis 1). For a native-source provider
2515
+ * (transcriptAuthority=provider — its authoritative history is an on-disk
2516
+ * transcript file, e.g. kimi's wire.jsonl), sample the transcript's current
2517
+ * progress fingerprint (record count + source-file mtime) WITHOUT parsing the
2518
+ * PTY. Used by the stall watchdog to distinguish "PTY render is quiet but the
2519
+ * worker is still doing long tool work (transcript growing)" from a genuine
2520
+ * wedge. Returns null for a pure-PTY provider (no native source) or when the
2521
+ * source cannot be resolved this tick — the caller then falls back to the
2522
+ * unchanged lastOutputAt-only judgment.
2523
+ *
2524
+ * Generalized on the provider's native-source flag, NOT a hardcoded provider
2525
+ * type, so every current and future pure-PTY long-tool native-source provider
2526
+ * benefits. Cheap enough for the stall path: it runs only at the stall threshold
2527
+ * (≥180s of PTY stasis), never on the routine 5s tick.
2528
+ */
2529
+ private sampleNativeTranscriptProgress(): { msgCount: number; sourceMtimeMs: number } | null {
2530
+ if (!isNativeSourceCanonicalHistory(this.provider?.nativeHistory)) return null;
2531
+ // readExternalCompletionMessages resolves this session's OWN native-source
2532
+ // conversation (providerSessionId / persisted pin / floor claim) and, as a
2533
+ // side effect, refreshes this.lastExternalCompletionProbe with the source
2534
+ // path, mtime, and message count. Reusing it keeps the resolution logic in
2535
+ // one place and immune to the antigravity-style session-id quirks.
2536
+ let messages: unknown[] | null = null;
2537
+ try {
2538
+ messages = this.readExternalCompletionMessages();
2539
+ } catch {
2540
+ return null; // best-effort: an unresolved transcript → fall back to PTY clock
2541
+ }
2542
+ const probe = this.lastExternalCompletionProbe;
2543
+ if (!probe) return null;
2544
+ return {
2545
+ msgCount: typeof probe.msgCount === 'number' ? probe.msgCount : (Array.isArray(messages) ? messages.length : 0),
2546
+ sourceMtimeMs: typeof probe.sourceMtimeMs === 'number' ? probe.sourceMtimeMs : 0,
2547
+ };
2548
+ }
2549
+
2550
+ /**
2551
+ * KIMI-MESH-COMPLETION-EMIT (axis 2). Last-chance completion emit for a mesh
2552
+ * DELEGATED worker whose PTY has already exited (e.g. killed by a false stall)
2553
+ * and is about to be auto-cleaned (cli-manager.startCliExitMonitor →
2554
+ * removeInstance closes the event-emit window forever). If the worker actually
2555
+ * FINISHED its assigned turn — its authoritative native transcript holds a final
2556
+ * assistant message for the injected task — but the completion event never fired
2557
+ * (the stall-kill happened before the FSM's idle transition), emit it now so the
2558
+ * coordinator learns the task completed instead of waiting ~180s for the reconcile
2559
+ * transcript-poll to reclaim it.
2560
+ *
2561
+ * Scope & guards (must all hold to emit):
2562
+ * • mesh worker session only — a normal standalone session's ordinary exit is
2563
+ * never synthesized (isMeshWorkerSession()).
2564
+ * • DOUBLE-EMIT guard — refuse if this turn's completion already fired
2565
+ * (lastEmittedCompletion matches the current taskId). A worker that completed
2566
+ * cleanly and is merely being cleaned up never double-emits.
2567
+ * • evidence gate — reuse the same final-assistant/turn-scoped summary machinery
2568
+ * as the normal completion path (completionFinalSummary over the native
2569
+ * transcript). No in-turn assistant summary ⇒ no proof of completion ⇒ leave
2570
+ * the reclaim path to handle a genuinely-unfinished worker.
2571
+ *
2572
+ * Returns true when a synthetic completion was emitted, false otherwise. Safe to
2573
+ * call unconditionally from the cleanup path.
2574
+ */
2575
+ flushMeshCompletionBeforeCleanup(): boolean {
2576
+ if (!this.isMeshWorkerSession()) return false;
2577
+ const taskId = this.completingTurnTaskId();
2578
+
2579
+ // DOUBLE-EMIT guard: this turn's completion already fired — never re-emit.
2580
+ if (this.lastEmittedCompletion && this.lastEmittedCompletion.taskId === (taskId ?? '')) {
2581
+ return false;
2582
+ }
2583
+
2584
+ // Evidence gate: the injected task's own turn must have genuinely started
2585
+ // (guards against synthesizing a completion off a reused session's stale tail
2586
+ // before this task ran) and the native transcript must hold an in-turn final
2587
+ // assistant summary. turnStartedAt anchors the turn-scoped read.
2588
+ if (!this.injectedTaskHasStartedGenerating()) return false;
2589
+ const turnStartedAt = typeof (this.adapter as any)?.currentTurnStartedAt === 'number'
2590
+ ? (this.adapter as any).currentTurnStartedAt as number
2591
+ : this.meshTaskInjectedAt || undefined;
2592
+ let parsedMessages: unknown;
2593
+ try {
2594
+ parsedMessages = this.adapter?.getScriptParsedStatus?.()?.messages;
2595
+ } catch { parsedMessages = undefined; }
2596
+ let finalSummary: string | undefined;
2597
+ try {
2598
+ finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
2599
+ } catch { finalSummary = undefined; }
2600
+ // No in-turn final assistant evidence → not a proven turn-end. Defer to the
2601
+ // coordinator's reclaim/reconcile path rather than fabricate a completion.
2602
+ if (!finalSummary) return false;
2603
+
2604
+ LOG.warn('CLI', `[${this.type}] emitting pre-cleanup mesh completion for session ${this.instanceId} `
2605
+ + `task=${taskId ?? '(none)'} — PTY exited before the completion event fired but the native transcript `
2606
+ + `shows a finished turn; synthesizing completion so the coordinator is not left waiting for reclaim.`);
2607
+ if (this.isMeshWorkerSession()) {
2608
+ traceMeshEventStage('fired', this.meshTraceCtx(), 'pre_cleanup_transcript_completion');
2609
+ }
2610
+ this.emitGeneratingCompleted({
2611
+ chatTitle: '',
2612
+ duration: undefined,
2613
+ timestamp: Date.now(),
2614
+ taskId,
2615
+ finalSummary,
2616
+ evidenceLevel: 'transcript',
2617
+ completionDiagnostic: { source: 'pre_cleanup_transcript_completion' },
2618
+ });
2619
+ return true;
2457
2620
  }
2458
2621
 
2459
2622
  /**
@@ -2997,6 +3160,10 @@ export class CliProviderInstance implements ProviderInstance {
2997
3160
  if (summary) {
2998
3161
  this.lastCompletionSummary = { content: summary, receivedAt: opts.timestamp };
2999
3162
  }
3163
+ // KIMI-MESH-COMPLETION-EMIT (axis 2, double-emit guard): record that THIS turn's
3164
+ // completion has now been emitted, keyed by its taskId, so the pre-cleanup
3165
+ // completion flush never fires a duplicate for the same turn.
3166
+ this.lastEmittedCompletion = { taskId: typeof opts.taskId === 'string' ? opts.taskId : '', at: Date.now() };
3000
3167
  this.pushEvent({
3001
3168
  event: 'agent:generating_completed',
3002
3169
  chatTitle: opts.chatTitle,
@@ -775,6 +775,12 @@ export interface DashboardBootstrapDaemonEntry extends Partial<CloudDaemonSummar
775
775
  export type DaemonStatusEventName =
776
776
  | 'agent:generating_started'
777
777
  | 'agent:waiting_approval'
778
+ // A question picker (AskUserQuestion / InteractivePrompt) parks the agent
779
+ // awaiting a human decision — distinct from an approval modal, but equally a
780
+ // state the user must answer before work continues. Relayed to the server so
781
+ // push notifications fire (owner requirement: coordinator sessions must be
782
+ // pinged for pending questions).
783
+ | 'agent:waiting_choice'
778
784
  | 'agent:generating_completed'
779
785
  | 'agent:stopped'
780
786
  | 'monitor:no_progress'
@@ -118,6 +118,7 @@ export class DaemonStatusReporter {
118
118
  switch (value) {
119
119
  case 'agent:generating_started':
120
120
  case 'agent:waiting_approval':
121
+ case 'agent:waiting_choice':
121
122
  case 'agent:generating_completed':
122
123
  case 'agent:stopped':
123
124
  case 'monitor:no_progress':