@adhdev/daemon-core 1.0.18-rc.14 → 1.0.18-rc.16
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 +33 -0
- package/dist/index.js +198 -10
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +198 -10
- package/dist/index.mjs.map +1 -1
- package/dist/providers/chat-message-normalization.d.ts +22 -0
- package/dist/providers/cli-provider-instance.d.ts +27 -0
- package/package.json +3 -3
- package/src/cli-adapters/cli-state-engine.ts +17 -1
- package/src/cli-adapters/provider-cli-adapter.ts +2 -6
- package/src/cli-adapters/provider-cli-shared.ts +41 -0
- package/src/mesh/mesh-completion-synthesis.ts +12 -1
- package/src/mesh/mesh-reconcile-loop.ts +209 -1
- package/src/providers/chat-message-normalization.ts +53 -0
- package/src/providers/cli-provider-instance.ts +92 -0
|
@@ -58,6 +58,28 @@ export declare function extractFinalAssistantSummaryEvidence(messages: ChatMessa
|
|
|
58
58
|
* grace gate) on top of this structural check.
|
|
59
59
|
*/
|
|
60
60
|
export declare function selectFinalAssistantTurnEndMessage(messages: ChatMessage[] | null | undefined): ChatMessage | null;
|
|
61
|
+
/**
|
|
62
|
+
* EARLY-IDLE-COMPLETION-FALSE-POSITIVE — trailing tool/terminal activity detector.
|
|
63
|
+
*
|
|
64
|
+
* True when the transcript's LAST non-empty user-facing assistant bubble is followed
|
|
65
|
+
* by one or more TOOL / TERMINAL activity bubbles (a tool_use/command the assistant
|
|
66
|
+
* fired AFTER its text) — i.e. the assistant emitted a preamble ("Let me explore…"),
|
|
67
|
+
* then started running Read/Grep, so the turn is still executing and its answer has
|
|
68
|
+
* not landed. Callers that would otherwise promote that preamble to a turn-end summary
|
|
69
|
+
* off a momentary (startup-grace / inter-tool) idle read use this as a veto.
|
|
70
|
+
*
|
|
71
|
+
* Deliberately NARROW so the pure-PTY completion rescue is preserved:
|
|
72
|
+
* - Only TOOL/TERMINAL activity trailing the assistant vetoes. A trailing THOUGHT or
|
|
73
|
+
* status bubble does NOT (a finished turn can end on an internal thought), matching
|
|
74
|
+
* selectFinalAssistantTurnEndMessage's skip set minus the "still-working" signals.
|
|
75
|
+
* - A genuinely FINISHED worker (final assistant last, no trailing tool activity —
|
|
76
|
+
* the kimi pure-PTY continuous-idle shape) returns false, so its early completion
|
|
77
|
+
* is untouched.
|
|
78
|
+
*
|
|
79
|
+
* Returns false when there is no final assistant bubble at all (the caller's
|
|
80
|
+
* selectFinalAssistantTurnEndMessage already handles that as "not a turn end").
|
|
81
|
+
*/
|
|
82
|
+
export declare function hasTrailingToolActivityAfterFinalAssistant(messages: ChatMessage[] | null | undefined): boolean;
|
|
61
83
|
export declare const BUILTIN_CHAT_MESSAGE_KINDS: readonly ["standard", "thought", "tool", "terminal", "system"];
|
|
62
84
|
export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
|
|
63
85
|
export type ChatMessageKind = BuiltinChatMessageKind | (string & {});
|
|
@@ -619,6 +619,33 @@ export declare class CliProviderInstance implements ProviderInstance {
|
|
|
619
619
|
* call unconditionally from the cleanup path.
|
|
620
620
|
*/
|
|
621
621
|
flushMeshCompletionBeforeCleanup(): boolean;
|
|
622
|
+
/**
|
|
623
|
+
* KIMI-PURE-PTY-COMPLETION-EMIT (Fix 3): stall-path completion reconcile for the
|
|
624
|
+
* PURE-PTY transcript class (kimi and kin — no native transcript, no provider
|
|
625
|
+
* authority; see isPurePtyTranscriptProvider). Such a worker whose
|
|
626
|
+
* generating_completed never emitted (the onTurnStarted idle→idle collapse Fix 1
|
|
627
|
+
* fixes at the source) sits at a STATIC idle prompt: its PTY output stops the
|
|
628
|
+
* instant the answer is rendered, so the status-agnostic no-progress watchdog
|
|
629
|
+
* (checkMeshWorkerStall) reads the finished-but-quiet session as a stall and would
|
|
630
|
+
* false-fire monitor:no_progress. The native-transcript reconcile
|
|
631
|
+
* (sampleNativeTranscriptProgress) does NOT cover this class (no native source →
|
|
632
|
+
* null sample), so the stall path needs its own guard.
|
|
633
|
+
*
|
|
634
|
+
* Called from checkMeshWorkerStall just before the fire. Returns true when the
|
|
635
|
+
* session is a finished pure-PTY turn — idle, no pending response, and a PTY-parsed
|
|
636
|
+
* in-turn final assistant summary — in which case it emits the missing
|
|
637
|
+
* generating_completed (idempotent: a real/late emit writes the terminal ledger and
|
|
638
|
+
* the coordinator's reconcile makes any duplicate a no-op) and the caller SUPPRESSES
|
|
639
|
+
* the stall. Returns false for every other class/state (native-source provider, a
|
|
640
|
+
* genuinely mid-turn or wedged worker with no final assistant), so a real stall still
|
|
641
|
+
* fires unchanged.
|
|
642
|
+
*
|
|
643
|
+
* Evidence bar is identical to flushMeshCompletionBeforeCleanup: injected task's turn
|
|
644
|
+
* genuinely started + an in-turn final assistant summary. Conservative by
|
|
645
|
+
* construction — no summary ⇒ no proof of completion ⇒ return false and let the real
|
|
646
|
+
* stall fire.
|
|
647
|
+
*/
|
|
648
|
+
private tryReconcilePurePtyCompletionForStall;
|
|
622
649
|
/**
|
|
623
650
|
* AUTOAPPROVE-FLAP-RECUR (Fix A+B): how long a busy blip / modal scroll-out may
|
|
624
651
|
* persist before the in-progress settle gate is torn down. For a delegated
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "1.0.18-rc.
|
|
3
|
+
"version": "1.0.18-rc.16",
|
|
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.
|
|
51
|
-
"@adhdev/session-host-core": "1.0.18-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "1.0.18-rc.16",
|
|
51
|
+
"@adhdev/session-host-core": "1.0.18-rc.16",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
import {
|
|
16
16
|
buildCliScreenSnapshot,
|
|
17
17
|
compactPromptText,
|
|
18
|
+
isPurePtyTranscriptProvider,
|
|
18
19
|
normalizePromptText,
|
|
19
20
|
promptLikelyVisible,
|
|
20
21
|
type CliChatMessage,
|
|
@@ -361,7 +362,22 @@ export class CliStateEngine {
|
|
|
361
362
|
// idle transition, unchanged. Scoped to transcriptAuthority:'provider'
|
|
362
363
|
// only, so PTY-authoritative providers (whose spinner/settled parsing
|
|
363
364
|
// already drives generating promptly) are unaffected.
|
|
364
|
-
|
|
365
|
+
//
|
|
366
|
+
// (fix: kimi pure-PTY completion-emit) The pure-PTY full-buffer class
|
|
367
|
+
// (kimi and kin — see isPurePtyTranscriptProvider) is NOT
|
|
368
|
+
// transcriptAuthority:'provider', so without this it stays idle when a
|
|
369
|
+
// prompt is submitted from idle: the FSM never crosses generating→idle,
|
|
370
|
+
// detectStatusTransition's generating|waiting_approval→idle arm never
|
|
371
|
+
// runs, and agent:generating_completed is never emitted — the mesh
|
|
372
|
+
// coordinator leaves the task 'assigned' and the status-agnostic stall
|
|
373
|
+
// watchdog then false-fires task_stalled on the finished-but-idle
|
|
374
|
+
// session. Promoting this class to generating on turn-start makes the
|
|
375
|
+
// existing PTY-parsed final-assistant idle gate produce a real
|
|
376
|
+
// generating→idle completion edge. Same idle safety as above: applyIdle
|
|
377
|
+
// / finishResponse still own the actual idle transition.
|
|
378
|
+
const promoteOnTurnStart = this.provider.transcriptAuthority === 'provider'
|
|
379
|
+
|| isPurePtyTranscriptProvider(this.provider);
|
|
380
|
+
if (promoteOnTurnStart && this.currentStatus !== 'waiting_approval') {
|
|
365
381
|
this.setStatus('generating', 'turn_started');
|
|
366
382
|
this.callbacks.onStatusChange();
|
|
367
383
|
}
|
|
@@ -39,6 +39,7 @@ import {
|
|
|
39
39
|
compactPromptText,
|
|
40
40
|
estimatePromptDisplayLines,
|
|
41
41
|
extractPromptRetrySnippet,
|
|
42
|
+
isPurePtyTranscriptProvider,
|
|
42
43
|
listCliScriptNames,
|
|
43
44
|
normalizePromptText,
|
|
44
45
|
normalizeScreenSnapshot,
|
|
@@ -456,12 +457,7 @@ export class ProviderCliAdapter implements CliAdapter {
|
|
|
456
457
|
* provider-owned) so no other provider's turn-scoped parse changes.
|
|
457
458
|
*/
|
|
458
459
|
private parsesFullPtyTranscriptFromBuffer(): boolean {
|
|
459
|
-
|
|
460
|
-
// nativeHistory is a top-level provider field not surfaced on
|
|
461
|
-
// CliProviderModule; read it via the same structural cast used for `tui`.
|
|
462
|
-
if ((this.provider as { nativeHistory?: unknown }).nativeHistory) return false;
|
|
463
|
-
const transcriptPty = (this.provider.tui as { transcriptPty?: { scope?: unknown } } | undefined)?.transcriptPty;
|
|
464
|
-
return transcriptPty?.scope === 'buffer';
|
|
460
|
+
return isPurePtyTranscriptProvider(this.provider);
|
|
465
461
|
}
|
|
466
462
|
|
|
467
463
|
/**
|
|
@@ -254,6 +254,47 @@ export interface CliProviderModule {
|
|
|
254
254
|
_versionWarning?: string | null;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* PURE-PTY TRANSCRIPT CLASS predicate (kimi and kin).
|
|
259
|
+
*
|
|
260
|
+
* A provider is "pure-PTY full-buffer" when it reconstructs its ENTIRE transcript
|
|
261
|
+
* from the rendered PTY buffer on every read and has NO alternate transcript
|
|
262
|
+
* source:
|
|
263
|
+
* - transcriptAuthority !== 'provider' (the daemon PTY parser owns the transcript,
|
|
264
|
+
* not a provider-side canonical source)
|
|
265
|
+
* - NO nativeHistory (no on-disk / native-source history to fall back to)
|
|
266
|
+
* - tui.transcriptPty.scope === 'buffer' (the parser walks the full rendered buffer)
|
|
267
|
+
*
|
|
268
|
+
* This class is invisible to two provider-authority-keyed code paths that other
|
|
269
|
+
* providers rely on for mesh completion semantics:
|
|
270
|
+
* 1. CliStateEngine.onTurnStarted only promotes to 'generating' for
|
|
271
|
+
* transcriptAuthority==='provider' providers — so a pure-PTY session that
|
|
272
|
+
* submits a prompt while already idle collapses idle→idle, the
|
|
273
|
+
* generating→idle edge never occurs, and agent:generating_completed is never
|
|
274
|
+
* emitted (the coordinator ledger leaves the task 'assigned' forever).
|
|
275
|
+
* 2. checkMeshWorkerStall's native-transcript completion reconcile is gated on
|
|
276
|
+
* the native-source shape, so a finished pure-PTY worker's static idle is
|
|
277
|
+
* misread as monitor:no_progress (a false task_stalled).
|
|
278
|
+
*
|
|
279
|
+
* The runtime capability, NOT any single spec field value, is authoritative: a
|
|
280
|
+
* given kimi manifest checkout may declare nativeHistory/transcriptAuthority, but
|
|
281
|
+
* the live-loaded pure-PTY session has none. Callers that only hold a
|
|
282
|
+
* CliProviderModule (the engine) share this exact predicate with the adapter
|
|
283
|
+
* (parsesFullPtyTranscriptFromBuffer) so the two never drift.
|
|
284
|
+
*/
|
|
285
|
+
export function isPurePtyTranscriptProvider(provider: {
|
|
286
|
+
transcriptAuthority?: 'provider' | 'daemon';
|
|
287
|
+
nativeHistory?: unknown;
|
|
288
|
+
tui?: Record<string, unknown>;
|
|
289
|
+
}): boolean {
|
|
290
|
+
if (provider.transcriptAuthority === 'provider') return false;
|
|
291
|
+
// nativeHistory is a top-level provider field not surfaced on
|
|
292
|
+
// CliProviderModule; read it via the same structural shape the adapter uses.
|
|
293
|
+
if (provider.nativeHistory) return false;
|
|
294
|
+
const transcriptPty = (provider.tui as { transcriptPty?: { scope?: unknown } } | undefined)?.transcriptPty;
|
|
295
|
+
return transcriptPty?.scope === 'buffer';
|
|
296
|
+
}
|
|
297
|
+
|
|
257
298
|
function stripAnsi(str: string): string {
|
|
258
299
|
// eslint-disable-next-line no-control-regex
|
|
259
300
|
return str
|
|
@@ -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 } from '../providers/chat-message-normalization.js';
|
|
29
|
+
import { extractFinalAssistantSummaryEvidence, hasTrailingToolActivityAfterFinalAssistant } from '../providers/chat-message-normalization.js';
|
|
30
30
|
import type { ChatMessage } from '../types.js';
|
|
31
31
|
import {
|
|
32
32
|
getMeshV2BackstopCounters,
|
|
@@ -483,6 +483,17 @@ export async function pollAssignedTaskTerminalEvidence(
|
|
|
483
483
|
const evidence = extractFinalAssistantSummaryEvidence(messages);
|
|
484
484
|
if (!evidence.finalSummary) return null; // idle but no assistant result yet → not a turn-end
|
|
485
485
|
|
|
486
|
+
// EARLY-IDLE-COMPLETION-FALSE-POSITIVE (poll defense-in-depth): a momentary idle read
|
|
487
|
+
// (startup-grace, or the sliver between an assistant preamble and the tool it fires) can
|
|
488
|
+
// show an assistant "Let me explore…" bubble FOLLOWED by trailing Read/Grep tool_use — a
|
|
489
|
+
// turn that is still executing, not a turn-end. selectFinalAssistantTurnEndMessage skips
|
|
490
|
+
// those trailing tool bubbles and would promote the preamble, so guard here: if a
|
|
491
|
+
// tool/terminal activity bubble trails the final assistant message, the worker is mid-turn
|
|
492
|
+
// — refuse the completion (fall through to the caller's reclaim/grace path). A genuinely
|
|
493
|
+
// finished pure-PTY worker ends on its final assistant with no trailing tool activity, so
|
|
494
|
+
// the kimi rescue is preserved.
|
|
495
|
+
if (hasTrailingToolActivityAfterFinalAssistant(messages)) return null;
|
|
496
|
+
|
|
486
497
|
// Stale-summary guard (same bar as PHASE 4): a reused session's transcript tail may hold a
|
|
487
498
|
// PRIOR task's summary. Require the final assistant message to be dated at/after THIS task's
|
|
488
499
|
// dispatch. When either timestamp is unusable, do NOT short-circuit — fall through so we never
|
|
@@ -69,13 +69,15 @@ import {
|
|
|
69
69
|
resolveCoordinatorDaemonIds,
|
|
70
70
|
daemonHostsMesh,
|
|
71
71
|
resolveCoordinatorSelfIds,
|
|
72
|
+
daemonIdListIncludes,
|
|
72
73
|
} from './mesh-reconcile-identity.js';
|
|
73
74
|
import {
|
|
74
75
|
resolveAutoPruneMinAgeMs,
|
|
75
76
|
resolvePendingHeldDrainEscalateMs,
|
|
76
77
|
resolveReconcileIntervalMs,
|
|
77
78
|
} from './mesh-reconcile-config.js';
|
|
78
|
-
import { pullRemoteNodeQueues, pullPendingEventsFromNode } from './mesh-remote-event-pull.js';
|
|
79
|
+
import { pullRemoteNodeQueues, pullPendingEventsFromNode, reprobeWorkerStatus } from './mesh-remote-event-pull.js';
|
|
80
|
+
import { isPurePtyTranscriptProvider } from '../cli-adapters/provider-cli-shared.js';
|
|
79
81
|
import { runDiskRetentionSweep, detectAndSignalOrphanWorktrees } from './mesh-disk-retention.js';
|
|
80
82
|
import {
|
|
81
83
|
reconcileUnterminatedDirectDispatches,
|
|
@@ -732,10 +734,32 @@ const deliveredNoTurnUnknownStreak = new Map<string, number>();
|
|
|
732
734
|
// immediately; UNKNOWN accrues a streak and re-drives only after RECLAIM_UNKNOWN_GRACE_TICKS.
|
|
733
735
|
const deliveredUnconsumedUnknownStreak = new Map<string, number>();
|
|
734
736
|
|
|
737
|
+
// KIMI-PURE-PTY-COMPLETION-EMIT (Fix 2): how long a delivered assigned row whose worker is
|
|
738
|
+
// CONTINUOUSLY idle-WITH-a-final-assistant-message must stay so before we short-circuit it to
|
|
739
|
+
// 'completed' from transcript evidence — WITHOUT waiting the full DELIVERED_NO_TURN_DEADLINE_MS
|
|
740
|
+
// (15min). A pure-PTY worker (kimi and kin — no native transcript, no provider authority) whose
|
|
741
|
+
// generating_completed never emitted (the onTurnStarted idle→idle collapse Fix 1 addresses at the
|
|
742
|
+
// source) leaves the row 'assigned' with an idle worker that already rendered its answer. The
|
|
743
|
+
// F3 long-deadline reclaim's pollAssignedTaskTerminalEvidence would eventually recover it — but only
|
|
744
|
+
// after 15min, during which the coordinator ledger wrongly shows the task in flight. This EARLY
|
|
745
|
+
// reconcile applies the SAME idle-with-final-assistant-after-dispatch evidence bar (via
|
|
746
|
+
// pollAssignedTaskTerminalEvidence) after a few continuous-idle ticks, so a finished worker's task
|
|
747
|
+
// is marked complete promptly. Conservative by construction: the streak resets the instant a read is
|
|
748
|
+
// non-idle / lacks a final summary / is unreadable, and the poll itself enforces the after-dispatch
|
|
749
|
+
// stale-summary guard, so a mid-turn or warming-up worker is never falsely completed.
|
|
750
|
+
const ASSIGNED_IDLE_TRANSCRIPT_COMPLETE_MS = 8_000;
|
|
751
|
+
|
|
752
|
+
// Per-row continuous idle-with-final-assistant streak for the early transcript-evidence completion,
|
|
753
|
+
// keyed `${meshId}::${taskId}`, storing the wall-clock ms when the continuous run began. Pruned each
|
|
754
|
+
// pass to the currently-assigned rows (same as the UNKNOWN streaks). A non-idle / no-summary /
|
|
755
|
+
// unreadable tick clears the entry so the grace must re-accumulate from scratch.
|
|
756
|
+
const assignedIdleFinalAssistantSince = new Map<string, number>();
|
|
757
|
+
|
|
735
758
|
// Test hook: clear the delivered-no-turn UNKNOWN streaks between cases.
|
|
736
759
|
export function __resetReclaimUnknownStreakForTests(): void {
|
|
737
760
|
deliveredNoTurnUnknownStreak.clear();
|
|
738
761
|
deliveredUnconsumedUnknownStreak.clear();
|
|
762
|
+
assignedIdleFinalAssistantSince.clear();
|
|
739
763
|
}
|
|
740
764
|
|
|
741
765
|
// APPROVAL-INBOX-BLINDSPOT (Fix A.3): true when the assigned row's bound session is, per the
|
|
@@ -765,6 +789,118 @@ function assignedRowLiveStatusIsAwaitingApproval(
|
|
|
765
789
|
}
|
|
766
790
|
}
|
|
767
791
|
|
|
792
|
+
// EARLY-IDLE-COMPLETION-FALSE-POSITIVE — resolve whether the assigned session's provider is
|
|
793
|
+
// the pure-PTY transcript class (kimi and kin — no native history, no provider authority), from
|
|
794
|
+
// the LOCAL instance when it is present. Returns:
|
|
795
|
+
// true → local instance is a pure-PTY provider (the class the early rescue exists for)
|
|
796
|
+
// false → local instance is NOT pure-PTY (native / provider-authoritative)
|
|
797
|
+
// undefined → no local instance (remote worker / gone / id-form skew) — pure-PTY unknowable here
|
|
798
|
+
// A remote pure-PTY worker is handled by the reprobe path (a fresh-idle read positively confirms
|
|
799
|
+
// the session is settled before the streak accrues), not by this local-only capability check.
|
|
800
|
+
function resolveLocalSessionPurePty(components: DaemonComponents, sessionId: string): boolean | undefined {
|
|
801
|
+
try {
|
|
802
|
+
const instances = components.instanceManager?.getByCategory?.('cli') || [];
|
|
803
|
+
const inst = instances.find((i: any) => {
|
|
804
|
+
const sid = readNonEmptyString(i?.getState?.().instanceId);
|
|
805
|
+
return sid && sessionIdsEquivalent(sid, sessionId);
|
|
806
|
+
}) as { provider?: unknown } | undefined;
|
|
807
|
+
if (!inst) return undefined; // remote / gone / id-form skew — not locally observable
|
|
808
|
+
const provider = inst.provider;
|
|
809
|
+
if (!provider || typeof provider !== 'object') return undefined;
|
|
810
|
+
return isPurePtyTranscriptProvider(provider as Parameters<typeof isPurePtyTranscriptProvider>[0]);
|
|
811
|
+
} catch {
|
|
812
|
+
return undefined;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// EARLY-IDLE-COMPLETION-FALSE-POSITIVE — decide whether the early transcript-evidence completion
|
|
817
|
+
// streak may ACCRUE this tick for an assigned row. This hardens the original arm gate, which only
|
|
818
|
+
// checked `resolveSessionBusyVerdict(...) !== 'GENERATING'` — a gate a REMOTE worker (local verdict
|
|
819
|
+
// UNKNOWN, not GENERATING) passed unconditionally, so its "8s continuous idle" streak degenerated to
|
|
820
|
+
// 8s of wall-clock even while the worker was genuinely mid-turn, and a startup-grace boot-window idle
|
|
821
|
+
// (before the first turn ever started) also passed. The row was then early-completed off a preamble.
|
|
822
|
+
//
|
|
823
|
+
// Two added requirements, defense-in-depth on top of the existing confirmed-delivery / no-terminal-
|
|
824
|
+
// ledger / bound-session gate the caller applies:
|
|
825
|
+
// (a) POSITIVE idle evidence, not merely "not GENERATING". A LOCAL IDLE_CONFIRMED verdict qualifies
|
|
826
|
+
// directly. A remote/UNKNOWN verdict is RE-PROBED with a fresh read_chat status this tick: only
|
|
827
|
+
// a definitively 'idle' read lets the streak accrue; any active status (generating/waiting/…),
|
|
828
|
+
// or an inconclusive read, resets it. This is what stops the remote worker's mid-turn busy from
|
|
829
|
+
// silently passing as "continuous idle".
|
|
830
|
+
// (b) TURN-START evidence, so a boot-window / never-consumed idle cannot early-complete a preamble.
|
|
831
|
+
// taskDeliveryConsumed===true (the worker emitted agent:generating_started) satisfies it. When
|
|
832
|
+
// the delivery was never consumed, only a LOCAL pure-PTY provider — the exact class that never
|
|
833
|
+
// emits generating_started and that this rescue exists for — is still allowed; a native/
|
|
834
|
+
// provider-authoritative worker that hasn't started its turn is NOT armed (it will complete via
|
|
835
|
+
// its own emit or the normal grace). A remote-not-consumed worker whose provider class is
|
|
836
|
+
// unknowable locally falls back to the reprobe: it may accrue only once (a) reads it positively
|
|
837
|
+
// idle, and the poll's post-dispatch + trailing-tool-activity guards remain the final net.
|
|
838
|
+
async function evaluateEarlyIdleTranscriptArm(
|
|
839
|
+
components: DaemonComponents,
|
|
840
|
+
mesh: { id: string; nodes?: Array<{ id: string; daemonId?: string; workspace?: string }> },
|
|
841
|
+
store: MeshRuntimeStore,
|
|
842
|
+
row: { id: string; assignedSessionId?: string; assignedNodeId?: string; assignedProviderType?: string },
|
|
843
|
+
localDaemonId?: string,
|
|
844
|
+
selfIds: string[] = [],
|
|
845
|
+
): Promise<boolean> {
|
|
846
|
+
const sessionId = readNonEmptyString(row.assignedSessionId);
|
|
847
|
+
if (!sessionId) return false;
|
|
848
|
+
|
|
849
|
+
// (a) POSITIVE idle evidence.
|
|
850
|
+
const verdict = resolveSessionBusyVerdict(components, sessionId);
|
|
851
|
+
if (verdict === 'GENERATING') return false; // locally observed mid-turn — never accrue
|
|
852
|
+
if (verdict === 'UNKNOWN') {
|
|
853
|
+
// Remote / gone / id-form skew. First consult the LIVE mesh node snapshot (the same
|
|
854
|
+
// cross-daemon status feed the approval-hold guard and mesh_status use): if it already
|
|
855
|
+
// reports a definitively NON-idle status (generating / waiting_approval / awaiting_choice /
|
|
856
|
+
// …), the worker is not settled — reset the streak WITHOUT a read_chat. This keeps the
|
|
857
|
+
// streak the cheap per-tick gate for the common case and avoids probing a worker the graph
|
|
858
|
+
// already knows is busy or parked at an approval.
|
|
859
|
+
const nodeId = readNonEmptyString(row.assignedNodeId);
|
|
860
|
+
const node = (mesh.nodes ?? []).find(n => n.id === nodeId);
|
|
861
|
+
const liveStatus = nodeId
|
|
862
|
+
? readNonEmptyString(sessionStatusFromNodes(mesh.nodes, nodeId, sessionId).status).toLowerCase()
|
|
863
|
+
: '';
|
|
864
|
+
if (liveStatus && liveStatus !== 'idle') return false; // live graph says non-idle → not a turn-end
|
|
865
|
+
if (!liveStatus) {
|
|
866
|
+
// No live snapshot for this session — re-probe the worker's own status this tick so a
|
|
867
|
+
// genuinely busy remote worker breaks the streak instead of coasting through as "not
|
|
868
|
+
// GENERATING". getInstance may be absent on some component surfaces (tests / standalone),
|
|
869
|
+
// so guard it.
|
|
870
|
+
const nodeDaemonId = readNonEmptyString(node?.daemonId);
|
|
871
|
+
const isLocalNode = !nodeDaemonId
|
|
872
|
+
|| daemonIdsEquivalent(nodeDaemonId, localDaemonId)
|
|
873
|
+
|| daemonIdListIncludes(selfIds, nodeDaemonId)
|
|
874
|
+
|| !!components.instanceManager?.getInstance?.(sessionId);
|
|
875
|
+
const providerType = readNonEmptyString(row.assignedProviderType);
|
|
876
|
+
const readArgs: Record<string, unknown> = {
|
|
877
|
+
sessionId,
|
|
878
|
+
targetSessionId: sessionId,
|
|
879
|
+
tailLimit: 1,
|
|
880
|
+
...(node?.workspace ? { workspace: node.workspace } : {}),
|
|
881
|
+
...(providerType ? { agentType: providerType, providerType } : {}),
|
|
882
|
+
};
|
|
883
|
+
const probedStatus = await reprobeWorkerStatus(components, { isLocalNode, nodeDaemonId, readArgs });
|
|
884
|
+
// Only a positively-idle re-probe lets the streak accrue. A non-idle status (the worker
|
|
885
|
+
// IS busy) or an inconclusive read (null — transport error / no payload) resets it: we
|
|
886
|
+
// never treat "couldn't tell" as idle for this early-completion path.
|
|
887
|
+
if (probedStatus !== 'idle') return false;
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// (b) TURN-START evidence (guards the boot-window / preamble false positive).
|
|
892
|
+
if (store.taskDeliveryConsumed(mesh.id, row.id)) return true; // worker emitted generating_started
|
|
893
|
+
// Delivery never consumed. Preserve the kimi rescue ONLY for the LOCAL pure-PTY class (which
|
|
894
|
+
// never emits generating_started); a native/provider-authoritative local worker that hasn't
|
|
895
|
+
// started its turn must NOT be armed off a boot-window idle. For a remote worker (pure-PTY
|
|
896
|
+
// unknowable here) the positive-idle reprobe in (a) is the gate — allow accrual and let the poll
|
|
897
|
+
// enforce post-dispatch + trailing-tool-activity finality.
|
|
898
|
+
const localPurePty = resolveLocalSessionPurePty(components, sessionId);
|
|
899
|
+
if (localPurePty === true) return true; // local pure-PTY — the class this rescue targets
|
|
900
|
+
if (localPurePty === false) return false; // local native/provider-owned, turn not started — hold
|
|
901
|
+
return verdict === 'UNKNOWN'; // remote, unknowable class — trust the (a) reprobe
|
|
902
|
+
}
|
|
903
|
+
|
|
768
904
|
// PHASE 2.5 — assigned-stranded dispatch watchdog (Bug B). claimNextTask atomically
|
|
769
905
|
// flips a row to 'assigned' BEFORE the fire-and-forget dispatch runs. If that dispatch
|
|
770
906
|
// neither rejects (→ no .catch requeue) nor is confirmed delivered — a relay that hangs
|
|
@@ -799,10 +935,82 @@ async function recoverStrandedAssignedDispatches(
|
|
|
799
935
|
for (const key of [...deliveredUnconsumedUnknownStreak.keys()]) {
|
|
800
936
|
if (key.startsWith(meshKeyPrefix) && !assignedKeys.has(key)) deliveredUnconsumedUnknownStreak.delete(key);
|
|
801
937
|
}
|
|
938
|
+
for (const key of [...assignedIdleFinalAssistantSince.keys()]) {
|
|
939
|
+
if (key.startsWith(meshKeyPrefix) && !assignedKeys.has(key)) assignedIdleFinalAssistantSince.delete(key);
|
|
940
|
+
}
|
|
802
941
|
for (const row of assigned) {
|
|
803
942
|
const dispatchedAtMs = Date.parse(row.dispatchTimestamp ?? '');
|
|
804
943
|
if (!Number.isFinite(dispatchedAtMs)) continue; // no dispatch ts → can't age it
|
|
805
944
|
const ageMs = nowMs - dispatchedAtMs;
|
|
945
|
+
const idleTranscriptStreakKey = `${meshId}::${row.id}`;
|
|
946
|
+
// KIMI-PURE-PTY-COMPLETION-EMIT (Fix 2): EARLY transcript-evidence completion for a
|
|
947
|
+
// delivered assigned row whose worker is already idle with a final assistant answer, so a
|
|
948
|
+
// pure-PTY worker whose generating_completed never emitted is marked done PROMPTLY instead of
|
|
949
|
+
// sitting 'assigned' until the 15-min DELIVERED_NO_TURN_DEADLINE_MS reclaim. Gate on a
|
|
950
|
+
// CONFIRMED delivery (a never-delivered row is handled by dispatch/redrive, not completion)
|
|
951
|
+
// and a bound session, then require the worker to read idle-WITH-final-assistant continuously
|
|
952
|
+
// for ASSIGNED_IDLE_TRANSCRIPT_COMPLETE_MS before polling the decisive transcript evidence.
|
|
953
|
+
// The streak is the cheap gate (avoids a read every tick); the read only runs once the grace
|
|
954
|
+
// elapses. pollAssignedTaskTerminalEvidence enforces idle + final-assistant-after-dispatch, so
|
|
955
|
+
// a mid-turn / stale-tail / unreadable worker is never falsely completed — a non-qualifying
|
|
956
|
+
// tick clears the streak below.
|
|
957
|
+
// EARLY-IDLE-COMPLETION-FALSE-POSITIVE: the arm gate now requires POSITIVE idle evidence
|
|
958
|
+
// (a fresh-idle reprobe for a remote/UNKNOWN worker, not merely "not GENERATING") AND
|
|
959
|
+
// turn-start evidence (delivery consumed, or the local pure-PTY class this rescue targets),
|
|
960
|
+
// so a mid-turn remote worker or a boot-window preamble can no longer coast the continuous-
|
|
961
|
+
// idle streak to a false completion. See evaluateEarlyIdleTranscriptArm.
|
|
962
|
+
const earlyArm =
|
|
963
|
+
store.taskHasConfirmedDelivery(meshId, row.id)
|
|
964
|
+
&& !findTerminalLedgerEvidenceForTask({ meshId, taskId: row.id })
|
|
965
|
+
&& readNonEmptyString(row.assignedSessionId)
|
|
966
|
+
? await evaluateEarlyIdleTranscriptArm(components, mesh, store, row, localDaemonId, selfIds)
|
|
967
|
+
: false;
|
|
968
|
+
if (earlyArm) {
|
|
969
|
+
const since = assignedIdleFinalAssistantSince.get(idleTranscriptStreakKey);
|
|
970
|
+
if (since === undefined) {
|
|
971
|
+
assignedIdleFinalAssistantSince.set(idleTranscriptStreakKey, nowMs);
|
|
972
|
+
} else if (nowMs - since >= ASSIGNED_IDLE_TRANSCRIPT_COMPLETE_MS) {
|
|
973
|
+
const terminalEvidence = await pollAssignedTaskTerminalEvidence(components, mesh, row);
|
|
974
|
+
if (terminalEvidence) {
|
|
975
|
+
assignedIdleFinalAssistantSince.delete(idleTranscriptStreakKey);
|
|
976
|
+
updateTaskStatus(meshId, row.id, terminalEvidence);
|
|
977
|
+
if (!findTerminalLedgerEvidenceForTask({ meshId, taskId: row.id })) {
|
|
978
|
+
try {
|
|
979
|
+
appendLedgerEntry(meshId, {
|
|
980
|
+
kind: terminalEvidence === 'completed' ? 'task_completed' : 'task_failed',
|
|
981
|
+
nodeId: row.assignedNodeId,
|
|
982
|
+
sessionId: row.assignedSessionId,
|
|
983
|
+
providerType: row.assignedProviderType,
|
|
984
|
+
payload: {
|
|
985
|
+
taskId: row.id,
|
|
986
|
+
event: 'agent:generating_completed',
|
|
987
|
+
source: 'early_idle_transcript_evidence',
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
} catch { /* best-effort ledger write */ }
|
|
991
|
+
}
|
|
992
|
+
LOG.warn('MeshReconcile', `Early-completed assigned task ${row.id} on mesh ${meshId} `
|
|
993
|
+
+ `(node=${row.assignedNodeId ?? '?'} session=${row.assignedSessionId ?? '?'}): worker idle with a `
|
|
994
|
+
+ `final assistant message after dispatch for ≥${Math.round(ASSIGNED_IDLE_TRANSCRIPT_COMPLETE_MS / 1000)}s — `
|
|
995
|
+
+ `the completion event was lost/late (pure-PTY provider), task is ${terminalEvidence} without waiting the 15-min deadline`);
|
|
996
|
+
traceMeshEventDrop('assigned_early_transcript_completed', {
|
|
997
|
+
taskId: row.id,
|
|
998
|
+
sessionId: row.assignedSessionId,
|
|
999
|
+
nodeId: row.assignedNodeId,
|
|
1000
|
+
meshId,
|
|
1001
|
+
event: 'agent:generating_completed',
|
|
1002
|
+
}, terminalEvidence);
|
|
1003
|
+
continue;
|
|
1004
|
+
}
|
|
1005
|
+
// Poll was inconclusive (mid-turn re-check / stale tail / unreadable) — reset the
|
|
1006
|
+
// streak so it must re-accumulate a fresh continuous-idle run before the next poll.
|
|
1007
|
+
assignedIdleFinalAssistantSince.delete(idleTranscriptStreakKey);
|
|
1008
|
+
}
|
|
1009
|
+
} else {
|
|
1010
|
+
// Not a qualifying tick (no confirmed delivery, generating, terminal ledger present, or
|
|
1011
|
+
// no bound session) — break the continuous-idle run.
|
|
1012
|
+
assignedIdleFinalAssistantSince.delete(idleTranscriptStreakKey);
|
|
1013
|
+
}
|
|
806
1014
|
// DELIVERED-NOT-CONSUMED short-grace re-drive (remote autoLaunch delivered≠consumed gap).
|
|
807
1015
|
// Runs BEFORE the ASSIGNED_STRANDED_DEADLINE_MS confirm-window gate below because its whole
|
|
808
1016
|
// point is to recover a delivered-but-unconsumed row well inside that window. A remote
|
|
@@ -156,6 +156,59 @@ export function selectFinalAssistantTurnEndMessage(
|
|
|
156
156
|
return null;
|
|
157
157
|
}
|
|
158
158
|
|
|
159
|
+
/**
|
|
160
|
+
* EARLY-IDLE-COMPLETION-FALSE-POSITIVE — trailing tool/terminal activity detector.
|
|
161
|
+
*
|
|
162
|
+
* True when the transcript's LAST non-empty user-facing assistant bubble is followed
|
|
163
|
+
* by one or more TOOL / TERMINAL activity bubbles (a tool_use/command the assistant
|
|
164
|
+
* fired AFTER its text) — i.e. the assistant emitted a preamble ("Let me explore…"),
|
|
165
|
+
* then started running Read/Grep, so the turn is still executing and its answer has
|
|
166
|
+
* not landed. Callers that would otherwise promote that preamble to a turn-end summary
|
|
167
|
+
* off a momentary (startup-grace / inter-tool) idle read use this as a veto.
|
|
168
|
+
*
|
|
169
|
+
* Deliberately NARROW so the pure-PTY completion rescue is preserved:
|
|
170
|
+
* - Only TOOL/TERMINAL activity trailing the assistant vetoes. A trailing THOUGHT or
|
|
171
|
+
* status bubble does NOT (a finished turn can end on an internal thought), matching
|
|
172
|
+
* selectFinalAssistantTurnEndMessage's skip set minus the "still-working" signals.
|
|
173
|
+
* - A genuinely FINISHED worker (final assistant last, no trailing tool activity —
|
|
174
|
+
* the kimi pure-PTY continuous-idle shape) returns false, so its early completion
|
|
175
|
+
* is untouched.
|
|
176
|
+
*
|
|
177
|
+
* Returns false when there is no final assistant bubble at all (the caller's
|
|
178
|
+
* selectFinalAssistantTurnEndMessage already handles that as "not a turn end").
|
|
179
|
+
*/
|
|
180
|
+
export function hasTrailingToolActivityAfterFinalAssistant(
|
|
181
|
+
messages: ChatMessage[] | null | undefined,
|
|
182
|
+
): boolean {
|
|
183
|
+
if (!Array.isArray(messages) || messages.length === 0) return false;
|
|
184
|
+
let sawTrailingToolActivity = false;
|
|
185
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
186
|
+
const msg = messages[i];
|
|
187
|
+
if (!msg) continue;
|
|
188
|
+
const classification = classifyChatMessageVisibility(msg);
|
|
189
|
+
// A trailing user-facing assistant/model bubble with real text ends the scan:
|
|
190
|
+
// whether we saw a tool AFTER it is the verdict.
|
|
191
|
+
if (classification.isUserFacing && (msg.role === 'assistant' || msg.role === 'model')) {
|
|
192
|
+
if (flattenContent(msg.content).trim()) return sawTrailingToolActivity;
|
|
193
|
+
// Empty streaming assistant bubble — keep scanning back past it, same as
|
|
194
|
+
// selectFinalAssistantTurnEndMessage (which returns null here); an empty tail
|
|
195
|
+
// isn't a turn end, so there is nothing to veto.
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
if (classification.isUserFacing) {
|
|
199
|
+
// A trailing user bubble (freshly dispatched task, no reply) → no assistant
|
|
200
|
+
// turn end below it to veto.
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
// Non-user-facing activity/internal bubble sitting AFTER the (not-yet-seen)
|
|
204
|
+
// assistant bubble. Only tool/terminal activity signals an in-flight turn.
|
|
205
|
+
if (classification.kind === 'tool' || classification.kind === 'terminal') {
|
|
206
|
+
sawTrailingToolActivity = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
159
212
|
export const BUILTIN_CHAT_MESSAGE_KINDS = ['standard', 'thought', 'tool', 'terminal', 'system'] as const;
|
|
160
213
|
|
|
161
214
|
export type BuiltinChatMessageKind = typeof BUILTIN_CHAT_MESSAGE_KINDS[number];
|
|
@@ -17,6 +17,7 @@ import { ProviderCliAdapter } from '../cli-adapters/provider-cli-adapter.js';
|
|
|
17
17
|
import { shortHash } from '../system/hash.js';
|
|
18
18
|
import type { CliProviderModule } from '../cli-adapters/provider-cli-adapter.js';
|
|
19
19
|
import type { MeshSendKeyItem, MeshSendKeyName } from '../cli-adapters/provider-cli-shared.js';
|
|
20
|
+
import { isPurePtyTranscriptProvider } from '../cli-adapters/provider-cli-shared.js';
|
|
20
21
|
import { createCliAdapter } from './spec/route.js';
|
|
21
22
|
import type { PtyRuntimeMetadata, PtyTransportFactory } from '../cli-adapters/pty-transport.js';
|
|
22
23
|
import { StatusMonitor } from './status-monitor.js';
|
|
@@ -2457,6 +2458,20 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2457
2458
|
}
|
|
2458
2459
|
}
|
|
2459
2460
|
|
|
2461
|
+
// KIMI-PURE-PTY-COMPLETION-EMIT (Fix 3): before firing a false stall, check the
|
|
2462
|
+
// pure-PTY class (no native transcript → nativeSample was null above). A finished
|
|
2463
|
+
// pure-PTY worker sits at a static idle prompt with an in-turn final assistant
|
|
2464
|
+
// message but never emitted generating_completed — the status-agnostic watchdog
|
|
2465
|
+
// would misread that quiet as a wedge. If the PTY transcript proves a finished
|
|
2466
|
+
// turn, emit the missing completion and SUPPRESS the stall (mark the anchor
|
|
2467
|
+
// emitted so this static idle is not re-checked every tick). No-op for
|
|
2468
|
+
// native-source providers and for a genuinely mid-turn / wedged worker with no
|
|
2469
|
+
// final assistant → the real stall fires below unchanged.
|
|
2470
|
+
if (this.tryReconcilePurePtyCompletionForStall(observedStatus)) {
|
|
2471
|
+
this.meshStallEmittedForAnchor = true;
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
|
|
2460
2475
|
// FALSE-STALL-WATCHDOG-OVERFIRE (fix E): per-session refire cooldown. Mark
|
|
2461
2476
|
// this anchor emitted regardless so a still-static anchor does not re-check
|
|
2462
2477
|
// every tick, but suppress the actual coordinator notification (and the
|
|
@@ -2619,6 +2634,83 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2619
2634
|
return true;
|
|
2620
2635
|
}
|
|
2621
2636
|
|
|
2637
|
+
/**
|
|
2638
|
+
* KIMI-PURE-PTY-COMPLETION-EMIT (Fix 3): stall-path completion reconcile for the
|
|
2639
|
+
* PURE-PTY transcript class (kimi and kin — no native transcript, no provider
|
|
2640
|
+
* authority; see isPurePtyTranscriptProvider). Such a worker whose
|
|
2641
|
+
* generating_completed never emitted (the onTurnStarted idle→idle collapse Fix 1
|
|
2642
|
+
* fixes at the source) sits at a STATIC idle prompt: its PTY output stops the
|
|
2643
|
+
* instant the answer is rendered, so the status-agnostic no-progress watchdog
|
|
2644
|
+
* (checkMeshWorkerStall) reads the finished-but-quiet session as a stall and would
|
|
2645
|
+
* false-fire monitor:no_progress. The native-transcript reconcile
|
|
2646
|
+
* (sampleNativeTranscriptProgress) does NOT cover this class (no native source →
|
|
2647
|
+
* null sample), so the stall path needs its own guard.
|
|
2648
|
+
*
|
|
2649
|
+
* Called from checkMeshWorkerStall just before the fire. Returns true when the
|
|
2650
|
+
* session is a finished pure-PTY turn — idle, no pending response, and a PTY-parsed
|
|
2651
|
+
* in-turn final assistant summary — in which case it emits the missing
|
|
2652
|
+
* generating_completed (idempotent: a real/late emit writes the terminal ledger and
|
|
2653
|
+
* the coordinator's reconcile makes any duplicate a no-op) and the caller SUPPRESSES
|
|
2654
|
+
* the stall. Returns false for every other class/state (native-source provider, a
|
|
2655
|
+
* genuinely mid-turn or wedged worker with no final assistant), so a real stall still
|
|
2656
|
+
* fires unchanged.
|
|
2657
|
+
*
|
|
2658
|
+
* Evidence bar is identical to flushMeshCompletionBeforeCleanup: injected task's turn
|
|
2659
|
+
* genuinely started + an in-turn final assistant summary. Conservative by
|
|
2660
|
+
* construction — no summary ⇒ no proof of completion ⇒ return false and let the real
|
|
2661
|
+
* stall fire.
|
|
2662
|
+
*/
|
|
2663
|
+
private tryReconcilePurePtyCompletionForStall(observedStatus: string): boolean {
|
|
2664
|
+
// Only the pure-PTY class — native-source providers are handled by
|
|
2665
|
+
// sampleNativeTranscriptProgress above.
|
|
2666
|
+
if (!isPurePtyTranscriptProvider(this.provider)) return false;
|
|
2667
|
+
// A finished turn is idle with nothing pending. A generating/waiting session is
|
|
2668
|
+
// mid-turn (the real stall path should evaluate it), so never complete it here.
|
|
2669
|
+
if (observedStatus !== 'idle') return false;
|
|
2670
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
2671
|
+
|
|
2672
|
+
const taskId = this.completingTurnTaskId();
|
|
2673
|
+
// DOUBLE-EMIT guard: this turn's completion already fired — never re-emit.
|
|
2674
|
+
if (this.lastEmittedCompletion && this.lastEmittedCompletion.taskId === (taskId ?? '')) {
|
|
2675
|
+
return false;
|
|
2676
|
+
}
|
|
2677
|
+
// The injected task's own turn must have genuinely started (guards against a
|
|
2678
|
+
// reused session's stale tail before this task ran).
|
|
2679
|
+
if (!this.injectedTaskHasStartedGenerating()) return false;
|
|
2680
|
+
|
|
2681
|
+
const turnStartedAt = typeof (this.adapter as any)?.currentTurnStartedAt === 'number'
|
|
2682
|
+
? (this.adapter as any).currentTurnStartedAt as number
|
|
2683
|
+
: this.meshTaskInjectedAt || undefined;
|
|
2684
|
+
let parsedMessages: unknown;
|
|
2685
|
+
try {
|
|
2686
|
+
parsedMessages = this.adapter?.getScriptParsedStatus?.()?.messages;
|
|
2687
|
+
} catch { parsedMessages = undefined; }
|
|
2688
|
+
let finalSummary: string | undefined;
|
|
2689
|
+
try {
|
|
2690
|
+
finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
|
|
2691
|
+
} catch { finalSummary = undefined; }
|
|
2692
|
+
// No in-turn final assistant evidence → not a proven turn-end. Let the real
|
|
2693
|
+
// stall fire so a genuinely-wedged worker is still surfaced.
|
|
2694
|
+
if (!finalSummary) return false;
|
|
2695
|
+
|
|
2696
|
+
LOG.warn('CLI', `[${this.type}] reconciling pure-PTY mesh completion from the stall path for session ${this.instanceId} `
|
|
2697
|
+
+ `task=${taskId ?? '(none)'} — PTY is idle-quiet with an in-turn final assistant message but the completion `
|
|
2698
|
+
+ `event never fired (pure-PTY provider); emitting it instead of a false monitor:no_progress.`);
|
|
2699
|
+
if (this.isMeshWorkerSession()) {
|
|
2700
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), 'stall_pure_pty_transcript_completion');
|
|
2701
|
+
}
|
|
2702
|
+
this.emitGeneratingCompleted({
|
|
2703
|
+
chatTitle: '',
|
|
2704
|
+
duration: undefined,
|
|
2705
|
+
timestamp: Date.now(),
|
|
2706
|
+
taskId,
|
|
2707
|
+
finalSummary,
|
|
2708
|
+
evidenceLevel: 'transcript',
|
|
2709
|
+
completionDiagnostic: { source: 'stall_pure_pty_transcript_completion' },
|
|
2710
|
+
});
|
|
2711
|
+
return true;
|
|
2712
|
+
}
|
|
2713
|
+
|
|
2622
2714
|
/**
|
|
2623
2715
|
* AUTOAPPROVE-FLAP-RECUR (Fix A+B): how long a busy blip / modal scroll-out may
|
|
2624
2716
|
* persist before the in-progress settle gate is torn down. For a delegated
|