@adhdev/daemon-core 1.0.18-rc.6 → 1.0.18-rc.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3857 -3391
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3853 -3381
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/mesh-disk-retention.d.ts +105 -0
- package/dist/mesh/mesh-ledger.d.ts +28 -1
- package/dist/mesh/mesh-runtime-store.d.ts +11 -0
- package/dist/providers/cli-provider-instance-types.d.ts +2 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -0
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +10 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +17 -2
- package/src/commands/windows-atomic-upgrade.ts +19 -1
- package/src/config/mesh-json-config.ts +8 -0
- package/src/mesh/coordinator-prompt.ts +256 -10
- package/src/mesh/mesh-disk-retention.ts +370 -0
- package/src/mesh/mesh-ledger.ts +72 -0
- package/src/mesh/mesh-reconcile-loop.ts +49 -0
- package/src/mesh/mesh-runtime-store.ts +21 -0
- package/src/providers/cli-provider-instance-types.ts +25 -0
- package/src/providers/cli-provider-instance.ts +116 -0
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -88,6 +88,14 @@ export type MeshLedgerKind =
|
|
|
88
88
|
// payload: { keys: string[], hasDestructive: boolean, result: 'injected'|'refused'|'error',
|
|
89
89
|
// refused?: string, submits?: boolean, confirmDestructive?: boolean }
|
|
90
90
|
| 'key_injection'
|
|
91
|
+
// Disk/worktree retention (mission 86def38d): DETECTION-ONLY signal that a git
|
|
92
|
+
// worktree present on disk has no matching live mesh node — an orphan cleanup
|
|
93
|
+
// candidate. The reconcile loop emits this so the coordinator can decide whether
|
|
94
|
+
// to remove it; retention NEVER auto-deletes a worktree (manual/coordinator-driven).
|
|
95
|
+
// Keyed by worktreePath so a re-emit for the same orphan is idempotent (a prior
|
|
96
|
+
// unresolved entry within the dedupe window suppresses the repeat).
|
|
97
|
+
// payload: { worktreePath, branch?, head?, reason: 'no_matching_live_node', state: 'cleanup_candidate' }
|
|
98
|
+
| 'worktree_cleanup_candidate'
|
|
91
99
|
;
|
|
92
100
|
|
|
93
101
|
export interface MeshLedgerEntry {
|
|
@@ -317,6 +325,70 @@ export const OPERATING_NOTE_DEDUPE_WINDOW = 40;
|
|
|
317
325
|
// a coordinator actually sees while still capping unbounded store growth.
|
|
318
326
|
export const OPERATING_NOTE_KEEP_LATEST = 100;
|
|
319
327
|
|
|
328
|
+
// ─── Operating-note lifecycle: category TTL + expiry (read-side only) ──────────
|
|
329
|
+
// Minimal first cut of the operating-notes lifecycle. Expiry is READ/INJECTION
|
|
330
|
+
// side ONLY — the store prune (keep-latest-100 above) stays purely count-based
|
|
331
|
+
// and NEVER deletes by age, so audit history is preserved. isNoteExpired decides
|
|
332
|
+
// whether an UNPINNED note still rides into a coordinator prompt.
|
|
333
|
+
//
|
|
334
|
+
// Per-category retention (days). A category not listed here — including the
|
|
335
|
+
// uncategorized case — is durable (never expires). provider_quirk is durable
|
|
336
|
+
// because a runtime quirk stays true until the provider changes.
|
|
337
|
+
export const OPERATING_NOTE_CATEGORY_TTL_DAYS: Readonly<Record<string, number>> = {
|
|
338
|
+
recovery_lesson: 14,
|
|
339
|
+
pattern_to_avoid: 30,
|
|
340
|
+
// provider_quirk: durable (intentionally absent → never expires)
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Shape isNoteExpired reads. Structural so mesh-ledger stays free of a
|
|
347
|
+
* coordinator-prompt import (CoordinatorOperatingNote satisfies this).
|
|
348
|
+
*/
|
|
349
|
+
export interface OperatingNoteExpiryInput {
|
|
350
|
+
category?: string;
|
|
351
|
+
pinned?: boolean;
|
|
352
|
+
createdAt?: string;
|
|
353
|
+
/** Explicit expiry override; wins over the category TTL when parseable. */
|
|
354
|
+
expiresAt?: string;
|
|
355
|
+
/** Fallback creation time (ledger entry timestamp) when createdAt absent. */
|
|
356
|
+
timestamp?: string;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Pure helper: is this UNPINNED operating note expired as of `now` (epoch ms)?
|
|
361
|
+
*
|
|
362
|
+
* Rules:
|
|
363
|
+
* - pinned notes NEVER expire (always false).
|
|
364
|
+
* - an explicit, parseable `expiresAt` in the past → expired.
|
|
365
|
+
* - otherwise the category TTL applies; a durable category (provider_quirk,
|
|
366
|
+
* uncategorized, or any category not in the TTL map) never expires.
|
|
367
|
+
* - age is measured from createdAt, falling back to `timestamp` (ledger entry
|
|
368
|
+
* time). If neither is a valid date, the note is treated as NOT expired
|
|
369
|
+
* (never silently drop a note we cannot age).
|
|
370
|
+
*/
|
|
371
|
+
export function isNoteExpired(note: OperatingNoteExpiryInput, now: number): boolean {
|
|
372
|
+
if (!note || note.pinned) return false;
|
|
373
|
+
|
|
374
|
+
// Explicit expiresAt wins when present and parseable.
|
|
375
|
+
if (typeof note.expiresAt === 'string') {
|
|
376
|
+
const exp = new Date(note.expiresAt).getTime();
|
|
377
|
+
if (!Number.isNaN(exp)) return exp <= now;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const ttlDays = note.category ? OPERATING_NOTE_CATEGORY_TTL_DAYS[note.category] : undefined;
|
|
381
|
+
if (typeof ttlDays !== 'number' || !Number.isFinite(ttlDays)) {
|
|
382
|
+
// Durable category (provider_quirk / uncategorized / unknown) → never expires.
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const created = new Date(note.createdAt ?? note.timestamp ?? '').getTime();
|
|
387
|
+
if (Number.isNaN(created)) return false; // cannot age → keep
|
|
388
|
+
|
|
389
|
+
return now - created >= ttlDays * MS_PER_DAY;
|
|
390
|
+
}
|
|
391
|
+
|
|
320
392
|
// ─── Path Helpers ───────────────────────────────
|
|
321
393
|
|
|
322
394
|
export function getLedgerDir(): string {
|
|
@@ -76,6 +76,7 @@ import {
|
|
|
76
76
|
resolveReconcileIntervalMs,
|
|
77
77
|
} from './mesh-reconcile-config.js';
|
|
78
78
|
import { pullRemoteNodeQueues } from './mesh-remote-event-pull.js';
|
|
79
|
+
import { runDiskRetentionSweep, detectAndSignalOrphanWorktrees } from './mesh-disk-retention.js';
|
|
79
80
|
import {
|
|
80
81
|
reconcileUnterminatedDirectDispatches,
|
|
81
82
|
autoPruneStaleDirectDispatches,
|
|
@@ -126,6 +127,16 @@ interface LiveCoordinator {
|
|
|
126
127
|
// "restart does not clear it" symptom the operator needs visibility into).
|
|
127
128
|
const coordinatorModalParkState = new Map<string, boolean>();
|
|
128
129
|
|
|
130
|
+
// Disk/worktree retention throttle. The reconcile tick runs every ~4s, but the
|
|
131
|
+
// retention sweep (fs walks + git worktree list) is far too heavy for that cadence
|
|
132
|
+
// and its artifacts age in days, so it runs at most once per hour. `undefined`
|
|
133
|
+
// means "never run yet" → runs on the first tick after daemon start so a long-lived
|
|
134
|
+
// backlog is reclaimed promptly rather than an hour later. Per-process; a restart
|
|
135
|
+
// re-runs it immediately, which is the desired behavior (a restart is exactly when
|
|
136
|
+
// stale artifacts from the previous run should be swept).
|
|
137
|
+
const DISK_RETENTION_INTERVAL_MS = 60 * 60 * 1000; // 1h
|
|
138
|
+
let lastDiskRetentionRunAt: number | undefined;
|
|
139
|
+
|
|
129
140
|
// Find live CLI coordinator instances on THIS daemon, keyed by mesh.
|
|
130
141
|
function findLiveCoordinators(components: DaemonComponents): LiveCoordinator[] {
|
|
131
142
|
const out: LiveCoordinator[] = [];
|
|
@@ -1354,6 +1365,44 @@ export async function runMeshReconcileTick(components: DaemonComponents): Promis
|
|
|
1354
1365
|
}
|
|
1355
1366
|
}
|
|
1356
1367
|
|
|
1368
|
+
// ── PHASE 6: disk / worktree retention (hourly, mission 86def38d) ──────────
|
|
1369
|
+
// Legacy on-disk artifacts under ~/.adhdev accumulated with NO lifetime and grew
|
|
1370
|
+
// the data volume until a refine bootstrap failed at 98% disk. This throttled pass
|
|
1371
|
+
// (≤1×/hour — the artifacts age in days and the fs/git walk is too heavy for the 4s
|
|
1372
|
+
// tick) reclaims them:
|
|
1373
|
+
// • JSONL ledger files older than 30d (legacy after the SQLite ledger)
|
|
1374
|
+
// • terminated session-host runtime files older than 14d (live never touched)
|
|
1375
|
+
// • mesh-runtime.db.bak-* backups older than 7d
|
|
1376
|
+
// • orphan worktree DETECTION → cleanup_candidate ledger signal (NEVER deleted).
|
|
1377
|
+
// Runs BEFORE PHASE 2's early return so it fires whether or not a live CLI
|
|
1378
|
+
// coordinator exists on this daemon. Isolated in try/catch so it can never kill the
|
|
1379
|
+
// tick. All file deletions are defensive (age + live-runtime guards in the pure
|
|
1380
|
+
// selectors); worktree cleanup stays manual/coordinator-driven.
|
|
1381
|
+
{
|
|
1382
|
+
const nowMs = Date.now();
|
|
1383
|
+
const due = lastDiskRetentionRunAt === undefined
|
|
1384
|
+
|| (nowMs - lastDiskRetentionRunAt) >= DISK_RETENTION_INTERVAL_MS;
|
|
1385
|
+
if (due) {
|
|
1386
|
+
lastDiskRetentionRunAt = nowMs;
|
|
1387
|
+
try {
|
|
1388
|
+
runDiskRetentionSweep(nowMs);
|
|
1389
|
+
} catch (e: any) {
|
|
1390
|
+
LOG.warn('MeshReconcile', `Disk retention sweep failed: ${e?.message || e}`);
|
|
1391
|
+
}
|
|
1392
|
+
// Orphan-worktree detection is per-mesh (needs the mesh's base repo + node set)
|
|
1393
|
+
// and only for meshes this daemon hosts (its local base checkout is the git anchor).
|
|
1394
|
+
for (const mesh of listMeshes()) {
|
|
1395
|
+
const selfIds = resolveCoordinatorSelfIds(mesh, drainDaemonIds);
|
|
1396
|
+
if (!daemonHostsMesh(mesh, selfIds)) continue;
|
|
1397
|
+
try {
|
|
1398
|
+
await detectAndSignalOrphanWorktrees(mesh, nowMs);
|
|
1399
|
+
} catch (e: any) {
|
|
1400
|
+
LOG.warn('MeshReconcile', `Orphan worktree detection failed for mesh ${mesh.id}: ${e?.message || e}`);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1357
1406
|
// ── PHASE 2: inject into live CLI coordinators on this daemon ──────────────
|
|
1358
1407
|
const coordinators = findLiveCoordinators(components);
|
|
1359
1408
|
if (coordinators.length === 0) {
|
|
@@ -174,6 +174,27 @@ export class MeshRuntimeStore {
|
|
|
174
174
|
this.instance = undefined;
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/**
|
|
178
|
+
* VACUUM the SQLite database to reclaim on-disk space. Retention prunes rows
|
|
179
|
+
* with DELETE, which frees pages inside the file but does NOT shrink it — the
|
|
180
|
+
* mesh-runtime.db grew to hundreds of MB (mission 86def38d disk-accumulation
|
|
181
|
+
* bootstrap failure) precisely because the file was never compacted. This
|
|
182
|
+
* rewrites the DB into a minimal footprint. Best-effort: a VACUUM failure (e.g.
|
|
183
|
+
* insufficient temp space, a read lock) is logged and swallowed so it can never
|
|
184
|
+
* block daemon shutdown. Called once on shutdown (see daemon-lifecycle), never
|
|
185
|
+
* on the hot path — VACUUM takes an exclusive lock and rewrites the whole file.
|
|
186
|
+
*/
|
|
187
|
+
vacuum(): void {
|
|
188
|
+
try {
|
|
189
|
+
// Fold the WAL back into the main DB first so VACUUM reclaims those pages too.
|
|
190
|
+
try { this.db.pragma('wal_checkpoint(TRUNCATE)'); } catch { /* checkpoint best-effort */ }
|
|
191
|
+
this.db.exec('VACUUM;');
|
|
192
|
+
LOG.info('MeshRuntimeStore', 'VACUUM completed on shutdown');
|
|
193
|
+
} catch (err: any) {
|
|
194
|
+
LOG.warn('MeshRuntimeStore', `VACUUM on shutdown failed (ignored): ${err?.message || err}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
177
198
|
close(): void {
|
|
178
199
|
this.db.close();
|
|
179
200
|
}
|
|
@@ -120,6 +120,31 @@ export const NATIVE_HISTORY_MESH_IDLE_SETTLE_MS = 4000;
|
|
|
120
120
|
// met. Scoped (at the call site) to autonomous mesh sessions, so interactive sessions are
|
|
121
121
|
// untouched.
|
|
122
122
|
export const PTY_PARSED_FINAL_ASSISTANT_QUIET_DWELL_MS = 1200;
|
|
123
|
+
// (ANTIGRAVITY-30S-CAP-PREMATURE) Minimum PTY quiet dwell required before the
|
|
124
|
+
// COMPLETED_FINALIZATION_MAX_WAIT_MS (30s) cap may RELEASE an antigravity `holdForTranscript`
|
|
125
|
+
// block into the forced weak completion. Antigravity is a native-source provider: its
|
|
126
|
+
// idle/generating verdict is PTY-screen-derived, but its assistant answer lands in
|
|
127
|
+
// native-history and can legitimately lag past 30s on a long turn (tool phase + slow reply).
|
|
128
|
+
// The 30s cap releases on ELAPSED TIME, not proof-of-idle — so on a long turn it fired a
|
|
129
|
+
// premature completed/idle to the coordinator while the PTY was still generating (live-repro:
|
|
130
|
+
// a routed RCA task emitted "completed" in 10s with no result). Require instead that the PTY
|
|
131
|
+
// has been quiet (no raw output) for at least this long AND the adapter reports no pending
|
|
132
|
+
// response before the cap may force-emit. A genuinely quiescent tool-only turn (no assistant
|
|
133
|
+
// bubble) has a stable screen well past this bound and still finalizes; a turn whose PTY is
|
|
134
|
+
// still producing output keeps holding past 30s (up to ANTIGRAVITY_HOLD_HARD_CAP_MS). Scoped
|
|
135
|
+
// (at the call site) to antigravity-cli holdForTranscript blocks, so claude-cli/codex-cli
|
|
136
|
+
// completion timing is untouched.
|
|
137
|
+
export const ANTIGRAVITY_HOLD_QUIET_DWELL_MS = 3000;
|
|
138
|
+
// (ANTIGRAVITY-30S-CAP-PREMATURE) Absolute upper bound on how long an antigravity
|
|
139
|
+
// `holdForTranscript` block may be held once the 30s cap is reached but the PTY is still
|
|
140
|
+
// active. The quiet-dwell gate above keeps holding while the PTY keeps emitting output, so a
|
|
141
|
+
// truly runaway turn (PTY never falls quiet) must still eventually release rather than wedge
|
|
142
|
+
// the session in generating forever — strictly worse than the original premature-emit bug.
|
|
143
|
+
// Once a pending completion has been held this long we force-emit regardless of PTY activity.
|
|
144
|
+
// Sized generously (5 min) so realistic long antigravity turns (multi-tool + slow reply)
|
|
145
|
+
// finish and let the transcript land / the PTY fall quiet naturally, while still guaranteeing
|
|
146
|
+
// eventual release. Mirrors BACKGROUND_TASK_HOLD_MAX_MS's escape-hatch rationale.
|
|
147
|
+
export const ANTIGRAVITY_HOLD_HARD_CAP_MS = 5 * 60_000;
|
|
123
148
|
// (FALSE-IDLE-BACKGROUND-CMD) Hard cap on how long a pending completion may be HELD
|
|
124
149
|
// solely because the claude-cli transcript still shows an unresolved run_in_background
|
|
125
150
|
// bash job (backgroundTaskActive). The hold is the correct behaviour while the job is
|
|
@@ -58,6 +58,8 @@ import {
|
|
|
58
58
|
COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
59
59
|
NATIVE_HISTORY_MESH_IDLE_SETTLE_MS,
|
|
60
60
|
PTY_PARSED_FINAL_ASSISTANT_QUIET_DWELL_MS,
|
|
61
|
+
ANTIGRAVITY_HOLD_QUIET_DWELL_MS,
|
|
62
|
+
ANTIGRAVITY_HOLD_HARD_CAP_MS,
|
|
61
63
|
BACKGROUND_TASK_HOLD_MAX_MS,
|
|
62
64
|
USER_INPUT_ACK_DEDUP_WINDOW_MS,
|
|
63
65
|
STARTUP_GRACE_IDLE_COLLAPSE_WINDOW_MS,
|
|
@@ -369,6 +371,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
369
371
|
private generatingDebounceTimer: NodeJS.Timeout | null = null;
|
|
370
372
|
private generatingDebouncePending: { chatTitle: string; timestamp: number } | null = null;
|
|
371
373
|
private lastApprovalEventFingerprint = '';
|
|
374
|
+
// INTERACTIVE-PROMPT-PUSH: edge-trigger key for the AskUserQuestion / waiting_choice
|
|
375
|
+
// notification. An AskUserQuestion prompt is surfaced only as a display-only
|
|
376
|
+
// `waiting_choice` overlay in getState(); the raw adapter status stays idle/generating,
|
|
377
|
+
// so detectStatusTransition()'s status-keyed arms never fire and no push-worthy
|
|
378
|
+
// agent:* event is emitted — the owner misses the "ACTION REQUIRED" prompt when the
|
|
379
|
+
// app is backgrounded. We emit one agent:waiting_approval on ENTRY into the prompt
|
|
380
|
+
// state (reusing the existing server push path, no cloud change) and dedupe on this
|
|
381
|
+
// key so repeated status ticks with the same prompt do not re-fire. '' means no
|
|
382
|
+
// prompt is currently active (cleared when the prompt is answered/gone).
|
|
383
|
+
private lastInteractivePromptEventKey = '';
|
|
372
384
|
private autoApproveBusy = false;
|
|
373
385
|
private autoApproveBusyTimer: NodeJS.Timeout | null = null;
|
|
374
386
|
private lastAutoApprovalSignature = '';
|
|
@@ -1897,6 +1909,33 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1897
1909
|
return false;
|
|
1898
1910
|
}
|
|
1899
1911
|
|
|
1912
|
+
// (ANTIGRAVITY-30S-CAP-PREMATURE) Discriminator gating the 30s-cap release of an antigravity
|
|
1913
|
+
// `holdForTranscript` block. Antigravity's idle verdict is PTY-screen-derived but its assistant
|
|
1914
|
+
// answer lands in native-history, which can legitimately lag past COMPLETED_FINALIZATION_MAX_WAIT_MS
|
|
1915
|
+
// (30s) on a long turn. The cap releases on elapsed time, not proof-of-idle, so it force-emitted a
|
|
1916
|
+
// premature weak completion WHILE THE PTY WAS STILL GENERATING. Returns true when the PTY is still
|
|
1917
|
+
// active — i.e. the adapter reports a pending response OR raw PTY output arrived within the last
|
|
1918
|
+
// ANTIGRAVITY_HOLD_QUIET_DWELL_MS — meaning the 30s cap must KEEP HOLDING (the turn is not proven
|
|
1919
|
+
// over). Returns false when the PTY is genuinely quiescent (no pending response AND no recent
|
|
1920
|
+
// output), so a real tool-only turn with no assistant bubble still force-emits a weak completion
|
|
1921
|
+
// rather than wedging. The absolute ANTIGRAVITY_HOLD_HARD_CAP_MS bound is enforced at the call site
|
|
1922
|
+
// so a runaway PTY that never falls quiet still eventually releases. Fails OPEN (returns false =
|
|
1923
|
+
// allow release) when lastOutputAt is unreadable, so the gate can never wedge a session.
|
|
1924
|
+
private antigravityHoldPtyStillActive(): boolean {
|
|
1925
|
+
if (this.hasAdapterPendingResponse()) return true;
|
|
1926
|
+
try {
|
|
1927
|
+
const outStatus = this.adapter.getStatus({ allowParse: false }) as any;
|
|
1928
|
+
const lastOutputAt = typeof outStatus?.lastOutputAt === 'number' && Number.isFinite(outStatus.lastOutputAt)
|
|
1929
|
+
? outStatus.lastOutputAt as number
|
|
1930
|
+
: undefined;
|
|
1931
|
+
if (typeof lastOutputAt === 'number') {
|
|
1932
|
+
const quietMs = Date.now() - lastOutputAt;
|
|
1933
|
+
if (quietMs < ANTIGRAVITY_HOLD_QUIET_DWELL_MS) return true;
|
|
1934
|
+
}
|
|
1935
|
+
} catch { /* defensive: dwell read is best-effort — fall through to allow release */ }
|
|
1936
|
+
return false;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1900
1939
|
private shouldSuppressStaleParsedBusyStatus(parsedStatus: any, adapterStatus: any): boolean {
|
|
1901
1940
|
const parsedRawStatus = typeof parsedStatus?.status === 'string' ? parsedStatus.status.trim() : '';
|
|
1902
1941
|
const adapterRawStatus = typeof adapterStatus?.status === 'string' ? adapterStatus.status.trim() : '';
|
|
@@ -2751,6 +2790,42 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2751
2790
|
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
2752
2791
|
return;
|
|
2753
2792
|
}
|
|
2793
|
+
// (ANTIGRAVITY-30S-CAP-PREMATURE) The 30s cap has been reached for an antigravity
|
|
2794
|
+
// `holdForTranscript` block. The cap releases on ELAPSED TIME, not proof-of-idle — but
|
|
2795
|
+
// antigravity is native-source (assistant answer in native-history) with a PTY-derived
|
|
2796
|
+
// idle verdict, so the transcript's final assistant bubble can legitimately land past 30s
|
|
2797
|
+
// on a long turn. Releasing here would fire a premature weak completed/idle to the
|
|
2798
|
+
// coordinator WHILE THE PTY IS STILL GENERATING (the live-reproduced defect). Gate the
|
|
2799
|
+
// release on the PTY being genuinely quiet: if it is still active (adapter pending
|
|
2800
|
+
// response OR raw output within ANTIGRAVITY_HOLD_QUIET_DWELL_MS) and we are under the
|
|
2801
|
+
// absolute ANTIGRAVITY_HOLD_HARD_CAP_MS bound, KEEP HOLDING (re-probe next retry — the
|
|
2802
|
+
// transcript branch above clears the block for a genuine emit once the answer lands).
|
|
2803
|
+
// A genuinely quiescent tool-only turn (no assistant bubble, PTY stable) falls through
|
|
2804
|
+
// to the existing weak force-emit; a runaway PTY that never quiets releases at the hard
|
|
2805
|
+
// cap so it cannot wedge forever. Scoped to antigravity holdForTranscript so
|
|
2806
|
+
// claude-cli/codex-cli completion timing is unchanged.
|
|
2807
|
+
if (this.type === 'antigravity-cli'
|
|
2808
|
+
&& block.holdForTranscript === true
|
|
2809
|
+
&& waitedMs < ANTIGRAVITY_HOLD_HARD_CAP_MS
|
|
2810
|
+
&& this.antigravityHoldPtyStillActive()) {
|
|
2811
|
+
if (pending.loggedBlockReason !== 'antigravity_hold_pty_active') {
|
|
2812
|
+
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})`);
|
|
2813
|
+
if (this.isMeshWorkerSession()) {
|
|
2814
|
+
traceMeshEventDrop('completion_gate_hold', this.meshTraceCtx(), `antigravity_hold_pty_active waited=${waitedMs}ms`);
|
|
2815
|
+
}
|
|
2816
|
+
if (this.completionTraceOn()) this.recordCompletionGateTrace('hold', {
|
|
2817
|
+
blockReason,
|
|
2818
|
+
latestVisibleStatus,
|
|
2819
|
+
terminal: block.terminal === true,
|
|
2820
|
+
holdForTranscript: true,
|
|
2821
|
+
antigravityPtyStillActive: true,
|
|
2822
|
+
waitedMs,
|
|
2823
|
+
});
|
|
2824
|
+
pending.loggedBlockReason = 'antigravity_hold_pty_active';
|
|
2825
|
+
}
|
|
2826
|
+
this.scheduleCompletedDebounceFlush(COMPLETED_FINALIZATION_RETRY_MS);
|
|
2827
|
+
return;
|
|
2828
|
+
}
|
|
2754
2829
|
const emittedAfterFinalizationTimeout = waitedMs >= COMPLETED_FINALIZATION_MAX_WAIT_MS;
|
|
2755
2830
|
const completionDiagnostic = this.buildCompletedFinalizationDiagnostic({
|
|
2756
2831
|
blockReason,
|
|
@@ -3885,6 +3960,47 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3885
3960
|
this.lastStatus = newStatus;
|
|
3886
3961
|
}
|
|
3887
3962
|
|
|
3963
|
+
// INTERACTIVE-PROMPT-PUSH: fire a push-worthy notification when the session
|
|
3964
|
+
// ENTERS an AskUserQuestion / waiting_choice state. This is orthogonal to the
|
|
3965
|
+
// status-change block above: an AskUserQuestion prompt is surfaced only as a
|
|
3966
|
+
// display-only `waiting_choice` overlay in getState() (mirrored here off
|
|
3967
|
+
// adapterStatus.activeInteractivePrompt, the exact signal getState uses), while
|
|
3968
|
+
// the raw adapter status stays idle/generating — so none of the status-keyed
|
|
3969
|
+
// arms above ever emit an agent:* event for it and the owner gets no web-push.
|
|
3970
|
+
//
|
|
3971
|
+
// We REUSE agent:waiting_approval (the question message as modalMessage, the
|
|
3972
|
+
// choice labels as modalButtons) so the existing server push path fires with
|
|
3973
|
+
// zero cloud change — the semantics ("the agent needs your input") match.
|
|
3974
|
+
//
|
|
3975
|
+
// Edge-triggered: emit exactly once on entry, keyed on promptId + question text,
|
|
3976
|
+
// and reset the key when the prompt clears so a fresh prompt re-fires and a later
|
|
3977
|
+
// real completion still flows through the idle arm normally. We do NOT emit when
|
|
3978
|
+
// the session is ALSO in a genuine approval state (newStatus === 'waiting_approval'):
|
|
3979
|
+
// that arm already emits agent:waiting_approval, and firing here too would double
|
|
3980
|
+
// up on the same modal.
|
|
3981
|
+
const interactivePrompt = adapterStatus.activeInteractivePrompt ?? null;
|
|
3982
|
+
if (interactivePrompt && newStatus !== 'waiting_approval') {
|
|
3983
|
+
const firstQuestion = interactivePrompt.questions?.[0];
|
|
3984
|
+
const promptKey = `${interactivePrompt.promptId}::${firstQuestion?.question ?? ''}`;
|
|
3985
|
+
if (promptKey !== this.lastInteractivePromptEventKey) {
|
|
3986
|
+
this.lastInteractivePromptEventKey = promptKey;
|
|
3987
|
+
const modalMessage = firstQuestion
|
|
3988
|
+
? (firstQuestion.header
|
|
3989
|
+
? `${firstQuestion.header}: ${firstQuestion.question}`
|
|
3990
|
+
: firstQuestion.question)
|
|
3991
|
+
: undefined;
|
|
3992
|
+
const modalButtons = firstQuestion?.options?.map((option: { label: string }) => option.label);
|
|
3993
|
+
this.pushEvent({
|
|
3994
|
+
event: 'agent:waiting_approval', chatTitle, timestamp: now,
|
|
3995
|
+
modalMessage,
|
|
3996
|
+
modalButtons,
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
} else if (!interactivePrompt && this.lastInteractivePromptEventKey) {
|
|
4000
|
+
// Prompt answered / gone — reset so the next AskUserQuestion re-fires.
|
|
4001
|
+
this.lastInteractivePromptEventKey = '';
|
|
4002
|
+
}
|
|
4003
|
+
|
|
3888
4004
|
// GENERATING-BOUNDARY idle-stayed collapse (R4b): the starting→idle
|
|
3889
4005
|
// fast-collapse arm above only fires when the FIRST turn itself drives the
|
|
3890
4006
|
// starting→idle transition. When the launch settle already drained
|