@adhdev/daemon-core 1.0.21-rc.4 → 1.0.21-rc.5
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/cli-adapters/provider-cli-shared.d.ts +11 -1
- package/dist/index.js +76 -24
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +76 -24
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-completion-synthesis.d.ts +10 -1
- package/dist/mesh/mesh-events-stale.d.ts +21 -0
- package/dist/providers/spec/fsm-types.d.ts +3 -0
- package/package.json +3 -3
- package/src/cli-adapters/provider-cli-shared.ts +11 -1
- package/src/mesh/mesh-completion-synthesis.ts +31 -2
- package/src/mesh/mesh-events-stale.ts +28 -3
- package/src/mesh/mesh-reconcile-loop.ts +84 -13
- package/src/providers/cli-provider-instance.ts +26 -7
- package/src/providers/provider-schema.ts +1 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +6 -1
- package/src/providers/spec/fsm-types.ts +3 -0
|
@@ -2,6 +2,15 @@ import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
|
2
2
|
import type { LocalMeshEntry } from '../repo-mesh-types.js';
|
|
3
3
|
export declare function reconcileUnterminatedDirectDispatches(components: DaemonComponents, mesh: LocalMeshEntry, selfIds: string[], localDaemonId: string | undefined): Promise<void>;
|
|
4
4
|
export declare function autoPruneStaleDirectDispatches(components: DaemonComponents, mesh: LocalMeshEntry, selfIds: string[], localDaemonId: string | undefined, minAgeMs: number): Promise<void>;
|
|
5
|
+
export interface AssignedTaskTerminalEvidence {
|
|
6
|
+
outcome: 'completed';
|
|
7
|
+
finalSummary: string;
|
|
8
|
+
transcriptMessageAt?: string;
|
|
9
|
+
providerSessionId?: string;
|
|
10
|
+
providerType?: string;
|
|
11
|
+
nodeId?: string;
|
|
12
|
+
sessionId: string;
|
|
13
|
+
}
|
|
5
14
|
export declare function pollAssignedTaskTerminalEvidence(components: DaemonComponents, mesh: {
|
|
6
15
|
id: string;
|
|
7
16
|
nodes?: Array<{
|
|
@@ -15,4 +24,4 @@ export declare function pollAssignedTaskTerminalEvidence(components: DaemonCompo
|
|
|
15
24
|
assignedNodeId?: string;
|
|
16
25
|
assignedProviderType?: string;
|
|
17
26
|
dispatchTimestamp?: string;
|
|
18
|
-
}): Promise<
|
|
27
|
+
}): Promise<AssignedTaskTerminalEvidence | null>;
|
|
@@ -43,6 +43,27 @@ export declare function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
43
43
|
* ledger entry below; absent on legacy rows → daemon-level routing (unchanged).
|
|
44
44
|
*/
|
|
45
45
|
targetCoordinatorSessionId?: string;
|
|
46
|
+
/**
|
|
47
|
+
* WATCHDOG-FINALSUMMARY-LOST: fallback dispatch anchor for a CLAIM-PATH row that has no
|
|
48
|
+
* `task_dispatched` direct-dispatch ledger entry (findDirectDispatchLedgerEntry → null). The
|
|
49
|
+
* assigned-stranded watchdog already PROVED the transcript is idle-with-final-assistant AND
|
|
50
|
+
* dated at/after this timestamp before calling; supplying it dates the completion for the
|
|
51
|
+
* ledger/diagnostics. Direct-dispatch callers omit it → the ledger entry's timestamp is used
|
|
52
|
+
* (unchanged).
|
|
53
|
+
*/
|
|
54
|
+
dispatchTimestamp?: string;
|
|
55
|
+
/**
|
|
56
|
+
* WATCHDOG-FINALSUMMARY-LOST: the caller (the assigned-stranded watchdog poll,
|
|
57
|
+
* pollAssignedTaskTerminalEvidence) has ALREADY enforced the turn-end proof this function's
|
|
58
|
+
* grace + transcript_not_proven gates exist for — a settled-idle read, a final assistant dated
|
|
59
|
+
* at/after dispatch, no trailing tool activity, and a continuous-idle streak. Those gates guard
|
|
60
|
+
* the direct-dispatch first-idle-tick synth that fires ~1s after dispatch off a possibly-stale
|
|
61
|
+
* tail; the watchdog does not fire until that proof holds, so re-applying the (60s/120s) grace
|
|
62
|
+
* here would only DELAY a completion the watchdog already validated. When set, skip the grace +
|
|
63
|
+
* transcript_not_proven gates (the terminal-ledger dedup + weak-supersession backstops remain).
|
|
64
|
+
* Direct-dispatch callers omit it → the gates apply exactly as before (no regression).
|
|
65
|
+
*/
|
|
66
|
+
preValidatedTranscriptEvidence?: boolean;
|
|
46
67
|
source?: string;
|
|
47
68
|
}): {
|
|
48
69
|
reconciled: boolean;
|
|
@@ -181,6 +181,9 @@ export interface CliSpecV4 {
|
|
|
181
181
|
delegate?: DelegateTrigger[];
|
|
182
182
|
native_history?: NativeHistoryConfig;
|
|
183
183
|
requiresFinalAssistantBeforeIdle?: boolean;
|
|
184
|
+
/** Completion-timing HOLD class (native-history provider whose answer lands in native history, e.g.
|
|
185
|
+
* antigravity-cli): idle-without-final-assistant holds for the transcript instead of emitting/flooring. */
|
|
186
|
+
holdCompletionForTranscript?: boolean;
|
|
184
187
|
}
|
|
185
188
|
export declare function isV4Spec(raw: unknown): raw is CliSpecV4;
|
|
186
189
|
export declare function initialState(spec: CliSpecV4): FsmState;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "1.0.21-rc.
|
|
3
|
+
"version": "1.0.21-rc.5",
|
|
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.21-rc.
|
|
51
|
-
"@adhdev/session-host-core": "1.0.21-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "1.0.21-rc.5",
|
|
51
|
+
"@adhdev/session-host-core": "1.0.21-rc.5",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -215,8 +215,18 @@ export interface CliProviderModule {
|
|
|
215
215
|
requirePromptEchoBeforeSubmit?: boolean;
|
|
216
216
|
/** Allow sending another prompt while the CLI is still generating so users can intervene mid-turn. */
|
|
217
217
|
allowInputDuringGeneration?: boolean;
|
|
218
|
-
/** When true, only transition to idle after the parsed transcript includes a final standard assistant message.
|
|
218
|
+
/** When true, only transition to idle after the parsed transcript includes a final standard assistant message.
|
|
219
|
+
* Also selects the completion-timing FLOOR (mid-tool-valley-prone native source, e.g. codex/kimi/cursor/opencode):
|
|
220
|
+
* an idle-without-final-assistant holds the decoupled CANON-C completion under the min-elapsed floor. False = a
|
|
221
|
+
* write-lag native source (e.g. claude-cli) that emits immediately. See holdCompletionForTranscript for the
|
|
222
|
+
* third (hold) class. */
|
|
219
223
|
requiresFinalAssistantBeforeIdle?: boolean;
|
|
224
|
+
/** Completion-timing HOLD class (e.g. antigravity-cli): a native-history provider whose live idle verdict is
|
|
225
|
+
* PTY-derived but whose authoritative answer lands in native history. When true, an idle-without-final-assistant
|
|
226
|
+
* HOLDS for the transcript to land (holdForTranscript) instead of emitting/flooring, and keeps holding past the
|
|
227
|
+
* 30s finalization cap while the PTY is still active (bounded by the hard cap). The third completion-timing class
|
|
228
|
+
* alongside immediate (requiresFinalAssistantBeforeIdle=false) and floor (requiresFinalAssistantBeforeIdle=true). */
|
|
229
|
+
holdCompletionForTranscript?: boolean;
|
|
220
230
|
/** When true, allow providers to augment stale snapshot data before parse. Reserved for future use. */
|
|
221
231
|
augmentStaleSnapshot?: boolean;
|
|
222
232
|
/** When provider-owned, daemon treats provider parser output as canonical transcript authority. */
|
|
@@ -432,11 +432,28 @@ export async function autoPruneStaleDirectDispatches(
|
|
|
432
432
|
// Conservative: a non-idle read, a read failure, no final summary, or a summary provably BEFORE
|
|
433
433
|
// dispatch all yield null (fall through to the caller's normal reclaim decision) — the poll can
|
|
434
434
|
// only PREVENT a wrong re-drive, never invent a completion.
|
|
435
|
+
//
|
|
436
|
+
// WATCHDOG-FINALSUMMARY-LOST: the poll now returns the FINAL ASSISTANT SUMMARY it read (not just
|
|
437
|
+
// the 'completed' verdict) so the watchdog caller can propagate a finalSummary-bearing completion
|
|
438
|
+
// to the coordinator (the SAME [System] notification a native generating_completed produces),
|
|
439
|
+
// instead of dropping the summary and only flipping the row + tracing a structural DROP. The
|
|
440
|
+
// evidence carried here is exactly what reconcileDirectDispatchCompletionFromTranscript needs to
|
|
441
|
+
// build and queue that completion.
|
|
442
|
+
export interface AssignedTaskTerminalEvidence {
|
|
443
|
+
outcome: 'completed';
|
|
444
|
+
finalSummary: string;
|
|
445
|
+
transcriptMessageAt?: string;
|
|
446
|
+
providerSessionId?: string;
|
|
447
|
+
providerType?: string;
|
|
448
|
+
nodeId?: string;
|
|
449
|
+
sessionId: string;
|
|
450
|
+
}
|
|
451
|
+
|
|
435
452
|
export async function pollAssignedTaskTerminalEvidence(
|
|
436
453
|
components: DaemonComponents,
|
|
437
454
|
mesh: { id: string; nodes?: Array<{ id: string; daemonId?: string; workspace?: string }> },
|
|
438
455
|
row: { id: string; assignedSessionId?: string; assignedNodeId?: string; assignedProviderType?: string; dispatchTimestamp?: string },
|
|
439
|
-
): Promise<
|
|
456
|
+
): Promise<AssignedTaskTerminalEvidence | null> {
|
|
440
457
|
const sessionId = readNonEmptyString(row.assignedSessionId);
|
|
441
458
|
const nodeId = readNonEmptyString(row.assignedNodeId);
|
|
442
459
|
if (!sessionId || !nodeId) return null; // no worker to read
|
|
@@ -508,5 +525,17 @@ export async function pollAssignedTaskTerminalEvidence(
|
|
|
508
525
|
// cannot distinguish a self-reported failure from the plain transcript tail here (that lives
|
|
509
526
|
// in buildTaskCompletionEvidence's structured-result path), and the alternative — re-driving a
|
|
510
527
|
// finished worker — is strictly worse, so a proven turn-end short-circuits to 'completed'.
|
|
511
|
-
|
|
528
|
+
//
|
|
529
|
+
// WATCHDOG-FINALSUMMARY-LOST: carry the read evidence back to the caller so it can propagate a
|
|
530
|
+
// finalSummary-bearing completion (not just flip the row). providerSessionId is best-effort from
|
|
531
|
+
// the read payload — absent → daemon-level routing (unchanged from the reconcile paths).
|
|
532
|
+
return {
|
|
533
|
+
outcome: 'completed',
|
|
534
|
+
finalSummary: evidence.finalSummary,
|
|
535
|
+
...(evidence.transcriptMessageAt ? { transcriptMessageAt: evidence.transcriptMessageAt } : {}),
|
|
536
|
+
...(readNonEmptyString(payload.providerSessionId) ? { providerSessionId: readNonEmptyString(payload.providerSessionId) } : {}),
|
|
537
|
+
...(providerType ? { providerType } : {}),
|
|
538
|
+
...(nodeId ? { nodeId } : {}),
|
|
539
|
+
sessionId,
|
|
540
|
+
};
|
|
512
541
|
}
|
|
@@ -208,6 +208,27 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
208
208
|
* ledger entry below; absent on legacy rows → daemon-level routing (unchanged).
|
|
209
209
|
*/
|
|
210
210
|
targetCoordinatorSessionId?: string;
|
|
211
|
+
/**
|
|
212
|
+
* WATCHDOG-FINALSUMMARY-LOST: fallback dispatch anchor for a CLAIM-PATH row that has no
|
|
213
|
+
* `task_dispatched` direct-dispatch ledger entry (findDirectDispatchLedgerEntry → null). The
|
|
214
|
+
* assigned-stranded watchdog already PROVED the transcript is idle-with-final-assistant AND
|
|
215
|
+
* dated at/after this timestamp before calling; supplying it dates the completion for the
|
|
216
|
+
* ledger/diagnostics. Direct-dispatch callers omit it → the ledger entry's timestamp is used
|
|
217
|
+
* (unchanged).
|
|
218
|
+
*/
|
|
219
|
+
dispatchTimestamp?: string;
|
|
220
|
+
/**
|
|
221
|
+
* WATCHDOG-FINALSUMMARY-LOST: the caller (the assigned-stranded watchdog poll,
|
|
222
|
+
* pollAssignedTaskTerminalEvidence) has ALREADY enforced the turn-end proof this function's
|
|
223
|
+
* grace + transcript_not_proven gates exist for — a settled-idle read, a final assistant dated
|
|
224
|
+
* at/after dispatch, no trailing tool activity, and a continuous-idle streak. Those gates guard
|
|
225
|
+
* the direct-dispatch first-idle-tick synth that fires ~1s after dispatch off a possibly-stale
|
|
226
|
+
* tail; the watchdog does not fire until that proof holds, so re-applying the (60s/120s) grace
|
|
227
|
+
* here would only DELAY a completion the watchdog already validated. When set, skip the grace +
|
|
228
|
+
* transcript_not_proven gates (the terminal-ledger dedup + weak-supersession backstops remain).
|
|
229
|
+
* Direct-dispatch callers omit it → the gates apply exactly as before (no regression).
|
|
230
|
+
*/
|
|
231
|
+
preValidatedTranscriptEvidence?: boolean;
|
|
211
232
|
source?: string;
|
|
212
233
|
}): { reconciled: boolean; kind?: MeshLedgerKind; alreadyTerminal?: boolean; workerResult?: unknown; ledgerEntryId?: string; reason?: string } {
|
|
213
234
|
const finalSummary = readNonEmptyString(args.finalSummary);
|
|
@@ -251,10 +272,14 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
251
272
|
// `…::genuine` slot free for the worker's own later agent:generating_completed to surface (the
|
|
252
273
|
// CANON-B weak→genuine supersession) instead of being dropped as a duplicate.
|
|
253
274
|
const selfAttributing = workerResult.source === 'final_summary_json';
|
|
254
|
-
|
|
275
|
+
// WATCHDOG-FINALSUMMARY-LOST: prefer the ledger dispatch timestamp; fall back to the caller's
|
|
276
|
+
// proven anchor (a claim-path row has no task_dispatched ledger entry to date).
|
|
277
|
+
const dispatchTime = dispatch?.timestamp
|
|
278
|
+
? new Date(dispatch.timestamp).getTime()
|
|
279
|
+
: (args.dispatchTimestamp ? new Date(args.dispatchTimestamp).getTime() : Number.NaN);
|
|
255
280
|
const transcriptTime = args.transcriptMessageAt ? new Date(args.transcriptMessageAt).getTime() : Number.NaN;
|
|
256
281
|
const transcriptAfterDispatch = Number.isFinite(dispatchTime) && Number.isFinite(transcriptTime) && transcriptTime >= dispatchTime;
|
|
257
|
-
if (workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
|
|
282
|
+
if (!args.preValidatedTranscriptEvidence && workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
|
|
258
283
|
return { reconciled: false, reason: 'transcript_not_proven_after_dispatch' };
|
|
259
284
|
}
|
|
260
285
|
// NOTIF-MISS grace gate (FIX 1): refuse to synthesize a completion for a direct dispatch
|
|
@@ -271,7 +296,7 @@ export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
|
271
296
|
// so it is exempt; the grace only guards the ambiguous plain-text-tail case. When the dispatch
|
|
272
297
|
// entry is missing we cannot date it and do NOT block (the stale-summary timestamp guard +
|
|
273
298
|
// FIX 2 supersession remain the correctness backstops).
|
|
274
|
-
if (Number.isFinite(dispatchTime) && workerResult.source !== 'final_summary_json') {
|
|
299
|
+
if (!args.preValidatedTranscriptEvidence && Number.isFinite(dispatchTime) && workerResult.source !== 'final_summary_json') {
|
|
275
300
|
const dispatchedToIdleSession = dispatch?.payload?.dispatchedToIdleSession === true;
|
|
276
301
|
const graceMs = dispatchedToIdleSession
|
|
277
302
|
? DIRECT_DISPATCH_IDLE_SESSION_RECONCILE_GRACE_MS
|
|
@@ -64,7 +64,7 @@ import { getQueue, reclaimStrandedAssignedTask, updateTaskStatus } from './mesh-
|
|
|
64
64
|
import { resolveSessionBusyVerdict, runContinuousAutoFastForwardScan, runPendingCoordinatorCatchupScan } from './mesh-queue-assignment.js';
|
|
65
65
|
import { readLedgerEntries } from './mesh-ledger.js';
|
|
66
66
|
import type { MeshLedgerEntry } from './mesh-ledger.js';
|
|
67
|
-
import { findTerminalLedgerEvidenceForTask } from './mesh-events-stale.js';
|
|
67
|
+
import { findTerminalLedgerEvidenceForTask, reconcileDirectDispatchCompletionFromTranscript } from './mesh-events-stale.js';
|
|
68
68
|
import {
|
|
69
69
|
resolveCoordinatorDaemonIds,
|
|
70
70
|
daemonHostsMesh,
|
|
@@ -84,6 +84,7 @@ import {
|
|
|
84
84
|
reconcileUnterminatedDirectDispatches,
|
|
85
85
|
autoPruneStaleDirectDispatches,
|
|
86
86
|
pollAssignedTaskTerminalEvidence,
|
|
87
|
+
type AssignedTaskTerminalEvidence,
|
|
87
88
|
} from './mesh-completion-synthesis.js';
|
|
88
89
|
import { sessionStatusFromNodes } from './mesh-active-work.js';
|
|
89
90
|
|
|
@@ -941,6 +942,60 @@ async function evaluateEarlyIdleTranscriptArm(
|
|
|
941
942
|
return verdict === 'UNKNOWN'; // remote, unknowable class — trust the (a) reprobe
|
|
942
943
|
}
|
|
943
944
|
|
|
945
|
+
// WATCHDOG-FINALSUMMARY-LOST. When the assigned-stranded watchdog / delivered-no-turn deadline
|
|
946
|
+
// proves a task terminal from the worker transcript (pollAssignedTaskTerminalEvidence), it must
|
|
947
|
+
// PROPAGATE that completion to the coordinator the SAME way a native agent:generating_completed
|
|
948
|
+
// does — a finalSummary-bearing [System] notification — not merely flip the queue row and trace a
|
|
949
|
+
// structural DROP. A provider that finishes early (e.g. codex ~25s) is watchdog-completed here; if
|
|
950
|
+
// the summary is dropped the coordinator NEVER learns what the worker produced (kimi only got it by
|
|
951
|
+
// the lucky timing of the 180s stall-reconcile's second emit).
|
|
952
|
+
//
|
|
953
|
+
// reconcileDirectDispatchCompletionFromTranscript is the shared native-completion synth: it writes
|
|
954
|
+
// the terminal ledger WITH the finalSummary and queues the coordinator event with the same
|
|
955
|
+
// buildMeshSystemMessage the native path uses. It is idempotent — hasTerminalLedgerAfterDispatch
|
|
956
|
+
// makes a second call (or a later real emit) a no-op (alreadyTerminal), and its non-self-attributing
|
|
957
|
+
// synth is marked WEAK so the worker's own later emit can still supersede it rather than being
|
|
958
|
+
// dropped as a duplicate. That is the dedup guarantee: at most one [System] completion surfaces.
|
|
959
|
+
//
|
|
960
|
+
// Returns true if a terminal ledger for this task now exists (freshly written OR already present) —
|
|
961
|
+
// the caller then skips its own bare-payload ledger write, which lacked the finalSummary entirely.
|
|
962
|
+
function propagateWatchdogTranscriptCompletion(
|
|
963
|
+
meshId: string,
|
|
964
|
+
row: { id: string; assignedNodeId?: string; assignedSessionId?: string; assignedProviderType?: string; dispatchTimestamp?: string },
|
|
965
|
+
evidence: AssignedTaskTerminalEvidence,
|
|
966
|
+
source: string,
|
|
967
|
+
): boolean {
|
|
968
|
+
const sessionId = readNonEmptyString(row.assignedSessionId) || evidence.sessionId;
|
|
969
|
+
if (!sessionId || !readNonEmptyString(evidence.finalSummary)) {
|
|
970
|
+
// No routable session or no summary to carry — fall back to the caller's bare ledger write.
|
|
971
|
+
return false;
|
|
972
|
+
}
|
|
973
|
+
try {
|
|
974
|
+
const result = reconcileDirectDispatchCompletionFromTranscript({
|
|
975
|
+
meshId,
|
|
976
|
+
nodeId: readNonEmptyString(row.assignedNodeId) || evidence.nodeId,
|
|
977
|
+
sessionId,
|
|
978
|
+
providerType: readNonEmptyString(row.assignedProviderType) || evidence.providerType,
|
|
979
|
+
providerSessionId: evidence.providerSessionId,
|
|
980
|
+
taskId: row.id,
|
|
981
|
+
finalSummary: evidence.finalSummary,
|
|
982
|
+
...(evidence.transcriptMessageAt ? { transcriptMessageAt: evidence.transcriptMessageAt } : {}),
|
|
983
|
+
...(readNonEmptyString(row.dispatchTimestamp) ? { dispatchTimestamp: readNonEmptyString(row.dispatchTimestamp) } : {}),
|
|
984
|
+
// The watchdog poll already enforced idle + post-dispatch + no-trailing-tool + streak;
|
|
985
|
+
// skip the reconcile's own grace/transcript_not_proven gates (dedup backstops remain).
|
|
986
|
+
preValidatedTranscriptEvidence: true,
|
|
987
|
+
source,
|
|
988
|
+
});
|
|
989
|
+
// reconciled → freshly queued the coordinator completion; alreadyTerminal → a terminal
|
|
990
|
+
// ledger (a real emit that raced in, or a prior watchdog synth) is already present. Either
|
|
991
|
+
// way a finalSummary-bearing completion has been (or will be) delivered; the caller must NOT
|
|
992
|
+
// add its summary-less ledger row on top.
|
|
993
|
+
return result.reconciled || result.alreadyTerminal === true;
|
|
994
|
+
} catch {
|
|
995
|
+
return false; // best-effort — fall back to the caller's bare ledger write
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
|
|
944
999
|
// PHASE 2.5 — assigned-stranded dispatch watchdog (Bug B). claimNextTask atomically
|
|
945
1000
|
// flips a row to 'assigned' BEFORE the fire-and-forget dispatch runs. If that dispatch
|
|
946
1001
|
// neither rejects (→ no .catch requeue) nor is confirmed delivered — a relay that hangs
|
|
@@ -1013,11 +1068,19 @@ async function recoverStrandedAssignedDispatches(
|
|
|
1013
1068
|
const terminalEvidence = await pollAssignedTaskTerminalEvidence(components, mesh, row);
|
|
1014
1069
|
if (terminalEvidence) {
|
|
1015
1070
|
assignedIdleFinalAssistantSince.delete(idleTranscriptStreakKey);
|
|
1016
|
-
updateTaskStatus(meshId, row.id, terminalEvidence);
|
|
1017
|
-
|
|
1071
|
+
updateTaskStatus(meshId, row.id, terminalEvidence.outcome);
|
|
1072
|
+
// WATCHDOG-FINALSUMMARY-LOST: propagate the completion to the coordinator WITH the
|
|
1073
|
+
// finalSummary the poll read — the SAME [System] notification the native
|
|
1074
|
+
// generating_completed produces — instead of only tracing a structural DROP. When
|
|
1075
|
+
// propagation delivered (or a terminal ledger already exists), skip the bare
|
|
1076
|
+
// summary-less ledger write below; only fall back to it if propagation could not run.
|
|
1077
|
+
const propagated = propagateWatchdogTranscriptCompletion(
|
|
1078
|
+
meshId, row, terminalEvidence, 'early_idle_transcript_evidence',
|
|
1079
|
+
);
|
|
1080
|
+
if (!propagated && !findTerminalLedgerEvidenceForTask({ meshId, taskId: row.id })) {
|
|
1018
1081
|
try {
|
|
1019
1082
|
appendLedgerEntry(meshId, {
|
|
1020
|
-
kind: terminalEvidence === 'completed' ? 'task_completed' : 'task_failed',
|
|
1083
|
+
kind: terminalEvidence.outcome === 'completed' ? 'task_completed' : 'task_failed',
|
|
1021
1084
|
nodeId: row.assignedNodeId,
|
|
1022
1085
|
sessionId: row.assignedSessionId,
|
|
1023
1086
|
providerType: row.assignedProviderType,
|
|
@@ -1032,14 +1095,15 @@ async function recoverStrandedAssignedDispatches(
|
|
|
1032
1095
|
LOG.warn('MeshReconcile', `Early-completed assigned task ${row.id} on mesh ${meshId} `
|
|
1033
1096
|
+ `(node=${row.assignedNodeId ?? '?'} session=${row.assignedSessionId ?? '?'}): worker idle with a `
|
|
1034
1097
|
+ `final assistant message after dispatch for ≥${Math.round(ASSIGNED_IDLE_TRANSCRIPT_COMPLETE_MS / 1000)}s — `
|
|
1035
|
-
+ `the completion event was lost/late (pure-PTY provider), task is ${terminalEvidence} without waiting the 15-min deadline`
|
|
1036
|
-
|
|
1098
|
+
+ `the completion event was lost/late (pure-PTY provider), task is ${terminalEvidence.outcome} without waiting the 15-min deadline`
|
|
1099
|
+
+ `${propagated ? ' (finalSummary propagated to coordinator)' : ''}`);
|
|
1100
|
+
traceMeshEventStage('assigned_early_transcript_completed', {
|
|
1037
1101
|
taskId: row.id,
|
|
1038
1102
|
sessionId: row.assignedSessionId,
|
|
1039
1103
|
nodeId: row.assignedNodeId,
|
|
1040
1104
|
meshId,
|
|
1041
1105
|
event: 'agent:generating_completed',
|
|
1042
|
-
}, terminalEvidence);
|
|
1106
|
+
}, propagated ? 'propagated' : terminalEvidence.outcome);
|
|
1043
1107
|
continue;
|
|
1044
1108
|
}
|
|
1045
1109
|
// Poll was inconclusive (mid-turn re-check / stale tail / unreadable) — reset the
|
|
@@ -1290,11 +1354,17 @@ async function recoverStrandedAssignedDispatches(
|
|
|
1290
1354
|
deliveredNoTurnUnknownStreak.delete(streakKey);
|
|
1291
1355
|
// updateTaskStatus ends the single-flight dispatch window on any transition off
|
|
1292
1356
|
// 'assigned', so a later requeue/re-dispatch is never blocked by a stale mark.
|
|
1293
|
-
updateTaskStatus(meshId, row.id, terminalEvidence);
|
|
1294
|
-
|
|
1357
|
+
updateTaskStatus(meshId, row.id, terminalEvidence.outcome);
|
|
1358
|
+
// WATCHDOG-FINALSUMMARY-LOST: propagate the finalSummary-bearing completion to the
|
|
1359
|
+
// coordinator (same [System] notification as the native path); only fall back to the
|
|
1360
|
+
// bare summary-less ledger write when propagation could not run.
|
|
1361
|
+
const propagated = propagateWatchdogTranscriptCompletion(
|
|
1362
|
+
meshId, row, terminalEvidence, 'redrive_deadline_transcript_evidence',
|
|
1363
|
+
);
|
|
1364
|
+
if (!propagated && !findTerminalLedgerEvidenceForTask({ meshId, taskId: row.id })) {
|
|
1295
1365
|
try {
|
|
1296
1366
|
appendLedgerEntry(meshId, {
|
|
1297
|
-
kind: terminalEvidence === 'completed' ? 'task_completed' : 'task_failed',
|
|
1367
|
+
kind: terminalEvidence.outcome === 'completed' ? 'task_completed' : 'task_failed',
|
|
1298
1368
|
nodeId: row.assignedNodeId,
|
|
1299
1369
|
sessionId: row.assignedSessionId,
|
|
1300
1370
|
providerType: row.assignedProviderType,
|
|
@@ -1308,14 +1378,15 @@ async function recoverStrandedAssignedDispatches(
|
|
|
1308
1378
|
}
|
|
1309
1379
|
LOG.warn('MeshReconcile', `Skipped delivered-no-turn re-drive for task ${row.id} on mesh ${meshId} `
|
|
1310
1380
|
+ `(node=${row.assignedNodeId ?? '?'} session=${row.assignedSessionId ?? '?'}): worker transcript is idle with a `
|
|
1311
|
-
+ `final assistant message after dispatch — the completion event was lost/late, task is ${terminalEvidence}, NOT re-driving`
|
|
1312
|
-
|
|
1381
|
+
+ `final assistant message after dispatch — the completion event was lost/late, task is ${terminalEvidence.outcome}, NOT re-driving`
|
|
1382
|
+
+ `${propagated ? ' (finalSummary propagated to coordinator)' : ''}`);
|
|
1383
|
+
traceMeshEventStage('redrive_deadline_transcript_completed', {
|
|
1313
1384
|
taskId: row.id,
|
|
1314
1385
|
sessionId: row.assignedSessionId,
|
|
1315
1386
|
nodeId: row.assignedNodeId,
|
|
1316
1387
|
meshId,
|
|
1317
1388
|
event: 'agent:generating_completed',
|
|
1318
|
-
}, `${reclaimReason} → transcript ${terminalEvidence}`);
|
|
1389
|
+
}, `${reclaimReason} → transcript ${terminalEvidence.outcome}${propagated ? ' propagated' : ''}`);
|
|
1319
1390
|
continue;
|
|
1320
1391
|
}
|
|
1321
1392
|
const reclaimedLost = reclaimStrandedAssignedTask(meshId, row.id, {
|
|
@@ -2086,7 +2086,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2086
2086
|
// eventually emits" fix while filtering the mid-turn false-idle. Scoped to
|
|
2087
2087
|
// autonomous mesh sessions (allowMissingAssistantTimeout) so an interactive
|
|
2088
2088
|
// antigravity session, which has no coordinator to misfire at, is untouched.
|
|
2089
|
-
|
|
2089
|
+
//
|
|
2090
|
+
// SPEC-DRIVEN completion timing (mission f2f6da1b root 2): the HOLD class is the
|
|
2091
|
+
// manifest flag `holdCompletionForTranscript` (antigravity-cli declares it), NOT a
|
|
2092
|
+
// hardcoded provider name — a native-history provider whose PTY-derived idle can
|
|
2093
|
+
// precede the authoritative transcript write. write-lag (claude) and floor
|
|
2094
|
+
// (codex/kimi/…) providers leave it false and fall through to the decision below.
|
|
2095
|
+
if ((this.provider as any)?.holdCompletionForTranscript === true) {
|
|
2090
2096
|
if (allowMissingAssistantTimeout) {
|
|
2091
2097
|
return { reason: 'missing_final_assistant', terminal: false, holdForTranscript: true };
|
|
2092
2098
|
}
|
|
@@ -2135,9 +2141,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2135
2141
|
// reaches the floor because the next tool call's idle→busy transition cancels
|
|
2136
2142
|
// the pending.
|
|
2137
2143
|
//
|
|
2138
|
-
//
|
|
2139
|
-
//
|
|
2140
|
-
|
|
2144
|
+
// SPEC-DRIVEN completion timing (mission f2f6da1b root 2): the HOLD class already
|
|
2145
|
+
// returned above (holdCompletionForTranscript). The remaining two classes are the
|
|
2146
|
+
// manifest flag `requiresFinalAssistantBeforeIdle`:
|
|
2147
|
+
// • FLOOR — declared (codex-cli / cursor-cli / kimi / opencode): "an idle without
|
|
2148
|
+
// a final assistant reply is NOT a genuine turn-end", exactly the CANON-C
|
|
2149
|
+
// min-elapsed floor's precondition, so it gets the floor
|
|
2150
|
+
// (noExternalTranscriptSource).
|
|
2151
|
+
// • IMMEDIATE (write-lag) — not declared (claude-cli): idle is authoritative, the
|
|
2152
|
+
// transcript write merely trails by a fraction of a second, so it emits
|
|
2153
|
+
// IMMEDIATELY, un-floored.
|
|
2154
|
+
// This is the SAME flag the sibling branch below (source !== 'external-native')
|
|
2155
|
+
// uses for the identical noExternalTranscriptSource decision, so the two branches
|
|
2156
|
+
// agree; and it covers third-party providers instead of a hardcoded provider name.
|
|
2157
|
+
const isWriteLagNativeSource = (this.provider as any).requiresFinalAssistantBeforeIdle !== true;
|
|
2141
2158
|
return {
|
|
2142
2159
|
reason: 'missing_final_assistant',
|
|
2143
2160
|
terminal: true,
|
|
@@ -3241,9 +3258,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3241
3258
|
// transcript branch above clears the block for a genuine emit once the answer lands).
|
|
3242
3259
|
// A genuinely quiescent tool-only turn (no assistant bubble, PTY stable) falls through
|
|
3243
3260
|
// to the existing weak force-emit; a runaway PTY that never quiets releases at the hard
|
|
3244
|
-
// cap so it cannot wedge forever.
|
|
3245
|
-
//
|
|
3246
|
-
|
|
3261
|
+
// cap so it cannot wedge forever. SPEC-DRIVEN (mission f2f6da1b root 2): scoped to the
|
|
3262
|
+
// HOLD completion-timing class (`holdCompletionForTranscript`, e.g. antigravity-cli) via
|
|
3263
|
+
// its holdForTranscript block — NOT a hardcoded provider name — so claude-cli/codex-cli
|
|
3264
|
+
// (immediate/floor classes, which never produce a holdForTranscript block) are unchanged.
|
|
3265
|
+
if ((this.provider as any)?.holdCompletionForTranscript === true
|
|
3247
3266
|
&& block.holdForTranscript === true
|
|
3248
3267
|
&& waitedMs < ANTIGRAVITY_HOLD_HARD_CAP_MS
|
|
3249
3268
|
&& this.antigravityHoldPtyStillActive()) {
|
|
@@ -201,7 +201,12 @@
|
|
|
201
201
|
"requiresFinalAssistantBeforeIdle": {
|
|
202
202
|
"type": "boolean",
|
|
203
203
|
"default": false,
|
|
204
|
-
"description": "When true, daemon defers generating→idle until parseSession includes a final standard assistant message."
|
|
204
|
+
"description": "When true, daemon defers generating→idle until parseSession includes a final standard assistant message. Also selects the completion-timing floor for a native-source provider: an idle without a final assistant reply is treated as a possible mid-tool-call quiet valley, so the decoupled CANON-C completion is held under the min-elapsed floor (noExternalTranscriptSource) rather than emitted immediately. A provider that leaves this false is a write-lag native source (e.g. claude-cli) whose transcript merely trails the finished idle by a write, so it emits immediately."
|
|
205
|
+
},
|
|
206
|
+
"holdCompletionForTranscript": {
|
|
207
|
+
"type": "boolean",
|
|
208
|
+
"default": false,
|
|
209
|
+
"description": "Completion-timing authority for a native-history provider whose live idle verdict is PTY-derived but whose authoritative answer lands in native history (e.g. antigravity-cli). When true, an idle-without-final-assistant HOLDS for the native transcript to land (holdForTranscript) instead of emitting or applying the min-elapsed floor, and the hold persists past the 30s finalization cap while the PTY is still active (bounded by the hard cap). This is the third completion-timing class alongside immediate (requiresFinalAssistantBeforeIdle=false) and floor (requiresFinalAssistantBeforeIdle=true)."
|
|
205
210
|
},
|
|
206
211
|
"augmentStaleSnapshot": {
|
|
207
212
|
"type": "boolean",
|
|
@@ -246,6 +246,9 @@ export interface CliSpecV4 {
|
|
|
246
246
|
delegate?: DelegateTrigger[];
|
|
247
247
|
native_history?: NativeHistoryConfig;
|
|
248
248
|
requiresFinalAssistantBeforeIdle?: boolean;
|
|
249
|
+
/** Completion-timing HOLD class (native-history provider whose answer lands in native history, e.g.
|
|
250
|
+
* antigravity-cli): idle-without-final-assistant holds for the transcript instead of emitting/flooring. */
|
|
251
|
+
holdCompletionForTranscript?: boolean;
|
|
249
252
|
}
|
|
250
253
|
|
|
251
254
|
export function isV4Spec(raw: unknown): raw is CliSpecV4 {
|