@adhdev/daemon-core 1.0.27 → 1.0.28-rc.2
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/index.js +60 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +60 -4
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-completion-synthesis.d.ts +14 -0
- package/package.json +3 -3
- package/src/mesh/mesh-completion-synthesis.ts +85 -1
- package/src/mesh/mesh-reconcile-loop.ts +32 -0
|
@@ -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.
|
|
3
|
+
"version": "1.0.28-rc.2",
|
|
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": "
|
|
51
|
-
"@adhdev/session-host-core": "
|
|
50
|
+
"@adhdev/mesh-shared": "1.0.28-rc.2",
|
|
51
|
+
"@adhdev/session-host-core": "1.0.28-rc.2",
|
|
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)) {
|