switchroom 0.18.3 → 0.18.7
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/agent-scheduler/index.js +3 -1
- package/dist/auth-broker/index.js +3 -1
- package/dist/cli/autoaccept-poll.js +140 -33
- package/dist/cli/notion-write-pretool.mjs +3 -1
- package/dist/cli/switchroom.js +386 -128
- package/dist/host-control/main.js +4 -2
- package/dist/vault/approvals/kernel-server.js +3 -1
- package/dist/vault/broker/server.js +38 -8
- package/package.json +3 -3
- package/profiles/_base/cron-session.sh.hbs +55 -16
- package/profiles/_base/start.sh.hbs +35 -16
- package/profiles/default/CLAUDE.md.hbs +1 -1
- package/skills/switchroom-release/SKILL.md +78 -0
- package/telegram-plugin/auth-snapshot-format.ts +15 -1
- package/telegram-plugin/dist/bridge/bridge.js +22 -0
- package/telegram-plugin/dist/gateway/gateway.js +2852 -1032
- package/telegram-plugin/dist/server.js +24 -0
- package/telegram-plugin/gateway/always-allow-persist-queue.ts +438 -0
- package/telegram-plugin/gateway/approval-timeout-inbound-builders.ts +150 -0
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +68 -20
- package/telegram-plugin/gateway/gateway.ts +1331 -151
- package/telegram-plugin/gateway/inbound-spool.ts +2 -1
- package/telegram-plugin/gateway/inject-handler.test.ts +19 -0
- package/telegram-plugin/gateway/inject-handler.ts +17 -0
- package/telegram-plugin/gateway/ipc-protocol.ts +44 -2
- package/telegram-plugin/gateway/ipc-server.ts +40 -0
- package/telegram-plugin/gateway/model-command.ts +212 -51
- package/telegram-plugin/gateway/pending-card-expiry.ts +98 -0
- package/telegram-plugin/gateway/pending-card-store.ts +173 -0
- package/telegram-plugin/gateway/pending-inbound-buffer.ts +12 -2
- package/telegram-plugin/gateway/resolve-person.ts +304 -0
- package/telegram-plugin/gateway/resume-inbound-builder.ts +240 -2
- package/telegram-plugin/gateway/session-model-source.ts +73 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +21 -1
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +24 -1
- package/telegram-plugin/hooks/silent-end-scan.mjs +164 -40
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +30 -7
- package/telegram-plugin/model-label.ts +69 -0
- package/telegram-plugin/operator-events.ts +45 -0
- package/telegram-plugin/pending-work-progress.ts +42 -7
- package/telegram-plugin/permission-diff.ts +128 -0
- package/telegram-plugin/quota-bar-format.ts +360 -0
- package/telegram-plugin/registry/subagents-schema.ts +80 -1
- package/telegram-plugin/registry/subagents.test.ts +90 -0
- package/telegram-plugin/session-tail.ts +28 -0
- package/telegram-plugin/silent-end.ts +49 -4
- package/telegram-plugin/subagent-watcher.ts +249 -46
- package/telegram-plugin/tests/always-allow-persist-queue.test.ts +529 -0
- package/telegram-plugin/tests/approval-timeout-inbound-builders.test.ts +94 -0
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +21 -0
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +263 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +85 -27
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +4 -2
- package/telegram-plugin/tests/ipc-server-query-pending-permission.test.ts +157 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -5
- package/telegram-plugin/tests/model-command.test.ts +202 -42
- package/telegram-plugin/tests/model-label.test.ts +64 -0
- package/telegram-plugin/tests/operator-events.test.ts +17 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +202 -0
- package/telegram-plugin/tests/pending-card-expiry.test.ts +190 -0
- package/telegram-plugin/tests/pending-card-store.test.ts +173 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +116 -3
- package/telegram-plugin/tests/permission-diff.test.ts +111 -0
- package/telegram-plugin/tests/quota-bar-format.test.ts +444 -0
- package/telegram-plugin/tests/resolve-person.test.ts +290 -0
- package/telegram-plugin/tests/resume-inbound-builder.test.ts +286 -0
- package/telegram-plugin/tests/session-model-source.test.ts +67 -0
- package/telegram-plugin/tests/session-tail.test.ts +64 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-integration.test.ts +53 -0
- package/telegram-plugin/tests/silent-end-interrupt-stop-scan.test.ts +138 -0
- package/telegram-plugin/tests/silent-end.test.ts +46 -1
- package/telegram-plugin/tests/subagent-tracker-hooks.test.ts +39 -0
- package/telegram-plugin/tests/subagent-watcher-boot-promotion-replay.test.ts +107 -4
- package/telegram-plugin/tests/subagent-watcher-handback-gaps.test.ts +42 -4
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +47 -0
- package/telegram-plugin/tests/subagent-watcher-terminated-ids-cap.test.ts +150 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +115 -0
- package/telegram-plugin/tests/tool-activity-summary.test.ts +37 -0
- package/telegram-plugin/tests/typing-wrap.test.ts +23 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +19 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +108 -0
- package/telegram-plugin/tests/worker-feed-dispatch.test.ts +126 -0
- package/telegram-plugin/tool-activity-summary.ts +22 -2
- package/telegram-plugin/typing-wrap.ts +72 -25
- package/telegram-plugin/worker-activity-feed.ts +229 -15
- package/profiles/default/CLAUDE.md +0 -116
- package/telegram-plugin/node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json +0 -1
- package/vendor/hindsight-memory/scripts/__pycache__/directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/recall.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/retain.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/__pycache__/session_end.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/daemon.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/llm.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/state.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/lib/__pycache__/switchroom_envelope.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/__init__.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_capture_nudge.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directives.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_gateway_ipc.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_trivial_skip.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_retain_window.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_sender_routing.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/scripts/tests/__pycache__/test_switchroom_envelope.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_bank.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_client.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.0.3.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_config.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_content.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_drain_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_hooks.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_manifest.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_recall_exit_codes.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_session_end_pending.cpython-313.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313-pytest-9.1.1.pyc +0 -0
- package/vendor/hindsight-memory/tests/__pycache__/test_state.cpython-313.pyc +0 -0
|
@@ -48,7 +48,7 @@ import { sanitiseToolArg } from './fleet-state.js'
|
|
|
48
48
|
import { clipNarrative, describeToolUse } from './tool-activity-summary.js'
|
|
49
49
|
import { REPLY_TOOLS, isDraftOfReply } from './narrative-dedup.js'
|
|
50
50
|
import { truncate } from './card-format.js'
|
|
51
|
-
import { bumpSubagentActivity, recordSubagentStall, recordSubagentResume, recordSubagentEnd, reapStuckRunningRows, countRunningBackgroundSubagents, recordNestedSubagentDispatch } from './registry/subagents-schema.js'
|
|
51
|
+
import { bumpSubagentActivity, recordSubagentStall, recordSubagentResume, recordSubagentEnd, reapStuckRunningRows, countRunningBackgroundSubagents, recordNestedSubagentDispatch, recordSubagentModel } from './registry/subagents-schema.js'
|
|
52
52
|
import { touchTurnActiveMarker } from './gateway/turn-active-marker.js'
|
|
53
53
|
|
|
54
54
|
// ─── Types ───────────────────────────────────────────────────────────────────
|
|
@@ -193,6 +193,44 @@ export interface WorkerEntry {
|
|
|
193
193
|
* Pre hook pending" window that froze nested cards on "starting…".
|
|
194
194
|
*/
|
|
195
195
|
lastBackfillAttemptAt?: number
|
|
196
|
+
/**
|
|
197
|
+
* Fix #5: set on a boot-scan `running` file whose mtime passed the
|
|
198
|
+
* `inflightPromoteMaxAgeMs` freshness gate but has NOT yet shown any
|
|
199
|
+
* post-boot JSONL growth. mtime freshness alone can't distinguish a
|
|
200
|
+
* worker killed moments before the restart (indistinguishable file state)
|
|
201
|
+
* from a genuinely still-running one — so promotion is deferred until
|
|
202
|
+
* `checkBootPromotionGrowth` observes the file's size actually advance
|
|
203
|
+
* past its boot-time snapshot (real proof of life), or `deadlineAt`
|
|
204
|
+
* passes with no growth (treated as historical/orphan — never promoted).
|
|
205
|
+
* `undefined` once resolved either way.
|
|
206
|
+
*/
|
|
207
|
+
bootPromotionPending?: { deadlineAt: number }
|
|
208
|
+
/**
|
|
209
|
+
* Fix #1(+#2): the `run_in_background` flag as last observed from the
|
|
210
|
+
* registry `subagents` row, carried on the entry itself rather than
|
|
211
|
+
* re-derived only at `onFinish` time. The registry row is the sole
|
|
212
|
+
* source of truth for this flag (the watcher never sees the dispatching
|
|
213
|
+
* tool_input directly), but the row is looked up by `jsonl_agent_id` —
|
|
214
|
+
* exactly the linkage that can be permanently missing (meta.json
|
|
215
|
+
* unreadable, or the fuzzy backfill found no unambiguous candidate).
|
|
216
|
+
* Once we DO observe the row (at registration or on any later liveness
|
|
217
|
+
* tick — the backfill retry can link it after the fact), we cache the
|
|
218
|
+
* flag here so a subsequent `onFinish` can fall back to it even if
|
|
219
|
+
* `getSubagentByJsonlId` fails again at finish time. `undefined` until
|
|
220
|
+
* the row has been observed at least once.
|
|
221
|
+
*/
|
|
222
|
+
background?: boolean
|
|
223
|
+
/**
|
|
224
|
+
* Live model this worker is running, as a raw resolved model id (e.g.
|
|
225
|
+
* `claude-opus-4-8`). Set from the worker's OWN transcript `message.model`
|
|
226
|
+
* (`sub_agent_model` event) — the authoritative live source — and persisted to
|
|
227
|
+
* the registry row on change (recordSubagentModel) so boot-replay / handback
|
|
228
|
+
* cards can render it without a live entry. Threaded onto the onProgress
|
|
229
|
+
* payload so the worker card's metrics line shows the model. Undefined until
|
|
230
|
+
* the worker's first assistant line lands; before that the card falls back to
|
|
231
|
+
* the dispatch-time `tool_input.model` persisted on the registry row.
|
|
232
|
+
*/
|
|
233
|
+
currentModel?: string
|
|
196
234
|
}
|
|
197
235
|
|
|
198
236
|
export interface SubagentWatcherConfig {
|
|
@@ -271,6 +309,13 @@ export interface SubagentWatcherConfig {
|
|
|
271
309
|
* stale-handback replay regression.
|
|
272
310
|
*/
|
|
273
311
|
inflightPromoteMaxAgeMs?: number
|
|
312
|
+
/**
|
|
313
|
+
* Fix #9a: override for `TERMINATED_AGENT_IDS_CAP` — the bound on the
|
|
314
|
+
* `terminatedAgentIds` re-discovery dedup guard. Exposed mainly so tests
|
|
315
|
+
* can exercise eviction without inserting thousands of entries. Defaults
|
|
316
|
+
* to `TERMINATED_AGENT_IDS_CAP` (a few thousand) in production.
|
|
317
|
+
*/
|
|
318
|
+
terminatedAgentIdsCap?: number
|
|
274
319
|
/**
|
|
275
320
|
* Kill-switch for the boot-scan promotion path. When false, a
|
|
276
321
|
* running-at-boot worker is never promoted — the watcher reverts to the
|
|
@@ -390,6 +435,15 @@ export interface SubagentWatcherConfig {
|
|
|
390
435
|
* no `sub_agent_text` line was ever observed. Feeds the
|
|
391
436
|
* `subagent_handback` inbound. */
|
|
392
437
|
resultText: string
|
|
438
|
+
/**
|
|
439
|
+
* Fix #1(+#2): the entry's own cached `background` flag (see
|
|
440
|
+
* `WorkerEntry.background`), threaded through so the gateway can fall
|
|
441
|
+
* back to it when its own registry lookup at finish time also comes up
|
|
442
|
+
* empty (unlinked `jsonl_agent_id`). `undefined` when the row was never
|
|
443
|
+
* observed by the watcher either — the gateway degrades further from
|
|
444
|
+
* there (see the onFinish handler's non-empty-resultText fallback).
|
|
445
|
+
*/
|
|
446
|
+
background: boolean | undefined
|
|
393
447
|
}) => void
|
|
394
448
|
/**
|
|
395
449
|
* #1720: fires on every `sub_agent_text` event for a running
|
|
@@ -419,6 +473,12 @@ export interface SubagentWatcherConfig {
|
|
|
419
473
|
* feed. Undefined on `sub_agent_text` ticks — the gateway falls back
|
|
420
474
|
* to `latestSummary` (the narrative line), preserving prior behavior. */
|
|
421
475
|
progressLine?: string
|
|
476
|
+
/** Live model this worker is running (raw resolved id, e.g.
|
|
477
|
+
* `claude-opus-4-8`), from `WorkerEntry.currentModel`. Threaded onto the
|
|
478
|
+
* worker/nested card's metrics line. Undefined before the worker's first
|
|
479
|
+
* assistant line — the gateway then falls back to the registry's
|
|
480
|
+
* dispatch-time model. */
|
|
481
|
+
model?: string
|
|
422
482
|
}) => void
|
|
423
483
|
/** `Date.now` override for tests. */
|
|
424
484
|
now?: () => number
|
|
@@ -572,6 +632,18 @@ const TERMINAL_CLEANUP_GRACE_MS = 30_000
|
|
|
572
632
|
*/
|
|
573
633
|
const BACKFILL_RETRY_INTERVAL_MS = 3000
|
|
574
634
|
|
|
635
|
+
/**
|
|
636
|
+
* Fix #9a: cap on `terminatedAgentIds` (the re-discovery dedup guard in
|
|
637
|
+
* `cleanupTerminalAgent` / `scanSubagentsDir`). Pre-fix the Set only grew
|
|
638
|
+
* (added on every terminal cleanup, only ever cleared wholesale in
|
|
639
|
+
* `stop()`), so a long-lived gateway with sustained sub-agent throughput
|
|
640
|
+
* accumulated ids without bound. A `Set` preserves insertion order, so once
|
|
641
|
+
* the cap is hit the OLDEST id is evicted on each new insert (simple ring
|
|
642
|
+
* buffer semantics) — recently-terminated ids (the ones actually at risk of
|
|
643
|
+
* a re-discovery race) always stay covered.
|
|
644
|
+
*/
|
|
645
|
+
const TERMINATED_AGENT_IDS_CAP = 5000
|
|
646
|
+
|
|
575
647
|
// ─── JSONL tail per sub-agent ─────────────────────────────────────────────
|
|
576
648
|
|
|
577
649
|
interface SubTail {
|
|
@@ -662,20 +734,30 @@ export function backfillJsonlAgentId(
|
|
|
662
734
|
|
|
663
735
|
// Fallback path: fuzzy (agentType, description) match for older Claude Code
|
|
664
736
|
// versions whose meta.json predates the toolUseId field.
|
|
737
|
+
//
|
|
738
|
+
// Fix #3: with N unlinked rows sharing (agent_type, description), a bare
|
|
739
|
+
// `ORDER BY started_at DESC LIMIT 1` assigns by start-order, not true
|
|
740
|
+
// correspondence — a benign FIFO for interchangeable rows, but a genuine
|
|
741
|
+
// mislink for a cross-topic identical dispatch. Read up to 2 candidates
|
|
742
|
+
// (not just 1): if more than one row shares the key, the match is
|
|
743
|
+
// ambiguous — refuse to link (leave jsonl_agent_id NULL) and let the
|
|
744
|
+
// marker/window path attribute it instead, rather than guessing.
|
|
665
745
|
if (candidateId == null && (meta.agentType || meta.description)) {
|
|
666
|
-
const
|
|
746
|
+
const fuzzyCandidates = db
|
|
667
747
|
.prepare(`
|
|
668
748
|
SELECT id FROM subagents
|
|
669
749
|
WHERE jsonl_agent_id IS NULL
|
|
670
750
|
AND agent_type IS ?
|
|
671
751
|
AND description IS ?
|
|
672
752
|
ORDER BY started_at DESC
|
|
673
|
-
LIMIT
|
|
753
|
+
LIMIT 2
|
|
674
754
|
`)
|
|
675
|
-
.
|
|
676
|
-
if (
|
|
677
|
-
candidateId =
|
|
755
|
+
.all(meta.agentType ?? null, meta.description ?? null) as { id: string }[]
|
|
756
|
+
if (fuzzyCandidates.length === 1) {
|
|
757
|
+
candidateId = fuzzyCandidates[0]!.id
|
|
678
758
|
log?.(`subagent-watcher: backfill fuzzy match ${agentId} → ${candidateId} (type=${meta.agentType} desc=${meta.description})`)
|
|
759
|
+
} else if (fuzzyCandidates.length > 1) {
|
|
760
|
+
log?.(`subagent-watcher: backfill fuzzy match refused for ${agentId} — ${fuzzyCandidates.length} ambiguous candidates share (type=${meta.agentType} desc=${meta.description}); leaving unlinked`)
|
|
679
761
|
}
|
|
680
762
|
}
|
|
681
763
|
|
|
@@ -809,6 +891,8 @@ export function readSubTail(
|
|
|
809
891
|
/** Friendly display line for THIS tick (set on tool ticks; see the
|
|
810
892
|
* SubagentWatcherConfig.onProgress doc). */
|
|
811
893
|
progressLine?: string
|
|
894
|
+
/** Live model this worker is running (see SubagentWatcherConfig.onProgress). */
|
|
895
|
+
model?: string
|
|
812
896
|
}) => void,
|
|
813
897
|
): void {
|
|
814
898
|
try {
|
|
@@ -868,6 +952,12 @@ export function readSubTail(
|
|
|
868
952
|
} else {
|
|
869
953
|
bumpSubagentActivity(db, { id: existing.id, ts: now })
|
|
870
954
|
isForeground = existing.background === 0
|
|
955
|
+
// Fix #1(+#2): keep the entry's cached background flag fresh —
|
|
956
|
+
// this is also how a NESTED worker (whose row links only via the
|
|
957
|
+
// throttled backfill retry above, after the one-shot attempt at
|
|
958
|
+
// registration lost the race) picks up its background flag before
|
|
959
|
+
// its own `onFinish` fires.
|
|
960
|
+
entry.background = existing.background === 1
|
|
871
961
|
}
|
|
872
962
|
} catch (dbErr) {
|
|
873
963
|
log?.(`subagent-watcher: liveness write error ${entry.agentId}: ${(dbErr as Error).message}`)
|
|
@@ -918,8 +1008,8 @@ export function readSubTail(
|
|
|
918
1008
|
// (stage-on-text, resolve-on-tool/turn_end) can replay a previously
|
|
919
1009
|
// pending block exactly once. `latestSummary` carries the worker's
|
|
920
1010
|
// narrative result (entry.lastResultText), never tool labels.
|
|
921
|
-
const fireNarrativeProgress = ():
|
|
922
|
-
if (onProgress == null || entry.state !== 'running' || entry.historical) return
|
|
1011
|
+
const fireNarrativeProgress = (): boolean => {
|
|
1012
|
+
if (onProgress == null || entry.state !== 'running' || entry.historical) return false
|
|
923
1013
|
try {
|
|
924
1014
|
onProgress({
|
|
925
1015
|
agentId: entry.agentId,
|
|
@@ -932,9 +1022,12 @@ export function readSubTail(
|
|
|
932
1022
|
},
|
|
933
1023
|
lastTool: entry.lastTool,
|
|
934
1024
|
toolCount: entry.toolCount,
|
|
1025
|
+
model: entry.currentModel,
|
|
935
1026
|
})
|
|
1027
|
+
return true
|
|
936
1028
|
} catch (cbErr) {
|
|
937
1029
|
log?.(`subagent-watcher: onProgress callback error ${entry.agentId}: ${(cbErr as Error).message}`)
|
|
1030
|
+
return false
|
|
938
1031
|
}
|
|
939
1032
|
}
|
|
940
1033
|
// Resolve a pending sub-agent narrative against a lookahead event.
|
|
@@ -952,21 +1045,27 @@ export function readSubTail(
|
|
|
952
1045
|
// compare the trailing block against the worker's last reply text
|
|
953
1046
|
// (`entry.lastReplyText`) and suppress a draft. Background workers
|
|
954
1047
|
// never set lastReplyText, so their trailing narration still SHOWs.
|
|
1048
|
+
// Returns true iff a narrative onProgress cue actually fired this
|
|
1049
|
+
// call — callers use this to skip a redundant/clobbering tool-label
|
|
1050
|
+
// onProgress cue for the SAME tick (see #1042 below: without this,
|
|
1051
|
+
// the tool-description onProgress unconditionally fires right after
|
|
1052
|
+
// and its replace-on-write onProgress always wins, so the narration
|
|
1053
|
+
// shown here is never actually visible on the pinned card).
|
|
955
1054
|
const resolvePendingSubNarrative = (
|
|
956
1055
|
toolName: string | null,
|
|
957
1056
|
toolInput: Record<string, unknown> | undefined,
|
|
958
|
-
):
|
|
959
|
-
if (entry.pendingNarrative == null) return
|
|
1057
|
+
): boolean => {
|
|
1058
|
+
if (entry.pendingNarrative == null) return false
|
|
960
1059
|
const pending = entry.pendingNarrative
|
|
961
1060
|
entry.pendingNarrative = null
|
|
962
1061
|
if (toolName != null && REPLY_TOOLS.has(toolName)) {
|
|
963
1062
|
const replyText = typeof toolInput?.text === 'string' ? (toolInput.text as string) : ''
|
|
964
|
-
if (isDraftOfReply(pending.text, replyText)) return // draft of the reply → SUPPRESS
|
|
1063
|
+
if (isDraftOfReply(pending.text, replyText)) return false // draft of the reply → SUPPRESS
|
|
965
1064
|
} else if (toolName == null && entry.lastReplyText != null && entry.lastReplyText.length > 0) {
|
|
966
1065
|
// turn_end path: suppress a trailing draft of the delivered answer.
|
|
967
|
-
if (isDraftOfReply(pending.text, entry.lastReplyText)) return
|
|
1066
|
+
if (isDraftOfReply(pending.text, entry.lastReplyText)) return false
|
|
968
1067
|
}
|
|
969
|
-
fireNarrativeProgress()
|
|
1068
|
+
return fireNarrativeProgress()
|
|
970
1069
|
}
|
|
971
1070
|
for (const ev of events) {
|
|
972
1071
|
const idleSecBeforeBump = Math.round((now - entry.lastActivityAt) / 1000)
|
|
@@ -1007,12 +1106,37 @@ export function readSubTail(
|
|
|
1007
1106
|
}
|
|
1008
1107
|
log?.(`subagent-watcher: stall cleared for ${entry.agentId} (activity resumed after ${idleSecBeforeBump}s — re-arming detection)`)
|
|
1009
1108
|
}
|
|
1109
|
+
if (ev.kind === 'sub_agent_model') {
|
|
1110
|
+
// Live model capture for this worker. The projection already filtered
|
|
1111
|
+
// sentinels, so `ev.model` is a real resolved id. Record it on the
|
|
1112
|
+
// entry (transcript wins over the dispatch-time tool_input.model) and
|
|
1113
|
+
// persist to the registry row on CHANGE — update-on-change like
|
|
1114
|
+
// last_activity_at — so a boot-replay / handback card can render the
|
|
1115
|
+
// model without a live entry. No card render here: the model rides the
|
|
1116
|
+
// next onProgress tick's payload (below).
|
|
1117
|
+
if (entry.currentModel !== ev.model) {
|
|
1118
|
+
entry.currentModel = ev.model
|
|
1119
|
+
if (db != null) {
|
|
1120
|
+
try {
|
|
1121
|
+
const rowRef = db
|
|
1122
|
+
.prepare('SELECT id FROM subagents WHERE jsonl_agent_id = ?')
|
|
1123
|
+
.get(entry.agentId) as { id: string } | null
|
|
1124
|
+
if (rowRef != null) {
|
|
1125
|
+
recordSubagentModel(db, { id: rowRef.id, model: ev.model })
|
|
1126
|
+
}
|
|
1127
|
+
} catch (dbErr) {
|
|
1128
|
+
log?.(`subagent-watcher: model DB write error ${entry.agentId}: ${(dbErr as Error).message}`)
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
continue
|
|
1133
|
+
}
|
|
1010
1134
|
if (ev.kind === 'sub_agent_tool_use') {
|
|
1011
1135
|
// Narrative-dedup gate step 2: a sub_agent_text block was pending;
|
|
1012
1136
|
// this tool is the lookahead that decides it (SHOW unless it drafts
|
|
1013
1137
|
// a reply tool's text). Runs before the tool's own progress cue so
|
|
1014
1138
|
// a working preamble surfaces just ahead of its tool step.
|
|
1015
|
-
resolvePendingSubNarrative(ev.toolName, ev.input)
|
|
1139
|
+
const narrativeJustFired = resolvePendingSubNarrative(ev.toolName, ev.input)
|
|
1016
1140
|
// NIT 3: capture a foreground sub-agent's actual reply text so the
|
|
1017
1141
|
// turn_end path can suppress a trailing draft of it (see
|
|
1018
1142
|
// resolvePendingSubNarrative). Only REPLY_TOOLS carry the answer.
|
|
@@ -1039,7 +1163,17 @@ export function readSubTail(
|
|
|
1039
1163
|
// stays the worker's narrative result (never polluted with tool
|
|
1040
1164
|
// labels — the handback payload depends on it). Pure jsonl-tail →
|
|
1041
1165
|
// render, no model call.
|
|
1042
|
-
|
|
1166
|
+
//
|
|
1167
|
+
// Clobber guard: if a pending narrative just fired an onProgress
|
|
1168
|
+
// cue THIS SAME tick (above), skip this one — replace-on-write
|
|
1169
|
+
// rendering means whichever onProgress call fires last wins, so
|
|
1170
|
+
// firing both back-to-back always threw away the narration in
|
|
1171
|
+
// favour of the generic tool label. Narration already told the
|
|
1172
|
+
// user what's happening for this tick; the label is redundant
|
|
1173
|
+
// here. When nothing preceded this tool call (no pending
|
|
1174
|
+
// narrative), this still fires — that's the named foreground
|
|
1175
|
+
// blindspot fix, unchanged.
|
|
1176
|
+
if (onProgress != null && entry.state === 'running' && !entry.historical && !narrativeJustFired) {
|
|
1043
1177
|
const toolLine = describeToolUse(ev.toolName, ev.input ?? {})
|
|
1044
1178
|
if (toolLine != null && toolLine.length > 0) {
|
|
1045
1179
|
try {
|
|
@@ -1055,6 +1189,7 @@ export function readSubTail(
|
|
|
1055
1189
|
lastTool: entry.lastTool,
|
|
1056
1190
|
toolCount: entry.toolCount,
|
|
1057
1191
|
progressLine: toolLine,
|
|
1192
|
+
model: entry.currentModel,
|
|
1058
1193
|
})
|
|
1059
1194
|
} catch (cbErr) {
|
|
1060
1195
|
log?.(`subagent-watcher: onProgress (tool) callback error ${entry.agentId}: ${(cbErr as Error).message}`)
|
|
@@ -1215,6 +1350,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1215
1350
|
config.inflightPromoteMaxAgeMs
|
|
1216
1351
|
?? parseEnvMs('SWITCHROOM_SUBAGENT_INFLIGHT_MAX_AGE_MS')
|
|
1217
1352
|
?? DEFAULT_INFLIGHT_PROMOTE_MAX_AGE_MS
|
|
1353
|
+
const terminatedAgentIdsCap = config.terminatedAgentIdsCap ?? TERMINATED_AGENT_IDS_CAP
|
|
1218
1354
|
// Kill-switch: not parseEnvMs (which rejects `0`) — an explicit `=0`
|
|
1219
1355
|
// here MUST disable promotion (revert to pre-v0.14.23 suppression).
|
|
1220
1356
|
const bootPromoteEnabled =
|
|
@@ -1343,6 +1479,17 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1343
1479
|
} catch (err) {
|
|
1344
1480
|
log?.(`subagent-watcher: backfill error for ${agentId}: ${(err as Error).message}`)
|
|
1345
1481
|
}
|
|
1482
|
+
// Fix #1(+#2): if the backfill above (or an already-linked row from a
|
|
1483
|
+
// prior registration attempt) resolved jsonl_agent_id, cache the
|
|
1484
|
+
// row's `background` flag on the entry NOW — the earliest point it
|
|
1485
|
+
// can ever be known — so it survives even if a later `onFinish`
|
|
1486
|
+
// can't re-resolve the row (see `WorkerEntry.background` doc).
|
|
1487
|
+
try {
|
|
1488
|
+
const row = db
|
|
1489
|
+
.prepare('SELECT background FROM subagents WHERE jsonl_agent_id = ?')
|
|
1490
|
+
.get(agentId) as { background: number } | null
|
|
1491
|
+
if (row != null) entry.background = row.background === 1
|
|
1492
|
+
} catch { /* best-effort — entry.background stays undefined */ }
|
|
1346
1493
|
}
|
|
1347
1494
|
|
|
1348
1495
|
const tail: SubTail = {
|
|
@@ -1391,37 +1538,16 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1391
1538
|
} else if (fileAgeMs > inflightPromoteMaxAgeMs) {
|
|
1392
1539
|
log?.(`subagent-watcher: ${agentId} running at boot but stale (last write ${Math.round(fileAgeMs / 1000)}s ago > ${Math.round(inflightPromoteMaxAgeMs / 1000)}s) — leaving historical (dead prior-session worker, not in-flight)`)
|
|
1393
1540
|
} else {
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
//
|
|
1397
|
-
//
|
|
1398
|
-
//
|
|
1399
|
-
//
|
|
1400
|
-
//
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
} catch (err) {
|
|
1405
|
-
log?.(`subagent-watcher: backfill error for ${agentId}: ${(err as Error).message}`)
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
// Historical/onProgress gate-ordering fix: the initial read above ran
|
|
1409
|
-
// while `entry.historical` was still TRUE, so every onProgress cue it
|
|
1410
|
-
// would have fired was suppressed by the `!entry.historical` guard —
|
|
1411
|
-
// and the tail cursor is now at EOF, so for a quiet worker no later
|
|
1412
|
-
// tick ever re-fires them. The card painted as a bare stub and froze
|
|
1413
|
-
// on "starting…" forever. Re-read from the start now that the entry
|
|
1414
|
-
// is live: the replayed events rebuild toolCount/lastTool/narrative
|
|
1415
|
-
// AND fire onProgress so the worker's card reaches real activity.
|
|
1416
|
-
entry.toolCount = 0
|
|
1417
|
-
entry.lastTool = null
|
|
1418
|
-
entry.pendingNarrative = null
|
|
1419
|
-
tail.cursor = 0
|
|
1420
|
-
tail.pendingPartial = ''
|
|
1421
|
-
tail.hasEmittedStart = false
|
|
1422
|
-
readSubTail(entry, tail, n, (desc) => {
|
|
1423
|
-
log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`)
|
|
1424
|
-
}, fs, log, db, parentStateDir, config.onUnstall, undefined, config.onProgress)
|
|
1541
|
+
// Fix #5: a fresh mtime alone does NOT confirm liveness — a worker
|
|
1542
|
+
// killed seconds before this restart looks byte-for-byte identical
|
|
1543
|
+
// to a genuinely still-running one. Defer promotion until
|
|
1544
|
+
// `checkBootPromotionGrowth` observes the file actually grow AFTER
|
|
1545
|
+
// this boot (real proof of life), bounded by the same
|
|
1546
|
+
// `inflightPromoteMaxAgeMs` window as an outer timeout. Until then
|
|
1547
|
+
// the entry stays `historical` (no stall detection, no completion
|
|
1548
|
+
// synthesis) — see checkBootPromotionGrowth / promoteBootEntry.
|
|
1549
|
+
entry.bootPromotionPending = { deadlineAt: n + inflightPromoteMaxAgeMs }
|
|
1550
|
+
log?.(`subagent-watcher: ${agentId} running at boot (last write ${Math.round(fileAgeMs / 1000)}s ago) — awaiting post-boot JSONL growth before promoting to live (mtime freshness alone can't rule out a just-killed worker)`)
|
|
1425
1551
|
}
|
|
1426
1552
|
}
|
|
1427
1553
|
|
|
@@ -1452,6 +1578,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1452
1578
|
const entry = registry.get(agentId)
|
|
1453
1579
|
const t = tails.get(agentId)
|
|
1454
1580
|
if (!entry || !t) return
|
|
1581
|
+
checkBootPromotionGrowth(entry, t, nowFn())
|
|
1455
1582
|
readSubTail(entry, t, nowFn(), (desc) => {
|
|
1456
1583
|
log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`)
|
|
1457
1584
|
}, fs, log, db, parentStateDir, config.onUnstall, cleanupTerminalAgent, config.onProgress)
|
|
@@ -1462,6 +1589,68 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1462
1589
|
}
|
|
1463
1590
|
}
|
|
1464
1591
|
|
|
1592
|
+
/**
|
|
1593
|
+
* Fix #5: the growth-confirmation half of boot promotion. Called on every
|
|
1594
|
+
* tick (poll + fs.watch) for an entry that's still awaiting confirmation
|
|
1595
|
+
* (`entry.bootPromotionPending` set — see the freshness-gate branch in
|
|
1596
|
+
* `registerAgent`). Promotes to live the moment the file's size advances
|
|
1597
|
+
* past `tail.cursor` (the boot-time snapshot — the initial full read in
|
|
1598
|
+
* `registerAgent` already parked the cursor at EOF-at-boot, so ANY further
|
|
1599
|
+
* growth is unambiguously post-boot). If the bounded window elapses with
|
|
1600
|
+
* no growth, gives up permanently — the entry stays `historical`, so it
|
|
1601
|
+
* can never synthesise a stale `completed` handback from pre-restart bytes
|
|
1602
|
+
* (checkStalls / onFinish both gate on `!entry.historical`).
|
|
1603
|
+
*/
|
|
1604
|
+
function checkBootPromotionGrowth(entry: WorkerEntry, tail: SubTail, n: number): void {
|
|
1605
|
+
const pending = entry.bootPromotionPending
|
|
1606
|
+
if (!pending) return
|
|
1607
|
+
let size: number | null = null
|
|
1608
|
+
try {
|
|
1609
|
+
size = fs.statSync(entry.filePath).size
|
|
1610
|
+
} catch {
|
|
1611
|
+
/* unreadable — fall through to the deadline check below */
|
|
1612
|
+
}
|
|
1613
|
+
if (size != null && size > tail.cursor) {
|
|
1614
|
+
entry.bootPromotionPending = undefined
|
|
1615
|
+
entry.historical = false
|
|
1616
|
+
log?.(`subagent-watcher: ${entry.agentId} confirmed live — observed post-boot JSONL growth (${tail.cursor} → ${size} bytes); promoting`)
|
|
1617
|
+
// The prior gateway life's registration normally linked
|
|
1618
|
+
// jsonl_agent_id already, but re-run the backfill idempotently in
|
|
1619
|
+
// case that life crashed before the link persisted — the handback's
|
|
1620
|
+
// isBackground lookup is keyed on jsonl_agent_id, and an unlinked row
|
|
1621
|
+
// would mis-resolve the worker as foreground and drop the handback.
|
|
1622
|
+
if (db != null) {
|
|
1623
|
+
try {
|
|
1624
|
+
backfillJsonlAgentId(db, entry.filePath, entry.agentId, log)
|
|
1625
|
+
} catch (err) {
|
|
1626
|
+
log?.(`subagent-watcher: backfill error for ${entry.agentId}: ${(err as Error).message}`)
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
// Historical/onProgress gate-ordering fix (carried over from the old
|
|
1630
|
+
// synchronous promotion path): the initial read in registerAgent ran
|
|
1631
|
+
// while `entry.historical` was still TRUE, so any onProgress cue it
|
|
1632
|
+
// would have fired was suppressed, and the cursor is at EOF-at-boot —
|
|
1633
|
+
// for a quiet worker no later tick would ever re-fire them. Re-read
|
|
1634
|
+
// from the start now that the entry is live: the replayed events
|
|
1635
|
+
// rebuild toolCount/lastTool/narrative AND fire onProgress so the
|
|
1636
|
+
// worker's card reaches real activity instead of a frozen stub.
|
|
1637
|
+
entry.toolCount = 0
|
|
1638
|
+
entry.lastTool = null
|
|
1639
|
+
entry.pendingNarrative = null
|
|
1640
|
+
tail.cursor = 0
|
|
1641
|
+
tail.pendingPartial = ''
|
|
1642
|
+
tail.hasEmittedStart = false
|
|
1643
|
+
readSubTail(entry, tail, n, (desc) => {
|
|
1644
|
+
log?.(`subagent-watcher: description updated for ${entry.agentId}: ${desc}`)
|
|
1645
|
+
}, fs, log, db, parentStateDir, config.onUnstall, undefined, config.onProgress)
|
|
1646
|
+
return
|
|
1647
|
+
}
|
|
1648
|
+
if (n >= pending.deadlineAt) {
|
|
1649
|
+
entry.bootPromotionPending = undefined
|
|
1650
|
+
log?.(`subagent-watcher: ${entry.agentId} never observed post-boot JSONL growth within the window — leaving historical/orphan (not promoting; avoids synthesising a stale 'completed' handback from a worker killed before this restart)`)
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1465
1654
|
// ─── State-transition notifications ─────────────────────────────────────
|
|
1466
1655
|
|
|
1467
1656
|
function maybySendStateTransition(agentId: string): void {
|
|
@@ -1504,6 +1693,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1504
1693
|
resultText: entry.errored
|
|
1505
1694
|
? entry.lastResultText || entry.errorDetail || ''
|
|
1506
1695
|
: entry.lastResultText,
|
|
1696
|
+
background: entry.background,
|
|
1507
1697
|
})
|
|
1508
1698
|
} catch (cbErr) {
|
|
1509
1699
|
log?.(`subagent-watcher: onFinish callback error ${agentId}: ${(cbErr as Error).message}`)
|
|
@@ -1525,6 +1715,7 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1525
1715
|
durationMs: nowFn() - entry.dispatchedAt,
|
|
1526
1716
|
description: entry.description,
|
|
1527
1717
|
resultText: entry.lastResultText,
|
|
1718
|
+
background: entry.background,
|
|
1528
1719
|
})
|
|
1529
1720
|
} catch (cbErr) {
|
|
1530
1721
|
log?.(`subagent-watcher: onFinish callback error ${agentId}: ${(cbErr as Error).message}`)
|
|
@@ -1575,6 +1766,14 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1575
1766
|
// Issue #1116 (Bug B): record that this agent has been fully
|
|
1576
1767
|
// processed so a rescan that rediscovers the still-present JSONL
|
|
1577
1768
|
// doesn't re-register and re-notify.
|
|
1769
|
+
//
|
|
1770
|
+
// Fix #9a: bound the Set so it can't grow unboundedly over a long-lived
|
|
1771
|
+
// gateway — evict the oldest id once the cap is hit (Set iteration
|
|
1772
|
+
// order is insertion order, so `.values().next().value` is the oldest).
|
|
1773
|
+
if (!terminatedAgentIds.has(agentId) && terminatedAgentIds.size >= terminatedAgentIdsCap) {
|
|
1774
|
+
const oldest = terminatedAgentIds.values().next().value
|
|
1775
|
+
if (oldest != null) terminatedAgentIds.delete(oldest)
|
|
1776
|
+
}
|
|
1578
1777
|
terminatedAgentIds.add(agentId)
|
|
1579
1778
|
log?.(`subagent-watcher: cleaned up terminal agent ${agentId}`)
|
|
1580
1779
|
}
|
|
@@ -1876,6 +2075,10 @@ export function startSubagentWatcher(config: SubagentWatcherConfig): SubagentWat
|
|
|
1876
2075
|
if (entry.state !== 'running') continue
|
|
1877
2076
|
const tail = tails.get(agentId)
|
|
1878
2077
|
if (!tail) continue
|
|
2078
|
+
// Fix #5: defensive poll-loop fallback for the same growth check the
|
|
2079
|
+
// fs.watch callback runs — covers the case where fs.watch missed the
|
|
2080
|
+
// event that would otherwise have confirmed liveness.
|
|
2081
|
+
checkBootPromotionGrowth(entry, tail, n)
|
|
1879
2082
|
readSubTail(entry, tail, n, (desc) => {
|
|
1880
2083
|
log?.(`subagent-watcher: description updated for ${agentId}: ${desc}`)
|
|
1881
2084
|
}, fs, log, db, parentStateDir, config.onUnstall, cleanupTerminalAgent, config.onProgress)
|