fix(cron): keep deferred Bot Chat delivery bound to admission
Carry the original destination home and delivery ID into deferred drain and its child, rather than re-resolving a mutable profile/root. Missing destinations fail closed; supported-owner handoffs remain transferred, not ambiguous failures. Capture the producer root before the background thread starts, and retain/log malformed JSON without stopping healthy admissions or the whole cron tick. Two invariants reproduced failures on the published head. Real Electron root change and malformed-record cases are red before and green after; nested DM control remains passing. No automatic retry of claimed or uncertain turns.
This commit is contained in:
@@ -16,7 +16,7 @@ rm apps/desktop/e2e/probe-dm-delivery.spec.ts
|
||||
```
|
||||
|
||||
Use the current seat's actual Xauthority path and an existing runtime venv.
|
||||
Artifacts are retained in `/tmp/botmode-dm-recovery`; sandbox path is printed. The
|
||||
Artifacts default to `/tmp/botmode-dm-review/native` (override with `BOT_DM_EVIDENCE`); sandbox path is printed. The
|
||||
fixture's generated hermes shim pins every child to this checkout, not an installed launcher.
|
||||
|
||||
## Verified results
|
||||
@@ -40,3 +40,21 @@ is profile-DB-based, not workspace selection; the named live-owner route is posi
|
||||
|
||||
The queue is deliberately at-most-once after claim. A crash before spawning but
|
||||
after claiming remains inspectable as claimed; it is not retried automatically.
|
||||
|
||||
## Independent review follow-up
|
||||
|
||||
The probe now admits the named Beta destination from the default scheduler, then
|
||||
changes the ticker's `HOME` to a different existing directory before drain.
|
||||
Published head `c91dfcbfe810c` fails with `Profile 'beta' does not exist`, leaving
|
||||
zero sentinel inputs in Beta. The follow-up pins the admitted home and ID; Beta
|
||||
renders one input and reply. Set `BOT_DM_CORRUPT=1` to add one malformed JSON
|
||||
record alongside the valid admission: before the follow-up the real tick raises
|
||||
`JSONDecodeError`; afterward the damaged record stays on disk while Beta delivers.
|
||||
|
||||
Fresh built native run with both root change and corruption: **2 passed (1.4m)**,
|
||||
including the existing nested `message_agent` control. Receipts/screenshots:
|
||||
`/tmp/botmode-dm-review/{native-red2,corrupt-red,final-native}` and matching `.log`
|
||||
files. The first follow-up run also exposed a fixture mistake (the changed HOME
|
||||
was not created, so `--in ~` correctly refused); that failed receipt is retained
|
||||
in `native-green.log`, and both source legs were rerun with an existing HOME.
|
||||
Prior `/tmp/botmode-dm-recovery*` evidence remains untouched.
|
||||
|
||||
@@ -9,7 +9,7 @@ const repo = path.resolve(import.meta.dirname, '../../..')
|
||||
const python = path.join(process.env.VIRTUAL_ENV || path.join(repo, '.venv'), 'bin', 'python')
|
||||
let fixture: MockBackendFixture
|
||||
let env: Record<string, string>
|
||||
const evidence = '/tmp/botmode-dm-recovery'
|
||||
const evidence = process.env.BOT_DM_EVIDENCE || '/tmp/botmode-dm-review/native'
|
||||
|
||||
test.beforeAll(async () => {
|
||||
fs.mkdirSync(evidence, { recursive: true })
|
||||
@@ -45,16 +45,18 @@ test('cron output waits for a CLI-only owner and arrives after owner release', a
|
||||
test.setTimeout(240_000)
|
||||
const output = fs.openSync(path.join(evidence, 'cli-owner.log'), 'w')
|
||||
const child = spawn(python, ['-m', 'hermes_cli.main', '-p', 'beta', 'chat', '--in', '~', '-c', 'Bot Chat', '--create-if-missing', '-Q', '-q', 'CLI_OWNER_HOLD'], { cwd: repo, env, stdio: ['ignore', output, output] })
|
||||
const cronEnv = { ...env, HERMES_HOME: path.join(fixture.sandbox.hermesHome, 'profiles', 'beta') }
|
||||
const cronEnv = { ...env, HERMES_HOME: fixture.sandbox.hermesHome }
|
||||
try {
|
||||
await fixture.mock.waitForHeldCompletion()
|
||||
const script = 'import json; from cron.scheduler_delivery import _deliver_to_bot_chat; j={"id":"cli-residual","name":"CLI residual","execution_id":"fixed-execution"}; result=_deliver_to_bot_chat(j,"CLI_OWNER_CRON_SENTINEL",""); print(json.dumps({"result":result,"job":j}))'
|
||||
const script = 'import json; from cron.scheduler_delivery import _deliver_to_bot_chat; j={"id":"cli-residual","name":"CLI residual","execution_id":"fixed-execution"}; result=_deliver_to_bot_chat(j,"CLI_OWNER_CRON_SENTINEL","beta"); print(json.dumps({"result":result,"job":j}))'
|
||||
const result = JSON.parse(execFileSync(python, ['-c', script], { env: cronEnv, cwd: repo, encoding: 'utf8', timeout: 30_000 }))
|
||||
console.log('CLI_OWNER_CRON_ADMISSION', JSON.stringify(result))
|
||||
fs.writeFileSync(path.join(evidence, 'cli-owner-admission.json'), JSON.stringify(result, null, 2))
|
||||
if (process.env.BOT_DM_CORRUPT) fs.writeFileSync(path.join(fixture.sandbox.hermesHome, 'cron', 'bot_chat_pending', 'broken.json'), '{')
|
||||
fixture.mock.releaseHeldStream()
|
||||
await expect.poll(() => child.exitCode, { timeout: 60_000 }).toBe(0)
|
||||
const ticker = spawn(python, ['-c', 'import time; from cron.scheduler import tick; from cron.bot_chat_delivery import _running; tick(verbose=False);\nwhile _running: time.sleep(0.1)'], { env: cronEnv, cwd: repo, stdio: ['ignore', output, output] })
|
||||
fs.mkdirSync(path.join(fixture.sandbox.root, 'changed-launch-home'), { recursive: true })
|
||||
const ticker = spawn(python, ['-c', 'import time; from cron.scheduler import tick; from cron.bot_chat_delivery import _running; tick(verbose=False);\nwhile _running: time.sleep(0.1)'], { env: { ...cronEnv, HOME: path.join(fixture.sandbox.root, 'changed-launch-home') }, cwd: repo, stdio: ['ignore', output, output] })
|
||||
await expect.poll(() => ticker.exitCode, { timeout: 90_000 }).toBe(0)
|
||||
await openBot('beta')
|
||||
expect(dbMessages('beta').filter(([role, text]) => role === 'user' && text.includes('CLI_OWNER_CRON_SENTINEL'))).toHaveLength(1)
|
||||
|
||||
Reference in New Issue
Block a user