switchroom 0.18.3 → 0.18.6
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 +2 -1
- package/dist/auth-broker/index.js +2 -1
- package/dist/cli/notion-write-pretool.mjs +2 -1
- package/dist/cli/switchroom.js +119 -74
- package/dist/host-control/main.js +3 -2
- package/dist/vault/approvals/kernel-server.js +2 -1
- package/dist/vault/broker/server.js +37 -8
- package/package.json +1 -1
- package/skills/switchroom-release/SKILL.md +78 -0
- package/telegram-plugin/auth-snapshot-format.ts +15 -1
- package/telegram-plugin/dist/gateway/gateway.js +922 -459
- package/telegram-plugin/gateway/gateway.ts +260 -21
- package/telegram-plugin/gateway/resolve-person.ts +304 -0
- package/telegram-plugin/gateway/unhandled-rejection-policy.ts +21 -1
- package/telegram-plugin/hooks/silent-end-scan.mjs +164 -40
- package/telegram-plugin/operator-events.ts +21 -0
- package/telegram-plugin/pending-work-progress.ts +42 -7
- package/telegram-plugin/quota-bar-format.ts +360 -0
- package/telegram-plugin/subagent-watcher.ts +27 -9
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +21 -0
- package/telegram-plugin/tests/gateway-boot-marker-clear.test.ts +3 -3
- package/telegram-plugin/tests/operator-events.test.ts +16 -0
- package/telegram-plugin/tests/pending-work-progress.test.ts +116 -3
- 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/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/subagent-watcher.test.ts +61 -0
- package/telegram-plugin/tests/unhandled-rejection-policy.test.ts +19 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +97 -0
- package/telegram-plugin/worker-activity-feed.ts +220 -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
|
@@ -945,6 +945,103 @@ describe('createWorkerActivityFeed — heartbeat', () => {
|
|
|
945
945
|
})
|
|
946
946
|
})
|
|
947
947
|
|
|
948
|
+
// ─── Resurrection guard + deferred finalize (review findings #2 & #3) ─────────
|
|
949
|
+
// #3: a late watcher onProgress tick arriving after `finish()` queued its
|
|
950
|
+
// chain must NOT resurrect the handle and paint a fresh running message.
|
|
951
|
+
// #2: a terminal edit that hit a 429 cooldown stages on `pendingFinish` and
|
|
952
|
+
// is re-driven by the heartbeat after cooldown — the card can't get stuck
|
|
953
|
+
// on its last running render. "not modified" / message-gone are success/drop.
|
|
954
|
+
describe('createWorkerActivityFeed — resurrection guard + deferred finalize', () => {
|
|
955
|
+
const drain = () => new Promise((r) => setTimeout(r, 0))
|
|
956
|
+
|
|
957
|
+
it('#3: a late update tick after finish does not resurrect a running card', async () => {
|
|
958
|
+
const bot = makeFakeBot()
|
|
959
|
+
let clock = 10_000
|
|
960
|
+
const feed = createWorkerActivityFeed({ bot, now: () => clock, minEditIntervalMs: 0 })
|
|
961
|
+
await feed.update('w1', 'chat', view({ toolCount: 1, latestSummary: 'step one' }))
|
|
962
|
+
expect(bot.sent).toHaveLength(1)
|
|
963
|
+
|
|
964
|
+
await feed.finish('w1', view({ state: 'done', toolCount: 1, latestSummary: 'done result' }))
|
|
965
|
+
// finish landed a terminal edit and dropped the handle.
|
|
966
|
+
expect(bot.edits.some((e) => e.text.includes('_done ·'))).toBe(true)
|
|
967
|
+
expect(feed.has('w1')).toBe(false)
|
|
968
|
+
|
|
969
|
+
// Late watcher tick arrives AFTER finish. Pre-fix this would create a
|
|
970
|
+
// fresh handle and paint a new running message on a finalized worker.
|
|
971
|
+
clock = 12_000
|
|
972
|
+
await feed.update('w1', 'chat', view({ toolCount: 2, latestSummary: 'step two' }))
|
|
973
|
+
expect(bot.sent).toHaveLength(1) // no new running message
|
|
974
|
+
expect(feed.has('w1')).toBe(false)
|
|
975
|
+
expect(feed.size).toBe(0)
|
|
976
|
+
})
|
|
977
|
+
|
|
978
|
+
it('#2: a 429 on the finish edit stages pendingFinish; the heartbeat re-drives it after cooldown', async () => {
|
|
979
|
+
const bot = makeFakeBot()
|
|
980
|
+
let clock = 10_000
|
|
981
|
+
const feed = createWorkerActivityFeed({
|
|
982
|
+
bot,
|
|
983
|
+
now: () => clock,
|
|
984
|
+
minEditIntervalMs: 0,
|
|
985
|
+
heartbeatTickMs: 6000,
|
|
986
|
+
setInterval: () => 1,
|
|
987
|
+
clearInterval: () => {},
|
|
988
|
+
})
|
|
989
|
+
await feed.update('w1', 'chat', view({ toolCount: 1, latestSummary: 'running step' }))
|
|
990
|
+
expect(bot.sent).toHaveLength(1)
|
|
991
|
+
|
|
992
|
+
// The terminal edit hits a 429 with a 2s retry_after.
|
|
993
|
+
bot.failNextEditWith = { error_code: 429, parameters: { retry_after: 2 } }
|
|
994
|
+
await feed.finish('w1', view({ state: 'done', toolCount: 1, latestSummary: 'final result' }))
|
|
995
|
+
await drain()
|
|
996
|
+
// No terminal edit landed yet — the last edit is still the running render.
|
|
997
|
+
expect(bot.edits.some((e) => e.text.includes('_done ·'))).toBe(false)
|
|
998
|
+
|
|
999
|
+
// Inside the cooldown — a heartbeat tick must NOT retry (would re-429).
|
|
1000
|
+
clock = 11_000
|
|
1001
|
+
feed.heartbeatTick()
|
|
1002
|
+
await drain()
|
|
1003
|
+
expect(bot.edits.some((e) => e.text.includes('_done ·'))).toBe(false)
|
|
1004
|
+
|
|
1005
|
+
// Past the cooldown (10_000 + 2000 + 500 jitter = 12_500) — the heartbeat
|
|
1006
|
+
// re-drives the deferred finalize and the terminal edit lands.
|
|
1007
|
+
clock = 13_000
|
|
1008
|
+
feed.heartbeatTick()
|
|
1009
|
+
await drain()
|
|
1010
|
+
expect(bot.edits.some((e) => e.text.includes('_done ·'))).toBe(true)
|
|
1011
|
+
expect(feed.has('w1')).toBe(false) // handle dropped after the terminal edit landed
|
|
1012
|
+
})
|
|
1013
|
+
|
|
1014
|
+
it('#2: "message is not modified" on finish is treated as success (card already correct)', async () => {
|
|
1015
|
+
const bot = makeFakeBot()
|
|
1016
|
+
let clock = 10_000
|
|
1017
|
+
const feed = createWorkerActivityFeed({ bot, now: () => clock, minEditIntervalMs: 0 })
|
|
1018
|
+
await feed.update('w1', 'chat', view({ toolCount: 1, latestSummary: 'x' }))
|
|
1019
|
+
expect(bot.sent).toHaveLength(1)
|
|
1020
|
+
|
|
1021
|
+
bot.failNextEditWith = new Error('Bad Request: message is not modified')
|
|
1022
|
+
await feed.finish('w1', view({ state: 'done', toolCount: 1, latestSummary: 'x' }))
|
|
1023
|
+
await drain()
|
|
1024
|
+
// The handle is dropped — the not-modified outcome is success, no retry.
|
|
1025
|
+
expect(feed.has('w1')).toBe(false)
|
|
1026
|
+
})
|
|
1027
|
+
|
|
1028
|
+
it('#2: a gone message on finish drops the handle silently (no card to finalize)', async () => {
|
|
1029
|
+
const bot = makeFakeBot()
|
|
1030
|
+
const logs: string[] = []
|
|
1031
|
+
let clock = 10_000
|
|
1032
|
+
const feed = createWorkerActivityFeed({ bot, now: () => clock, minEditIntervalMs: 0, log: (m) => logs.push(m) })
|
|
1033
|
+
await feed.update('w1', 'chat', view({ toolCount: 1, latestSummary: 'x' }))
|
|
1034
|
+
expect(bot.sent).toHaveLength(1)
|
|
1035
|
+
|
|
1036
|
+
bot.failNextEditWith = new Error('Bad Request: message to edit not found')
|
|
1037
|
+
await feed.finish('w1', view({ state: 'done', toolCount: 1, latestSummary: 'done' }))
|
|
1038
|
+
await drain()
|
|
1039
|
+
expect(feed.has('w1')).toBe(false)
|
|
1040
|
+
// No scary "finish edit failed" warning for a transport-gone outcome.
|
|
1041
|
+
expect(logs.some((l) => l.includes('finish edit failed'))).toBe(false)
|
|
1042
|
+
})
|
|
1043
|
+
})
|
|
1044
|
+
|
|
948
1045
|
// ─── Extreme-edge: single oversized narrative line (no-truncate ON) ──────────
|
|
949
1046
|
// Reproduces the bug where accumulateNarrative's char-budget splice would push
|
|
950
1047
|
// the oversized line then immediately splice it out, making the narrative empty
|
|
@@ -220,6 +220,28 @@ interface WorkerHandle {
|
|
|
220
220
|
chain: Promise<void>
|
|
221
221
|
/** Last view rendered into the message (drives the heartbeat re-render). */
|
|
222
222
|
lastView: WorkerActivityView | null
|
|
223
|
+
/**
|
|
224
|
+
* A terminal (`finish`) view whose edit could not land yet — most often
|
|
225
|
+
* because a 429 cooldown was in effect when `doFinish` ran. The heartbeat
|
|
226
|
+
* re-drives `doFinish` with this view once the cooldown expires so a
|
|
227
|
+
* transport hiccup can't leave the card stuck on its last running render
|
|
228
|
+
* ("worker done, card says running"). Cleared on a successful terminal
|
|
229
|
+
* edit, on a permanent failure (message gone), or when the handle is
|
|
230
|
+
* deleted. Null when no finalize is pending.
|
|
231
|
+
*/
|
|
232
|
+
pendingFinish: WorkerActivityView | null
|
|
233
|
+
/**
|
|
234
|
+
* Latched in `doFinish` before the terminal edit. A late watcher
|
|
235
|
+
* `onProgress` tick that arrives after `finish()` queued its chain (but
|
|
236
|
+
* before the `.finally(handles.delete)` microtask drains) must NOT
|
|
237
|
+
* resurrect the handle and paint a fresh `running` message on an
|
|
238
|
+
* already-finalized worker. The heartbeat's orphan-paint guard
|
|
239
|
+
* (`if (!handles.has(h.agentId)) continue`) only covers the heartbeat
|
|
240
|
+
* tick — this flag covers the `update` entry point. Set synchronously
|
|
241
|
+
* inside `doFinish` (runs on the chain), checked synchronously in
|
|
242
|
+
* `update` before handle creation.
|
|
243
|
+
*/
|
|
244
|
+
finished: boolean
|
|
223
245
|
/**
|
|
224
246
|
* Wall-clock ms the worker was dispatched, derived from `now - view.elapsedMs`
|
|
225
247
|
* on the first update. The heartbeat computes a live elapsed from this so the
|
|
@@ -246,6 +268,47 @@ function extractRetryAfterSecs(err: unknown): number | null {
|
|
|
246
268
|
return null
|
|
247
269
|
}
|
|
248
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Classify a card-edit transport error. Card edits are a best-effort
|
|
273
|
+
* liveness surface — a recoverable hiccup must never freeze the card, and
|
|
274
|
+
* a permanent failure must never log a scary warning for something with
|
|
275
|
+
* nothing to update.
|
|
276
|
+
*
|
|
277
|
+
* 'not_modified' — content identical to what's already shown. The card
|
|
278
|
+
* already reads correctly; treat as SUCCESS (no retry, no warning).
|
|
279
|
+
* 'rate_limited' — 429 with retry_after. Back off; the heartbeat re-drives
|
|
280
|
+
* the edit after cooldown (running renders + deferred terminal edits).
|
|
281
|
+
* 'gone' — message/chat deleted or edit window expired. Nothing to
|
|
282
|
+
* update; drop the handle silently (no warning — there is no card).
|
|
283
|
+
* 'transient' — anything else (network blip, 5xx). Retry on the next
|
|
284
|
+
* heartbeat tick; don't spam stderr.
|
|
285
|
+
*/
|
|
286
|
+
type EditOutcome = 'not_modified' | 'rate_limited' | 'gone' | 'transient'
|
|
287
|
+
function classifyEditError(err: unknown): EditOutcome {
|
|
288
|
+
const retryAfter = extractRetryAfterSecs(err)
|
|
289
|
+
if (retryAfter != null) return 'rate_limited'
|
|
290
|
+
const desc =
|
|
291
|
+
err instanceof Error ? err.message : err != null && typeof err === 'object' && 'description' in err
|
|
292
|
+
? String((err as { description?: unknown }).description)
|
|
293
|
+
: String(err)
|
|
294
|
+
const low = desc.toLowerCase()
|
|
295
|
+
// "message is not modified" / "message was not modified" — Telegram's
|
|
296
|
+
// identical-content signal. The card already shows the right thing.
|
|
297
|
+
if (low.includes('not modified')) return 'not_modified'
|
|
298
|
+
// Message or chat no longer exists, or the edit window (48h) has closed.
|
|
299
|
+
// "message to edit not found" / "message to delete not found" / "chat not
|
|
300
|
+
// found" / "message can't be edited". Nothing to update.
|
|
301
|
+
if (
|
|
302
|
+
low.includes('not found') ||
|
|
303
|
+
low.includes("can't be edited") ||
|
|
304
|
+
low.includes('cannot be edited') ||
|
|
305
|
+
low.includes('not enough rights')
|
|
306
|
+
) {
|
|
307
|
+
return 'gone'
|
|
308
|
+
}
|
|
309
|
+
return 'transient'
|
|
310
|
+
}
|
|
311
|
+
|
|
249
312
|
/**
|
|
250
313
|
* Manager owning one live message per background worker. Keyed by jsonl
|
|
251
314
|
* agent id. The gateway calls `update` on each watcher activity cue and
|
|
@@ -294,6 +357,28 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
294
357
|
})
|
|
295
358
|
const clearIntervalFn = opts.clearInterval ?? ((handle: unknown) => clearInterval(handle as ReturnType<typeof setInterval>))
|
|
296
359
|
const handles = new Map<string, WorkerHandle>()
|
|
360
|
+
/**
|
|
361
|
+
* Agent ids that have been finalized (`doFinish` latched). Survives handle
|
|
362
|
+
* deletion so a LATE watcher `onProgress` tick — which can arrive after
|
|
363
|
+
* `finish()`'s chain has fully settled and the handle was deleted — cannot
|
|
364
|
+
* resurrect a fresh handle and paint a running card on a worker that is
|
|
365
|
+
* already done. The per-handle `finished` flag only covers the narrow
|
|
366
|
+
* window between latch and delete; this set is the durable gate. A late
|
|
367
|
+
* tick arrives within seconds of finish (watcher poll cadence), so the set
|
|
368
|
+
* only needs to cover recent finalizations — capped at FINALIZED_CAP and
|
|
369
|
+
* trimmed FIFO to stay bounded across a long gateway lifetime.
|
|
370
|
+
*/
|
|
371
|
+
const finalized = new Set<string>()
|
|
372
|
+
const FINALIZED_CAP = 256
|
|
373
|
+
function markFinalized(agentId: string): void {
|
|
374
|
+
if (finalized.has(agentId)) return
|
|
375
|
+
finalized.add(agentId)
|
|
376
|
+
if (finalized.size > FINALIZED_CAP) {
|
|
377
|
+
// Map-free FIFO trim: Set iterates in insertion order; drop the oldest.
|
|
378
|
+
const oldest = finalized.values().next().value
|
|
379
|
+
if (oldest != null) finalized.delete(oldest)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
297
382
|
let heartbeatTimer: unknown = null
|
|
298
383
|
|
|
299
384
|
function sendOptsFor(h: WorkerHandle): Record<string, unknown> {
|
|
@@ -384,39 +469,99 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
384
469
|
`thread=${h.threadId ?? '-'} msgId=${h.messageId} bytes=${body.length}`,
|
|
385
470
|
)
|
|
386
471
|
} catch (err) {
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
472
|
+
const outcome = classifyEditError(err)
|
|
473
|
+
if (outcome === 'rate_limited') {
|
|
474
|
+
noteRateLimited(h, err, 'edit')
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
if (outcome === 'not_modified') {
|
|
478
|
+
// Card already shows this body — record it as landed and move on.
|
|
479
|
+
h.lastBody = body
|
|
480
|
+
h.lastEditAt = nowFn()
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
if (outcome === 'gone') {
|
|
484
|
+
// Message/chat deleted or edit window closed — there is no card to
|
|
485
|
+
// update. Drop the handle silently; a fresh first-paint on the next
|
|
486
|
+
// running tick re-establishes one if the worker is still active. No
|
|
487
|
+
// warning: "no card" is not a liveness-logic error.
|
|
488
|
+
h.messageId = null
|
|
489
|
+
h.lastBody = null
|
|
490
|
+
return
|
|
491
|
+
}
|
|
492
|
+
// 'transient' — network blip / 5xx. Leave the handle intact; the
|
|
493
|
+
// heartbeat re-attempts on its next tick. Log at debug, not stderr-warn:
|
|
494
|
+
// a transport hiccup on a best-effort card is not "shit code", it's a
|
|
495
|
+
// retryable blip the framework rides out deterministically.
|
|
496
|
+
log(`worker-feed: edit transient error agent=${h.agentId}: ${(err as Error).message}`)
|
|
394
497
|
}
|
|
395
498
|
}
|
|
396
499
|
|
|
397
500
|
async function doFinish(h: WorkerHandle, view: WorkerActivityView): Promise<void> {
|
|
501
|
+
// Latch FIRST, before any early return. A `running`-cue tick arriving
|
|
502
|
+
// after `finish()` queued this chain (but before its `.finally(delete)`
|
|
503
|
+
// drains) would otherwise resurrect a handle via `update()` and paint a
|
|
504
|
+
// fresh running message on a finalized worker. Setting this synchronously
|
|
505
|
+
// on the chain — ahead of the cooldown/no-message guards — makes the
|
|
506
|
+
// gate in `update()` authoritative regardless of which guard path runs.
|
|
507
|
+
// The durable `finalized` set survives the subsequent handle deletion so
|
|
508
|
+
// a tick arriving AFTER the full settle still can't resurrect.
|
|
509
|
+
h.finished = true
|
|
510
|
+
markFinalized(h.agentId)
|
|
398
511
|
// No message ever posted → nothing to finalize. The worker's result
|
|
399
512
|
// reaches the user via the handback reply; a bare "done" recap with
|
|
400
513
|
// no preceding activity would be noise.
|
|
401
|
-
if (h.messageId == null)
|
|
514
|
+
if (h.messageId == null) {
|
|
515
|
+
h.pendingFinish = null
|
|
516
|
+
return
|
|
517
|
+
}
|
|
402
518
|
if (nowFn() < h.cooldownUntil) {
|
|
403
|
-
// Honour the flood-wait; a terminal edit isn't worth a ban.
|
|
404
|
-
//
|
|
519
|
+
// Honour the flood-wait; a terminal edit isn't worth a ban. But
|
|
520
|
+
// unlike the prior "stale but harmless" surrender, STAGE the terminal
|
|
521
|
+
// view so the heartbeat re-drives the finalize edit the instant the
|
|
522
|
+
// cooldown expires — a transport hiccup can no longer leave a
|
|
523
|
+
// finished worker's card stuck on its last running render.
|
|
524
|
+
h.pendingFinish = view
|
|
405
525
|
return
|
|
406
526
|
}
|
|
407
527
|
const body = renderWorkerActivity({ ...view, narrativeLines: h.narrative })
|
|
408
|
-
if (body === h.lastBody)
|
|
528
|
+
if (body === h.lastBody) {
|
|
529
|
+
h.pendingFinish = null
|
|
530
|
+
return
|
|
531
|
+
}
|
|
409
532
|
try {
|
|
410
533
|
await opts.bot.editMessageText(h.chatId, h.messageId, body, sendOptsFor(h))
|
|
411
534
|
h.lastBody = body
|
|
412
535
|
h.lastEditAt = nowFn()
|
|
536
|
+
h.pendingFinish = null
|
|
413
537
|
log(
|
|
414
538
|
`worker-feed: finish agent=${h.agentId} chat=${h.chatId} ` +
|
|
415
539
|
`thread=${h.threadId ?? '-'} msgId=${h.messageId} state=${view.state} bytes=${body.length}`,
|
|
416
540
|
)
|
|
417
541
|
} catch (err) {
|
|
418
|
-
|
|
419
|
-
|
|
542
|
+
const outcome = classifyEditError(err)
|
|
543
|
+
if (outcome === 'rate_limited') {
|
|
544
|
+
noteRateLimited(h, err, 'finish')
|
|
545
|
+
// Re-stage for the heartbeat to re-drive after cooldown.
|
|
546
|
+
h.pendingFinish = view
|
|
547
|
+
return
|
|
548
|
+
}
|
|
549
|
+
if (outcome === 'not_modified') {
|
|
550
|
+
// Card already shows the finalized body — terminal edit succeeded.
|
|
551
|
+
h.lastBody = body
|
|
552
|
+
h.lastEditAt = nowFn()
|
|
553
|
+
h.pendingFinish = null
|
|
554
|
+
return
|
|
555
|
+
}
|
|
556
|
+
if (outcome === 'gone') {
|
|
557
|
+
// Message/chat gone — no card to finalize. Drop silently; the
|
|
558
|
+
// handback reply carries the result regardless.
|
|
559
|
+
h.pendingFinish = null
|
|
560
|
+
return
|
|
561
|
+
}
|
|
562
|
+
// 'transient' — re-stage for a heartbeat retry; log at debug.
|
|
563
|
+
h.pendingFinish = view
|
|
564
|
+
log(`worker-feed: finish transient error agent=${h.agentId}: ${(err as Error).message}`)
|
|
420
565
|
}
|
|
421
566
|
}
|
|
422
567
|
|
|
@@ -456,6 +601,36 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
456
601
|
// (which would orphan a card that never finalizes). Restores the
|
|
457
602
|
// structural safety the pre-first-paint `messageId == null` skip gave.
|
|
458
603
|
if (!handles.has(h.agentId)) continue
|
|
604
|
+
|
|
605
|
+
// Deferred-finalize re-drive: a terminal edit that hit a 429 cooldown
|
|
606
|
+
// (or a transient error) was staged on `pendingFinish` by `doFinish`.
|
|
607
|
+
// Re-drive it once the cooldown has expired so a finished worker's card
|
|
608
|
+
// can't get stuck on its last running render. This is the deterministic
|
|
609
|
+
// backstop that replaces the old "stale but harmless" surrender — the
|
|
610
|
+
// framework owns ALIVE-and-done, wall-clock driven, no model in the loop.
|
|
611
|
+
// (The handle is still in the map because `finish()`'s `.finally(delete)`
|
|
612
|
+
// is chained AFTER `doFinish` and won't drain while a re-drive keeps the
|
|
613
|
+
// chain busy; once the terminal edit lands, `pendingFinish` is cleared
|
|
614
|
+
// and the `.finally` runs on the next chain settle.)
|
|
615
|
+
if (h.pendingFinish != null && now >= h.cooldownUntil) {
|
|
616
|
+
const view = h.pendingFinish
|
|
617
|
+
h.chain = h.chain
|
|
618
|
+
.then(() => doFinish(h, view))
|
|
619
|
+
.catch((err) => {
|
|
620
|
+
log(`worker-feed: heartbeat finalize re-drive error ${h.agentId}: ${(err as Error).message}`)
|
|
621
|
+
})
|
|
622
|
+
.finally(() => {
|
|
623
|
+
// Mirror `finish()`'s teardown: once the re-driven `doFinish`
|
|
624
|
+
// clears `pendingFinish` (terminal edit landed OR permanently
|
|
625
|
+
// failed), drop the handle. If it re-staged (another 429), the
|
|
626
|
+
// handle survives for the next heartbeat tick to retry.
|
|
627
|
+
if (handles.get(h.agentId)?.pendingFinish == null) {
|
|
628
|
+
handles.delete(h.agentId)
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
continue
|
|
632
|
+
}
|
|
633
|
+
|
|
459
634
|
if (h.lastView == null) continue
|
|
460
635
|
if (h.lastView.state !== 'running') continue
|
|
461
636
|
if (now < h.cooldownUntil) continue
|
|
@@ -523,7 +698,19 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
523
698
|
// No chat to post to (owner DM unconfigured) — don't create a
|
|
524
699
|
// handle that would retry a failing send('') every tick.
|
|
525
700
|
if (chatId.length === 0) return Promise.resolve()
|
|
526
|
-
|
|
701
|
+
// Resurrection guard: a worker that has already been finalized
|
|
702
|
+
// (`doFinish` latched `finalized`) must not get a fresh running cue.
|
|
703
|
+
// A late watcher `onProgress` tick can arrive after `finish()`'s chain
|
|
704
|
+
// has fully settled and the handle was deleted — without this durable
|
|
705
|
+
// gate the tick would create a brand-new handle and paint a fresh
|
|
706
|
+
// `running` message on an already-done worker (the card lies). The
|
|
707
|
+
// heartbeat's orphan-paint guard covers the heartbeat tick only; the
|
|
708
|
+
// per-handle `finished` flag covers the pre-delete window; this set
|
|
709
|
+
// covers the post-delete window.
|
|
710
|
+
if (finalized.has(agentId)) return Promise.resolve()
|
|
711
|
+
const existing = handles.get(agentId)
|
|
712
|
+
if (existing?.finished === true) return Promise.resolve()
|
|
713
|
+
let h = existing
|
|
527
714
|
if (h == null) {
|
|
528
715
|
h = {
|
|
529
716
|
agentId,
|
|
@@ -538,6 +725,8 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
538
725
|
lastView: null,
|
|
539
726
|
dispatchAtMs: null,
|
|
540
727
|
stepStartedAtMs: null,
|
|
728
|
+
finished: false,
|
|
729
|
+
pendingFinish: null,
|
|
541
730
|
}
|
|
542
731
|
handles.set(agentId, h)
|
|
543
732
|
}
|
|
@@ -556,11 +745,27 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
556
745
|
log(`worker-feed: finish chain error ${agentId}: ${(err as Error).message}`)
|
|
557
746
|
})
|
|
558
747
|
.finally(() => {
|
|
559
|
-
|
|
748
|
+
// Only tear down the handle once the terminal edit has actually
|
|
749
|
+
// landed (or permanently failed). If `doFinish` staged the edit on
|
|
750
|
+
// `pendingFinish` (a 429 cooldown / transient error was in effect),
|
|
751
|
+
// the handle must survive so the heartbeat can re-drive the
|
|
752
|
+
// finalize after cooldown. The heartbeat's re-drive chain ends by
|
|
753
|
+
// re-entering `doFinish`, which clears `pendingFinish` on success
|
|
754
|
+
// or permanent-failure — so this `.finally` deletes on the NEXT
|
|
755
|
+
// chain settle once there is nothing left to finalize. Without this
|
|
756
|
+
// guard, the `.finally` would delete the handle (and its staged
|
|
757
|
+
// pendingFinish) immediately after the first staged doFinish,
|
|
758
|
+
// stranding the card on its last running render.
|
|
759
|
+
if (handles.get(agentId)?.pendingFinish == null) {
|
|
760
|
+
handles.delete(agentId)
|
|
761
|
+
}
|
|
560
762
|
})
|
|
561
763
|
return h.chain
|
|
562
764
|
},
|
|
563
765
|
drop(agentId) {
|
|
766
|
+
// A dropped worker is also done — mark finalized so a late watcher
|
|
767
|
+
// tick can't resurrect a running card on it (same gate as `finish`).
|
|
768
|
+
markFinalized(agentId)
|
|
564
769
|
handles.delete(agentId)
|
|
565
770
|
},
|
|
566
771
|
heartbeatTick,
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# Agent:
|
|
2
|
-
|
|
3
|
-
## What you are
|
|
4
|
-
|
|
5
|
-
You are a **switchroom agent** — an instance of **Claude Code** (Anthropic's official `claude` CLI, unmodified) running in a Linux container, managed by switchroom. Your `$SWITCHROOM_AGENT_NAME` is ``. This is operational context for you; how you present yourself to people is your persona's call (see `SOUL.md`).
|
|
6
|
-
|
|
7
|
-
You are one of several agents here. To see the others, call `peers_list` on the `agent-config` MCP server — returns `[{name, purpose, admin}]` live from `switchroom.yaml`. **Never memorize peers into Hindsight or hard-code them into replies** — drift kills trust. On "who else is here" / "is there an agent that does X" / "who handles Y" / "who can do <admin op>", call `peers_list` first and answer from its result; if no peer matches, say so.
|
|
8
|
-
|
|
9
|
-
## Who you are
|
|
10
|
-
|
|
11
|
-
See `SOUL.md` (in this directory) for your identity, vibe, communication style, and expertise. That file is your persona source of truth.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## Core Behavior
|
|
15
|
-
- Respond helpfully, concisely, and conversationally.
|
|
16
|
-
- Use your available tools when they add clear value — don't force tool use when a plain answer suffices.
|
|
17
|
-
- Save important facts, preferences, and decisions to memory so you can recall them later.
|
|
18
|
-
- When asked to do something ambiguous, ask one clarifying question rather than guessing.
|
|
19
|
-
- If a task has multiple steps, outline your plan before executing.
|
|
20
|
-
|
|
21
|
-
## Safety
|
|
22
|
-
- Don't exfiltrate private data. Ever.
|
|
23
|
-
- Don't run destructive commands without asking.
|
|
24
|
-
- Prefer `trash` over `rm` when available (recoverable beats gone forever).
|
|
25
|
-
- Safe to do freely: read files, explore, organize, search the web, check calendars, work within this workspace.
|
|
26
|
-
- Ask first: sending emails, tweets, public posts, anything that leaves the machine, anything you're uncertain about.
|
|
27
|
-
- **Batch foreseeable approvals; don't drip surprises.** When you can already see that several actions will each need the user's approval, tell them up front which approvals are coming and why. Request independent ones together so they can decide once; for dependent ones (one's input comes from another), say what you're doing first and what approval comes next — a permission card should never arrive out of the blue.
|
|
28
|
-
- **A timed-out approval isn't a denial.** If a request came back denied only because the user was away (a timeout, not an explicit "no"), don't silently abandon it. When they're back, remind them it's still pending and re-offer it if they still want it.
|
|
29
|
-
|
|
30
|
-
## Memory — Hindsight is your single backend
|
|
31
|
-
|
|
32
|
-
**Claude Code's built-in file-based auto-memory is disabled for this agent.** Don't try to write `.md` files under `.claude/projects/.../memory/` or maintain a `MEMORY.md` index — that whole system is off. There's exactly one memory backend: **Hindsight**.
|
|
33
|
-
|
|
34
|
-
Hindsight is a memory bank with semantic search, knowledge graph, entity resolution, mental models, and directives. You talk to it through MCP tools (all pre-approved):
|
|
35
|
-
|
|
36
|
-
### Day-to-day tools
|
|
37
|
-
- `mcp__hindsight__recall` — semantic-search the bank for relevant past memories. Auto-fires on every inbound user message via the plugin's UserPromptSubmit hook (you'll see "Relevant memories from past conversations" in your context). Call manually when you need a more specific query than the auto-fired one.
|
|
38
|
-
- `mcp__hindsight__retain` — store a new memory. The plugin auto-retains every turn via the Stop hook, but in chunked mode each retain only processes a small recent window (~3 turns) — so it captures memory promptly and survives restarts without re-sending the whole transcript, and you usually don't need this. Call manually for significant decisions, corrections, or facts you want immediately searchable.
|
|
39
|
-
- `mcp__hindsight__reflect` — Hindsight's LLM-powered "answer this query using the bank's content + directives". Use when the user asks a question that requires synthesis across multiple past memories.
|
|
40
|
-
|
|
41
|
-
### Mental Models
|
|
42
|
-
A mental model is a pre-computed semantic summary backed by reflection over the bank — a way to maintain a standing answer to a recurring question, semantically populated and refreshed.
|
|
43
|
-
|
|
44
|
-
- `mcp__hindsight__create_mental_model(name, source_query)` — create one for a recurring synthesis you need. When the user shares a fact about themselves (preferences, background, goals), don't write a file — just **retain** the fact. You do NOT need to build or maintain a per-agent "user profile": who the user is lives in dedicated per-user profile banks that the operator curates out-of-band, and recall surfaces it automatically.
|
|
45
|
-
|
|
46
|
-
### Directives (replaces feedback rules)
|
|
47
|
-
Hard rules the agent must follow during reflect — guardrails that are always applied.
|
|
48
|
-
|
|
49
|
-
- `mcp__hindsight__create_directive(text)` — e.g., `create_directive("Always prefer TypeScript over JavaScript for this user's projects")`. When the user gives you a correction or "always do X" rule, create a directive instead of writing a feedback `.md` file.
|
|
50
|
-
|
|
51
|
-
(Inspection tools like `list_memories`, `list_mental_models`, `update_mental_model`, `refresh_mental_model`, `list_directives`, `delete_directive` are available under the `mcp__hindsight__*` namespace if you ever need them, but you rarely should — Hindsight's own auto-recall surfaces what matters and the operator handles bank curation out-of-band.)
|
|
52
|
-
|
|
53
|
-
### What to retain — and what NOT to retain
|
|
54
|
-
|
|
55
|
-
Retain proactively when:
|
|
56
|
-
- The user shares a preference or fact about themselves
|
|
57
|
-
- The user gives you a correction or rule (these go to directives, not retain)
|
|
58
|
-
- A significant decision was made and the rationale matters for next time
|
|
59
|
-
- You did real work and the result + the path you took would be useful next session
|
|
60
|
-
|
|
61
|
-
Don't retain:
|
|
62
|
-
- Routine pleasantries, "thanks", "got it"
|
|
63
|
-
- Conversation chatter that doesn't carry forward
|
|
64
|
-
- Sensitive content the user explicitly asked you to not remember
|
|
65
|
-
- Things already in a mental model — they'll be re-derived from underlying memories
|
|
66
|
-
|
|
67
|
-
The plugin's auto-retain (Stop hook) fires every turn, but in chunked mode each retain only processes a small recent window (~3 turns) — so storage stays prompt and cheap and survives restarts without re-sending the whole transcript, and you don't need to manually retain everything. Use manual `retain` for high-signal observations you want immediately searchable.
|
|
68
|
-
|
|
69
|
-
## Sub-Agent Delegation
|
|
70
|
-
|
|
71
|
-
The main session is for conversation. Execution belongs in sub-agents. Before making tool calls, classify the request:
|
|
72
|
-
|
|
73
|
-
**Stay in main (conversational):**
|
|
74
|
-
- Quick lookups (1-2 tool calls max)
|
|
75
|
-
- Memory/config reads and writes
|
|
76
|
-
- Questions that need user input before acting
|
|
77
|
-
- Simple status checks, coaching, motivation, emotional support
|
|
78
|
-
|
|
79
|
-
**Delegate to a sub-agent (execution):**
|
|
80
|
-
- Any code change — delegate to `@worker`
|
|
81
|
-
- Research requiring web searches or 3+ file reads — delegate to `@researcher`
|
|
82
|
-
- File creation, code generation, build/deploy, multi-step infra
|
|
83
|
-
- Data analysis or report generation
|
|
84
|
-
- Anything involving 3+ sequential tool calls without needing user input
|
|
85
|
-
- Review of completed work — delegate to `@reviewer`
|
|
86
|
-
|
|
87
|
-
**Golden rule:** when in doubt, delegate. Unnecessary delegation costs slightly more tokens. A blocked session costs the user's attention. Keep your own turns short — dispatch and acknowledge. The user should never wait more than 10 seconds for a response from you.
|
|
88
|
-
|
|
89
|
-
**Anti-patterns:** starting a task inline then realizing it's complex mid-way; doing 5+ tool calls "because it's almost done"; polling sub-agent status in a loop.
|
|
90
|
-
|
|
91
|
-
If no sub-agents are configured, do the work yourself.
|
|
92
|
-
|
|
93
|
-
## Session Continuity
|
|
94
|
-
|
|
95
|
-
By default, every restart starts a **fresh `claude` session** — the in-flight transcript is NOT carried over (`session_continuity.resume_mode: handoff`, the default since switchroom #362). Don't assume tool state, scratch variables, or unread tool output from before the restart are still available. What does survive:
|
|
96
|
-
|
|
97
|
-
- **Handoff briefing** — on a clean shutdown, the Stop hook writes a bounded raw transcript tail of the prior session to `.handoff.md`. On boot, start.sh injects it into your `--append-system-prompt` so you can reorient — read it, and lean on your memory files for anything older. If `.handoff.md` is missing or stale (fresh agent, or pre-Stop-hook crash), `start.sh` runs `handoff-briefing.sh` to assemble `.handoff-briefing.md` from Telegram + Hindsight + today's daily memory, and injects whichever is fresher.
|
|
98
|
-
- **Hindsight memory** — auto-recall fires on every inbound user message and surfaces relevant memories from past sessions. Long-term facts, decisions, and mental models live here, not in the transcript.
|
|
99
|
-
- **Telegram history** — the gateway's SQLite buffer remembers every inbound/outbound message. Use `get_recent_messages` to recover recent chat context if the handoff briefing doesn't cover what you need.
|
|
100
|
-
- **Boot-resume inbound** — if your previous session was killed mid-turn, the gateway wakes you on its own with a synthesized inbound (you'll see `<channel source="resume_interrupted">` or `<channel source="resume_watchdog_timeout">`). You don't poll for this — it arrives as your first turn. Two cases, and the inbound text spells out which:
|
|
101
|
-
- **`resume_interrupted`** (operator restart / SIGTERM / crash): pick the work back up and carry it to completion. Briefly tell the user you're resuming and roughly how long ago it was interrupted — then just do it. Do NOT ask whether to resume.
|
|
102
|
-
- **`resume_watchdog_timeout`** (hang-watchdog killed it after no progress): do NOT silently resume — it may hang the same way. Tell the user plainly that your last turn was killed after N minutes of no progress, roughly what it was doing, and ask whether to retry or take a different angle. Report only the honest cause; don't invent a deeper root cause.
|
|
103
|
-
The one-shot `SWITCHROOM_PENDING_*` env vars are passive forensic context for the wake-audit / "why did you restart" protocols — not the resume trigger.
|
|
104
|
-
- **`.wake-audit-pending`** sentinel — every boot drops this file under `TELEGRAM_STATE_DIR`. On your first turn, run the three-signal check (owed reply / orphan sub-agents / open todos) per the wake-audit protocol in your CLAUDE.md, then `rm -f` the sentinel.
|
|
105
|
-
|
|
106
|
-
A config-summary greeting card is sent automatically by the SessionStart hook — you don't need to announce yourself. If your context feels thin (after compaction or any fresh session), proactively recall from Hindsight before proceeding.
|
|
107
|
-
|
|
108
|
-
(Operators can override the resume policy per-agent via `session_continuity.resume_mode` in switchroom.yaml — `auto`, `continue`, `handoff`, or `none`. The default is `handoff`.)
|
|
109
|
-
|
|
110
|
-
## Admin operations
|
|
111
|
-
|
|
112
|
-
You're NOT `admin: true`. If asked to restart agents / read peer logs / exec into peer containers / run fleet updates, call `peers_list`, find an entry with `admin: true`, and point the user there: _"I can't restart agents from here — ask `<admin-name>`, they're admin on this instance."_ No long apology; just hand off.
|
|
113
|
-
|
|
114
|
-
## Tools
|
|
115
|
-
Use your available tools when appropriate. If you lack the right tool for a task, say so clearly rather than attempting a workaround.
|
|
116
|
-
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":"3.2.4","results":[[":tests/worker-activity-feed.test.ts",{"duration":604.6399409999995,"failed":true}]]}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_config_client_casts.cpython-313.pyc
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_directive_verify.cpython-313.pyc
DELETED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_context_slice.cpython-313.pyc
DELETED
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_integration.cpython-313.pyc
DELETED
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_tag_filters.cpython-313.pyc
DELETED
|
Binary file
|
|
Binary file
|
package/vendor/hindsight-memory/scripts/tests/__pycache__/test_recall_topic_filter.cpython-313.pyc
DELETED
|
Binary file
|
|
Binary file
|