switchroom 0.14.91 → 0.14.92
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 +1030 -56
- package/dist/auth-broker/index.js +50 -3
- package/dist/cli/notion-write-pretool.mjs +50 -3
- package/dist/cli/switchroom.js +306 -21
- package/dist/host-control/main.js +50 -3
- package/dist/vault/approvals/kernel-server.js +51 -4
- package/dist/vault/broker/server.js +51 -4
- package/package.json +1 -1
- package/profiles/_base/cron-session.sh.hbs +77 -0
- package/profiles/_base/start.sh.hbs +13 -0
- package/telegram-plugin/dist/gateway/gateway.js +95 -15
- package/telegram-plugin/gateway/cron-session.ts +45 -0
- package/telegram-plugin/gateway/gateway.ts +52 -6
- package/telegram-plugin/tests/cron-session.test.ts +32 -0
|
@@ -287,6 +287,7 @@ import { handleRequestDriveApproval } from './drive-write-approval.js'
|
|
|
287
287
|
import { handleRequestMs365Approval } from './ms365-write-approval.js'
|
|
288
288
|
import { buildDiffPreviewCard } from './diff-preview-card.js'
|
|
289
289
|
import { createPendingInboundBuffer, redeliverBufferedInbound, idleDrainTick } from './pending-inbound-buffer.js'
|
|
290
|
+
import { isCronIdentity, resolveInjectTarget } from './cron-session.js'
|
|
290
291
|
import {
|
|
291
292
|
ObligationLedger,
|
|
292
293
|
buildObligationRepresentInbound,
|
|
@@ -5474,6 +5475,19 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
5474
5475
|
|
|
5475
5476
|
onClientRegistered(client: IpcClient) {
|
|
5476
5477
|
process.stderr.write(`telegram gateway: bridge registered — agent=${client.agentName}\n`)
|
|
5478
|
+
// Cheap-cron (§2.4/§3.3): a `<agent>-cron` bridge is the Tier-1 cheap
|
|
5479
|
+
// session. It is STATUS-SILENT — it must NOT drive the gateway's
|
|
5480
|
+
// singleton machinery (shadow bridge-state, warmup, boot card, which all
|
|
5481
|
+
// track the MAIN agent's liveness). Drain any buffered cron fire to it
|
|
5482
|
+
// (so a fire that triggered a lazy spawn lands), then return early.
|
|
5483
|
+
if (isCronIdentity(client.agentName)) {
|
|
5484
|
+
client.send({ type: 'status', status: 'agent_connected' })
|
|
5485
|
+
const pending = pendingInboundBuffer.drain(client.agentName ?? '')
|
|
5486
|
+
for (const m of pending) {
|
|
5487
|
+
try { client.send(m) } catch { /* cron fire drop — best-effort, like today's cron */ }
|
|
5488
|
+
}
|
|
5489
|
+
return
|
|
5490
|
+
}
|
|
5477
5491
|
// Phase 2b shadow: ONLY emit bridgeUp for the REAL bridge sidecar
|
|
5478
5492
|
// (with an agent name). Anonymous IPC clients (recall.py, mcp
|
|
5479
5493
|
// handshakes, etc.) connect briefly without a name and would
|
|
@@ -5652,6 +5666,17 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
5652
5666
|
},
|
|
5653
5667
|
|
|
5654
5668
|
onClientDisconnected(client: IpcClient) {
|
|
5669
|
+
// Cheap-cron §2.4: a `<agent>-cron` bridge is the status-silent cron
|
|
5670
|
+
// session. Its disconnect (e.g. B3 lazy idle-teardown) must NOT touch
|
|
5671
|
+
// the MAIN agent's singleton state — emitting bridgeDown (the shadow
|
|
5672
|
+
// state is unkeyed) or flushOnAgentDisconnect (flushes the main agent's
|
|
5673
|
+
// active reactions / disposes its progress driver mid-turn) would be the
|
|
5674
|
+
// "premature 👍" bug for the main session. Symmetric to the cron gate in
|
|
5675
|
+
// onClientRegistered / onSessionEvent / onPtyPartial.
|
|
5676
|
+
if (isCronIdentity(client.agentName)) {
|
|
5677
|
+
process.stderr.write(`telegram gateway: cron-session bridge disconnected — agent=${client.agentName}\n`)
|
|
5678
|
+
return
|
|
5679
|
+
}
|
|
5655
5680
|
// ONLY log "bridge disconnected" + emit bridgeDown for the REAL
|
|
5656
5681
|
// bridge sidecar (matching the bridgeUp gate above). Anonymous IPC
|
|
5657
5682
|
// clients — e.g. recall.py's one-shot legacy update_placeholder
|
|
@@ -5722,7 +5747,13 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
5722
5747
|
}
|
|
5723
5748
|
},
|
|
5724
5749
|
|
|
5725
|
-
onSessionEvent(
|
|
5750
|
+
onSessionEvent(client: IpcClient, msg: SessionEventForward) {
|
|
5751
|
+
// Cheap-cron §2.4: the cron session is status-silent — its session
|
|
5752
|
+
// events must not drive the main agent's progress card, transcript
|
|
5753
|
+
// tail, currentTurn, or silence-poke. Its reply still flows (onToolCall
|
|
5754
|
+
// is not gated). currentTurn was never set for a cron fire (onInjectInbound
|
|
5755
|
+
// skips markClaudeBusy), so the card machinery has nothing to attach to.
|
|
5756
|
+
if (isCronIdentity(client.agentName)) return
|
|
5726
5757
|
// Track the session-tail's attached file for the proactive-
|
|
5727
5758
|
// compaction occupancy read (see maybeProactiveCompact).
|
|
5728
5759
|
if (msg.activeFile) lastSessionActiveFile = msg.activeFile
|
|
@@ -5954,7 +5985,10 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
5954
5985
|
* `handlePtyPartial` does its own buffering for the
|
|
5955
5986
|
* partial-before-enqueue race.
|
|
5956
5987
|
*/
|
|
5957
|
-
onPtyPartial(
|
|
5988
|
+
onPtyPartial(client: IpcClient, msg: PtyPartialForward) {
|
|
5989
|
+
// Cheap-cron §2.4: cron session is status-silent — no visible reply
|
|
5990
|
+
// stream from its PTY tail (it would edit a card the main session owns).
|
|
5991
|
+
if (isCronIdentity(client.agentName)) return
|
|
5958
5992
|
handlePtyPartial(msg.text)
|
|
5959
5993
|
},
|
|
5960
5994
|
|
|
@@ -6291,16 +6325,28 @@ const ipcServer: IpcServer = createIpcServer({
|
|
|
6291
6325
|
const source = typeof msg.inbound.meta?.source === 'string'
|
|
6292
6326
|
? msg.inbound.meta.source
|
|
6293
6327
|
: 'unknown'
|
|
6294
|
-
|
|
6295
|
-
|
|
6328
|
+
// Cheap-cron (docs/rfcs/cheap-cron-sessions.md §3.3): a Tier-1 fire
|
|
6329
|
+
// carries meta.session='cron' → route to the derived `<agent>-cron`
|
|
6330
|
+
// bridge (a 2nd interactive Sonnet session in the same container).
|
|
6331
|
+
// Every other fire (and all of today's callers) routes to the agent
|
|
6332
|
+
// unchanged. Route+buffer share the same target so a fire that lands
|
|
6333
|
+
// mid cron-session-spawn buffers under the cron identity and drains to
|
|
6334
|
+
// it on register.
|
|
6335
|
+
const target = resolveInjectTarget(msg.agentName, msg.inbound.meta)
|
|
6336
|
+
const toCron = target !== msg.agentName
|
|
6337
|
+
const delivered = ipcServer.sendToAgent(target, msg.inbound)
|
|
6338
|
+
// Status-silent (§2.4): a cron fire must NOT set the MAIN agent's
|
|
6339
|
+
// currentTurn (progress card / silence-poke). The cron session is
|
|
6340
|
+
// fire-and-forget; its reply is its only Telegram surface.
|
|
6341
|
+
if (delivered && !toCron) markClaudeBusyForInbound(msg.inbound)
|
|
6296
6342
|
process.stderr.write(
|
|
6297
|
-
`telegram gateway: inject_inbound agent=${msg.agentName} source=${source} prompt_key=${promptKey} delivered=${delivered}\n`,
|
|
6343
|
+
`telegram gateway: inject_inbound agent=${msg.agentName} target=${target} source=${source} prompt_key=${promptKey} delivered=${delivered}\n`,
|
|
6298
6344
|
)
|
|
6299
6345
|
// #1150: same buffer-on-failure pattern as vault_grant_approved.
|
|
6300
6346
|
// Cron fires use this path too — if a cron-driven wake-up lands
|
|
6301
6347
|
// mid bridge-reconnect, buffer it for the next register.
|
|
6302
6348
|
if (!delivered) {
|
|
6303
|
-
pendingInboundBuffer.push(
|
|
6349
|
+
pendingInboundBuffer.push(target, msg.inbound)
|
|
6304
6350
|
}
|
|
6305
6351
|
},
|
|
6306
6352
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
CRON_IDENTITY_SUFFIX,
|
|
4
|
+
baseAgent,
|
|
5
|
+
cronIdentity,
|
|
6
|
+
isCronIdentity,
|
|
7
|
+
resolveInjectTarget,
|
|
8
|
+
} from '../gateway/cron-session.js'
|
|
9
|
+
|
|
10
|
+
describe('cron-session identity helpers', () => {
|
|
11
|
+
it('derives and detects the cron identity', () => {
|
|
12
|
+
expect(cronIdentity('clerk')).toBe(`clerk${CRON_IDENTITY_SUFFIX}`)
|
|
13
|
+
expect(isCronIdentity('clerk-cron')).toBe(true)
|
|
14
|
+
expect(isCronIdentity('clerk')).toBe(false)
|
|
15
|
+
expect(isCronIdentity(null)).toBe(false)
|
|
16
|
+
expect(isCronIdentity(undefined)).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('round-trips base agent', () => {
|
|
20
|
+
expect(baseAgent(cronIdentity('marko'))).toBe('marko')
|
|
21
|
+
expect(baseAgent('marko')).toBe('marko')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('resolveInjectTarget routes only meta.session=cron to the cron bridge', () => {
|
|
25
|
+
expect(resolveInjectTarget('clerk', { session: 'cron', source: 'cron' })).toBe('clerk-cron')
|
|
26
|
+
expect(resolveInjectTarget('clerk', { session: 'main', source: 'cron' })).toBe('clerk')
|
|
27
|
+
expect(resolveInjectTarget('clerk', { source: 'cron' })).toBe('clerk')
|
|
28
|
+
expect(resolveInjectTarget('clerk', undefined)).toBe('clerk')
|
|
29
|
+
// back-compat: every legacy caller (no session) is unchanged.
|
|
30
|
+
expect(resolveInjectTarget('clerk', { source: 'telegram' })).toBe('clerk')
|
|
31
|
+
})
|
|
32
|
+
})
|