@adhdev/daemon-core 1.0.28-rc.1 → 1.0.28-rc.3

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.
@@ -11,6 +11,20 @@ export interface AssignedTaskTerminalEvidence {
11
11
  nodeId?: string;
12
12
  sessionId: string;
13
13
  }
14
+ export declare function pollAssignedTaskInTurnProgress(components: DaemonComponents, mesh: {
15
+ id: string;
16
+ nodes?: Array<{
17
+ id: string;
18
+ daemonId?: string;
19
+ workspace?: string;
20
+ }>;
21
+ }, row: {
22
+ id: string;
23
+ assignedSessionId?: string;
24
+ assignedNodeId?: string;
25
+ assignedProviderType?: string;
26
+ dispatchTimestamp?: string;
27
+ }): Promise<boolean>;
14
28
  export declare function pollAssignedTaskTerminalEvidence(components: DaemonComponents, mesh: {
15
29
  id: string;
16
30
  nodes?: Array<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "1.0.28-rc.1",
3
+ "version": "1.0.28-rc.3",
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.28-rc.1",
51
- "@adhdev/session-host-core": "1.0.28-rc.1",
50
+ "@adhdev/mesh-shared": "1.0.28-rc.3",
51
+ "@adhdev/session-host-core": "1.0.28-rc.3",
52
52
  "@agentclientprotocol/sdk": "^0.16.1",
53
53
  "ajv": "^8.20.0",
54
54
  "ajv-formats": "^3.0.1",
@@ -26,7 +26,7 @@ import { getActiveDirectDispatches, getQueue } from './mesh-work-queue.js';
26
26
  import { readLedgerEntries } from './mesh-ledger.js';
27
27
  import { pruneStaleDirectDispatches } from './mesh-active-work.js';
28
28
  import { reconcileDirectDispatchCompletionFromTranscript } from './mesh-events-stale.js';
29
- import { extractFinalAssistantSummaryEvidence, hasTrailingToolActivityAfterFinalAssistant } from '../providers/chat-message-normalization.js';
29
+ import { extractFinalAssistantSummaryEvidence, hasTrailingToolActivityAfterFinalAssistant, readChatMessageTimestampMs } from '../providers/chat-message-normalization.js';
30
30
  import type { ChatMessage } from '../types.js';
31
31
  import {
32
32
  getMeshV2BackstopCounters,
@@ -449,6 +449,90 @@ export interface AssignedTaskTerminalEvidence {
449
449
  sessionId: string;
450
450
  }
451
451
 
452
+ // STARTED-REDRIVE-NATIVE-SOURCE-BLINDSPOT. Single-shot transcript poll that asks the NARROWER
453
+ // question the delivered-not-consumed short re-drive needs: did the worker START this turn
454
+ // (in-turn progress), regardless of whether it ever FINISHED it?
455
+ //
456
+ // The short re-drive treats "no agent:generating_started arrived" as positive evidence the worker
457
+ // never consumed the task. That inference holds for a PTY-event provider, whose turn start IS an
458
+ // emitted event. It is FALSE for a NATIVE-SOURCE provider (transcriptAuthority=provider +
459
+ // on-disk nativeHistory — kimi and kin): its start/finish signals live in the native transcript,
460
+ // not in the PTY event stream, so a worker that is demonstrably mid-task still reads
461
+ // delivered-but-never-acked. The observed symptom (2026-07-25): a kimi worker answering the same
462
+ // question four times as the watchdog re-injected the prompt at 35s/28s/27s/43s, then marked the
463
+ // task failed — verdict IDLE_CONFIRMED, because between tool calls the session reads idle.
464
+ //
465
+ // pollAssignedTaskTerminalEvidence answers "did it FINISH" (idle + final-assistant + no trailing
466
+ // tool activity). That is the wrong bar here: a worker mid-task has NOT finished, yet must not be
467
+ // re-driven. This poll therefore accepts ANY post-dispatch transcript bubble — assistant text,
468
+ // thought, or tool/terminal activity — as proof the prompt was consumed and work is under way.
469
+ //
470
+ // Conservative in the same direction as its sibling: an unreadable transcript, a missing/unusable
471
+ // dispatch timestamp, or a tail containing nothing dated at/after dispatch all yield false, so the
472
+ // caller falls through to its normal re-drive decision. The poll can only PREVENT a re-drive of a
473
+ // working session, never create or suppress a completion.
474
+ export async function pollAssignedTaskInTurnProgress(
475
+ components: DaemonComponents,
476
+ mesh: { id: string; nodes?: Array<{ id: string; daemonId?: string; workspace?: string }> },
477
+ row: { id: string; assignedSessionId?: string; assignedNodeId?: string; assignedProviderType?: string; dispatchTimestamp?: string },
478
+ ): Promise<boolean> {
479
+ const sessionId = readNonEmptyString(row.assignedSessionId);
480
+ const nodeId = readNonEmptyString(row.assignedNodeId);
481
+ if (!sessionId || !nodeId) return false; // no worker to read
482
+
483
+ // A dispatch boundary is REQUIRED: without it a reused session's prior-task tail would read as
484
+ // progress on THIS task and suppress the re-drive forever.
485
+ const dispatchedAtMs = Date.parse(readNonEmptyString(row.dispatchTimestamp));
486
+ if (!Number.isFinite(dispatchedAtMs)) return false;
487
+
488
+ const node = (mesh.nodes ?? []).find(n => n.id === nodeId);
489
+ const nodeDaemonId = readNonEmptyString(node?.daemonId);
490
+ const localDaemonId = readNonEmptyString((components as { statusInstanceId?: string }).statusInstanceId);
491
+ const isLocalNode = !nodeDaemonId
492
+ || daemonIdsEquivalent(nodeDaemonId, localDaemonId)
493
+ || !!components.instanceManager.getInstance(sessionId);
494
+
495
+ const providerType = readNonEmptyString(row.assignedProviderType);
496
+ const readArgs: Record<string, unknown> = {
497
+ sessionId,
498
+ targetSessionId: sessionId,
499
+ tailLimit: 10,
500
+ ...(node?.workspace ? { workspace: node.workspace } : {}),
501
+ ...(providerType ? { agentType: providerType, providerType } : {}),
502
+ };
503
+
504
+ let payload: Record<string, unknown> | null = null;
505
+ try {
506
+ if (isLocalNode) {
507
+ const result = await components.commandHandler?.handle('read_chat', readArgs);
508
+ if (result && (result as { success?: boolean }).success === false) return false;
509
+ payload = unwrapReadChatPayload(result);
510
+ } else if (components.dispatchMeshCommand) {
511
+ const result = await components.dispatchMeshCommand(nodeDaemonId, 'read_chat', readArgs);
512
+ payload = unwrapReadChatPayload(result);
513
+ if (payload && (payload as { success?: boolean }).success === false) return false;
514
+ } else {
515
+ return false; // remote node, no P2P transport — can't read this tick
516
+ }
517
+ } catch {
518
+ return false; // transport error / session gone → inconclusive, let the caller decide
519
+ }
520
+ if (!payload) return false;
521
+
522
+ const messages = Array.isArray(payload.messages) ? payload.messages as ChatMessage[] : [];
523
+ // Any AGENT-side bubble dated at/after dispatch proves the prompt landed and the turn is under
524
+ // way. User bubbles are excluded: the injected task prompt itself is a post-dispatch user
525
+ // message, and counting it would suppress the re-drive for the very row this watchdog exists
526
+ // to rescue (delivered, echoed into the transcript, but never actually worked).
527
+ return messages.some(msg => {
528
+ if (!msg) return false;
529
+ if (msg.role === 'user' || msg.role === 'system') return false;
530
+ const ts = readChatMessageTimestampMs(msg);
531
+ if (typeof ts !== 'number' || !Number.isFinite(ts)) return false;
532
+ return ts >= dispatchedAtMs;
533
+ });
534
+ }
535
+
452
536
  export async function pollAssignedTaskTerminalEvidence(
453
537
  components: DaemonComponents,
454
538
  mesh: { id: string; nodes?: Array<{ id: string; daemonId?: string; workspace?: string }> },
@@ -84,6 +84,7 @@ import {
84
84
  reconcileUnterminatedDirectDispatches,
85
85
  autoPruneStaleDirectDispatches,
86
86
  pollAssignedTaskTerminalEvidence,
87
+ pollAssignedTaskInTurnProgress,
87
88
  type AssignedTaskTerminalEvidence,
88
89
  } from './mesh-completion-synthesis.js';
89
90
  import { sessionStatusFromNodes } from './mesh-active-work.js';
@@ -1199,6 +1200,37 @@ async function recoverStrandedAssignedDispatches(
1199
1200
  if (verdict === 'GENERATING') {
1200
1201
  deliveredUnconsumedUnknownStreak.delete(shortStreakKey);
1201
1202
  } else {
1203
+ // STARTED-REDRIVE-NATIVE-SOURCE-BLINDSPOT. Everything below infers "the worker never
1204
+ // consumed the task" from the ABSENCE of agent:generating_started. That inference is
1205
+ // only valid for a provider whose turn start IS a PTY event. A NATIVE-SOURCE provider
1206
+ // (transcriptAuthority=provider + on-disk nativeHistory — kimi and kin) signals start
1207
+ // and finish through its native transcript, never emitting generating_started, so the
1208
+ // delivery legitimately stays 'delivered' while the worker is hard at work; and
1209
+ // between tool calls it reads IDLE_CONFIRMED, which re-drives IMMEDIATELY with no
1210
+ // grace at all. Live 2026-07-25: a kimi worker was re-injected the same prompt at
1211
+ // 35s/28s/27s/43s (answering it each time) and the task then marked failed.
1212
+ //
1213
+ // So for that class ONLY, replace the missing event with the evidence the provider
1214
+ // actually produces: any post-dispatch agent bubble in its transcript. Found → the
1215
+ // prompt WAS consumed and the turn is under way; hold the row (and clear the streak,
1216
+ // since progress is positive evidence, not a deferral). Not found / unreadable → fall
1217
+ // through unchanged. PTY-event providers never enter this branch
1218
+ // (resolveLocalSessionNativeSource false/undefined), so their behaviour is untouched.
1219
+ const nativeSourceRedriveCandidate = row.assignedSessionId
1220
+ ? resolveLocalSessionNativeSource(components, row.assignedSessionId) === true
1221
+ : false;
1222
+ if (nativeSourceRedriveCandidate
1223
+ && await pollAssignedTaskInTurnProgress(components, { id: meshId, nodes: mesh.nodes }, row)) {
1224
+ deliveredUnconsumedUnknownStreak.delete(shortStreakKey);
1225
+ traceMeshEventDrop('short_redrive_deferred_native_source_progress', {
1226
+ taskId: row.id,
1227
+ sessionId: row.assignedSessionId,
1228
+ nodeId: row.assignedNodeId,
1229
+ meshId,
1230
+ event: 'agent:generating_started',
1231
+ }, `native_source_in_turn_progress (verdict ${verdict})`);
1232
+ continue;
1233
+ }
1202
1234
  if (verdict === 'IDLE_CONFIRMED') {
1203
1235
  deliveredUnconsumedUnknownStreak.delete(shortStreakKey);
1204
1236
  } else if (assignedRowLiveStatusIsAwaitingApproval(mesh, row.assignedNodeId, row.assignedSessionId)) {