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
|
@@ -26,9 +26,11 @@ import {
|
|
|
26
26
|
recordSubagentStall,
|
|
27
27
|
recordSubagentResume,
|
|
28
28
|
bumpSubagentActivity,
|
|
29
|
+
recordSubagentModel,
|
|
29
30
|
getSubagent,
|
|
30
31
|
reapStuckRunningRows,
|
|
31
32
|
countRunningBackgroundSubagents,
|
|
33
|
+
listNonTerminalSubagentsForTurn,
|
|
32
34
|
} from './subagents-schema.js'
|
|
33
35
|
|
|
34
36
|
// ---------------------------------------------------------------------------
|
|
@@ -68,6 +70,54 @@ describe('migration on fresh DB', () => {
|
|
|
68
70
|
expect(() => applySubagentsSchema(db)).not.toThrow()
|
|
69
71
|
db.close()
|
|
70
72
|
})
|
|
73
|
+
|
|
74
|
+
it('creates the model column on a fresh DB', () => {
|
|
75
|
+
const db = openFreshSubagentsDbInMemory()
|
|
76
|
+
const cols = db.prepare("SELECT name FROM pragma_table_info('subagents')").all() as { name: string }[]
|
|
77
|
+
expect(cols.map((c) => c.name)).toContain('model')
|
|
78
|
+
db.close()
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Live-model column — migration + record helper
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
describe('live-model column', () => {
|
|
87
|
+
it('migrates a pre-model subagents table by adding the model column (idempotent)', () => {
|
|
88
|
+
const db = openFreshSubagentsDbInMemory()
|
|
89
|
+
// Simulate a legacy table with no model column.
|
|
90
|
+
db.exec('ALTER TABLE subagents DROP COLUMN model')
|
|
91
|
+
let cols = db.prepare("SELECT name FROM pragma_table_info('subagents')").all() as { name: string }[]
|
|
92
|
+
expect(cols.map((c) => c.name)).not.toContain('model')
|
|
93
|
+
// Re-applying the schema must add it back, and be safe to repeat.
|
|
94
|
+
applySubagentsSchema(db)
|
|
95
|
+
applySubagentsSchema(db)
|
|
96
|
+
cols = db.prepare("SELECT name FROM pragma_table_info('subagents')").all() as { name: string }[]
|
|
97
|
+
expect(cols.map((c) => c.name)).toContain('model')
|
|
98
|
+
db.close()
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('recordSubagentStart leaves model null; recordSubagentModel persists on change', () => {
|
|
102
|
+
const db = openFreshSubagentsDbInMemory()
|
|
103
|
+
const now = Date.now()
|
|
104
|
+
recordSubagentStart(db, { id: 'toolu_m1', background: true, startedAt: now, jsonlAgentId: 'a1' })
|
|
105
|
+
expect(getSubagent(db, 'toolu_m1')?.model).toBeNull()
|
|
106
|
+
|
|
107
|
+
recordSubagentModel(db, { id: 'toolu_m1', model: 'claude-opus-4-8' })
|
|
108
|
+
expect(getSubagent(db, 'toolu_m1')?.model).toBe('claude-opus-4-8')
|
|
109
|
+
|
|
110
|
+
// Update-on-change: a later model overwrites.
|
|
111
|
+
recordSubagentModel(db, { id: 'toolu_m1', model: 'sr-glm-5' })
|
|
112
|
+
expect(getSubagent(db, 'toolu_m1')?.model).toBe('sr-glm-5')
|
|
113
|
+
db.close()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('recordSubagentModel no-ops gracefully on a missing id', () => {
|
|
117
|
+
const db = openFreshSubagentsDbInMemory()
|
|
118
|
+
expect(() => recordSubagentModel(db, { id: 'nope', model: 'claude-opus-4-8' })).not.toThrow()
|
|
119
|
+
db.close()
|
|
120
|
+
})
|
|
71
121
|
})
|
|
72
122
|
|
|
73
123
|
// ---------------------------------------------------------------------------
|
|
@@ -616,3 +666,43 @@ describe('reapStuckRunningRows', () => {
|
|
|
616
666
|
expect(result.ids.sort()).toEqual(['sa-0', 'sa-1', 'sa-2', 'sa-3', 'sa-4'])
|
|
617
667
|
})
|
|
618
668
|
})
|
|
669
|
+
|
|
670
|
+
// ---------------------------------------------------------------------------
|
|
671
|
+
// listNonTerminalSubagentsForTurn — the boot-resume accessor
|
|
672
|
+
// ---------------------------------------------------------------------------
|
|
673
|
+
|
|
674
|
+
describe('listNonTerminalSubagentsForTurn', () => {
|
|
675
|
+
it('returns running + stalled workers of a turn, excluding terminals, ordered by started_at', () => {
|
|
676
|
+
const db = openFreshSubagentsDbInMemory()
|
|
677
|
+
const turn = 'chat-1:7'
|
|
678
|
+
recordSubagentStart(db, { id: 'sa-run', parentTurnKey: turn, agentType: 'worker', description: 'refactor auth', background: true, startedAt: 1000 })
|
|
679
|
+
recordSubagentStart(db, { id: 'sa-stall', parentTurnKey: turn, agentType: 'researcher', description: 'survey competitors', background: true, startedAt: 2000 })
|
|
680
|
+
recordSubagentStall(db, { id: 'sa-stall', stalledAt: 3000 })
|
|
681
|
+
recordSubagentStart(db, { id: 'sa-done', parentTurnKey: turn, agentType: 'worker', description: 'done work', background: true, startedAt: 500 })
|
|
682
|
+
recordSubagentEnd(db, { id: 'sa-done', endedAt: 4000, status: 'completed' })
|
|
683
|
+
recordSubagentStart(db, { id: 'sa-fail', parentTurnKey: turn, background: true, startedAt: 600 })
|
|
684
|
+
recordSubagentEnd(db, { id: 'sa-fail', endedAt: 4100, status: 'failed' })
|
|
685
|
+
|
|
686
|
+
const rows = listNonTerminalSubagentsForTurn(db, turn)
|
|
687
|
+
expect(rows.map((r) => r.id)).toEqual(['sa-run', 'sa-stall']) // started_at ASC, terminals excluded
|
|
688
|
+
expect(rows[0].agent_type).toBe('worker')
|
|
689
|
+
expect(rows[0].description).toBe('refactor auth')
|
|
690
|
+
expect(rows[1].status).toBe('stalled')
|
|
691
|
+
db.close()
|
|
692
|
+
})
|
|
693
|
+
|
|
694
|
+
it('does not return workers of a different turn', () => {
|
|
695
|
+
const db = openFreshSubagentsDbInMemory()
|
|
696
|
+
recordSubagentStart(db, { id: 'sa-a', parentTurnKey: 'chat:1', background: true, startedAt: 1000 })
|
|
697
|
+
recordSubagentStart(db, { id: 'sa-b', parentTurnKey: 'chat:2', background: true, startedAt: 1000 })
|
|
698
|
+
const rows = listNonTerminalSubagentsForTurn(db, 'chat:1')
|
|
699
|
+
expect(rows.map((r) => r.id)).toEqual(['sa-a'])
|
|
700
|
+
db.close()
|
|
701
|
+
})
|
|
702
|
+
|
|
703
|
+
it('returns empty for a turn with no in-flight workers', () => {
|
|
704
|
+
const db = openFreshSubagentsDbInMemory()
|
|
705
|
+
expect(listNonTerminalSubagentsForTurn(db, 'nope:0')).toEqual([])
|
|
706
|
+
db.close()
|
|
707
|
+
})
|
|
708
|
+
})
|
|
@@ -42,6 +42,7 @@ function isMultiAgentEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
|
|
|
42
42
|
}
|
|
43
43
|
import { classifyClaudeError, type OperatorEventKind } from './operator-events.js'
|
|
44
44
|
import { createToolLabelSidecar, type ToolLabelSidecar } from './tool-label-sidecar.js'
|
|
45
|
+
import { isModelSentinel } from './model-label.js'
|
|
45
46
|
|
|
46
47
|
/** Match Claude Code's cli.js VX() function. */
|
|
47
48
|
export function sanitizeCwdToProjectName(cwd: string): string {
|
|
@@ -92,6 +93,13 @@ export type SessionEvent =
|
|
|
92
93
|
| { kind: 'enqueue'; chatId: string | null; messageId: string | null; threadId: string | null; rawContent: string; isSync?: boolean }
|
|
93
94
|
| { kind: 'dequeue' }
|
|
94
95
|
| { kind: 'thinking' }
|
|
96
|
+
// Live model in use for the MAIN session, extracted from `message.model` on
|
|
97
|
+
// each `type:"assistant"` transcript line (the exact model that served that
|
|
98
|
+
// API call). Emitted FIRST among a line's events so a card rendered on the
|
|
99
|
+
// same batch already reflects the current model. Sentinels (`<synthetic>` on
|
|
100
|
+
// compaction lines, fixture junk) are filtered at projection — see
|
|
101
|
+
// isModelSentinel — so this only ever carries a real resolved model id.
|
|
102
|
+
| { kind: 'model'; model: string }
|
|
95
103
|
| { kind: 'tool_use'; toolName: string; toolUseId?: string | null; input?: Record<string, unknown>; precomputedLabel?: string }
|
|
96
104
|
// Real-time tool label from the PreToolUse-hook sidecar — fires when the
|
|
97
105
|
// hook writes the label (synchronous at tool-call time), independent of
|
|
@@ -115,6 +123,11 @@ export type SessionEvent =
|
|
|
115
123
|
// filename stem (e.g. "aac6f1…"). Routed through the same ingest path
|
|
116
124
|
// as parent events; the reducer fans them out to per-sub-agent state.
|
|
117
125
|
| { kind: 'sub_agent_started'; agentId: string; firstPromptText: string; subagentType?: string }
|
|
126
|
+
// Live model in use for a SUB-AGENT, extracted from `message.model` on each
|
|
127
|
+
// of its `type:"assistant"` transcript lines. Same contract as the main
|
|
128
|
+
// `model` kind (sentinel-filtered, emitted first) but agent-scoped so the
|
|
129
|
+
// watcher can track it per WorkerEntry and thread it onto the worker card.
|
|
130
|
+
| { kind: 'sub_agent_model'; agentId: string; model: string }
|
|
118
131
|
| { kind: 'sub_agent_tool_use'; agentId: string; toolUseId: string | null; toolName: string; input?: Record<string, unknown>; precomputedLabel?: string }
|
|
119
132
|
// Same shared contract as the main-agent `text` kind — see its doc above
|
|
120
133
|
// (including the `lastInMessage` projection-artifact note). The wire-kind
|
|
@@ -320,6 +333,15 @@ export function projectTranscriptLine(line: string): SessionEvent[] {
|
|
|
320
333
|
const content = message?.content as Array<Record<string, unknown>> | undefined
|
|
321
334
|
if (!Array.isArray(content)) return []
|
|
322
335
|
const events: SessionEvent[] = []
|
|
336
|
+
// Live model capture: `message.model` is the exact resolved model that
|
|
337
|
+
// served THIS assistant API call. Emit it FIRST (before the content events)
|
|
338
|
+
// so a card rendered on the same read batch already carries the current
|
|
339
|
+
// model. Sentinels (`<synthetic>` on compaction lines, fixture junk) are
|
|
340
|
+
// skipped — the reducer keeps the last real value.
|
|
341
|
+
const mainModel = message?.model
|
|
342
|
+
if (typeof mainModel === 'string' && !isModelSentinel(mainModel)) {
|
|
343
|
+
events.push({ kind: 'model', model: mainModel })
|
|
344
|
+
}
|
|
323
345
|
// Text→narrative projection comes from the ONE shared kernel
|
|
324
346
|
// (projectAssistantTextBlocks): it owns the empty-drop + blockIndex +
|
|
325
347
|
// lastInMessage contract. We emit its events at their source positions
|
|
@@ -468,6 +490,12 @@ export function projectSubagentLine(
|
|
|
468
490
|
const content = message?.content as Array<Record<string, unknown>> | undefined
|
|
469
491
|
if (!Array.isArray(content)) return []
|
|
470
492
|
const events: SessionEvent[] = []
|
|
493
|
+
// Live model capture (sub-agent tier): same contract as the main-agent
|
|
494
|
+
// branch — emit the sub-agent's resolved model first, sentinel-filtered.
|
|
495
|
+
const subModel = message?.model
|
|
496
|
+
if (typeof subModel === 'string' && !isModelSentinel(subModel)) {
|
|
497
|
+
events.push({ kind: 'sub_agent_model', agentId, model: subModel })
|
|
498
|
+
}
|
|
471
499
|
// Text→narrative projection comes from the SAME shared kernel as the
|
|
472
500
|
// main agent (projectAssistantTextBlocks): one source for the empty-drop
|
|
473
501
|
// + blockIndex + lastInMessage contract. The `make` adapter only changes
|
|
@@ -60,6 +60,8 @@ export interface SilentEndDeps {
|
|
|
60
60
|
* never suppress on doubt.
|
|
61
61
|
*/
|
|
62
62
|
hasOutboundDeliveredSince?: (chatId: string, sinceMs: number, threadId?: number | null) => boolean
|
|
63
|
+
/** Wall-clock now, ms. Defaults to `Date.now`; injectable for tests. */
|
|
64
|
+
now?: () => number
|
|
63
65
|
}
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -80,6 +82,31 @@ export interface SilentEndDeps {
|
|
|
80
82
|
*/
|
|
81
83
|
export const SILENT_END_MAX_RETRIES = 2
|
|
82
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Fix #8 (adversarial-validation progress-card fix pass): `turnKey` is the
|
|
87
|
+
* STABLE `statusKey(chatId, threadId)` — it never changes across turns on
|
|
88
|
+
* the same chat/thread — so `writeSilentEndState`'s "inherit retryCount IFF
|
|
89
|
+
* turnKey matches" rule has no per-turn discriminator on its own. Normally
|
|
90
|
+
* that's fine because `clearSilentEndState` always fires the moment a reply
|
|
91
|
+
* lands, wiping the exhausted record before the next turn starts. But if the
|
|
92
|
+
* gateway's own exhaust-read-and-clear is bypassed (crash/interrupt mid-turn,
|
|
93
|
+
* `gateway.ts` ~turn_end handler never runs), an exhausted
|
|
94
|
+
* (`retryCount >= SILENT_END_MAX_RETRIES`) record can survive into a LATER,
|
|
95
|
+
* unrelated turn. That later turn's first silent-end would then misread the
|
|
96
|
+
* old record as "this turn already exhausted its ladder" and skip its own
|
|
97
|
+
* re-prompt entirely.
|
|
98
|
+
*
|
|
99
|
+
* A record legitimately re-written within one turn's own retry ladder is
|
|
100
|
+
* always fresh (each Stop-hook block re-writes `timestamp`), so bounding by
|
|
101
|
+
* age is a safe, cheap staleness signal that doesn't require a new identity
|
|
102
|
+
* field: any record older than a turn's plausible lifetime could not still
|
|
103
|
+
* be "this turn's" in-flight retry state, and is therefore stale-carryover,
|
|
104
|
+
* not an active ladder. 30 minutes comfortably exceeds any real single-turn
|
|
105
|
+
* Stop-hook retry cycle (seconds, not minutes) while staying well inside the
|
|
106
|
+
* fleet's other staleness bounds (e.g. the 3h boot-resume window).
|
|
107
|
+
*/
|
|
108
|
+
export const SILENT_END_STALE_RECORD_MAX_AGE_MS = 30 * 60_000
|
|
109
|
+
|
|
83
110
|
/**
|
|
84
111
|
* User-facing fallback text delivered when a user-message turn ends with no
|
|
85
112
|
* final answer AND the deterministic Stop-hook re-prompt has already been
|
|
@@ -313,12 +340,30 @@ export function recordSilentTurnEnd(
|
|
|
313
340
|
// If so, the record is satisfied-but-misdetected — drop it silently
|
|
314
341
|
// and let this dark turn start its OWN fresh retry cycle instead of
|
|
315
342
|
// inheriting someone else's spent budget.
|
|
316
|
-
|
|
343
|
+
// Fix #8: age-based staleness bound. `turnKey` has no per-turn nonce
|
|
344
|
+
// (see SILENT_END_STALE_RECORD_MAX_AGE_MS doc), so a record whose
|
|
345
|
+
// `timestamp` is older than a turn's plausible lifetime cannot belong to
|
|
346
|
+
// THIS dark turn's own retry ladder even absent any delivery evidence —
|
|
347
|
+
// it's carryover from a prior turn whose gateway-side clear was
|
|
348
|
+
// bypassed (crash/interrupt). Check this before (and independent of)
|
|
349
|
+
// the delivery-based check below so a bypassed-clear record is caught
|
|
350
|
+
// even when no reply was ever delivered on the chat/thread at all.
|
|
351
|
+
const now = deps?.now?.() ?? Date.now()
|
|
352
|
+
const staleByAge = now - prev.timestamp > SILENT_END_STALE_RECORD_MAX_AGE_MS
|
|
353
|
+
if (
|
|
354
|
+
staleByAge ||
|
|
355
|
+
deps?.hasOutboundDeliveredSince?.(args.chatId, prev.timestamp, args.threadId)
|
|
356
|
+
) {
|
|
317
357
|
emitLog(
|
|
318
358
|
deps,
|
|
319
|
-
|
|
320
|
-
`
|
|
321
|
-
|
|
359
|
+
staleByAge
|
|
360
|
+
? `silent-end: stale exhausted record for turnKey=${args.turnKey} ` +
|
|
361
|
+
`(retryCount=${prev.retryCount}, age=${now - prev.timestamp}ms > ` +
|
|
362
|
+
`${SILENT_END_STALE_RECORD_MAX_AGE_MS}ms) — treating as carryover ` +
|
|
363
|
+
`from a prior turn, not exhausted\n`
|
|
364
|
+
: `silent-end: stale exhausted record for turnKey=${args.turnKey} ` +
|
|
365
|
+
`(retryCount=${prev.retryCount}) but a reply was delivered since ` +
|
|
366
|
+
`${prev.timestamp} — treating as satisfied-but-misdetected, not exhausted\n`,
|
|
322
367
|
)
|
|
323
368
|
// MUST clear before writing (adversarial review of #2892):
|
|
324
369
|
// writeSilentEndState re-inherits retryCount whenever the on-disk
|