@adhdev/daemon-core 1.0.28-rc.15 → 1.0.28-rc.17
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.d.ts +1 -1
- package/dist/index.js +258 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +257 -32
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-stale.d.ts +59 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +34 -0
- package/package.json +3 -3
- package/src/cli-adapters/provider-cli-adapter.ts +7 -1
- package/src/commands/med-family/cli-agent.ts +32 -13
- package/src/index.ts +1 -0
- package/src/mesh/mesh-completion-synthesis.ts +35 -1
- package/src/mesh/mesh-event-forwarding.ts +129 -1
- package/src/mesh/mesh-events-stale.ts +109 -0
- package/src/mesh/mesh-queue-assignment.ts +19 -1
- package/src/mesh/mesh-reconcile-loop.ts +59 -12
- package/src/providers/cli-provider-instance.ts +135 -7
|
@@ -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, reconcileDirectDispatchCompletionFromTranscript } from './mesh-events-stale.js';
|
|
67
|
+
import { findTerminalLedgerEvidenceForTask, reconcileDirectDispatchCompletionFromTranscript, resolveLiveTurnPendingEvidence } from './mesh-events-stale.js';
|
|
68
68
|
import {
|
|
69
69
|
resolveCoordinatorDaemonIds,
|
|
70
70
|
daemonHostsMesh,
|
|
@@ -945,18 +945,24 @@ async function evaluateEarlyIdleTranscriptArm(
|
|
|
945
945
|
// synth is marked WEAK so the worker's own later emit can still supersede it rather than being
|
|
946
946
|
// dropped as a duplicate. That is the dedup guarantee: at most one [System] completion surfaces.
|
|
947
947
|
//
|
|
948
|
-
// Returns
|
|
949
|
-
// the caller then skips its own bare-payload ledger write, which lacked
|
|
948
|
+
// Returns 'propagated' if a terminal ledger for this task now exists (freshly written OR
|
|
949
|
+
// already present) — the caller then skips its own bare-payload ledger write, which lacked
|
|
950
|
+
// the finalSummary entirely. Returns 'deferred' when the MID-TURN-CAUSAL-ADMISSION guard
|
|
951
|
+
// vetoed the synth on live mid-turn evidence — the caller must HOLD (no row flip, no bare
|
|
952
|
+
// ledger write) and re-evaluate next tick. Returns 'unavailable' when propagation could not
|
|
953
|
+
// run — the caller falls back to its bare ledger write (unchanged legacy behavior).
|
|
950
954
|
function propagateWatchdogTranscriptCompletion(
|
|
955
|
+
components: DaemonComponents,
|
|
951
956
|
meshId: string,
|
|
952
957
|
row: { id: string; assignedNodeId?: string; assignedSessionId?: string; assignedProviderType?: string; dispatchTimestamp?: string },
|
|
953
958
|
evidence: AssignedTaskTerminalEvidence,
|
|
954
959
|
source: string,
|
|
955
|
-
|
|
960
|
+
opts?: { boundedBackstop?: boolean },
|
|
961
|
+
): 'propagated' | 'deferred' | 'unavailable' {
|
|
956
962
|
const sessionId = readNonEmptyString(row.assignedSessionId) || evidence.sessionId;
|
|
957
963
|
if (!sessionId || !readNonEmptyString(evidence.finalSummary)) {
|
|
958
964
|
// No routable session or no summary to carry — fall back to the caller's bare ledger write.
|
|
959
|
-
return
|
|
965
|
+
return 'unavailable';
|
|
960
966
|
}
|
|
961
967
|
try {
|
|
962
968
|
const result = reconcileDirectDispatchCompletionFromTranscript({
|
|
@@ -972,15 +978,30 @@ function propagateWatchdogTranscriptCompletion(
|
|
|
972
978
|
// The watchdog poll already enforced idle + post-dispatch + no-trailing-tool + streak;
|
|
973
979
|
// skip the reconcile's own grace/transcript_not_proven gates (dedup backstops remain).
|
|
974
980
|
preValidatedTranscriptEvidence: true,
|
|
981
|
+
// MID-TURN-CAUSAL-ADMISSION (rc.16): the transcript poll's structural evidence can
|
|
982
|
+
// still straddle a genuinely mid-turn LOCAL worker (the incident shape: transcript
|
|
983
|
+
// reads idle-with-final-assistant while the raw PTY is mid-tool). Route the live
|
|
984
|
+
// adapter's synchronous probe through the unified choke point. The EARLY-IDLE
|
|
985
|
+
// caller is an eager path (no opts) → a pending verdict defers; the redrive-deadline
|
|
986
|
+
// caller passes boundedBackstop (the max-wait net) → the veto yields, preserving the
|
|
987
|
+
// genuine-final fail-open semantics.
|
|
988
|
+
causalAdmission: {
|
|
989
|
+
liveTurnPendingEvidence: resolveLiveTurnPendingEvidence(components, sessionId),
|
|
990
|
+
boundedBackstop: opts?.boundedBackstop === true,
|
|
991
|
+
},
|
|
975
992
|
source,
|
|
976
993
|
});
|
|
977
994
|
// reconciled → freshly queued the coordinator completion; alreadyTerminal → a terminal
|
|
978
995
|
// ledger (a real emit that raced in, or a prior watchdog synth) is already present. Either
|
|
979
996
|
// way a finalSummary-bearing completion has been (or will be) delivered; the caller must NOT
|
|
980
997
|
// add its summary-less ledger row on top.
|
|
981
|
-
|
|
998
|
+
if (result.reconciled || result.alreadyTerminal === true) return 'propagated';
|
|
999
|
+
if (result.reason === 'live_turn_pending_evidence' || result.reason === 'trailing_tool_activity_after_final_assistant') {
|
|
1000
|
+
return 'deferred'; // mid-turn causal evidence — HOLD; do not complete this tick
|
|
1001
|
+
}
|
|
1002
|
+
return 'unavailable';
|
|
982
1003
|
} catch {
|
|
983
|
-
return
|
|
1004
|
+
return 'unavailable'; // best-effort — fall back to the caller's bare ledger write
|
|
984
1005
|
}
|
|
985
1006
|
}
|
|
986
1007
|
|
|
@@ -1062,6 +1083,29 @@ async function recoverStrandedAssignedDispatches(
|
|
|
1062
1083
|
minFinalAssistantAgeMs: ASSIGNED_IDLE_TRANSCRIPT_COMPLETE_MS,
|
|
1063
1084
|
});
|
|
1064
1085
|
if (terminalEvidence) {
|
|
1086
|
+
// MID-TURN-CAUSAL-ADMISSION (rc.16): propagate FIRST. When the unified guard
|
|
1087
|
+
// defers (the LOCAL live adapter still reports the turn pending — transcript
|
|
1088
|
+
// idle-with-final-assistant can straddle a genuinely mid-turn worker), HOLD
|
|
1089
|
+
// the entire completion: no row flip, no bare ledger write, no queued event.
|
|
1090
|
+
// The streak resets so it re-accumulates; the next qualifying tick re-polls
|
|
1091
|
+
// and re-evaluates, releasing exactly once after the live state clears.
|
|
1092
|
+
const propagation = propagateWatchdogTranscriptCompletion(
|
|
1093
|
+
components, meshId, row, terminalEvidence, 'early_idle_transcript_evidence',
|
|
1094
|
+
);
|
|
1095
|
+
if (propagation === 'deferred') {
|
|
1096
|
+
assignedIdleFinalAssistantSince.delete(idleTranscriptStreakKey);
|
|
1097
|
+
LOG.info('MeshReconcile', `Deferred early transcript completion for task ${row.id} on mesh ${meshId} `
|
|
1098
|
+
+ `(node=${row.assignedNodeId ?? '?'} session=${row.assignedSessionId ?? '?'}): the live adapter still reports `
|
|
1099
|
+
+ `the turn pending (mid-tool / streaming / modal) — holding; the completion re-evaluates next tick`);
|
|
1100
|
+
traceMeshEventDrop('early_idle_completion_deferred_live_pending', {
|
|
1101
|
+
taskId: row.id,
|
|
1102
|
+
sessionId: row.assignedSessionId,
|
|
1103
|
+
nodeId: row.assignedNodeId,
|
|
1104
|
+
meshId,
|
|
1105
|
+
event: 'agent:generating_completed',
|
|
1106
|
+
});
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1065
1109
|
assignedIdleFinalAssistantSince.delete(idleTranscriptStreakKey);
|
|
1066
1110
|
updateTaskStatus(meshId, row.id, terminalEvidence.outcome);
|
|
1067
1111
|
// WATCHDOG-FINALSUMMARY-LOST: propagate the completion to the coordinator WITH the
|
|
@@ -1069,9 +1113,7 @@ async function recoverStrandedAssignedDispatches(
|
|
|
1069
1113
|
// generating_completed produces — instead of only tracing a structural DROP. When
|
|
1070
1114
|
// propagation delivered (or a terminal ledger already exists), skip the bare
|
|
1071
1115
|
// summary-less ledger write below; only fall back to it if propagation could not run.
|
|
1072
|
-
const propagated =
|
|
1073
|
-
meshId, row, terminalEvidence, 'early_idle_transcript_evidence',
|
|
1074
|
-
);
|
|
1116
|
+
const propagated = propagation === 'propagated';
|
|
1075
1117
|
if (!propagated && !findTerminalLedgerEvidenceForTask({ meshId, taskId: row.id })) {
|
|
1076
1118
|
try {
|
|
1077
1119
|
appendLedgerEntry(meshId, {
|
|
@@ -1388,9 +1430,14 @@ async function recoverStrandedAssignedDispatches(
|
|
|
1388
1430
|
// WATCHDOG-FINALSUMMARY-LOST: propagate the finalSummary-bearing completion to the
|
|
1389
1431
|
// coordinator (same [System] notification as the native path); only fall back to the
|
|
1390
1432
|
// bare summary-less ledger write when propagation could not run.
|
|
1391
|
-
|
|
1392
|
-
|
|
1433
|
+
// MID-TURN-CAUSAL-ADMISSION: this is the BOUNDED max-wait net (the 15-min
|
|
1434
|
+
// delivered-no-turn deadline) — boundedBackstop preserves the genuine-final
|
|
1435
|
+
// fail-open semantics against the live-pending veto.
|
|
1436
|
+
const propagation = propagateWatchdogTranscriptCompletion(
|
|
1437
|
+
components, meshId, row, terminalEvidence, 'redrive_deadline_transcript_evidence',
|
|
1438
|
+
{ boundedBackstop: true },
|
|
1393
1439
|
);
|
|
1440
|
+
const propagated = propagation === 'propagated';
|
|
1394
1441
|
if (!propagated && !findTerminalLedgerEvidenceForTask({ meshId, taskId: row.id })) {
|
|
1395
1442
|
try {
|
|
1396
1443
|
appendLedgerEntry(meshId, {
|
|
@@ -42,7 +42,7 @@ import {
|
|
|
42
42
|
claimAntigravityConversation,
|
|
43
43
|
releaseAntigravityOwner,
|
|
44
44
|
} from './native-history/antigravity-claim-registry.js';
|
|
45
|
-
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages, extractFinalSummaryFromMessagesAfter, readChatMessageTimestampMs } from './chat-message-normalization.js';
|
|
45
|
+
import { buildChatMessage, buildRuntimeSystemChatMessage, isUserFacingChatMessage, normalizeChatMessages, resolveChatMessageKind, extractFinalSummaryFromMessages, extractFinalSummaryFromMessagesAfter, readChatMessageTimestampMs, hasTrailingToolActivityAfterFinalAssistant } from './chat-message-normalization.js';
|
|
46
46
|
import { workingDirBasename } from './working-dir.js';
|
|
47
47
|
import { ManualAttendanceTracker } from './manual-attendance.js';
|
|
48
48
|
import { buildCliStructuredInputPrompt } from './cli-provider-input-prompt.js';
|
|
@@ -1209,6 +1209,50 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1209
1209
|
return this.resolveModalParkStatus() !== null;
|
|
1210
1210
|
}
|
|
1211
1211
|
|
|
1212
|
+
/**
|
|
1213
|
+
* MID-TURN-LIVE-STATE-GATE (broader false-idle RCA, mid-turn follow-up): a live,
|
|
1214
|
+
* synchronous re-check of whether this session's CURRENT turn genuinely still has
|
|
1215
|
+
* unresolved work. Public wrapper so the coordinator (mesh-event-forwarding) can
|
|
1216
|
+
* independently re-verify an incoming agent:generating_completed for a LOCAL session
|
|
1217
|
+
* before trusting it — defense-in-depth against a race where the completion emit and the
|
|
1218
|
+
* coordinator's receipt straddle a state change (screen-redraw parse artifact, decoupled-
|
|
1219
|
+
* immediate emit). Reuses the EXACT same discriminators this instance's own finalization
|
|
1220
|
+
* gate (getCompletedFinalizationBlock) uses — hasAdapterPendingResponse() (adapter
|
|
1221
|
+
* isWaitingForResponse / currentTurnScope / isProcessing() / a non-empty partial response)
|
|
1222
|
+
* OR isModalParked() (a live approval/choice modal) — so a session this method reports
|
|
1223
|
+
* pending is, by construction, one the local finalization gate would also refuse to
|
|
1224
|
+
* finalize right now.
|
|
1225
|
+
*
|
|
1226
|
+
* NATIVE-TRAILING-TOOL-GATE (rc.16 follow-up): the three adapter-state discriminators
|
|
1227
|
+
* above are blind to a background shell tool that keeps a turn alive without the adapter
|
|
1228
|
+
* reporting a pending response — e.g. a backgrounded `sleep 40 &` between narration
|
|
1229
|
+
* bubbles on a write-lag native-source provider (claude-cli), which is deliberately
|
|
1230
|
+
* un-floored (noExternalTranscriptSource omitted, mission f2f6da1b owner decision) so its
|
|
1231
|
+
* transcript write-lag emits promptly. That un-flooring is correct for the ordinary
|
|
1232
|
+
* fraction-of-a-second trail, but it also means the growth-hold / busy-lease protections
|
|
1233
|
+
* in getCompletedFinalizationBlock never engage for claude-cli, so an interim final-
|
|
1234
|
+
* LOOKING bubble followed by continuing tool calls can still finalize as a completion
|
|
1235
|
+
* while the transcript demonstrably shows the turn still executing. Close that gap here,
|
|
1236
|
+
* narrowly: for a native-source provider only, reuse the SAME bounded transcript read the
|
|
1237
|
+
* class's own completion judgment already performs (probeNativeTranscriptSignals) and
|
|
1238
|
+
* apply the SAME trailing-tool-activity veto the transcript-synth admission choke point
|
|
1239
|
+
* uses (hasTrailingToolActivityAfterFinalAssistant) — the latest final-looking assistant
|
|
1240
|
+
* bubble followed by tool/terminal activity is interim narration, not a turn end. Fail-open
|
|
1241
|
+
* by construction: a non-native-source class (probe returns null), an unresolved
|
|
1242
|
+
* transcript, or a read error never reaches the veto, so a missing/unavailable transcript
|
|
1243
|
+
* can never wedge a session as "pending" forever.
|
|
1244
|
+
*/
|
|
1245
|
+
hasLiveTurnPendingEvidence(): boolean {
|
|
1246
|
+
if (this.hasAdapterPendingResponse() || this.isModalParked()) return true;
|
|
1247
|
+
try {
|
|
1248
|
+
const probe = this.probeNativeTranscriptSignals();
|
|
1249
|
+
if (probe?.snapshot?.available === true && Array.isArray(probe.messages)) {
|
|
1250
|
+
if (hasTrailingToolActivityAfterFinalAssistant(probe.messages as ChatMessage[])) return true;
|
|
1251
|
+
}
|
|
1252
|
+
} catch { /* fail open — a probe error must never fabricate pending evidence */ }
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1212
1256
|
/**
|
|
1213
1257
|
* PTY-OVERTRUST-DRAIN (Defect B). The deliverability/drain status the mesh
|
|
1214
1258
|
* reconcile loop must consult — the RAW adapter turn-state, with the
|
|
@@ -1824,7 +1868,24 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1824
1868
|
// `completionHasFinalAssistantMessage(...) && !hasAdapterPendingResponse()` pairing already
|
|
1825
1869
|
// used by the no-progress monitor reconcile path.
|
|
1826
1870
|
const turnClosed = !this.hasAdapterPendingResponse();
|
|
1827
|
-
|
|
1871
|
+
// TX-FSM Stage 2.1 (KIMI-PARSED-RACE): a native-source provider that ALSO ships a
|
|
1872
|
+
// tui.transcriptPty scrape (kimi, like opencode/cursor-cli) keeps that scrape purely
|
|
1873
|
+
// for LIVE status convenience — the manifest itself says "Chat is authoritative from
|
|
1874
|
+
// nativeHistory; this PTY extraction only keeps live status available". But the parsed
|
|
1875
|
+
// scrape carries no tool-activity concept at all (transcriptPty only extracts
|
|
1876
|
+
// assistant/user text bullets), so an interim narration bullet Kimi renders just before
|
|
1877
|
+
// firing a tool call ("코드와 로그를 병행으로 확인하겠습니다.") satisfies
|
|
1878
|
+
// completionHasFinalAssistantMessage identically to a genuine final answer — and this
|
|
1879
|
+
// early-return fires BEFORE the richer external-native evidence below is ever consulted,
|
|
1880
|
+
// defeating the trailing-tool-activity veto and quiet-dwell guard added below it for
|
|
1881
|
+
// this exact defect (live Kimi declaring completion on an interim transcript while the
|
|
1882
|
+
// PTY kept generating). Skip the parsed short-circuit for the lease-gated canary
|
|
1883
|
+
// (busyLeaseGateEnabled: kimi/codex-cli today) so the authoritative native transcript is
|
|
1884
|
+
// judged first; codex-cli ships no transcriptPty so this is a no-op for it. Falls back to
|
|
1885
|
+
// the parsed evidence below when the native transcript is unresolved (fail-open, same as
|
|
1886
|
+
// before Stage 2.1).
|
|
1887
|
+
const preferNativeOverParsed = (this.adapter as any)?.chatMessagesOwnedExternally === true && this.busyLeaseGateEnabled();
|
|
1888
|
+
if (!preferNativeOverParsed && this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)) {
|
|
1828
1889
|
return {
|
|
1829
1890
|
present: turnClosed,
|
|
1830
1891
|
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
@@ -1850,8 +1911,19 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1850
1911
|
// the injected task's onTurnStarted fires, currentTurnStartedAt/currentTurnTaskId
|
|
1851
1912
|
// bind to it and a real final bubble still fires completion normally.
|
|
1852
1913
|
const injectedTaskGenerating = this.injectedTaskHasStartedGenerating();
|
|
1914
|
+
// TX-FSM Stage 2.1 (KIMI-PARSED-RACE, trailing-tool-activity veto): mirrors the
|
|
1915
|
+
// mesh coordinator's pollAssignedTaskTerminalEvidence guard (ledger 84594b15) on
|
|
1916
|
+
// the WORKER's own local evidence. A native-source transcript that captures
|
|
1917
|
+
// tool.call/tool.result as kind:'tool' bubbles (kimi's nativeHistory.records, after
|
|
1918
|
+
// the provider-manifest fix) proves the last VISIBLE assistant bubble was narration
|
|
1919
|
+
// ("Let me check the logs...") that fired a tool call, not the turn's genuine final
|
|
1920
|
+
// answer. A provider whose native transcript never records tool activity (nothing to
|
|
1921
|
+
// veto on) is unaffected — this can only ever turn a false present:true into false,
|
|
1922
|
+
// never the reverse.
|
|
1923
|
+
const trailingToolActivity = hasTrailingToolActivityAfterFinalAssistant(externalMessages as any);
|
|
1853
1924
|
const present = injectedTaskGenerating
|
|
1854
1925
|
&& turnClosed
|
|
1926
|
+
&& !trailingToolActivity
|
|
1855
1927
|
&& this.completionHasFinalAssistantMessage(externalMessages, turnStartedAt);
|
|
1856
1928
|
// Dashboard tail-repair cache: this runs on EVERY completion check
|
|
1857
1929
|
// (mesh AND non-mesh — the non-mesh path suppresses the
|
|
@@ -1873,6 +1945,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1873
1945
|
};
|
|
1874
1946
|
}
|
|
1875
1947
|
|
|
1948
|
+
// TX-FSM Stage 2.1: the native transcript is unresolved (no session pinned yet, file
|
|
1949
|
+
// not yet written) for a provider whose parsed short-circuit was skipped above — fall
|
|
1950
|
+
// back to the parsed evidence now rather than reporting present:false forever. Fail-open,
|
|
1951
|
+
// identical to the pre-Stage-2.1 behaviour for this narrow (transcript-unavailable) case.
|
|
1952
|
+
if (preferNativeOverParsed && this.completionHasFinalAssistantMessage(parsedMessages, turnStartedAt)) {
|
|
1953
|
+
return {
|
|
1954
|
+
present: turnClosed,
|
|
1955
|
+
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
1956
|
+
source: 'parsed',
|
|
1957
|
+
};
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1876
1960
|
return {
|
|
1877
1961
|
present: false,
|
|
1878
1962
|
messages: Array.isArray(parsedMessages) ? parsedMessages : [],
|
|
@@ -2336,6 +2420,43 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2336
2420
|
} catch { /* defensive: dwell read is best-effort — fall through to emit */ }
|
|
2337
2421
|
}
|
|
2338
2422
|
|
|
2423
|
+
// TX-FSM Stage 2.2 (KIMI-POST-FINAL-WEDGE): quiet-dwell protection for a
|
|
2424
|
+
// lease-gated NATIVE-SOURCE provider's OWN evidence
|
|
2425
|
+
// (source==='external-native', reached because preferNativeOverParsed skipped the
|
|
2426
|
+
// parsed short-circuit above). Use the authoritative transcript file's mtime snapshot,
|
|
2427
|
+
// produced by the SAME native-history read that supplied finalAssistantEvidence above,
|
|
2428
|
+
// rather than raw PTY output recency. Kimi repaints its idle prompt/status after the
|
|
2429
|
+
// genuine final answer, so lastOutputAt may advance forever even though wire.jsonl is
|
|
2430
|
+
// quiet; using that cosmetic clock wedged the pending completion. Conversely, before a
|
|
2431
|
+
// tool.call lands the interim narration itself has just advanced wire.jsonl, so the
|
|
2432
|
+
// transcript clock preserves the narrow pre-tool quiet-dwell protection.
|
|
2433
|
+
//
|
|
2434
|
+
// The snapshot is bounded and fail-open: ageMs below the existing dwell holds and
|
|
2435
|
+
// retries; a quiet transcript releases, while an unavailable/unresolved/clockless
|
|
2436
|
+
// snapshot cannot block completion. Deliberately NOT scoped to
|
|
2437
|
+
// allowMissingAssistantTimeout: this defect surfaces on a plain interactive session's
|
|
2438
|
+
// own idle/completion notification just as much as a mesh worker's, so restricting it to
|
|
2439
|
+
// autonomous mesh sessions would leave every non-mesh kimi session unprotected. A
|
|
2440
|
+
// provider outside the lease canary (or without adapterOwnsMessagesElsewhere) never
|
|
2441
|
+
// reaches this branch — untouched.
|
|
2442
|
+
if (adapterOwnsMessagesElsewhere
|
|
2443
|
+
&& finalAssistantEvidence.source === 'external-native'
|
|
2444
|
+
&& this.busyLeaseGateEnabled()) {
|
|
2445
|
+
try {
|
|
2446
|
+
const snapshot = this.lastTranscriptSignalSnapshot;
|
|
2447
|
+
const transcriptAgeMs = snapshot?.available === true
|
|
2448
|
+
&& typeof snapshot.detail?.ageMs === 'number'
|
|
2449
|
+
&& Number.isFinite(snapshot.detail.ageMs)
|
|
2450
|
+
? snapshot.detail.ageMs
|
|
2451
|
+
: undefined;
|
|
2452
|
+
if (typeof transcriptAgeMs === 'number') {
|
|
2453
|
+
if (transcriptAgeMs < PTY_PARSED_FINAL_ASSISTANT_QUIET_DWELL_MS) {
|
|
2454
|
+
return { reason: 'native_source_final_assistant_quiet_dwell', terminal: false };
|
|
2455
|
+
}
|
|
2456
|
+
}
|
|
2457
|
+
} catch { /* defensive: transcript dwell is best-effort — fail open */ }
|
|
2458
|
+
}
|
|
2459
|
+
|
|
2339
2460
|
return null;
|
|
2340
2461
|
}
|
|
2341
2462
|
|
|
@@ -3150,10 +3271,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3150
3271
|
// the settle window — so the single sample reads 'idle' even though the turn is still in
|
|
3151
3272
|
// flight (it re-enters generating ~0.5s later). Require instead that the session stayed
|
|
3152
3273
|
// CONTINUOUSLY idle since the debounce was armed: (1) no entry into a busy phase
|
|
3153
|
-
// (busyEpoch unchanged), and (2) no new raw PTY output
|
|
3154
|
-
//
|
|
3155
|
-
//
|
|
3156
|
-
//
|
|
3274
|
+
// (busyEpoch unchanged), and (2) on the FIRST settle check, no new raw PTY output
|
|
3275
|
+
// (lastOutputAt did not advance).
|
|
3276
|
+
//
|
|
3277
|
+
// Once a finalization block has claimed the pending completion (loggedBlockReason is
|
|
3278
|
+
// set), its own bounded retry/evidence policy owns release. Re-applying the raw-output
|
|
3279
|
+
// cancellation on every retry lets cosmetic idle redraws (notably Kimi's prompt/status
|
|
3280
|
+
// repaint) delete the pending after a genuine final assistant has landed. The adapter
|
|
3281
|
+
// emits no second idle edge for that already-idle turn, so deletion becomes a permanent
|
|
3282
|
+
// internal-generating wedge. Busy re-entry remains an unconditional cancel above:
|
|
3283
|
+
// busyEpoch is the structural continuity signal and must win even after a hold.
|
|
3157
3284
|
if (typeof pending.busyEpochAtArm === 'number' && this.busyEpoch !== pending.busyEpochAtArm) {
|
|
3158
3285
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (busy re-entry during settle: epoch ${pending.busyEpochAtArm}→${this.busyEpoch})`);
|
|
3159
3286
|
if (this.completionTraceOn()) this.recordCompletionGateTrace('cancel', {
|
|
@@ -3169,7 +3296,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3169
3296
|
return;
|
|
3170
3297
|
}
|
|
3171
3298
|
const latestOutputAt = typeof (latestStatus as any)?.lastOutputAt === 'number' ? (latestStatus as any).lastOutputAt as number : undefined;
|
|
3172
|
-
if (
|
|
3299
|
+
if (!pending.loggedBlockReason
|
|
3300
|
+
&& typeof pending.lastOutputAtArm === 'number'
|
|
3173
3301
|
&& typeof latestOutputAt === 'number'
|
|
3174
3302
|
&& latestOutputAt > pending.lastOutputAtArm) {
|
|
3175
3303
|
LOG.info('CLI', `[${this.type}] cancelled pending completed (new PTY output during settle: ${pending.lastOutputAtArm}→${latestOutputAt})`);
|