@adhdev/daemon-core 1.0.18-rc.2 → 1.0.18-rc.20
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/cli-state-engine.d.ts +77 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +33 -0
- package/dist/cli-adapters/pty-transport.d.ts +2 -1
- package/dist/commands/cli-manager.d.ts +9 -0
- package/dist/commands/process-lifecycle.d.ts +43 -0
- package/dist/commands/upgrade-helper.d.ts +1 -0
- package/dist/commands/windows-atomic-upgrade.d.ts +17 -1
- package/dist/config/mesh-json-config.d.ts +62 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6674 -4237
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6361 -3927
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-disk-retention.d.ts +105 -0
- package/dist/mesh/mesh-ledger.d.ts +28 -1
- package/dist/mesh/mesh-node-identity.d.ts +23 -0
- package/dist/mesh/mesh-refine-gates.d.ts +89 -0
- package/dist/mesh/mesh-remote-event-pull.d.ts +3 -0
- package/dist/mesh/mesh-runtime-store.d.ts +11 -0
- package/dist/mesh/mesh-work-queue.d.ts +24 -1
- package/dist/providers/auto-approve-modes.d.ts +14 -0
- package/dist/providers/chat-message-normalization.d.ts +22 -0
- package/dist/providers/cli-provider-instance-types.d.ts +4 -0
- package/dist/providers/cli-provider-instance.d.ts +78 -0
- package/dist/providers/contracts.d.ts +17 -0
- package/dist/providers/provider-schema.d.ts +1 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +14 -0
- package/dist/providers/types/interactive-prompt.d.ts +18 -0
- package/dist/repo-mesh-types.d.ts +38 -6
- package/dist/shared-types.d.ts +4 -2
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +10 -0
- package/src/cli-adapters/cli-state-engine.ts +220 -3
- package/src/cli-adapters/provider-cli-adapter.ts +41 -11
- package/src/cli-adapters/provider-cli-shared.ts +48 -0
- package/src/cli-adapters/pty-transport.d.ts +2 -1
- package/src/cli-adapters/pty-transport.ts +1 -1
- package/src/cli-adapters/session-host-transport.ts +8 -3
- package/src/commands/cli-manager.ts +72 -8
- package/src/commands/high-family/mesh-coordinator-launch.ts +17 -2
- package/src/commands/high-family/mesh-events.ts +13 -2
- package/src/commands/med-family/mesh-crud.ts +155 -0
- package/src/commands/med-family/mesh-queue.ts +74 -1
- package/src/commands/process-lifecycle.ts +248 -0
- package/src/commands/router-refine.ts +71 -4
- package/src/commands/router.ts +8 -0
- package/src/commands/upgrade-helper.ts +106 -82
- package/src/commands/windows-atomic-upgrade.ts +281 -35
- package/src/config/mesh-json-config.ts +111 -0
- package/src/index.ts +21 -1
- package/src/mesh/coordinator-prompt.ts +258 -11
- package/src/mesh/mesh-active-work.ts +11 -1
- package/src/mesh/mesh-completion-synthesis.ts +12 -1
- package/src/mesh/mesh-disk-retention.ts +370 -0
- package/src/mesh/mesh-event-classify.ts +19 -0
- package/src/mesh/mesh-event-forwarding.ts +64 -6
- package/src/mesh/mesh-events-utils.ts +42 -0
- package/src/mesh/mesh-ledger.ts +77 -0
- package/src/mesh/mesh-node-identity.ts +49 -0
- package/src/mesh/mesh-queue-assignment.ts +210 -11
- package/src/mesh/mesh-reconcile-loop.ts +306 -3
- package/src/mesh/mesh-refine-gates.ts +300 -0
- package/src/mesh/mesh-remote-event-pull.ts +68 -47
- package/src/mesh/mesh-runtime-store.ts +21 -0
- package/src/mesh/mesh-work-queue.ts +85 -2
- package/src/providers/auto-approve-modes.ts +103 -0
- package/src/providers/chat-message-normalization.ts +53 -0
- package/src/providers/cli-provider-instance-types.ts +53 -0
- package/src/providers/cli-provider-instance.ts +497 -15
- package/src/providers/contracts.ts +25 -1
- package/src/providers/provider-schema.ts +83 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +23 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +20 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +44 -0
- package/src/providers/types/interactive-prompt.ts +77 -0
- package/src/repo-mesh-types.ts +112 -12
- package/src/session-host/managed-host.ts +34 -0
- package/src/shared-types.ts +9 -1
- package/src/status/reporter.ts +1 -0
- package/src/status/snapshot.ts +2 -0
|
@@ -12,11 +12,12 @@ import * as fs from 'fs';
|
|
|
12
12
|
import { normalizeInputEnvelope, type ProviderModule, flattenContent, type InputEnvelope } from './contracts.js';
|
|
13
13
|
import { assertProviderSupportsDeclaredInput, getEffectiveMessageInputSupport } from './provider-input-support.js';
|
|
14
14
|
import type { ProviderInstance, ProviderState, ProviderEvent, InstanceContext, ProviderErrorReason, HotChatSessionState, SessionModalState } from './provider-instance.js';
|
|
15
|
-
import { normalizeInteractivePrompt, normalizeInteractivePromptResponse, type InteractivePrompt } from './types/interactive-prompt.js';
|
|
15
|
+
import { normalizeInteractivePrompt, normalizeInteractivePromptResponse, resolveInteractivePromptResponse, type InteractivePrompt } from './types/interactive-prompt.js';
|
|
16
16
|
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';
|
|
@@ -56,8 +57,11 @@ import {
|
|
|
56
57
|
STATUS_HYDRATION_TAIL_LIMIT,
|
|
57
58
|
COMPLETED_FINALIZATION_RETRY_MS,
|
|
58
59
|
COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
60
|
+
CANON_C_MISSING_ASSISTANT_MIN_ELAPSED_MS,
|
|
59
61
|
NATIVE_HISTORY_MESH_IDLE_SETTLE_MS,
|
|
60
62
|
PTY_PARSED_FINAL_ASSISTANT_QUIET_DWELL_MS,
|
|
63
|
+
ANTIGRAVITY_HOLD_QUIET_DWELL_MS,
|
|
64
|
+
ANTIGRAVITY_HOLD_HARD_CAP_MS,
|
|
61
65
|
BACKGROUND_TASK_HOLD_MAX_MS,
|
|
62
66
|
USER_INPUT_ACK_DEDUP_WINDOW_MS,
|
|
63
67
|
STARTUP_GRACE_IDLE_COLLAPSE_WINDOW_MS,
|
|
@@ -71,6 +75,7 @@ import type {
|
|
|
71
75
|
} from './cli-provider-instance-types.js';
|
|
72
76
|
import { mergeConversationMessages, buildExternalTranscriptProbe } from './cli-provider-transcript-merge.js';
|
|
73
77
|
import { getEffectDedupKey, formatApprovalRequestMessage, formatMarkerTimestamp } from './cli-provider-effect-format.js';
|
|
78
|
+
import { resolveProviderAutoApproveMode, type ResolvedAutoApproveMode } from './auto-approve-modes.js';
|
|
74
79
|
|
|
75
80
|
// Re-export moved public symbols so existing importers (index.ts, tests) keep
|
|
76
81
|
// their `./cli-provider-instance.js` path. Pure move — no behavior change.
|
|
@@ -329,6 +334,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
329
334
|
// MESH_WORKER_STALL_REFIRE_COOLDOWN_MS between successive emissions across
|
|
330
335
|
// anchor re-arms (the per-anchor guard only covers a single continuous stall).
|
|
331
336
|
private meshStallLastFiredAt = -1;
|
|
337
|
+
// KIMI-MESH-COMPLETION-EMIT (axis 1): the native-source transcript progress
|
|
338
|
+
// fingerprint (record count + source mtime) observed the LAST time the stall
|
|
339
|
+
// watchdog checked a native-source provider (transcriptAuthority=provider — e.g.
|
|
340
|
+
// kimi wire.jsonl). A pure-PTY native-source worker running a long, screen-quiet
|
|
341
|
+
// tool (no viewport bytes for minutes) looks stalled by the lastOutputAt clock
|
|
342
|
+
// even though its transcript file is still growing. Before firing the stall, we
|
|
343
|
+
// compare the current transcript fingerprint against this snapshot; if the
|
|
344
|
+
// transcript advanced, the "stall" is a false positive of PTY-render stasis, so
|
|
345
|
+
// we re-arm the anchor instead of paging the coordinator (whose no_progress
|
|
346
|
+
// handling can lead to the worker being stopped mid-work → completion never
|
|
347
|
+
// emitted). null = not yet sampled for this session/anchor.
|
|
348
|
+
private meshStallNativeTranscriptSample: { msgCount: number; sourceMtimeMs: number } | null = null;
|
|
332
349
|
// FALSE-IDLE continuity epoch: monotonically bumped on EVERY entry into a busy
|
|
333
350
|
// phase (→generating or →waiting_approval). The completedDebouncePending snapshots
|
|
334
351
|
// this value at arm time (busyEpochAtArm); the flush guard requires it UNCHANGED
|
|
@@ -369,6 +386,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
369
386
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
370
387
|
private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
|
|
371
388
|
private lastApprovalEventFingerprint = '';
|
|
389
|
+
// INTERACTIVE-PROMPT-PUSH: edge-trigger key for the AskUserQuestion / waiting_choice
|
|
390
|
+
// notification. An AskUserQuestion prompt is surfaced only as a display-only
|
|
391
|
+
// `waiting_choice` overlay in getState(); the raw adapter status stays idle/generating,
|
|
392
|
+
// so detectStatusTransition()'s status-keyed arms never fire and no push-worthy
|
|
393
|
+
// agent:* event is emitted — the owner misses the "ACTION REQUIRED" prompt when the
|
|
394
|
+
// app is backgrounded. We emit one agent:waiting_choice on ENTRY into the prompt
|
|
395
|
+
// state (its own coordinator event, NOT the approval channel — a multi-choice
|
|
396
|
+
// question is answered with mesh_answer_question, never mesh_approve) carrying the
|
|
397
|
+
// FULL InteractivePrompt payload, and dedupe on this key so repeated status ticks
|
|
398
|
+
// with the same prompt do not re-fire. '' means no prompt is currently active
|
|
399
|
+
// (cleared when the prompt is answered/gone).
|
|
400
|
+
private lastInteractivePromptEventKey = '';
|
|
372
401
|
private autoApproveBusy = false;
|
|
373
402
|
private autoApproveBusyTimer: NodeJS.Timeout | null = null;
|
|
374
403
|
private lastAutoApprovalSignature = '';
|
|
@@ -1260,7 +1289,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1260
1289
|
}
|
|
1261
1290
|
} else if (event === 'interactive_prompt_response' && data) {
|
|
1262
1291
|
try {
|
|
1263
|
-
|
|
1292
|
+
// mesh_answer_question (mission f1d25e11) sends a coordinator-friendly answer
|
|
1293
|
+
// form (per-question select by label/index) that must be resolved against the
|
|
1294
|
+
// AUTHORITATIVE active prompt held here. When the active prompt is present and
|
|
1295
|
+
// the promptId matches, resolve it; otherwise fall back to the strict keyed
|
|
1296
|
+
// form (dashboard local answers already send that shape).
|
|
1297
|
+
const response = (this.activeInteractivePrompt
|
|
1298
|
+
&& this.activeInteractivePrompt.promptId === (data as { promptId?: unknown })?.promptId
|
|
1299
|
+
&& Array.isArray((data as { answers?: unknown })?.answers))
|
|
1300
|
+
? resolveInteractivePromptResponse(this.activeInteractivePrompt, data)
|
|
1301
|
+
: normalizeInteractivePromptResponse(data);
|
|
1264
1302
|
if (this.activeInteractivePrompt?.promptId === response.promptId) {
|
|
1265
1303
|
this.activeInteractivePrompt = null;
|
|
1266
1304
|
}
|
|
@@ -1394,6 +1432,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1394
1432
|
*/
|
|
1395
1433
|
private lastCompletionSummary: { content: string; receivedAt: number; sourceTimestampMs?: number } | null = null;
|
|
1396
1434
|
|
|
1435
|
+
// KIMI-MESH-COMPLETION-EMIT (axis 2, double-emit guard): the (taskId, wall-clock)
|
|
1436
|
+
// of the most recent agent:generating_completed this instance emitted, stamped by
|
|
1437
|
+
// emitGeneratingCompleted. The pre-cleanup completion flush
|
|
1438
|
+
// (flushMeshCompletionBeforeCleanup, driven by cli-manager's exit monitor) reads
|
|
1439
|
+
// this to refuse a SECOND completion for a turn whose completion already fired —
|
|
1440
|
+
// so a worker that finished cleanly and is simply being auto-cleaned never emits a
|
|
1441
|
+
// duplicate. taskId '' covers an ad-hoc (no-task) turn. null = none emitted yet.
|
|
1442
|
+
private lastEmittedCompletion: { taskId: string; at: number } | null = null;
|
|
1443
|
+
|
|
1397
1444
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
1398
1445
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
1399
1446
|
if (!scriptName) return;
|
|
@@ -1897,6 +1944,33 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1897
1944
|
return false;
|
|
1898
1945
|
}
|
|
1899
1946
|
|
|
1947
|
+
// (ANTIGRAVITY-30S-CAP-PREMATURE) Discriminator gating the 30s-cap release of an antigravity
|
|
1948
|
+
// `holdForTranscript` block. Antigravity's idle verdict is PTY-screen-derived but its assistant
|
|
1949
|
+
// answer lands in native-history, which can legitimately lag past COMPLETED_FINALIZATION_MAX_WAIT_MS
|
|
1950
|
+
// (30s) on a long turn. The cap releases on elapsed time, not proof-of-idle, so it force-emitted a
|
|
1951
|
+
// premature weak completion WHILE THE PTY WAS STILL GENERATING. Returns true when the PTY is still
|
|
1952
|
+
// active — i.e. the adapter reports a pending response OR raw PTY output arrived within the last
|
|
1953
|
+
// ANTIGRAVITY_HOLD_QUIET_DWELL_MS — meaning the 30s cap must KEEP HOLDING (the turn is not proven
|
|
1954
|
+
// over). Returns false when the PTY is genuinely quiescent (no pending response AND no recent
|
|
1955
|
+
// output), so a real tool-only turn with no assistant bubble still force-emits a weak completion
|
|
1956
|
+
// rather than wedging. The absolute ANTIGRAVITY_HOLD_HARD_CAP_MS bound is enforced at the call site
|
|
1957
|
+
// so a runaway PTY that never falls quiet still eventually releases. Fails OPEN (returns false =
|
|
1958
|
+
// allow release) when lastOutputAt is unreadable, so the gate can never wedge a session.
|
|
1959
|
+
private antigravityHoldPtyStillActive(): boolean {
|
|
1960
|
+
if (this.hasAdapterPendingResponse()) return true;
|
|
1961
|
+
try {
|
|
1962
|
+
const outStatus = this.adapter.getStatus({ allowParse: false }) as any;
|
|
1963
|
+
const lastOutputAt = typeof outStatus?.lastOutputAt === 'number' && Number.isFinite(outStatus.lastOutputAt)
|
|
1964
|
+
? outStatus.lastOutputAt as number
|
|
1965
|
+
: undefined;
|
|
1966
|
+
if (typeof lastOutputAt === 'number') {
|
|
1967
|
+
const quietMs = Date.now() - lastOutputAt;
|
|
1968
|
+
if (quietMs < ANTIGRAVITY_HOLD_QUIET_DWELL_MS) return true;
|
|
1969
|
+
}
|
|
1970
|
+
} catch { /* defensive: dwell read is best-effort — fall through to allow release */ }
|
|
1971
|
+
return false;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1900
1974
|
private shouldSuppressStaleParsedBusyStatus(parsedStatus: any, adapterStatus: any): boolean {
|
|
1901
1975
|
const parsedRawStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status.trim() : '';
|
|
1902
1976
|
const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
@@ -2045,6 +2119,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2045
2119
|
reason: 'missing_final_assistant',
|
|
2046
2120
|
terminal: (this.provider as any).requiresFinalAssistantBeforeIdle === true,
|
|
2047
2121
|
allowTimeout: allowMissingAssistantTimeout,
|
|
2122
|
+
// PTY-parsed provider (codex-cli): no external transcript trails the idle
|
|
2123
|
+
// transition, so the CANON-C decoupled emit must observe the min-elapsed floor
|
|
2124
|
+
// rather than fire at the first-poll waitedMs. (claude-cli's external-native
|
|
2125
|
+
// branch above is exempt — its transcript legitimately lands moments later.)
|
|
2126
|
+
noExternalTranscriptSource: true,
|
|
2048
2127
|
};
|
|
2049
2128
|
}
|
|
2050
2129
|
}
|
|
@@ -2348,6 +2427,52 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2348
2427
|
const stalledMs = now - this.meshStallAnchorAt;
|
|
2349
2428
|
if (stalledMs < threshold) return;
|
|
2350
2429
|
|
|
2430
|
+
// KIMI-MESH-COMPLETION-EMIT (axis 1): transcript-aware stall for native-source
|
|
2431
|
+
// providers. A pure-PTY native-source worker (transcriptAuthority=provider,
|
|
2432
|
+
// e.g. kimi) running a long, screen-quiet tool emits no viewport bytes for
|
|
2433
|
+
// minutes, so the lastOutputAt clock above reads it as stalled — but its
|
|
2434
|
+
// authoritative transcript (wire.jsonl) is still being appended. Firing
|
|
2435
|
+
// monitor:no_progress here surfaces a false stall to the coordinator whose
|
|
2436
|
+
// downstream handling can stop the worker mid-work, killing the emit window
|
|
2437
|
+
// before the real completion propagates. So before firing, sample the native
|
|
2438
|
+
// transcript's progress fingerprint: if it advanced (more records OR a fresher
|
|
2439
|
+
// source mtime) since the last sample, the worker is demonstrably alive — treat
|
|
2440
|
+
// it as progress. Re-arm the anchor to `now` and reset the episode so the clock
|
|
2441
|
+
// restarts from this proven-live moment; only a transcript that is ALSO static
|
|
2442
|
+
// falls through to the genuine-stall fire below. No-op for pure-PTY providers
|
|
2443
|
+
// (sample is null → unchanged behavior).
|
|
2444
|
+
const nativeSample = this.sampleNativeTranscriptProgress();
|
|
2445
|
+
if (nativeSample) {
|
|
2446
|
+
const prev = this.meshStallNativeTranscriptSample;
|
|
2447
|
+
const advanced = !prev
|
|
2448
|
+
|| nativeSample.msgCount > prev.msgCount
|
|
2449
|
+
|| nativeSample.sourceMtimeMs > prev.sourceMtimeMs;
|
|
2450
|
+
this.meshStallNativeTranscriptSample = nativeSample;
|
|
2451
|
+
if (advanced) {
|
|
2452
|
+
if (this.isMeshWorkerSession()) {
|
|
2453
|
+
traceMeshEventDrop('mesh_worker_stall_transcript_advancing', this.meshTraceCtx('monitor:no_progress'),
|
|
2454
|
+
`msgCount=${nativeSample.msgCount} sourceMtime=${nativeSample.sourceMtimeMs} (PTY quiet ${Math.round(stalledMs / 1000)}s but transcript advancing)`);
|
|
2455
|
+
}
|
|
2456
|
+
this.meshStallAnchorAt = now;
|
|
2457
|
+
this.meshStallEmittedForAnchor = false;
|
|
2458
|
+
return;
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
|
|
2462
|
+
// KIMI-PURE-PTY-COMPLETION-EMIT (Fix 3): before firing a false stall, check the
|
|
2463
|
+
// pure-PTY class (no native transcript → nativeSample was null above). A finished
|
|
2464
|
+
// pure-PTY worker sits at a static idle prompt with an in-turn final assistant
|
|
2465
|
+
// message but never emitted generating_completed — the status-agnostic watchdog
|
|
2466
|
+
// would misread that quiet as a wedge. If the PTY transcript proves a finished
|
|
2467
|
+
// turn, emit the missing completion and SUPPRESS the stall (mark the anchor
|
|
2468
|
+
// emitted so this static idle is not re-checked every tick). No-op for
|
|
2469
|
+
// native-source providers and for a genuinely mid-turn / wedged worker with no
|
|
2470
|
+
// final assistant → the real stall fires below unchanged.
|
|
2471
|
+
if (this.tryReconcilePurePtyCompletionForStall(observedStatus)) {
|
|
2472
|
+
this.meshStallEmittedForAnchor = true;
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2351
2476
|
// FALSE-STALL-WATCHDOG-OVERFIRE (fix E): per-session refire cooldown. Mark
|
|
2352
2477
|
// this anchor emitted regardless so a still-static anchor does not re-check
|
|
2353
2478
|
// every tick, but suppress the actual coordinator notification (and the
|
|
@@ -2398,6 +2523,193 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2398
2523
|
this.meshStallEmittedForAnchor = false;
|
|
2399
2524
|
this.meshStallTurnActiveLast = undefined;
|
|
2400
2525
|
this.meshStallLastFiredAt = -1;
|
|
2526
|
+
this.meshStallNativeTranscriptSample = null;
|
|
2527
|
+
}
|
|
2528
|
+
|
|
2529
|
+
/**
|
|
2530
|
+
* KIMI-MESH-COMPLETION-EMIT (axis 1). For a native-source provider
|
|
2531
|
+
* (transcriptAuthority=provider — its authoritative history is an on-disk
|
|
2532
|
+
* transcript file, e.g. kimi's wire.jsonl), sample the transcript's current
|
|
2533
|
+
* progress fingerprint (record count + source-file mtime) WITHOUT parsing the
|
|
2534
|
+
* PTY. Used by the stall watchdog to distinguish "PTY render is quiet but the
|
|
2535
|
+
* worker is still doing long tool work (transcript growing)" from a genuine
|
|
2536
|
+
* wedge. Returns null for a pure-PTY provider (no native source) or when the
|
|
2537
|
+
* source cannot be resolved this tick — the caller then falls back to the
|
|
2538
|
+
* unchanged lastOutputAt-only judgment.
|
|
2539
|
+
*
|
|
2540
|
+
* Generalized on the provider's native-source flag, NOT a hardcoded provider
|
|
2541
|
+
* type, so every current and future pure-PTY long-tool native-source provider
|
|
2542
|
+
* benefits. Cheap enough for the stall path: it runs only at the stall threshold
|
|
2543
|
+
* (≥180s of PTY stasis), never on the routine 5s tick.
|
|
2544
|
+
*/
|
|
2545
|
+
private sampleNativeTranscriptProgress(): { msgCount: number; sourceMtimeMs: number } | null {
|
|
2546
|
+
if (!isNativeSourceCanonicalHistory(this.provider?.nativeHistory)) return null;
|
|
2547
|
+
// readExternalCompletionMessages resolves this session's OWN native-source
|
|
2548
|
+
// conversation (providerSessionId / persisted pin / floor claim) and, as a
|
|
2549
|
+
// side effect, refreshes this.lastExternalCompletionProbe with the source
|
|
2550
|
+
// path, mtime, and message count. Reusing it keeps the resolution logic in
|
|
2551
|
+
// one place and immune to the antigravity-style session-id quirks.
|
|
2552
|
+
let messages: unknown[] | null = null;
|
|
2553
|
+
try {
|
|
2554
|
+
messages = this.readExternalCompletionMessages();
|
|
2555
|
+
} catch {
|
|
2556
|
+
return null; // best-effort: an unresolved transcript → fall back to PTY clock
|
|
2557
|
+
}
|
|
2558
|
+
const probe = this.lastExternalCompletionProbe;
|
|
2559
|
+
if (!probe) return null;
|
|
2560
|
+
return {
|
|
2561
|
+
msgCount: typeof probe.msgCount === 'number' ? probe.msgCount : (Array.isArray(messages) ? messages.length : 0),
|
|
2562
|
+
sourceMtimeMs: typeof probe.sourceMtimeMs === 'number' ? probe.sourceMtimeMs : 0,
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2566
|
+
/**
|
|
2567
|
+
* KIMI-MESH-COMPLETION-EMIT (axis 2). Last-chance completion emit for a mesh
|
|
2568
|
+
* DELEGATED worker whose PTY has already exited (e.g. killed by a false stall)
|
|
2569
|
+
* and is about to be auto-cleaned (cli-manager.startCliExitMonitor →
|
|
2570
|
+
* removeInstance closes the event-emit window forever). If the worker actually
|
|
2571
|
+
* FINISHED its assigned turn — its authoritative native transcript holds a final
|
|
2572
|
+
* assistant message for the injected task — but the completion event never fired
|
|
2573
|
+
* (the stall-kill happened before the FSM's idle transition), emit it now so the
|
|
2574
|
+
* coordinator learns the task completed instead of waiting ~180s for the reconcile
|
|
2575
|
+
* transcript-poll to reclaim it.
|
|
2576
|
+
*
|
|
2577
|
+
* Scope & guards (must all hold to emit):
|
|
2578
|
+
* • mesh worker session only — a normal standalone session's ordinary exit is
|
|
2579
|
+
* never synthesized (isMeshWorkerSession()).
|
|
2580
|
+
* • DOUBLE-EMIT guard — refuse if this turn's completion already fired
|
|
2581
|
+
* (lastEmittedCompletion matches the current taskId). A worker that completed
|
|
2582
|
+
* cleanly and is merely being cleaned up never double-emits.
|
|
2583
|
+
* • evidence gate — reuse the same final-assistant/turn-scoped summary machinery
|
|
2584
|
+
* as the normal completion path (completionFinalSummary over the native
|
|
2585
|
+
* transcript). No in-turn assistant summary ⇒ no proof of completion ⇒ leave
|
|
2586
|
+
* the reclaim path to handle a genuinely-unfinished worker.
|
|
2587
|
+
*
|
|
2588
|
+
* Returns true when a synthetic completion was emitted, false otherwise. Safe to
|
|
2589
|
+
* call unconditionally from the cleanup path.
|
|
2590
|
+
*/
|
|
2591
|
+
flushMeshCompletionBeforeCleanup(): boolean {
|
|
2592
|
+
if (!this.isMeshWorkerSession()) return false;
|
|
2593
|
+
const taskId = this.completingTurnTaskId();
|
|
2594
|
+
|
|
2595
|
+
// DOUBLE-EMIT guard: this turn's completion already fired — never re-emit.
|
|
2596
|
+
if (this.lastEmittedCompletion && this.lastEmittedCompletion.taskId === (taskId ?? '')) {
|
|
2597
|
+
return false;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2600
|
+
// Evidence gate: the injected task's own turn must have genuinely started
|
|
2601
|
+
// (guards against synthesizing a completion off a reused session's stale tail
|
|
2602
|
+
// before this task ran) and the native transcript must hold an in-turn final
|
|
2603
|
+
// assistant summary. turnStartedAt anchors the turn-scoped read.
|
|
2604
|
+
if (!this.injectedTaskHasStartedGenerating()) return false;
|
|
2605
|
+
const turnStartedAt = typeof (this.adapter as any)?.currentTurnStartedAt === 'number'
|
|
2606
|
+
? (this.adapter as any).currentTurnStartedAt as number
|
|
2607
|
+
: this.meshTaskInjectedAt || undefined;
|
|
2608
|
+
let parsedMessages: unknown;
|
|
2609
|
+
try {
|
|
2610
|
+
parsedMessages = this.adapter?.getScriptParsedStatus?.()?.messages;
|
|
2611
|
+
} catch { parsedMessages = undefined; }
|
|
2612
|
+
let finalSummary: string | undefined;
|
|
2613
|
+
try {
|
|
2614
|
+
finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
|
|
2615
|
+
} catch { finalSummary = undefined; }
|
|
2616
|
+
// No in-turn final assistant evidence → not a proven turn-end. Defer to the
|
|
2617
|
+
// coordinator's reclaim/reconcile path rather than fabricate a completion.
|
|
2618
|
+
if (!finalSummary) return false;
|
|
2619
|
+
|
|
2620
|
+
LOG.warn('CLI', `[${this.type}] emitting pre-cleanup mesh completion for session ${this.instanceId} `
|
|
2621
|
+
+ `task=${taskId ?? '(none)'} — PTY exited before the completion event fired but the native transcript `
|
|
2622
|
+
+ `shows a finished turn; synthesizing completion so the coordinator is not left waiting for reclaim.`);
|
|
2623
|
+
if (this.isMeshWorkerSession()) {
|
|
2624
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), 'pre_cleanup_transcript_completion');
|
|
2625
|
+
}
|
|
2626
|
+
this.emitGeneratingCompleted({
|
|
2627
|
+
chatTitle: '',
|
|
2628
|
+
duration: undefined,
|
|
2629
|
+
timestamp: Date.now(),
|
|
2630
|
+
taskId,
|
|
2631
|
+
finalSummary,
|
|
2632
|
+
evidenceLevel: 'transcript',
|
|
2633
|
+
completionDiagnostic: { source: 'pre_cleanup_transcript_completion' },
|
|
2634
|
+
});
|
|
2635
|
+
return true;
|
|
2636
|
+
}
|
|
2637
|
+
|
|
2638
|
+
/**
|
|
2639
|
+
* KIMI-PURE-PTY-COMPLETION-EMIT (Fix 3): stall-path completion reconcile for the
|
|
2640
|
+
* PURE-PTY transcript class (kimi and kin — no native transcript, no provider
|
|
2641
|
+
* authority; see isPurePtyTranscriptProvider). Such a worker whose
|
|
2642
|
+
* generating_completed never emitted (the onTurnStarted idle→idle collapse Fix 1
|
|
2643
|
+
* fixes at the source) sits at a STATIC idle prompt: its PTY output stops the
|
|
2644
|
+
* instant the answer is rendered, so the status-agnostic no-progress watchdog
|
|
2645
|
+
* (checkMeshWorkerStall) reads the finished-but-quiet session as a stall and would
|
|
2646
|
+
* false-fire monitor:no_progress. The native-transcript reconcile
|
|
2647
|
+
* (sampleNativeTranscriptProgress) does NOT cover this class (no native source →
|
|
2648
|
+
* null sample), so the stall path needs its own guard.
|
|
2649
|
+
*
|
|
2650
|
+
* Called from checkMeshWorkerStall just before the fire. Returns true when the
|
|
2651
|
+
* session is a finished pure-PTY turn — idle, no pending response, and a PTY-parsed
|
|
2652
|
+
* in-turn final assistant summary — in which case it emits the missing
|
|
2653
|
+
* generating_completed (idempotent: a real/late emit writes the terminal ledger and
|
|
2654
|
+
* the coordinator's reconcile makes any duplicate a no-op) and the caller SUPPRESSES
|
|
2655
|
+
* the stall. Returns false for every other class/state (native-source provider, a
|
|
2656
|
+
* genuinely mid-turn or wedged worker with no final assistant), so a real stall still
|
|
2657
|
+
* fires unchanged.
|
|
2658
|
+
*
|
|
2659
|
+
* Evidence bar is identical to flushMeshCompletionBeforeCleanup: injected task's turn
|
|
2660
|
+
* genuinely started + an in-turn final assistant summary. Conservative by
|
|
2661
|
+
* construction — no summary ⇒ no proof of completion ⇒ return false and let the real
|
|
2662
|
+
* stall fire.
|
|
2663
|
+
*/
|
|
2664
|
+
private tryReconcilePurePtyCompletionForStall(observedStatus: string): boolean {
|
|
2665
|
+
// Only the pure-PTY class — native-source providers are handled by
|
|
2666
|
+
// sampleNativeTranscriptProgress above.
|
|
2667
|
+
if (!isPurePtyTranscriptProvider(this.provider)) return false;
|
|
2668
|
+
// A finished turn is idle with nothing pending. A generating/waiting session is
|
|
2669
|
+
// mid-turn (the real stall path should evaluate it), so never complete it here.
|
|
2670
|
+
if (observedStatus !== 'idle') return false;
|
|
2671
|
+
if (this.hasAdapterPendingResponse()) return false;
|
|
2672
|
+
|
|
2673
|
+
const taskId = this.completingTurnTaskId();
|
|
2674
|
+
// DOUBLE-EMIT guard: this turn's completion already fired — never re-emit.
|
|
2675
|
+
if (this.lastEmittedCompletion && this.lastEmittedCompletion.taskId === (taskId ?? '')) {
|
|
2676
|
+
return false;
|
|
2677
|
+
}
|
|
2678
|
+
// The injected task's own turn must have genuinely started (guards against a
|
|
2679
|
+
// reused session's stale tail before this task ran).
|
|
2680
|
+
if (!this.injectedTaskHasStartedGenerating()) return false;
|
|
2681
|
+
|
|
2682
|
+
const turnStartedAt = typeof (this.adapter as any)?.currentTurnStartedAt === 'number'
|
|
2683
|
+
? (this.adapter as any).currentTurnStartedAt as number
|
|
2684
|
+
: this.meshTaskInjectedAt || undefined;
|
|
2685
|
+
let parsedMessages: unknown;
|
|
2686
|
+
try {
|
|
2687
|
+
parsedMessages = this.adapter?.getScriptParsedStatus?.()?.messages;
|
|
2688
|
+
} catch { parsedMessages = undefined; }
|
|
2689
|
+
let finalSummary: string | undefined;
|
|
2690
|
+
try {
|
|
2691
|
+
finalSummary = this.completionFinalSummary(parsedMessages, turnStartedAt);
|
|
2692
|
+
} catch { finalSummary = undefined; }
|
|
2693
|
+
// No in-turn final assistant evidence → not a proven turn-end. Let the real
|
|
2694
|
+
// stall fire so a genuinely-wedged worker is still surfaced.
|
|
2695
|
+
if (!finalSummary) return false;
|
|
2696
|
+
|
|
2697
|
+
LOG.warn('CLI', `[${this.type}] reconciling pure-PTY mesh completion from the stall path for session ${this.instanceId} `
|
|
2698
|
+
+ `task=${taskId ?? '(none)'} — PTY is idle-quiet with an in-turn final assistant message but the completion `
|
|
2699
|
+
+ `event never fired (pure-PTY provider); emitting it instead of a false monitor:no_progress.`);
|
|
2700
|
+
if (this.isMeshWorkerSession()) {
|
|
2701
|
+
traceMeshEventStage('fired', this.meshTraceCtx(), 'stall_pure_pty_transcript_completion');
|
|
2702
|
+
}
|
|
2703
|
+
this.emitGeneratingCompleted({
|
|
2704
|
+
chatTitle: '',
|
|
2705
|
+
duration: undefined,
|
|
2706
|
+
timestamp: Date.now(),
|
|
2707
|
+
taskId,
|
|
2708
|
+
finalSummary,
|
|
2709
|
+
evidenceLevel: 'transcript',
|
|
2710
|
+
completionDiagnostic: { source: 'stall_pure_pty_transcript_completion' },
|
|
2711
|
+
});
|
|
2712
|
+
return true;
|
|
2401
2713
|
}
|
|
2402
2714
|
|
|
2403
2715
|
/**
|
|
@@ -2469,7 +2781,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2469
2781
|
* resolveModal, so a plain turn that never saw an approval always returns false.
|
|
2470
2782
|
*/
|
|
2471
2783
|
private inApprovalResumeGrace(now = Date.now()): boolean {
|
|
2472
|
-
if (!this.isAutonomousMeshSession() || !this.
|
|
2784
|
+
if (!this.isAutonomousMeshSession() || !this.shouldUsePtyAutoApprove()) return false;
|
|
2473
2785
|
const resolvedAt = typeof (this.adapter as any)?.lastApprovalResolvedAt === 'number'
|
|
2474
2786
|
? (this.adapter as any).lastApprovalResolvedAt as number
|
|
2475
2787
|
: 0;
|
|
@@ -2586,7 +2898,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2586
2898
|
}
|
|
2587
2899
|
|
|
2588
2900
|
const latestStatus = this.adapter.getStatus({ allowParse: false });
|
|
2589
|
-
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.
|
|
2901
|
+
const latestAutoApproveActive = latestStatus.status === 'waiting_approval' && this.shouldUsePtyAutoApprove();
|
|
2590
2902
|
const latestVisibleStatus = latestAutoApproveActive || this.autoApproveBusy ? 'generating' : latestStatus.status;
|
|
2591
2903
|
LOG.debug('CLI', `[${this.type}] flush attempt: adapterStatus=${latestStatus.status} latestVisible=${latestVisibleStatus} generatingStartedAt=${this.generatingStartedAt} isWaitingForResponse=${!!(this.adapter as any)?.isWaitingForResponse} hasPartial=${!!this.adapter.getPartialResponse?.()}`);
|
|
2592
2904
|
if (latestVisibleStatus !== 'idle') {
|
|
@@ -2751,6 +3063,79 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2751
3063
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
2752
3064
|
return;
|
|
2753
3065
|
}
|
|
3066
|
+
// (CANON-C EARLY-EMIT FLOOR) The transcript-evidence gate would otherwise fire its
|
|
3067
|
+
// decoupled-immediate completion at ANY waitedMs — including the ~13s a codex PLAIN
|
|
3068
|
+
// terminal `missing_final_assistant` block reaches on the first completion-gate poll,
|
|
3069
|
+
// with NO final assistant ever observed. That stamps a weak evidenceLevel=insufficient
|
|
3070
|
+
// completion the coordinator marks reviewRecommended but does not auto-hold, racing the
|
|
3071
|
+
// transcript before it can land and pre-empting the 180s mesh-worker stall watchdog.
|
|
3072
|
+
// Impose a minimum dwell — but ONLY for the no-external-transcript case
|
|
3073
|
+
// (noExternalTranscriptSource): a PTY-parsed provider has no separate transcript that
|
|
3074
|
+
// will land to upgrade the weak emit, so firing immediately is a pure timing guess. A
|
|
3075
|
+
// native-source block (claude-cli external-native) is deliberately NOT floored: its
|
|
3076
|
+
// transcript legitimately trails the idle transition by a write and the CANON-C
|
|
3077
|
+
// immediate emit is correct (upgraded by the reconcile). Keep holding (re-probe each
|
|
3078
|
+
// retry — the block clears for a GENUINE emit the moment the assistant turn lands) until
|
|
3079
|
+
// either the floor is met or the 30s cap force-releases the weak completion below.
|
|
3080
|
+
// Scoped to mesh worker sessions (the only place allowTimeout is set), so interactive
|
|
3081
|
+
// completion timing is untouched.
|
|
3082
|
+
if (isTranscriptEvidenceGate
|
|
3083
|
+
&& blockReason === 'missing_final_assistant'
|
|
3084
|
+
&& block.noExternalTranscriptSource === true
|
|
3085
|
+
&& waitedMs < CANON_C_MISSING_ASSISTANT_MIN_ELAPSED_MS) {
|
|
3086
|
+
if (pending.loggedBlockReason !== 'canon_c_min_elapsed_floor') {
|
|
3087
|
+
LOG.info('CLI', `[${this.type}] holding CANON-C decoupled emit until min-elapsed floor (waitedMs=${waitedMs} floor=${CANON_C_MISSING_ASSISTANT_MIN_ELAPSED_MS}); no final assistant yet (${blockReason})`);
|
|
3088
|
+
if (this.isMeshWorkerSession()) {
|
|
3089
|
+
traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `canon_c_min_elapsed_floor waited=${waitedMs}ms`);
|
|
3090
|
+
}
|
|
3091
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('hold', {
|
|
3092
|
+
blockReason,
|
|
3093
|
+
latestVisibleStatus,
|
|
3094
|
+
terminal: block.terminal === true,
|
|
3095
|
+
canonCMinElapsedFloor: true,
|
|
3096
|
+
waitedMs,
|
|
3097
|
+
});
|
|
3098
|
+
pending.loggedBlockReason = 'canon_c_min_elapsed_floor';
|
|
3099
|
+
}
|
|
3100
|
+
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
3101
|
+
return;
|
|
3102
|
+
}
|
|
3103
|
+
// (ANTIGRAVITY-30S-CAP-PREMATURE) The 30s cap has been reached for an antigravity
|
|
3104
|
+
// `holdForTranscript` block. The cap releases on ELAPSED TIME, not proof-of-idle — but
|
|
3105
|
+
// antigravity is native-source (assistant answer in native-history) with a PTY-derived
|
|
3106
|
+
// idle verdict, so the transcript's final assistant bubble can legitimately land past 30s
|
|
3107
|
+
// on a long turn. Releasing here would fire a premature weak completed/idle to the
|
|
3108
|
+
// coordinator WHILE THE PTY IS STILL GENERATING (the live-reproduced defect). Gate the
|
|
3109
|
+
// release on the PTY being genuinely quiet: if it is still active (adapter pending
|
|
3110
|
+
// response OR raw output within ANTIGRAVITY_HOLD_QUIET_DWELL_MS) and we are under the
|
|
3111
|
+
// absolute ANTIGRAVITY_HOLD_HARD_CAP_MS bound, KEEP HOLDING (re-probe next retry — the
|
|
3112
|
+
// transcript branch above clears the block for a genuine emit once the answer lands).
|
|
3113
|
+
// A genuinely quiescent tool-only turn (no assistant bubble, PTY stable) falls through
|
|
3114
|
+
// to the existing weak force-emit; a runaway PTY that never quiets releases at the hard
|
|
3115
|
+
// cap so it cannot wedge forever. Scoped to antigravity holdForTranscript so
|
|
3116
|
+
// claude-cli/codex-cli completion timing is unchanged.
|
|
3117
|
+
if (this.type === 'antigravity-cli'
|
|
3118
|
+
&& block.holdForTranscript === true
|
|
3119
|
+
&& waitedMs < ANTIGRAVITY_HOLD_HARD_CAP_MS
|
|
3120
|
+
&& this.antigravityHoldPtyStillActive()) {
|
|
3121
|
+
if (pending.loggedBlockReason !== 'antigravity_hold_pty_active') {
|
|
3122
|
+
LOG.info('CLI', `[${this.type}] 30s cap reached but PTY still generating; holding antigravity completion past cap (waitedMs=${waitedMs} hardCap=${ANTIGRAVITY_HOLD_HARD_CAP_MS}) (${blockReason})`);
|
|
3123
|
+
if (this.isMeshWorkerSession()) {
|
|
3124
|
+
traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `antigravity_hold_pty_active waited=${waitedMs}ms`);
|
|
3125
|
+
}
|
|
3126
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('hold', {
|
|
3127
|
+
blockReason,
|
|
3128
|
+
latestVisibleStatus,
|
|
3129
|
+
terminal: block.terminal === true,
|
|
3130
|
+
holdForTranscript: true,
|
|
3131
|
+
antigravityPtyStillActive: true,
|
|
3132
|
+
waitedMs,
|
|
3133
|
+
});
|
|
3134
|
+
pending.loggedBlockReason = 'antigravity_hold_pty_active';
|
|
3135
|
+
}
|
|
3136
|
+
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
3137
|
+
return;
|
|
3138
|
+
}
|
|
2754
3139
|
const emittedAfterFinalizationTimeout = waitedMs >= COMPLETED_FINALIZATION_MAX_WAIT_MS;
|
|
2755
3140
|
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
2756
3141
|
blockReason,
|
|
@@ -2868,6 +3253,10 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2868
3253
|
if (summary) {
|
|
2869
3254
|
this.lastCompletionSummary = { content: summary, receivedAt: opts.timestamp };
|
|
2870
3255
|
}
|
|
3256
|
+
// KIMI-MESH-COMPLETION-EMIT (axis 2, double-emit guard): record that THIS turn's
|
|
3257
|
+
// completion has now been emitted, keyed by its taskId, so the pre-cleanup
|
|
3258
|
+
// completion flush never fires a duplicate for the same turn.
|
|
3259
|
+
this.lastEmittedCompletion = { taskId: typeof opts.taskId === 'string' ? opts.taskId : '', at: Date.now() };
|
|
2871
3260
|
this.pushEvent({
|
|
2872
3261
|
event: 'agent:generating_completed',
|
|
2873
3262
|
chatTitle: opts.chatTitle,
|
|
@@ -2912,7 +3301,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2912
3301
|
private stabilizeFlappingApprovalStatus(adapterStatus: any, now = Date.now()): any {
|
|
2913
3302
|
// Only autonomous auto-approving mesh sessions are subject to the delegated flap;
|
|
2914
3303
|
// never overlay for attended/foreground/non-mesh sessions.
|
|
2915
|
-
if (!this.isAutonomousMeshSession() || !this.
|
|
3304
|
+
if (!this.isAutonomousMeshSession() || !this.shouldUsePtyAutoApprove()) return adapterStatus;
|
|
2916
3305
|
|
|
2917
3306
|
const rawStatus = adapterStatus?.status;
|
|
2918
3307
|
const resolvedAt = typeof (this.adapter as any)?.lastApprovalResolvedAt === 'number'
|
|
@@ -2959,6 +3348,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2959
3348
|
}
|
|
2960
3349
|
|
|
2961
3350
|
private maybeAutoApproveStatus(adapterStatus: any, now = Date.now()): boolean {
|
|
3351
|
+
// launch-args modes grant approval in the CLI process itself and therefore
|
|
3352
|
+
// must never enter the PTY modal parser/fire/settle/mask/nudge subsystem.
|
|
3353
|
+
// post-boot-command is reserved and resolves inactive in v1.
|
|
3354
|
+
if (!this.shouldUsePtyAutoApprove()) {
|
|
3355
|
+
this.resetPtyAutoApproveState();
|
|
3356
|
+
return false;
|
|
3357
|
+
}
|
|
2962
3358
|
// Manual-attendance suppression (provider-common): when a human is
|
|
2963
3359
|
// Manual-attendance suppression (provider-common): when a human is
|
|
2964
3360
|
// actively driving this session from the dashboard, hold auto-approve so
|
|
@@ -2970,7 +3366,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2970
3366
|
// would otherwise never re-drive this decision. Background mesh workers
|
|
2971
3367
|
// are never attended, so their delegated auto-approve is untouched.
|
|
2972
3368
|
if (adapterStatus?.status === 'waiting_approval'
|
|
2973
|
-
&& this.
|
|
3369
|
+
&& this.shouldUsePtyAutoApprove()
|
|
2974
3370
|
&& this.manualAttendance.isAttended(now)) {
|
|
2975
3371
|
this.lastAutoApprovalSignature = '';
|
|
2976
3372
|
this.pendingAutoApprovalSignature = '';
|
|
@@ -2988,7 +3384,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2988
3384
|
}, this.manualAttendance.remainingMs(now) + 20);
|
|
2989
3385
|
return false;
|
|
2990
3386
|
}
|
|
2991
|
-
const autoApproveActive = adapterStatus?.status === 'waiting_approval' && this.
|
|
3387
|
+
const autoApproveActive = adapterStatus?.status === 'waiting_approval' && this.shouldUsePtyAutoApprove();
|
|
2992
3388
|
// Guard re-entry: onStatusChange/getState can observe the same modal multiple
|
|
2993
3389
|
// times while the PTY absorbs the approval key. Without this flag, repeated
|
|
2994
3390
|
// snapshots would write stray keys into the input once the modal dismisses.
|
|
@@ -3885,6 +4281,68 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3885
4281
|
this.lastStatus = newStatus;
|
|
3886
4282
|
}
|
|
3887
4283
|
|
|
4284
|
+
// INTERACTIVE-QUESTION-EMIT: fire a coordinator/push-worthy notification when the
|
|
4285
|
+
// session ENTERS an AskUserQuestion / waiting_choice state. This is orthogonal to
|
|
4286
|
+
// the status-change block above: an AskUserQuestion prompt is surfaced only as a
|
|
4287
|
+
// display-only `waiting_choice` overlay in getState() (mirrored here off
|
|
4288
|
+
// adapterStatus.activeInteractivePrompt, the exact signal getState uses), while
|
|
4289
|
+
// the raw adapter status stays idle/generating — so none of the status-keyed
|
|
4290
|
+
// arms above ever emit an agent:* event for it and the owner gets no web-push.
|
|
4291
|
+
//
|
|
4292
|
+
// MESH-QUESTION-ANSWER-PATH (mission f1d25e11): this used to REUSE
|
|
4293
|
+
// agent:waiting_approval (question as modalMessage, choice labels as modalButtons).
|
|
4294
|
+
// That mis-classified a multi-choice QUESTION as an approval — the coordinator saw
|
|
4295
|
+
// a task_approval_needed ledger row and tried mesh_approve, which cannot answer a
|
|
4296
|
+
// question (it resolves a yes/no modal). We now emit a DISTINCT
|
|
4297
|
+
// agent:waiting_choice event carrying the FULL InteractivePrompt payload
|
|
4298
|
+
// (promptId + every question's header/question/multiSelect and each option's
|
|
4299
|
+
// label/description) so the coordinator can render the choices and answer with the
|
|
4300
|
+
// dedicated mesh_answer_question tool. The rich payload survives the cross-machine
|
|
4301
|
+
// relay (see buildRelayMetadataEvent) so a REMOTE worker's question reaches the
|
|
4302
|
+
// coordinator with its options intact.
|
|
4303
|
+
//
|
|
4304
|
+
// We keep modalMessage/modalButtons on the event too so the existing server
|
|
4305
|
+
// web-push path (which reads those two fields) still fires with no cloud change —
|
|
4306
|
+
// but the payload's `interactivePrompt` is the authoritative, structured signal.
|
|
4307
|
+
//
|
|
4308
|
+
// Edge-triggered: emit exactly once on entry, keyed on promptId + question text,
|
|
4309
|
+
// and reset the key when the prompt clears so a fresh prompt re-fires and a later
|
|
4310
|
+
// real completion still flows through the idle arm normally. We do NOT emit when
|
|
4311
|
+
// the session is ALSO in a genuine approval state (newStatus === 'waiting_approval'):
|
|
4312
|
+
// that arm already emits agent:waiting_approval. A question (waiting_choice) and a
|
|
4313
|
+
// real approval (waiting_approval) therefore NEVER both fire for the same tick —
|
|
4314
|
+
// the approval arm wins and this arm yields (owner requirement: the two are
|
|
4315
|
+
// mutually exclusive).
|
|
4316
|
+
const interactivePrompt = adapterStatus.activeInteractivePrompt ?? null;
|
|
4317
|
+
if (interactivePrompt && newStatus !== 'waiting_approval') {
|
|
4318
|
+
const firstQuestion = interactivePrompt.questions?.[0];
|
|
4319
|
+
const promptKey = `${interactivePrompt.promptId}::${firstQuestion?.question ?? ''}`;
|
|
4320
|
+
if (promptKey !== this.lastInteractivePromptEventKey) {
|
|
4321
|
+
this.lastInteractivePromptEventKey = promptKey;
|
|
4322
|
+
const modalMessage = firstQuestion
|
|
4323
|
+
? (firstQuestion.header
|
|
4324
|
+
? `${firstQuestion.header}: ${firstQuestion.question}`
|
|
4325
|
+
: firstQuestion.question)
|
|
4326
|
+
: undefined;
|
|
4327
|
+
const modalButtons = firstQuestion?.options?.map((option: { label: string }) => option.label);
|
|
4328
|
+
this.pushEvent({
|
|
4329
|
+
event: 'agent:waiting_choice', chatTitle, timestamp: now,
|
|
4330
|
+
// Structured, authoritative payload — the coordinator renders these and
|
|
4331
|
+
// answers via mesh_answer_question. Carried whole (all questions, all
|
|
4332
|
+
// options) so multi-question / multi-select prompts round-trip fully.
|
|
4333
|
+
interactivePrompt,
|
|
4334
|
+
promptId: interactivePrompt.promptId,
|
|
4335
|
+
multiSelect: firstQuestion?.multiSelect === true,
|
|
4336
|
+
// Push-notification-friendly projection (server push path reads these).
|
|
4337
|
+
modalMessage,
|
|
4338
|
+
modalButtons,
|
|
4339
|
+
});
|
|
4340
|
+
}
|
|
4341
|
+
} else if (!interactivePrompt && this.lastInteractivePromptEventKey) {
|
|
4342
|
+
// Prompt answered / gone — reset so the next AskUserQuestion re-fires.
|
|
4343
|
+
this.lastInteractivePromptEventKey = '';
|
|
4344
|
+
}
|
|
4345
|
+
|
|
3888
4346
|
// GENERATING-BOUNDARY idle-stayed collapse (R4b): the starting→idle
|
|
3889
4347
|
// fast-collapse arm above only fires when the FIRST turn itself drives the
|
|
3890
4348
|
// starting→idle transition. When the launch settle already drained
|
|
@@ -4202,15 +4660,37 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
4202
4660
|
get cliType(): string { return this.type; }
|
|
4203
4661
|
get cliName(): string { return this.provider.name; }
|
|
4204
4662
|
|
|
4663
|
+
private resolveAutoApproveMode(): ResolvedAutoApproveMode {
|
|
4664
|
+
return resolveProviderAutoApproveMode(this.provider, this.settings);
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
/** Legacy boolean view retained for internal/test compatibility. */
|
|
4205
4668
|
private shouldAutoApprove(): boolean {
|
|
4206
|
-
|
|
4207
|
-
|
|
4669
|
+
return this.resolveAutoApproveMode().active;
|
|
4670
|
+
}
|
|
4671
|
+
|
|
4672
|
+
private shouldUsePtyAutoApprove(): boolean {
|
|
4673
|
+
const resolved = this.resolveAutoApproveMode();
|
|
4674
|
+
return this.shouldAutoApprove() && resolved.strategy === 'pty-parse-default';
|
|
4675
|
+
}
|
|
4676
|
+
|
|
4677
|
+
private resetPtyAutoApproveState(): void {
|
|
4678
|
+
this.lastAutoApprovalSignature = '';
|
|
4679
|
+
this.pendingAutoApprovalSignature = '';
|
|
4680
|
+
this.pendingAutoApprovalSince = 0;
|
|
4681
|
+
this.autoApproveInactiveSince = 0;
|
|
4682
|
+
this.autoApproveMaskSince = 0;
|
|
4683
|
+
this.stalledApprovalNudgeEpisode = 0;
|
|
4684
|
+
this.autoApproveLastModalSeenAt = 0;
|
|
4685
|
+
this.autoApproveBusy = false;
|
|
4686
|
+
if (this.autoApproveSettleTimer) {
|
|
4687
|
+
clearTimeout(this.autoApproveSettleTimer);
|
|
4688
|
+
this.autoApproveSettleTimer = null;
|
|
4208
4689
|
}
|
|
4209
|
-
|
|
4210
|
-
|
|
4211
|
-
|
|
4690
|
+
if (this.autoApproveBusyTimer) {
|
|
4691
|
+
clearTimeout(this.autoApproveBusyTimer);
|
|
4692
|
+
this.autoApproveBusyTimer = null;
|
|
4212
4693
|
}
|
|
4213
|
-
return false;
|
|
4214
4694
|
}
|
|
4215
4695
|
|
|
4216
4696
|
/** @see ProviderInstance.noteManualInteraction */
|
|
@@ -4237,7 +4717,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
4237
4717
|
*/
|
|
4238
4718
|
private autoApproveEffectivelyActive(status: string | undefined, now = Date.now()): boolean {
|
|
4239
4719
|
return status === 'waiting_approval'
|
|
4240
|
-
&& this.
|
|
4720
|
+
&& this.shouldUsePtyAutoApprove()
|
|
4241
4721
|
&& !this.manualAttendance.isAttended(now);
|
|
4242
4722
|
}
|
|
4243
4723
|
|
|
@@ -4249,7 +4729,8 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
4249
4729
|
// maybeAutoApproveStatus (driven by getState + the recheck timer during a waiting episode);
|
|
4250
4730
|
// this read is side-effect-free so getStatusMetadata can consult it too.
|
|
4251
4731
|
private autoApproveMaskStalled(now = Date.now()): boolean {
|
|
4252
|
-
return this.
|
|
4732
|
+
return this.shouldUsePtyAutoApprove()
|
|
4733
|
+
&& this.autoApproveMaskSince > 0
|
|
4253
4734
|
&& now - this.autoApproveMaskSince > CliProviderInstance.AUTO_APPROVE_MASK_STALL_MS;
|
|
4254
4735
|
}
|
|
4255
4736
|
|
|
@@ -4275,6 +4756,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
4275
4756
|
* mask-clock value) so a modal that flaps between parsed/unparsed states is announced once.
|
|
4276
4757
|
*/
|
|
4277
4758
|
private maybeEmitStalledApprovalNudge(adapterStatus: any, now: number): void {
|
|
4759
|
+
if (!this.shouldUsePtyAutoApprove()) return;
|
|
4278
4760
|
if (!this.isMeshWorkerSession()) return;
|
|
4279
4761
|
if (adapterStatus?.status !== 'waiting_approval') return;
|
|
4280
4762
|
if (!this.autoApproveMaskStalled(now)) return;
|