switchroom 0.20.9 → 0.20.11
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/bin/handoff-briefing.sh +57 -5
- package/bin/working-state-reload-hook.sh +262 -0
- package/dist/agent-scheduler/index.js +65 -2
- package/dist/auth-broker/index.js +204 -24
- package/dist/cli/notion-write-pretool.mjs +65 -2
- package/dist/cli/self-improve-apply-guard-pretool.mjs +357 -92
- package/dist/cli/self-improve-stop.mjs +889 -7
- package/dist/cli/skill-validate-pretool.mjs +82 -3
- package/dist/cli/switchroom.js +3699 -2110
- package/dist/host-control/main.js +67 -4
- package/dist/vault/approvals/kernel-server.js +66 -3
- package/dist/vault/broker/server.js +66 -3
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +49 -0
- package/profiles/_shared/agent-self-service.md.hbs +15 -22
- package/profiles/_shared/delegation-golden-rule.md.hbs +1 -1
- package/profiles/_shared/dev-protocol.md.hbs +1 -1
- package/profiles/_shared/execution-discipline.md.hbs +4 -4
- package/profiles/_shared/vault-protocol.md.hbs +2 -18
- package/profiles/default/CLAUDE.md.hbs +3 -5
- package/telegram-plugin/auto-fallback-fleet.ts +37 -2
- package/telegram-plugin/dist/gateway/gateway.js +1414 -918
- package/telegram-plugin/fallback-card-collapse.ts +1 -0
- package/telegram-plugin/gateway/auth-command.ts +11 -1
- package/telegram-plugin/gateway/callback-query-handlers.ts +100 -0
- package/telegram-plugin/gateway/eval-case-proposal-card.ts +86 -0
- package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.test.ts +74 -0
- package/telegram-plugin/gateway/fleet-fallback-notice-cooldown.ts +71 -0
- package/telegram-plugin/gateway/gateway.ts +85 -90
- package/telegram-plugin/gateway/ipc-protocol.ts +43 -0
- package/telegram-plugin/gateway/ipc-server.ts +28 -0
- package/telegram-plugin/gateway/narrative-lane.ts +33 -2
- package/telegram-plugin/gateway/privacy-reset.test.ts +216 -0
- package/telegram-plugin/gateway/privacy-reset.ts +87 -0
- package/telegram-plugin/gateway/privacy-state.test.ts +165 -0
- package/telegram-plugin/gateway/privacy-state.ts +206 -0
- package/telegram-plugin/gateway/self-improve-proposal-wiring.ts +176 -0
- package/telegram-plugin/gateway/stale-pin-sweep-wiring.ts +24 -14
- package/telegram-plugin/gateway/stale-pin-sweep.test.ts +123 -26
- package/telegram-plugin/gateway/stale-pin-sweep.ts +48 -32
- package/telegram-plugin/gateway/throttle-tier-wiring.ts +15 -4
- package/telegram-plugin/slot-banner-driver.ts +42 -5
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +24 -0
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +2 -0
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +97 -0
- package/telegram-plugin/tests/privacy-reset-call-sites.test.ts +120 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +25 -0
- package/telegram-plugin/tests/throttle-tier.test.ts +16 -0
- package/telegram-plugin/tests/turn-flush-safety.test.ts +67 -0
- package/telegram-plugin/throttle-tier.ts +12 -3
- package/telegram-plugin/turn-flush-safety.ts +97 -0
- package/vendor/hindsight-memory/CHANGELOG.md +31 -0
- package/vendor/hindsight-memory/hooks/hooks.json +2 -1
- package/vendor/hindsight-memory/scripts/retain.py +306 -0
- package/vendor/hindsight-memory/scripts/session_start.py +35 -8
- package/vendor/hindsight-memory/scripts/subagent_retain.py +29 -1
- package/vendor/hindsight-memory/scripts/tests/test_private_mode.py +415 -0
- package/vendor/hindsight-memory/scripts/tests/test_self_improve_correction_tag.py +167 -0
- package/vendor/hindsight-memory/scripts/tests/test_session_start_durability.py +107 -0
|
@@ -328,7 +328,15 @@ export interface AuthBrokerClient {
|
|
|
328
328
|
* `setActive` this needs no admin — the account is derived from the caller's
|
|
329
329
|
* identity — so auto-fallback works from any agent.
|
|
330
330
|
*/
|
|
331
|
-
markExhausted(until?: number): Promise<{
|
|
331
|
+
markExhausted(until?: number): Promise<{
|
|
332
|
+
account: string
|
|
333
|
+
rolled: string[]
|
|
334
|
+
rolledTo?: string | null
|
|
335
|
+
/** True when the caller is a strict-pinned agent (`auth.strict`): its
|
|
336
|
+
* null `rolledTo` means "riding out the wall", not fleet all-blocked.
|
|
337
|
+
* Absent on pre-flag brokers. */
|
|
338
|
+
caller_pinned_strict?: boolean
|
|
339
|
+
}>
|
|
332
340
|
/**
|
|
333
341
|
* 429 throttle tier (broker `mark-throttled`). Records a transient
|
|
334
342
|
* per-account rate limit on the CALLER's own account — `throttled_until`
|
|
@@ -344,6 +352,8 @@ export interface AuthBrokerClient {
|
|
|
344
352
|
throttled_until: number
|
|
345
353
|
escalated: boolean
|
|
346
354
|
rolledTo?: string | null
|
|
355
|
+
/** See markExhausted.caller_pinned_strict. */
|
|
356
|
+
caller_pinned_strict?: boolean
|
|
347
357
|
}>
|
|
348
358
|
rmAccount(label: string): Promise<{ label: string }>
|
|
349
359
|
refreshAccount(label: string): Promise<{ account: string; expiresAt?: number }>
|
|
@@ -70,6 +70,11 @@ import {
|
|
|
70
70
|
getProposal as getSkillProposal,
|
|
71
71
|
setProposalStatus as setSkillProposalStatus,
|
|
72
72
|
} from '../../src/self-improve/skill-proposals.js'
|
|
73
|
+
import { parseEvalCaseProposalCallback } from './eval-case-proposal-card.js'
|
|
74
|
+
import {
|
|
75
|
+
getEvalCaseProposal,
|
|
76
|
+
setEvalCaseProposalStatus,
|
|
77
|
+
} from '../../src/self-improve/eval-case-proposals.js'
|
|
73
78
|
import { maskToken } from '../secret-detect/mask.js'
|
|
74
79
|
import {
|
|
75
80
|
defaultVaultWrite,
|
|
@@ -1328,6 +1333,100 @@ async function handleSkillProposalCallback(ctx: Context, data: string): Promise<
|
|
|
1328
1333
|
)
|
|
1329
1334
|
}
|
|
1330
1335
|
|
|
1336
|
+
/**
|
|
1337
|
+
* Eval-case proposal callback (RFC amendment §"corrections as eval cases").
|
|
1338
|
+
*
|
|
1339
|
+
* On Approve this runs the DETERMINISTIC `apply-eval-case` applier via
|
|
1340
|
+
* execFileSync — NOT a model turn — so the case lands byte-exact as approved
|
|
1341
|
+
* (precedent: the `switchroom vault set` on-tap execFileSync in this file).
|
|
1342
|
+
* The operator's tap IS the authorization; the gateway sets the proposal
|
|
1343
|
+
* `approved` BEFORE invoking the applier (whose own status check is
|
|
1344
|
+
* defense-in-depth, MJ2, since the store is agent-writable).
|
|
1345
|
+
*/
|
|
1346
|
+
async function handleEvalCaseProposalCallback(ctx: Context, data: string): Promise<void> {
|
|
1347
|
+
const senderId = String(ctx.from?.id ?? '')
|
|
1348
|
+
const access = loadAccess()
|
|
1349
|
+
if (!access.allowFrom.includes(senderId)) {
|
|
1350
|
+
await ctx.answerCallbackQuery({ text: 'Not authorized.' }).catch(() => {})
|
|
1351
|
+
return
|
|
1352
|
+
}
|
|
1353
|
+
const parsed = parseEvalCaseProposalCallback(data)
|
|
1354
|
+
if (parsed == null) {
|
|
1355
|
+
await ctx.answerCallbackQuery({ text: 'Bad request' }).catch(() => {})
|
|
1356
|
+
return
|
|
1357
|
+
}
|
|
1358
|
+
const stateDir = process.env.TELEGRAM_STATE_DIR
|
|
1359
|
+
const agent = process.env.SWITCHROOM_AGENT_NAME ?? ''
|
|
1360
|
+
if (stateDir == null || stateDir.length === 0) {
|
|
1361
|
+
await ctx.answerCallbackQuery({ text: 'State dir unset — cannot apply.' }).catch(() => {})
|
|
1362
|
+
return
|
|
1363
|
+
}
|
|
1364
|
+
const proposal = getEvalCaseProposal(stateDir, parsed.id)
|
|
1365
|
+
if (proposal == null) {
|
|
1366
|
+
await ctx.answerCallbackQuery({ text: 'Proposal expired or already actioned.' }).catch(() => {})
|
|
1367
|
+
if (ctx.callbackQuery?.message) {
|
|
1368
|
+
await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } }).catch(() => {})
|
|
1369
|
+
}
|
|
1370
|
+
return
|
|
1371
|
+
}
|
|
1372
|
+
if (proposal.status !== 'pending') {
|
|
1373
|
+
await ctx.answerCallbackQuery({ text: `Already ${proposal.status}.` }).catch(() => {})
|
|
1374
|
+
if (ctx.callbackQuery?.message) {
|
|
1375
|
+
await ctx.editMessageReplyMarkup({ reply_markup: { inline_keyboard: [] } }).catch(() => {})
|
|
1376
|
+
}
|
|
1377
|
+
return
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
if (parsed.action === 'deny') {
|
|
1381
|
+
setEvalCaseProposalStatus(stateDir, parsed.id, 'rejected')
|
|
1382
|
+
await ctx.answerCallbackQuery({ text: '🚫 Dismissed.' }).catch(() => {})
|
|
1383
|
+
if (ctx.callbackQuery?.message && 'text' in ctx.callbackQuery.message) {
|
|
1384
|
+
await ctx
|
|
1385
|
+
.editMessageText(
|
|
1386
|
+
`${escapeHtmlForTg(ctx.callbackQuery.message.text ?? '')}\n\n🚫 <i>Dismissed.</i>`,
|
|
1387
|
+
{ parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } },
|
|
1388
|
+
)
|
|
1389
|
+
.catch(() => {})
|
|
1390
|
+
}
|
|
1391
|
+
return
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
// Approve — authorize, then run the deterministic applier.
|
|
1395
|
+
setEvalCaseProposalStatus(stateDir, parsed.id, 'approved')
|
|
1396
|
+
await ctx.answerCallbackQuery({ text: '✅ Adding the eval case…' }).catch(() => {})
|
|
1397
|
+
|
|
1398
|
+
const cli = process.env.SWITCHROOM_CLI_PATH ?? 'switchroom'
|
|
1399
|
+
let applyOk = true
|
|
1400
|
+
let applyOut = ''
|
|
1401
|
+
try {
|
|
1402
|
+
applyOut = execFileSync(
|
|
1403
|
+
cli,
|
|
1404
|
+
['self-improve', 'apply-eval-case', '--id', parsed.id],
|
|
1405
|
+
{ encoding: 'utf8', timeout: 15000, env: process.env },
|
|
1406
|
+
).trim()
|
|
1407
|
+
} catch (err) {
|
|
1408
|
+
applyOk = false
|
|
1409
|
+
const e = err as { stdout?: string; stderr?: string; message?: string }
|
|
1410
|
+
applyOut = [e.stdout, e.stderr, e.message].filter(Boolean).join('\n').trim()
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
const footer = applyOk
|
|
1414
|
+
? '✅ <i>Added as a regression test.</i>'
|
|
1415
|
+
: `⚠️ <i>Apply failed:</i> ${escapeHtmlForTg(applyOut.slice(0, 200))}`
|
|
1416
|
+
if (ctx.callbackQuery?.message && 'text' in ctx.callbackQuery.message) {
|
|
1417
|
+
await ctx
|
|
1418
|
+
.editMessageText(
|
|
1419
|
+
`${escapeHtmlForTg(ctx.callbackQuery.message.text ?? '')}\n\n${footer}`,
|
|
1420
|
+
{ parse_mode: 'HTML', reply_markup: { inline_keyboard: [] } },
|
|
1421
|
+
)
|
|
1422
|
+
.catch(() => {})
|
|
1423
|
+
}
|
|
1424
|
+
process.stderr.write(
|
|
1425
|
+
`telegram gateway: eval_case_apply agent=${agent} proposal=${proposal.id} ` +
|
|
1426
|
+
`slug=${proposal.skill_slug} ok=${applyOk}\n`,
|
|
1427
|
+
)
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1331
1430
|
/**
|
|
1332
1431
|
* hindsight Phase 5 — handle a tap on the mental-model PROPOSAL card.
|
|
1333
1432
|
* mmp:approve:<stageId> — declare the model: append it to the agent's
|
|
@@ -3233,6 +3332,7 @@ async function handleAuthDashboardCallback(ctx: Context): Promise<void> {
|
|
|
3233
3332
|
performVaultAccessApproval,
|
|
3234
3333
|
resolveAccessApprovalPassphraseMismatch,
|
|
3235
3334
|
handleSkillProposalCallback,
|
|
3335
|
+
handleEvalCaseProposalCallback,
|
|
3236
3336
|
handleMentalModelProposeCallback,
|
|
3237
3337
|
handleVaultRequestAccessCallback,
|
|
3238
3338
|
handleVaultRequestSaveCallback,
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One-tap eval-case proposal card (RFC amendment §"corrections as eval
|
|
3
|
+
* cases").
|
|
4
|
+
*
|
|
5
|
+
* `switchroom self-improve add-eval-case` sends a `post_eval_case_proposal`
|
|
6
|
+
* IPC; the gateway persists it (eval-case-proposals store) and renders THIS
|
|
7
|
+
* card. Unlike the skill-proposal card, tapping Approve does NOT inject a
|
|
8
|
+
* model turn — the gateway callback runs the DETERMINISTIC `apply-eval-case`
|
|
9
|
+
* applier via execFileSync, so the case lands byte-exact as approved.
|
|
10
|
+
*
|
|
11
|
+
* Pure builders, kept out of gateway.ts so the card text + callback shape are
|
|
12
|
+
* pinned by tests independent of the bot plumbing.
|
|
13
|
+
*
|
|
14
|
+
* callback_data shape (must fit Telegram's 64-byte limit):
|
|
15
|
+
* evcase:approve:<id>
|
|
16
|
+
* evcase:deny:<id>
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export const EVAL_CASE_PROPOSAL_CALLBACK_PREFIX = 'evcase:'
|
|
20
|
+
|
|
21
|
+
/** Narrow view of a stored eval-case proposal the card needs. */
|
|
22
|
+
export interface EvalCaseProposalView {
|
|
23
|
+
id: string
|
|
24
|
+
skill_slug: string
|
|
25
|
+
held_out: boolean
|
|
26
|
+
case: { prompt: string; expectations?: string[] }
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function escapeHtml(s: string): string {
|
|
30
|
+
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Truncate a single-line preview of the prompt for the card. */
|
|
34
|
+
function previewPrompt(prompt: string, max = 240): string {
|
|
35
|
+
const oneLine = prompt.replace(/\s+/g, ' ').trim()
|
|
36
|
+
return oneLine.length > max ? oneLine.slice(0, max - 1) + '…' : oneLine
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Render the eval-case proposal card text (HTML parse mode). */
|
|
40
|
+
export function renderEvalCaseProposalCard(p: EvalCaseProposalView): string {
|
|
41
|
+
const lines: string[] = []
|
|
42
|
+
lines.push(`🧪 <b>Eval case proposed</b>`)
|
|
43
|
+
lines.push('')
|
|
44
|
+
lines.push(`<b>Skill:</b> <code>${escapeHtml(p.skill_slug)}</code>`)
|
|
45
|
+
if (p.held_out) lines.push(`<i>held-out (won't be added to evals.json)</i>`)
|
|
46
|
+
lines.push(`<b>Test prompt:</b> ${escapeHtml(previewPrompt(p.case.prompt))}`)
|
|
47
|
+
const exps = p.case.expectations ?? []
|
|
48
|
+
if (exps.length > 0) {
|
|
49
|
+
lines.push('<b>Checks:</b>')
|
|
50
|
+
for (const e of exps.slice(0, 5)) lines.push(`• ${escapeHtml(e.slice(0, 120))}`)
|
|
51
|
+
}
|
|
52
|
+
lines.push('')
|
|
53
|
+
lines.push(
|
|
54
|
+
'<i>Tap Add to append it as a regression test (re-scanned for ' +
|
|
55
|
+
'secrets/PII, written byte-exact — no model turn). Tap Dismiss to drop it.</i>',
|
|
56
|
+
)
|
|
57
|
+
return lines.join('\n')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Inline keyboard for the card. */
|
|
61
|
+
export function evalCaseProposalKeyboard(id: string): {
|
|
62
|
+
inline_keyboard: Array<Array<{ text: string; callback_data: string }>>
|
|
63
|
+
} {
|
|
64
|
+
return {
|
|
65
|
+
inline_keyboard: [
|
|
66
|
+
[
|
|
67
|
+
{ text: '✅ Add case', callback_data: `${EVAL_CASE_PROPOSAL_CALLBACK_PREFIX}approve:${id}` },
|
|
68
|
+
{ text: '🚫 Dismiss', callback_data: `${EVAL_CASE_PROPOSAL_CALLBACK_PREFIX}deny:${id}` },
|
|
69
|
+
],
|
|
70
|
+
],
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Parse an `evcase:` callback into { action, id }, or null if not ours. */
|
|
75
|
+
export function parseEvalCaseProposalCallback(
|
|
76
|
+
data: string,
|
|
77
|
+
): { action: 'approve' | 'deny'; id: string } | null {
|
|
78
|
+
if (!data.startsWith(EVAL_CASE_PROPOSAL_CALLBACK_PREFIX)) return null
|
|
79
|
+
const rest = data.slice(EVAL_CASE_PROPOSAL_CALLBACK_PREFIX.length)
|
|
80
|
+
const idx = rest.indexOf(':')
|
|
81
|
+
if (idx < 0) return null
|
|
82
|
+
const action = rest.slice(0, idx)
|
|
83
|
+
const id = rest.slice(idx + 1)
|
|
84
|
+
if ((action !== 'approve' && action !== 'deny') || id.length === 0) return null
|
|
85
|
+
return { action, id }
|
|
86
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
resetFleetFallbackNoticeCooldowns,
|
|
4
|
+
shouldSendAllBlockedNotice,
|
|
5
|
+
shouldSendStrictPinnedNotice,
|
|
6
|
+
} from './fleet-fallback-notice-cooldown.js'
|
|
7
|
+
|
|
8
|
+
const COOLDOWN_MS = 30 * 60_000
|
|
9
|
+
// Realistic wall-clock base: the initial state is { lastSentAtMs: 0 }, so the
|
|
10
|
+
// first call only "sends" when `now` is at least COOLDOWN_MS past the epoch —
|
|
11
|
+
// always true for a real Date.now(), so anchor the fakes there too.
|
|
12
|
+
const BASE = 1_700_000_000_000
|
|
13
|
+
|
|
14
|
+
function captureStderr(): { lines: string[]; restore: () => void } {
|
|
15
|
+
const lines: string[] = []
|
|
16
|
+
const spy = vi
|
|
17
|
+
.spyOn(process.stderr, 'write')
|
|
18
|
+
.mockImplementation((chunk: string | Uint8Array): boolean => {
|
|
19
|
+
lines.push(typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8'))
|
|
20
|
+
return true
|
|
21
|
+
})
|
|
22
|
+
return { lines, restore: () => spy.mockRestore() }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
afterEach(() => {
|
|
26
|
+
// Reset the module-scope windows so each test starts from a clean gate.
|
|
27
|
+
resetFleetFallbackNoticeCooldowns()
|
|
28
|
+
vi.restoreAllMocks()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('fleet-fallback notice cooldown gates', () => {
|
|
32
|
+
it('sends the first all-blocked card and suppresses a repeat inside the window', () => {
|
|
33
|
+
const { lines, restore } = captureStderr()
|
|
34
|
+
const t0 = BASE
|
|
35
|
+
expect(shouldSendAllBlockedNotice('agentA', t0)).toBe(true)
|
|
36
|
+
// Re-fire well inside the 30-min window: suppressed, with the exact log text.
|
|
37
|
+
expect(shouldSendAllBlockedNotice('agentA', t0 + 60_000)).toBe(false)
|
|
38
|
+
expect(lines).toEqual([
|
|
39
|
+
'telegram gateway: [fleet-fallback] all-blocked card suppressed (cooldown) agent=agentA\n',
|
|
40
|
+
])
|
|
41
|
+
// After the window elapses, it sends again.
|
|
42
|
+
expect(shouldSendAllBlockedNotice('agentA', t0 + COOLDOWN_MS)).toBe(true)
|
|
43
|
+
restore()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
it('sends the first strict-pinned card and suppresses a repeat inside the window', () => {
|
|
47
|
+
const { lines, restore } = captureStderr()
|
|
48
|
+
const t0 = BASE + 100_000_000
|
|
49
|
+
expect(shouldSendStrictPinnedNotice('agentB', t0)).toBe(true)
|
|
50
|
+
expect(shouldSendStrictPinnedNotice('agentB', t0 + 60_000)).toBe(false)
|
|
51
|
+
expect(lines).toEqual([
|
|
52
|
+
'telegram gateway: [fleet-fallback] strict-pinned card suppressed (cooldown) agent=agentB\n',
|
|
53
|
+
])
|
|
54
|
+
restore()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('keeps the two windows independent — a strict card never suppresses an all-blocked', () => {
|
|
58
|
+
const t0 = BASE + 200_000_000
|
|
59
|
+
expect(shouldSendStrictPinnedNotice('agentC', t0)).toBe(true)
|
|
60
|
+
// All-blocked has its own window and is unaffected by the strict send above.
|
|
61
|
+
expect(shouldSendAllBlockedNotice('agentC', t0 + 1000)).toBe(true)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('reset re-arms both windows so a post-recovery transition emits promptly', () => {
|
|
65
|
+
const t0 = BASE + 300_000_000
|
|
66
|
+
expect(shouldSendAllBlockedNotice('agentD', t0)).toBe(true)
|
|
67
|
+
expect(shouldSendStrictPinnedNotice('agentD', t0)).toBe(true)
|
|
68
|
+
// A successful swap resets both windows.
|
|
69
|
+
resetFleetFallbackNoticeCooldowns()
|
|
70
|
+
// Immediately after reset, both send again despite being inside the window.
|
|
71
|
+
expect(shouldSendAllBlockedNotice('agentD', t0 + 1000)).toBe(true)
|
|
72
|
+
expect(shouldSendStrictPinnedNotice('agentD', t0 + 1000)).toBe(true)
|
|
73
|
+
})
|
|
74
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* fleet-fallback-notice-cooldown.ts — per-gateway cooldown gates for the two
|
|
3
|
+
* NO-OP fleet-fallback outcomes: "all accounts blocked" and "strict-pinned".
|
|
4
|
+
*
|
|
5
|
+
* Both outcomes make `doFireFleetAutoFallback` return false WITHOUT swapping the
|
|
6
|
+
* active account, so the `fleetFallbackGate` dedup window never arms for them and
|
|
7
|
+
* the ~60s `quota_wall_detected` re-trigger would otherwise re-broadcast the
|
|
8
|
+
* identical card every minute for the life of the wall. Each outcome gets its own
|
|
9
|
+
* per-gateway cooldown window (separate state so a strict-pinned card never
|
|
10
|
+
* suppresses a genuine later all-blocked, or vice versa). A successful swap resets
|
|
11
|
+
* both windows so a fresh transition after recovery is not stale-suppressed.
|
|
12
|
+
*
|
|
13
|
+
* Extracted verbatim from gateway.ts (switchroom#4442) to keep the inline notice
|
|
14
|
+
* logic out of the file the line-ratchet guards. Behaviour is byte-identical to
|
|
15
|
+
* the previous inline gates: same 30-min cooldown window (via
|
|
16
|
+
* `evaluateAllBlockedNotice`), same stderr suppress-log text, same
|
|
17
|
+
* return-false-on-suppress contract.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
evaluateAllBlockedNotice,
|
|
22
|
+
type FallbackAllBlockedNoticeState,
|
|
23
|
+
} from '../auto-fallback-fleet.js'
|
|
24
|
+
|
|
25
|
+
let allBlockedState: FallbackAllBlockedNoticeState = { lastSentAtMs: 0 }
|
|
26
|
+
let strictPinnedState: FallbackAllBlockedNoticeState = { lastSentAtMs: 0 }
|
|
27
|
+
|
|
28
|
+
/** Reset both cooldown windows — called on a successful account swap. */
|
|
29
|
+
export function resetFleetFallbackNoticeCooldowns(): void {
|
|
30
|
+
allBlockedState = { lastSentAtMs: 0 }
|
|
31
|
+
strictPinnedState = { lastSentAtMs: 0 }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Gate the "all accounts blocked" card. Returns true if it should be sent now
|
|
36
|
+
* (advancing the cooldown window); false if suppressed by the window, writing the
|
|
37
|
+
* same stderr suppress log the gateway used inline.
|
|
38
|
+
*/
|
|
39
|
+
export function shouldSendAllBlockedNotice(
|
|
40
|
+
triggerAgent: string,
|
|
41
|
+
now: number = Date.now(),
|
|
42
|
+
): boolean {
|
|
43
|
+
const verdict = evaluateAllBlockedNotice(allBlockedState, now)
|
|
44
|
+
if (!verdict.send) {
|
|
45
|
+
process.stderr.write(
|
|
46
|
+
`telegram gateway: [fleet-fallback] all-blocked card suppressed (cooldown) agent=${triggerAgent}\n`,
|
|
47
|
+
)
|
|
48
|
+
return false
|
|
49
|
+
}
|
|
50
|
+
allBlockedState = verdict.next
|
|
51
|
+
return true
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Gate the "strict-pinned" card. Same contract as `shouldSendAllBlockedNotice`,
|
|
56
|
+
* against the separate strict-pinned cooldown window.
|
|
57
|
+
*/
|
|
58
|
+
export function shouldSendStrictPinnedNotice(
|
|
59
|
+
triggerAgent: string,
|
|
60
|
+
now: number = Date.now(),
|
|
61
|
+
): boolean {
|
|
62
|
+
const verdict = evaluateAllBlockedNotice(strictPinnedState, now)
|
|
63
|
+
if (!verdict.send) {
|
|
64
|
+
process.stderr.write(
|
|
65
|
+
`telegram gateway: [fleet-fallback] strict-pinned card suppressed (cooldown) agent=${triggerAgent}\n`,
|
|
66
|
+
)
|
|
67
|
+
return false
|
|
68
|
+
}
|
|
69
|
+
strictPinnedState = verdict.next
|
|
70
|
+
return true
|
|
71
|
+
}
|
|
@@ -433,7 +433,8 @@ import {
|
|
|
433
433
|
import { createThrottleTierRunner } from './throttle-tier-wiring.js'
|
|
434
434
|
import { parseLitellmNoticeWindowMs } from '../litellm-local-notice.js'
|
|
435
435
|
import { createLitellmLocalNoticeRunner, decideRateLimitedSurface } from './litellm-local-notice-wiring.js'
|
|
436
|
-
import { runFleetAutoFallback, renderFallbackFailureNotice, evaluateFallbackFailureNotice,
|
|
436
|
+
import { runFleetAutoFallback, renderFallbackFailureNotice, evaluateFallbackFailureNotice, type FallbackFailureNoticeState } from '../auto-fallback-fleet.js'
|
|
437
|
+
import { resetFleetFallbackNoticeCooldowns, shouldSendAllBlockedNotice, shouldSendStrictPinnedNotice } from './fleet-fallback-notice-cooldown.js'
|
|
437
438
|
import { startRestartWatchdog } from './restart-watchdog.js'
|
|
438
439
|
import { createAccessStore } from './access-store.js'
|
|
439
440
|
|
|
@@ -626,6 +627,13 @@ import { readTurnUsages } from '../../src/agents/perf.js'
|
|
|
626
627
|
import { buildContextOccupancy, writeContextOccupancySnapshot } from './context-occupancy.js'
|
|
627
628
|
import { decideProactiveCompact, initialCompactState, type CompactState } from './proactive-compact.js'
|
|
628
629
|
import { IdleTracker, idleDurationToMs, DEFAULT_IDLE_CLEAR_MS } from './idle-clear.js'
|
|
630
|
+
import {
|
|
631
|
+
openPrivateInterval,
|
|
632
|
+
closePrivateInterval,
|
|
633
|
+
PRIVATE_ON_REPLY,
|
|
634
|
+
PUBLIC_REPLY,
|
|
635
|
+
} from './privacy-state.js'
|
|
636
|
+
import { makePrivacyResetForNewSession, isContinueRestoreBoot } from './privacy-reset.js'
|
|
629
637
|
import { nextCompactNotify, idleCompactNotifyState, type CompactNotifyState } from './compact-notify.js'
|
|
630
638
|
import {
|
|
631
639
|
tryHostdDispatch,
|
|
@@ -769,13 +777,9 @@ import type { ChatKey as _ChatKey } from './inbound-delivery-machine.js'
|
|
|
769
777
|
import { dispatchEffects } from './inbound-delivery-machine-dispatch.js'
|
|
770
778
|
import { maybeFireWarmup } from './prefix-warmup.js'
|
|
771
779
|
import {
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
} from './
|
|
775
|
-
import {
|
|
776
|
-
enqueueProposal as enqueueSkillProposal,
|
|
777
|
-
isSuppressed as isSkillProposalSuppressed,
|
|
778
|
-
} from '../../src/self-improve/skill-proposals.js'
|
|
780
|
+
handlePostSkillProposal,
|
|
781
|
+
handlePostEvalCaseProposal,
|
|
782
|
+
} from './self-improve-proposal-wiring.js'
|
|
779
783
|
import { decideSubagentHandback } from './subagent-handback-inbound-builder.js'
|
|
780
784
|
import {
|
|
781
785
|
decideSubagentProgress,
|
|
@@ -803,6 +807,7 @@ import type {
|
|
|
803
807
|
QueryPendingPermissionMessage,
|
|
804
808
|
CheckPreApprovedMessage,
|
|
805
809
|
PostSkillProposalMessage,
|
|
810
|
+
PostEvalCaseProposalMessage,
|
|
806
811
|
PermissionEvent,
|
|
807
812
|
RolloutStatusPostMessage,
|
|
808
813
|
RolloutStatusEditMessage,
|
|
@@ -5097,6 +5102,10 @@ function maybeIdleClear(): void {
|
|
|
5097
5102
|
`telegram gateway: idle /clear suppressed for ${agentName} ` +
|
|
5098
5103
|
`(activity in check-to-send gap)\n`,
|
|
5099
5104
|
);
|
|
5105
|
+
} else {
|
|
5106
|
+
// The idle /clear fired → new logical session; reset privacy to public.
|
|
5107
|
+
const idleChat = loadAccess().allowFrom[0];
|
|
5108
|
+
if (idleChat) resetPrivacyForNewSession(String(idleChat), undefined);
|
|
5100
5109
|
}
|
|
5101
5110
|
})
|
|
5102
5111
|
.catch((err: unknown) => {
|
|
@@ -5600,6 +5609,11 @@ const swallowingApiCall = createSwallowingRetryApiCall(
|
|
|
5600
5609
|
robustApiCall,
|
|
5601
5610
|
(line) => process.stderr.write(line),
|
|
5602
5611
|
)
|
|
5612
|
+
// Privacy (#private-mode): reset to public on a genuine new session (boot / /clear); loud only on a private→public transition. See privacy-reset.ts.
|
|
5613
|
+
const resetPrivacyForNewSession = makePrivacyResetForNewSession((chatId, threadId, text) =>
|
|
5614
|
+
void swallowingApiCall(
|
|
5615
|
+
() => lockedBot.api.sendMessage(chatId, text, threadId != null ? { message_thread_id: threadId, disable_notification: false } : { disable_notification: false }),
|
|
5616
|
+
{ chat_id: chatId, verb: 'privacy-reset-alert', priorityClass: 'critical' }))
|
|
5603
5617
|
|
|
5604
5618
|
/**
|
|
5605
5619
|
* The ONE seam every `setMessageReaction` in this gateway goes through (#3155).
|
|
@@ -9058,6 +9072,11 @@ const stalePinSweeper: StalePinSweeper = createGatewayStalePinSweeper({
|
|
|
9058
9072
|
? loadStatusPins(STATUS_PIN_STORE_PATH, statusPinStoreFs)
|
|
9059
9073
|
: [],
|
|
9060
9074
|
eligible: () => stalePinSweepEligible,
|
|
9075
|
+
// Share the live path's per-process pin-rights negative cache so the sweep
|
|
9076
|
+
// and executePinLeg agree on rights-less chats (D3): the sweep skips a chat
|
|
9077
|
+
// the live path already proved rights-less, and feeds its own reactive
|
|
9078
|
+
// discoveries back so the live path skips too.
|
|
9079
|
+
rightsCache: statusPinRightsCache,
|
|
9061
9080
|
store: { path: STALE_PIN_SWEEP_STORE_PATH, fs: sweepStoreFs },
|
|
9062
9081
|
// Per-deployment override only. UNSET (the normal case) means "take the
|
|
9063
9082
|
// standing policy" — UNPIN_ALL_FORUM_TOPIC_ENABLED in stale-pin-sweep.ts,
|
|
@@ -11592,69 +11611,19 @@ if (isGatewayMain) ipcServer = createIpcServer({
|
|
|
11592
11611
|
// onQuotaWallDetected so it doesn't fall inside the source slice that
|
|
11593
11612
|
// send-outbound-wiring.test.ts takes between onSendOutbound and
|
|
11594
11613
|
// onQuotaWallDetected.
|
|
11614
|
+
// Thin delegate — body lives in self-improve-proposal-wiring.ts to keep the
|
|
11615
|
+
// gateway line ratchet flat. Placed AFTER onQuotaWallDetected so it doesn't
|
|
11616
|
+
// fall inside the source slice send-outbound-wiring.test.ts takes between
|
|
11617
|
+
// onSendOutbound and onQuotaWallDetected.
|
|
11595
11618
|
onPostSkillProposal(_client: IpcClient, msg: PostSkillProposalMessage) {
|
|
11596
|
-
|
|
11597
|
-
|
|
11598
|
-
|
|
11599
|
-
|
|
11600
|
-
|
|
11601
|
-
|
|
11602
|
-
|
|
11603
|
-
|
|
11604
|
-
assertAllowedChat(msg.chatId)
|
|
11605
|
-
} catch (err) {
|
|
11606
|
-
process.stderr.write(
|
|
11607
|
-
`telegram gateway: post_skill_proposal rejected — ${(err as Error).message}\n`,
|
|
11608
|
-
)
|
|
11609
|
-
return
|
|
11610
|
-
}
|
|
11611
|
-
const stateDir = process.env.TELEGRAM_STATE_DIR
|
|
11612
|
-
if (stateDir == null || stateDir.length === 0) {
|
|
11613
|
-
process.stderr.write(`telegram gateway: post_skill_proposal: TELEGRAM_STATE_DIR unset, skipping\n`)
|
|
11614
|
-
return
|
|
11615
|
-
}
|
|
11616
|
-
// Dedup against still-live rejection fingerprints — never re-surface a
|
|
11617
|
-
// proposal the operator already dismissed.
|
|
11618
|
-
if (isSkillProposalSuppressed(stateDir, {
|
|
11619
|
-
lesson: msg.lesson,
|
|
11620
|
-
draft: msg.draft,
|
|
11621
|
-
skill_slug: msg.skillSlug,
|
|
11622
|
-
})) {
|
|
11623
|
-
process.stderr.write(
|
|
11624
|
-
`telegram gateway: post_skill_proposal suppressed (rejected before) slug=${msg.skillSlug}\n`,
|
|
11625
|
-
)
|
|
11626
|
-
return
|
|
11627
|
-
}
|
|
11628
|
-
const proposal = enqueueSkillProposal(stateDir, {
|
|
11629
|
-
skill_slug: msg.skillSlug,
|
|
11630
|
-
is_new: msg.isNew,
|
|
11631
|
-
lesson: msg.lesson,
|
|
11632
|
-
draft: msg.draft,
|
|
11633
|
-
evidence: msg.evidence,
|
|
11634
|
-
chat_id: Number(msg.chatId),
|
|
11635
|
-
})
|
|
11636
|
-
const cardText = renderSkillProposalCard({
|
|
11637
|
-
id: proposal.id,
|
|
11638
|
-
skill_slug: proposal.skill_slug,
|
|
11639
|
-
is_new: proposal.is_new,
|
|
11640
|
-
lesson: proposal.lesson,
|
|
11641
|
-
evidence: proposal.evidence,
|
|
11642
|
-
skill_md: proposal.draft['SKILL.md'],
|
|
11643
|
-
})
|
|
11644
|
-
const threadId = msg.threadId
|
|
11645
|
-
void swallowingApiCall(
|
|
11646
|
-
() =>
|
|
11647
|
-
bot.api.sendMessage(msg.chatId, cardText, {
|
|
11648
|
-
parse_mode: 'HTML',
|
|
11649
|
-
reply_markup: skillProposalKeyboard(proposal.id),
|
|
11650
|
-
...(threadId != null && threadId !== 1 ? { message_thread_id: threadId } : {}),
|
|
11651
|
-
}),
|
|
11652
|
-
{ chat_id: msg.chatId, verb: 'skill-proposal-card', ...(threadId != null ? { threadId } : {}) },
|
|
11653
|
-
)
|
|
11654
|
-
process.stderr.write(
|
|
11655
|
-
`telegram gateway: post_skill_proposal agent=${msg.agentName} chat=${msg.chatId} ` +
|
|
11656
|
-
`proposal=${proposal.id} slug=${proposal.skill_slug} new=${proposal.is_new}\n`,
|
|
11657
|
-
)
|
|
11619
|
+
handlePostSkillProposal(msg, { bot, assertAllowedChat, swallowingApiCall })
|
|
11620
|
+
},
|
|
11621
|
+
|
|
11622
|
+
// RFC amendment §"corrections as eval cases" — thin delegate; the
|
|
11623
|
+
// DETERMINISTIC applier runs on Approve in handleEvalCaseProposalCallback,
|
|
11624
|
+
// NOT a model turn. Body in self-improve-proposal-wiring.ts.
|
|
11625
|
+
onPostEvalCaseProposal(_client: IpcClient, msg: PostEvalCaseProposalMessage) {
|
|
11626
|
+
handlePostEvalCaseProposal(msg, { bot, assertAllowedChat, swallowingApiCall })
|
|
11658
11627
|
},
|
|
11659
11628
|
|
|
11660
11629
|
// Buzz Phase 2b: the duplex peer's advisory publish outcome — no-op unless the hub mirror booted.
|
|
@@ -17683,6 +17652,10 @@ async function refreshPinnedBanner(reason: string): Promise<void> {
|
|
|
17683
17652
|
onError: (phase, err) => {
|
|
17684
17653
|
process.stderr.write(`telegram gateway: banner ${phase} failed (${reason}): ${err}\n`)
|
|
17685
17654
|
},
|
|
17655
|
+
// Share the one per-process pin-rights negative cache with the status-pin
|
|
17656
|
+
// path and the stale-pin sweep (D5): the banner skips a chat already known
|
|
17657
|
+
// rights-less and records/clears its own pin-verb discoveries there.
|
|
17658
|
+
rightsCache: statusPinRightsCache,
|
|
17686
17659
|
// Durable pin persistence into the SHARED status-pin store (distinct
|
|
17687
17660
|
// `banner:` pinKey). persist-BEFORE-pin ordering: pending() lands before
|
|
17688
17661
|
// the pinChatMessage call so a crash in that window is recoverable by the
|
|
@@ -17908,16 +17881,10 @@ const litellmLocalNoticeRunner = createLitellmLocalNoticeRunner({
|
|
|
17908
17881
|
*/
|
|
17909
17882
|
let fallbackFailureNoticeState: FallbackFailureNoticeState = { lastSentAtMs: 0 }
|
|
17910
17883
|
|
|
17911
|
-
|
|
17912
|
-
|
|
17913
|
-
|
|
17914
|
-
|
|
17915
|
-
* quota_wall_detected re-trigger would otherwise re-broadcast the identical card
|
|
17916
|
-
* every minute for the life of the wall. This bounds it to one card per window.
|
|
17917
|
-
* Reset on a successful swap so a fresh all-blocked after a recovery (a real new
|
|
17918
|
-
* transition) is not stale-suppressed.
|
|
17919
|
-
*/
|
|
17920
|
-
let fallbackAllBlockedNoticeState: FallbackAllBlockedNoticeState = { lastSentAtMs: 0 }
|
|
17884
|
+
// Per-gateway cooldown gates for the two NO-OP fleet-fallback outcomes
|
|
17885
|
+
// (all-blocked and strict-pinned) live in ./fleet-fallback-notice-cooldown.ts,
|
|
17886
|
+
// which owns their state and stderr suppress-logs (switchroom#4442, extracted to
|
|
17887
|
+
// keep the inline notice logic out of the line-ratchet-guarded gateway.ts).
|
|
17921
17888
|
|
|
17922
17889
|
function broadcastFleetFallbackFailure(triggerAgent: string, reason: string): void {
|
|
17923
17890
|
if (process.env.SWITCHROOM_FLEET_FALLBACK_FAILURE_NOTICE === '0') return
|
|
@@ -18167,7 +18134,11 @@ async function doFireFleetAutoFallback(
|
|
|
18167
18134
|
// weekly-capped account is never re-probed (and re-wedged) within the
|
|
18168
18135
|
// broker's ~5h default.
|
|
18169
18136
|
const r = await client.markExhausted(untilMs)
|
|
18170
|
-
return {
|
|
18137
|
+
return {
|
|
18138
|
+
rolledTo: r.rolledTo ?? null,
|
|
18139
|
+
rolled: r.rolled,
|
|
18140
|
+
callerPinnedStrict: r.caller_pinned_strict ?? false,
|
|
18141
|
+
}
|
|
18171
18142
|
},
|
|
18172
18143
|
triggerAgent,
|
|
18173
18144
|
tz,
|
|
@@ -18193,7 +18164,11 @@ async function doFireFleetAutoFallback(
|
|
|
18193
18164
|
// cooldown; a successful swap resets the window so a later (genuinely new)
|
|
18194
18165
|
// all-blocked still emits promptly.
|
|
18195
18166
|
if (outcome.kind === 'switched') {
|
|
18196
|
-
|
|
18167
|
+
resetFleetFallbackNoticeCooldowns()
|
|
18168
|
+
} else if (outcome.kind === 'strict-pinned') {
|
|
18169
|
+
// Same cooldown pattern as all-blocked (below): a no-op outcome the
|
|
18170
|
+
// ~60s wall re-trigger would otherwise re-broadcast every minute.
|
|
18171
|
+
if (!shouldSendStrictPinnedNotice(triggerAgent)) return false
|
|
18197
18172
|
} else if (outcome.kind === 'all-blocked') {
|
|
18198
18173
|
// ── Second recovery tier: MODEL-TIER downgrade (precedence A) ──────────
|
|
18199
18174
|
// Account-swap just came back all-blocked (no account still serves the
|
|
@@ -18210,14 +18185,7 @@ async function doFireFleetAutoFallback(
|
|
|
18210
18185
|
// card — a restart is coming that replays the interrupted turn.
|
|
18211
18186
|
return false
|
|
18212
18187
|
}
|
|
18213
|
-
|
|
18214
|
-
if (!verdict.send) {
|
|
18215
|
-
process.stderr.write(
|
|
18216
|
-
`telegram gateway: [fleet-fallback] all-blocked card suppressed (cooldown) agent=${triggerAgent}\n`,
|
|
18217
|
-
)
|
|
18218
|
-
return false
|
|
18219
|
-
}
|
|
18220
|
-
fallbackAllBlockedNoticeState = verdict.next
|
|
18188
|
+
if (!shouldSendAllBlockedNotice(triggerAgent)) return false
|
|
18221
18189
|
}
|
|
18222
18190
|
// Post the announcement to every authorized chat. Mirrors the
|
|
18223
18191
|
// operator-event broadcast pattern (line ~2290) — DM-only opts
|
|
@@ -19849,6 +19817,22 @@ bot.command('compact', async ctx => {
|
|
|
19849
19817
|
})
|
|
19850
19818
|
bot.command('clear', async ctx => {
|
|
19851
19819
|
await handleInjectCommand(ctx, buildInjectDeps({ open: true, fixedVerb: '/clear' }))
|
|
19820
|
+
// A /clear starts a new logical session → reset privacy to public.
|
|
19821
|
+
const clearChatId = String(ctx.chat!.id)
|
|
19822
|
+
resetPrivacyForNewSession(clearChatId, resolveThreadId(clearChatId, ctx.message?.message_thread_id))
|
|
19823
|
+
})
|
|
19824
|
+
// Per-operator session privacy controls. NOT admin verbs (deliberately kept
|
|
19825
|
+
// out of ADMIN_COMMAND_NAMES) — they gate memory writing for THIS session, so
|
|
19826
|
+
// they share the plain per-command isAuthorizedSender gate like inject/clear.
|
|
19827
|
+
bot.command('private', async ctx => {
|
|
19828
|
+
if (!isAuthorizedSender(ctx)) return
|
|
19829
|
+
openPrivateInterval()
|
|
19830
|
+
await switchroomReply(ctx, PRIVATE_ON_REPLY)
|
|
19831
|
+
})
|
|
19832
|
+
bot.command('public', async ctx => {
|
|
19833
|
+
if (!isAuthorizedSender(ctx)) return
|
|
19834
|
+
closePrivateInterval()
|
|
19835
|
+
await switchroomReply(ctx, PUBLIC_REPLY)
|
|
19852
19836
|
})
|
|
19853
19837
|
// /model and /effort extracted to bot-commands-model-effort.ts
|
|
19854
19838
|
// (switchroom#2996 P6 bot.command drain). Helper registration keeps grammy
|
|
@@ -21749,6 +21733,15 @@ bot.on('callback_query:data', async ctx => {
|
|
|
21749
21733
|
return
|
|
21750
21734
|
}
|
|
21751
21735
|
|
|
21736
|
+
// RFC amendment §"corrections as eval cases": one-tap eval-case card.
|
|
21737
|
+
// evcase:approve:<id> — run the DETERMINISTIC apply-eval-case applier
|
|
21738
|
+
// (byte-exact write; NOT a model turn)
|
|
21739
|
+
// evcase:deny:<id> — dismiss + mark the proposal rejected
|
|
21740
|
+
if (data.startsWith('evcase:')) {
|
|
21741
|
+
await callbackQueryHandlers.handleEvalCaseProposalCallback(ctx, data)
|
|
21742
|
+
return
|
|
21743
|
+
}
|
|
21744
|
+
|
|
21752
21745
|
// #2862: missed-approvals re-offer digest.
|
|
21753
21746
|
// missre:retry:<id> — inject a synthetic inbound asking the agent to
|
|
21754
21747
|
// re-attempt (re-raises a fresh approval card)
|
|
@@ -23527,6 +23520,8 @@ async function startGateway(): Promise<void> { // #2996 P0c: the boot IIFE, now
|
|
|
23527
23520
|
// Only when we KNOW it was a /model switch (reason) AND we will send the
|
|
23528
23521
|
// confirmation (marker chat captured); otherwise the boot card fires
|
|
23529
23522
|
// normally. Version/quota remain available via /status.
|
|
23523
|
+
// Privacy: genuine FRESH boot (cold/crash/planned/model-switch) → reset to public, reusing boot-card's chat. Suppressed on a --continue/auto transcript-restore (state must persist) and never wired to bridge-reconnect.
|
|
23524
|
+
if (target && !isContinueRestoreBoot(resolveAgentDirFromEnv())) resetPrivacyForNewSession(target.chatId, target.threadId)
|
|
23530
23525
|
const suppressBootCardForModelSwitch =
|
|
23531
23526
|
modelSwitchReason != null && modelSwitchMarkerChat != null
|
|
23532
23527
|
if (target && suppressBootCardForModelSwitch) {
|