@inetafrica/open-claudia 3.4.22 → 3.4.23

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/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 3.4.23
4
+
5
+ - Watch, wakeup and cron turns now stream their progress (interim narration, and thinking under `/verbose`) like chat turns. The live preview was gated to chat turns only, so a watch-woken turn showed the bell notice and "thinking…" and then nothing until its final answer, however long it worked.
6
+ - `/stop` no longer cancels the messages you send after it. It released the room the moment it killed the run, before that run had unwound, so a message sent in those seconds started, met the stop flag at its pre-spawn checkpoint and was cancelled "before the provider started" — three messages lost after one `/stop` on 24 Sep. The room now stays busy until the stopped run's own unwind releases it and drains the queue (a 30 s backstop covers a child that never reports its exit), and the stop flag names the run it stops, so it can never cancel another.
7
+ - "Queued." now says what the message is waiting on once the wait passes two minutes, e.g. `Queued behind the watch "Phone alarm" (running 42 min). /stop cancels it.`
8
+ - A watch that has triggered but waited 15 minutes behind a busy room posts one plain line per occurrence, so an alarm is never hidden for hours (24 Sep: 2 h and 7.5 h, with nothing in the chat).
9
+
3
10
  ## 3.4.22
4
11
 
5
12
  Supersedes 3.4.21, which was tagged but never published: its build failed because `test-upgrade-path.js` was wired into the test chain without being committed. Nothing shipped from that tag.
package/core/handlers.js CHANGED
@@ -20,7 +20,7 @@ const {
20
20
  getProviderSettings, resolveProviderSettings,
21
21
  getProjectSessions, getLastProjectSession, userOwnsProviderSession,
22
22
  createSessionCallbackToken, linkIdentity,
23
- roomSettingsFor, setRoomSetting,
23
+ roomSettingsFor, setRoomSetting, cancelFlagFor,
24
24
  } = require("./state");
25
25
  const { canonicalForChannel, canonicalForTelegram, normalizeCanonicalUserId, channelKey, identities, isConfiguredOwnerChannel, removeIdentityMapping } = require("./identity");
26
26
  const { isChatAuthorized, isChatOwner, recordPendingAuthRequest, authRequestLabel, hasOwner, bootstrapOwner } = require("./access");
@@ -1642,6 +1642,17 @@ register({
1642
1642
  },
1643
1643
  });
1644
1644
 
1645
+ // How long /stop waits for a stopped child to report its exit before it
1646
+ // releases the room anyway (SIGKILL follows SIGTERM after 3 s).
1647
+ const STOP_RELEASE_MS = 30 * 1000;
1648
+ function releaseStuckStop(state, proc, stopped) {
1649
+ if (state.runningProcess !== proc) return;
1650
+ if (proc.exitCode != null || proc.signalCode != null) return; // exited: its unwind releases the room
1651
+ console.warn(`[stop] child ${proc.pid} never reported its exit; releasing the room`);
1652
+ state.runningProcess = null;
1653
+ if (state.cancelRequested === stopped) state.cancelRequested = false;
1654
+ }
1655
+
1645
1656
  register({
1646
1657
  name: "stop", description: "Cancel running task",
1647
1658
  handler: async (env) => {
@@ -1655,14 +1666,23 @@ register({
1655
1666
  return n ? ` ${n} queued message${n === 1 ? "" : "s"} will run next.` : "";
1656
1667
  };
1657
1668
  if (state.runningProcess) {
1658
- const pid = state.runningProcess.pid;
1669
+ const proc = state.runningProcess;
1670
+ const pid = proc.pid;
1659
1671
  const { killProcessTree } = require("./process-tree");
1660
1672
  // Quiet unwind: deliverForegroundResult consumes this flag to suppress
1661
- // the "run failed" diagnostic the SIGTERM exit would otherwise emit.
1662
- state.cancelRequested = true;
1673
+ // the "run failed" diagnostic the SIGTERM exit would otherwise emit. It
1674
+ // names the run it stops, so it can never cancel a different one.
1675
+ const stopped = cancelFlagFor(state);
1676
+ state.cancelRequested = stopped;
1663
1677
  killProcessTree(pid, "SIGTERM");
1664
1678
  setTimeout(() => killProcessTree(pid, "SIGKILL"), 3000);
1665
- state.runningProcess = null;
1679
+ // The room stays busy until the stopped run has unwound: its own
1680
+ // finally releases the lock, clears this flag and drains the queue.
1681
+ // Releasing it here let a message sent in those seconds start, meet the
1682
+ // flag and be cancelled "before the provider started" (24 Sep: three
1683
+ // messages lost after one /stop). Backstop: a child that never reports
1684
+ // its exit must not pin the room.
1685
+ setTimeout(() => releaseStuckStop(state, proc, stopped), STOP_RELEASE_MS).unref?.();
1666
1686
  // Stop pending preview flushes but keep statusMessageId/Channel: the
1667
1687
  // killed run's delivery unwind edits the preview to drop its footer.
1668
1688
  if (state.streamInterval) { clearTimeout(state.streamInterval); state.streamInterval = null; }
@@ -1675,7 +1695,7 @@ register({
1675
1695
  } else if (state.preparingRun) {
1676
1696
  // Turn is mid-recall/prompt-build — no process to kill yet. Flag it so
1677
1697
  // the runner's pre-spawn shouldAbort checkpoint bails before spawning.
1678
- state.cancelRequested = true;
1698
+ state.cancelRequested = cancelFlagFor(state);
1679
1699
  if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; state.thinkingPhase = false; state.typingTick = null; }
1680
1700
  try { currentAdapter()?.typingStop?.(currentChannelId()); } catch (_) {}
1681
1701
  await sideCancellation;
package/core/runner.js CHANGED
@@ -13,7 +13,7 @@ const {
13
13
  } = require("./config");
14
14
  const {
15
15
  currentState, saveState, recordSession,
16
- resetSessionUsage, acquireRunLock, getActiveProvider, getProjectKey,
16
+ resetSessionUsage, acquireRunLock, cancelTargets, getActiveProvider, getProjectKey,
17
17
  getProviderSession, setProviderSession, clearProviderSession, quarantineProviderSession, getUsageForSession, commitProviderCompaction,
18
18
  resolveProviderSettings,
19
19
  } = require("./state");
@@ -1698,9 +1698,10 @@ async function deliverForegroundResult(result, runContext, state = currentState(
1698
1698
  // /stop unwind: the SIGTERM (or pre-spawn abort) that follows a user cancel
1699
1699
  // is not a failure worth announcing — /stop already replied "Cancelled.".
1700
1700
  // The flag is consumed here (and re-cleared on the run's unwind) so it can
1701
- // never suppress a later run's genuine failure.
1702
- if (!result.ok && (state.cancelRequested || result.error?.code === "RUN_CANCELLED")) {
1703
- state.cancelRequested = false;
1701
+ // never suppress a later run's genuine failure; it names the run it stopped.
1702
+ const stoppedHere = cancelTargets(state.cancelRequested, runContext);
1703
+ if (!result.ok && (stoppedHere || result.error?.code === "RUN_CANCELLED")) {
1704
+ if (stoppedHere) state.cancelRequested = false;
1704
1705
  return { ok: true, suppressed: "cancelled" };
1705
1706
  }
1706
1707
  const diagnostic = providerRunDiagnostic(result, runContext);
@@ -1871,6 +1872,18 @@ function settleImmediateEffects(result, runContext, { state, store, capture }) {
1871
1872
  const STREAM_PREVIEW_THROTTLE_MS = 2500;
1872
1873
  const STREAM_PREVIEW_FOOTER = "\n\n⏳ working — /stop to cancel";
1873
1874
 
1875
+ // Runs whose interim narration (and, under /verbose, thinking) streams into
1876
+ // the chat: the person's own turns, and scheduled wake-ups (watches, wakeups,
1877
+ // crons). A scheduled run answers in the same room and settles through
1878
+ // deliverForegroundResult, which cleans the preview up exactly as for a
1879
+ // foreground turn; without it a watch-woken turn showed only "thinking..."
1880
+ // until the final answer. Background runs (dream, review, recall, learn,
1881
+ // compaction, subagents, side chats, silent runs) stay silent.
1882
+ const STREAM_PREVIEW_PURPOSES = new Set(["foreground", "scheduled"]);
1883
+ function streamPreviewAllowed(runContext) {
1884
+ return STREAM_PREVIEW_PURPOSES.has(runContext?.purpose);
1885
+ }
1886
+
1874
1887
  // Edited-in-place interim preview: each completed text block the provider
1875
1888
  // emits mid-run is appended to one throttled status message, so the user sees
1876
1889
  // the narration as it happens instead of silence. finalizeStreamPreview
@@ -1911,7 +1924,7 @@ function createStreamPreview({ state, store }) {
1911
1924
  };
1912
1925
 
1913
1926
  const appendChunk = (chunk, runContext) => {
1914
- if (!enabled || runContext?.purpose !== "foreground" || state.cancelRequested) return;
1927
+ if (!enabled || !streamPreviewAllowed(runContext) || state.cancelRequested) return;
1915
1928
  state.streamBuffer = state.streamBuffer ? `${state.streamBuffer}\n\n${chunk}` : chunk;
1916
1929
  if (state.streamBuffer.length > 20000) state.streamBuffer = state.streamBuffer.slice(-16000);
1917
1930
  if (!state.streamInterval) state.streamInterval = setTimeout(flush, STREAM_PREVIEW_THROTTLE_MS);
@@ -1983,7 +1996,7 @@ function createForegroundService({ state, store, stopTyping, capture, ownsAdmiss
1983
1996
  },
1984
1997
  shouldAbort(runContext, { capture: isCapture } = {}) {
1985
1998
  if (isCapture || !ownsAdmissionLock) return false;
1986
- if (!state.cancelRequested) return false;
1999
+ if (!cancelTargets(state.cancelRequested, runContext)) return false;
1987
2000
  state.cancelRequested = false;
1988
2001
  return true;
1989
2002
  },
@@ -2197,7 +2210,7 @@ async function executeAdmittedRun(runContext, state, store, capture, ownsAdmissi
2197
2210
  // A cancel flag that survived this run (e.g. /stop raced a run that
2198
2211
  // finished successfully anyway) must not leak into the next turn, where
2199
2212
  // it would falsely abort a queued run or mute a genuine failure.
2200
- if (ownsAdmissionLock) state.cancelRequested = false;
2213
+ if (ownsAdmissionLock && cancelTargets(state.cancelRequested, runContext)) state.cancelRequested = false;
2201
2214
  if (tracksMainRun && state.activeRunContext === runContext) state.activeRunContext = null;
2202
2215
  stopTyping();
2203
2216
  }
@@ -2265,6 +2278,21 @@ function admitRunContext(prompt, cwd, replyToMsgId, opts = {}) {
2265
2278
  });
2266
2279
  }
2267
2280
 
2281
+ // What a message queued behind a long run is waiting for. A short wait is just
2282
+ // "Queued."; after two minutes it says what is running and how to stop it, so a
2283
+ // busy room never looks like a dead one (24 Sep: an hour behind a watch run).
2284
+ const QUEUED_DETAIL_AFTER_MS = 2 * 60 * 1000;
2285
+ function queuedNotice(activeRunContext, at = Date.now()) {
2286
+ const since = Number(activeRunContext?.admittedAt);
2287
+ if (!Number.isFinite(since) || at - since < QUEUED_DETAIL_AFTER_MS) return "Queued.";
2288
+ const minutes = Math.floor((at - since) / 60000);
2289
+ const header = String(activeRunContext.userPrompt || "").match(/^\[(Watch triggered|Watch expired|Scheduled cron|Scheduled wakeup) "([^"]*)"/);
2290
+ const kind = header ? { "Watch triggered": "watch", "Watch expired": "watch", "Scheduled cron": "scheduled job", "Scheduled wakeup": "wakeup" }[header[1]] : null;
2291
+ const label = header && header[2].length > 60 ? `${header[2].slice(0, 59).trimEnd()}…` : header?.[2];
2292
+ const what = kind ? `the ${kind} "${label}"` : "an earlier message";
2293
+ return `Queued behind ${what} (running ${minutes} min). /stop cancels it.`;
2294
+ }
2295
+
2268
2296
  function runAgent(prompt, cwd, replyToMsgId, opts = {}) {
2269
2297
  const state = currentState();
2270
2298
  const store = chatContext.getStore() || null;
@@ -2294,7 +2322,7 @@ function runAgent(prompt, cwd, replyToMsgId, opts = {}) {
2294
2322
  // task — noise. On a live call, send() is the TTS line: "Queued." would be
2295
2323
  // spoken to the caller. Both stay quiet and just deliver the real reply.
2296
2324
  if (currentAdapter()?.type !== "spaces" && !statusChatterMuted()) {
2297
- send(state.isCompacting ? "Compacting context, will pick this up next…" : "Queued.", { replyTo: replyToMsgId }).catch(() => {});
2325
+ send(state.isCompacting ? "Compacting context, will pick this up next…" : queuedNotice(state.activeRunContext), { replyTo: replyToMsgId }).catch(() => {});
2298
2326
  }
2299
2327
  return deferred.promise;
2300
2328
  }
@@ -2389,7 +2417,9 @@ function runAgentSilent(prompt, cwd, label, opts = {}) {
2389
2417
  }
2390
2418
 
2391
2419
  module.exports = {
2420
+ queuedNotice,
2392
2421
  admitRunContext,
2422
+ streamPreviewAllowed,
2393
2423
  capturedTranscriptMetadata,
2394
2424
  decorateForegroundInvocation,
2395
2425
  createRunnerService,
package/core/scheduler.js CHANGED
@@ -41,6 +41,11 @@ const { redactSensitive } = require("./redact");
41
41
  const MISSED_WAKEUP_GRACE_MS = 60 * 60 * 1000;
42
42
  const RETRY_DELAY_MS = 30 * 1000;
43
43
  const BUSY_DELAY_MS = 30 * 1000;
44
+ // A watch alarm that has waited this long behind a busy room says so in the
45
+ // chat, once per occurrence, so a busy room never hides an alarm for hours
46
+ // (24 Sep: the Phone alarm waited 2 h through 244 deferrals, the event-bus
47
+ // alarm 7.5 h through 902).
48
+ const BUSY_NOTICE_AFTER_MS = 15 * 60 * 1000;
44
49
  // A watch's `check` command is capped so a hung poll can't wedge the tick.
45
50
  const WATCH_CHECK_TIMEOUT_MS = 30 * 1000;
46
51
  // Every watch carries a hard expiry so none can idle-poll forever: when the
@@ -110,6 +115,7 @@ function createSchedulerService(dependencies = {}) {
110
115
  );
111
116
  const retryDelayMs = Number.isFinite(dependencies.retryDelayMs) ? dependencies.retryDelayMs : RETRY_DELAY_MS;
112
117
  const busyDelayMs = Number.isFinite(dependencies.busyDelayMs) ? dependencies.busyDelayMs : BUSY_DELAY_MS;
118
+ const busyNoticeAfterMs = Number.isFinite(dependencies.busyNoticeAfterMs) ? dependencies.busyNoticeAfterMs : BUSY_NOTICE_AFTER_MS;
113
119
  const missedGraceMs = Number.isFinite(dependencies.missedGraceMs) ? dependencies.missedGraceMs : MISSED_WAKEUP_GRACE_MS;
114
120
  const watchTimeoutMs = Number.isFinite(dependencies.watchTimeoutMs) ? dependencies.watchTimeoutMs : DEFAULT_WATCH_TIMEOUT_MS;
115
121
  const occurrenceIdFactory = dependencies.occurrenceIdFactory || (() => uniqueId("occ"));
@@ -345,14 +351,27 @@ function createSchedulerService(dependencies = {}) {
345
351
  function markBusy(job) {
346
352
  const latest = jobsStore.get(job.id) || job;
347
353
  const busyDeferralCount = (latest.busyDeferralCount || 0) + 1;
354
+ const waited = now() - Number(latest.watchTriggeredAt);
355
+ const noticeDue = !!latest.check
356
+ && latest.busyNoticeOccurrenceId !== latest.occurrenceId
357
+ && Number.isFinite(waited) && waited >= busyNoticeAfterMs;
348
358
  const patch = {
349
359
  status: "deferred",
350
360
  disabled: false,
351
361
  busyDeferralCount,
352
362
  nextAttemptAt: now() + busyDelayMs,
363
+ // At most once per occurrence: the marker is persisted with the deferral, before the send.
364
+ ...(noticeDue ? { busyNoticeOccurrenceId: latest.occurrenceId } : {}),
353
365
  };
354
366
  const updated = jobsStore.update(job.id, patch) || { ...latest, ...patch };
355
367
  armRetry(updated, busyDelayMs);
368
+ if (noticeDue) {
369
+ const minutes = Math.floor(waited / 60000);
370
+ const what = updated.watchExpired ? "timed out" : "triggered";
371
+ try {
372
+ Promise.resolve(send(`⏳ Watch "${updated.label}" ${what} ${minutes} min ago and is waiting for the current run to end. /stop ends that run so the watch can go.`)).catch(() => {});
373
+ } catch (_) { /* a notice only */ }
374
+ }
356
375
  return { ok: false, deferred: true, status: updated.status, job: updated };
357
376
  }
358
377
 
package/core/state.js CHANGED
@@ -796,6 +796,16 @@ function acquireRunLock(state = currentState()) {
796
796
  return true;
797
797
  }
798
798
 
799
+ // /stop arms state.cancelRequested with the id of the run it stops (true only
800
+ // when no run context is known), and a run acts on the flag only when it names
801
+ // that run. A bare boolean let a stop outlive its run and cancel the next one.
802
+ function cancelFlagFor(state = currentState()) {
803
+ return state.activeRunContext?.runId || true;
804
+ }
805
+ function cancelTargets(flag, runContext) {
806
+ return flag === true || (!!flag && flag === runContext?.runId);
807
+ }
808
+
799
809
  // True when ANY user has a turn running, admitted-and-preparing, compacting, or
800
810
  // waiting in its queue. Same busy predicate as acquireRunLock, but across every
801
811
  // user state rather than one — the Telegram poll-wedge backstop consults this
@@ -1535,6 +1545,8 @@ module.exports = {
1535
1545
  resetSessionUsage,
1536
1546
  resetSettings,
1537
1547
  acquireRunLock,
1548
+ cancelFlagFor,
1549
+ cancelTargets,
1538
1550
  anyTurnActive,
1539
1551
  saveState,
1540
1552
  loadSessions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.4.22",
3
+ "version": "3.4.23",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "setup": "node setup.js",
12
12
  "start": "node bot.js",
13
13
  "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-upgrade-path.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-kazee-context-media.js && node test-kazee-typing-join.js && node test-kazee-inbound-dedup.js && node test-typing-phase-push.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-call-speech.js && node test-kazee-call-session.js",
14
- "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-verify-contact.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-owner-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-intro-adoption.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js"
14
+ "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-stream-preview-purpose.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-verify-contact.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-loopback-owner-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-intro-adoption.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-grants.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-room-provider-settings.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-store-durability.js"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
@@ -494,7 +494,13 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
494
494
  // removing those would push recall, pack review and the enforcer onto opus.
495
495
  // Removal is cosmetic by design: /model accepts a typed name without checking
496
496
  // the list, so `/model claude-opus-4-8` remains a working rollback.
497
- assert.strictEqual(pkg.version, "3.4.22", "release version must remain unchanged without explicit approval");
497
+ // 3.4.23 approved by Sumeet 2026-09-24 ("push", answering "Shall I install
498
+ // 3.4.23 once that is done?" after "can you fix this in open claudia"): watch and
499
+ // wakeup turns stream like chat turns; /stop keeps the room busy until the
500
+ // stopped run unwinds and its flag names that run, so it no longer cancels the
501
+ // next messages; "Queued." names what it waits on; a watch alarm stuck 15 min
502
+ // behind a busy room posts one line.
503
+ assert.strictEqual(pkg.version, "3.4.23", "release version must remain unchanged without explicit approval");
498
504
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
499
505
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
500
506
  }
@@ -633,6 +633,43 @@ async function busyRestartAndMissedProbe(createSchedulerService) {
633
633
  assert.strictEqual(missedTimers.length, 1, "a missed wakeup gets exactly one startup attempt");
634
634
  }
635
635
 
636
+ async function busyWatchNoticeProbe(createSchedulerService) {
637
+ // A watch alarm waiting behind a busy room: silent for 15 minutes, then one
638
+ // plain line, never a second for the same occurrence; the next occurrence
639
+ // may say so again.
640
+ let clock = 1_720_000_000_000;
641
+ let checks = 0;
642
+ const h = schedulerHarness(createSchedulerService, [baseJob({
643
+ id: "alarm", kind: "cron", schedule: "*/15 * * * *", check: "fixture-check", repeat: true,
644
+ label: "Phone alarm", expiresAt: clock + 24 * 60 * 60 * 1000,
645
+ })], { now: () => clock, runWatchCheck: async () => { checks++; return { code: 0 }; } });
646
+ h.state.runningProcess = { pid: 1 };
647
+ await h.service.fireJob("alarm"); // triggers and latches
648
+ assert.strictEqual(checks, 1);
649
+ for (const minutes of [5, 10, 14]) {
650
+ clock = 1_720_000_000_000 + minutes * 60_000;
651
+ assert.strictEqual((await h.service.fireJob("alarm")).deferred, true);
652
+ }
653
+ assert.strictEqual(h.sends.length, 0, "no notice before 15 minutes");
654
+ clock = 1_720_000_000_000 + 16 * 60_000;
655
+ await h.service.fireJob("alarm");
656
+ assert.deepStrictEqual(h.sends, ['⏳ Watch "Phone alarm" triggered 16 min ago and is waiting for the current run to end. /stop ends that run so the watch can go.']);
657
+ for (const minutes of [20, 45, 120]) {
658
+ clock = 1_720_000_000_000 + minutes * 60_000;
659
+ await h.service.fireJob("alarm");
660
+ }
661
+ assert.strictEqual(h.sends.length, 1, "one notice per occurrence");
662
+ assert.strictEqual(h.jobsStore.get("alarm").busyNoticeOccurrenceId, "occ-wake-1", "the marker is persisted");
663
+ assert.strictEqual(checks, 1, "a latched alarm is not re-checked while it waits");
664
+
665
+ // A wakeup waiting behind the same run stays quiet.
666
+ const w = schedulerHarness(createSchedulerService, [baseJob({ id: "wake" })], { now: () => clock });
667
+ w.state.runningProcess = { pid: 1 };
668
+ clock += 60 * 60_000;
669
+ await w.service.fireJob("wake");
670
+ assert.strictEqual(w.sends.length, 0);
671
+ }
672
+
636
673
  async function durablePendingProbe(createSchedulerService, jobsApi) {
637
674
  const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "oc-pending-restart-"));
638
675
  let clock = 1_720_000_000_500;
@@ -659,7 +696,9 @@ async function durablePendingProbe(createSchedulerService, jobsApi) {
659
696
  assert.strictEqual(store.get(kind).attemptCount, 0);
660
697
  assert.strictEqual(store.get(kind).disabled, false);
661
698
  assert.strictEqual(checks, kind === "wakeup" ? 0 : 1);
662
- assert.strictEqual(h.sends.length, 0);
699
+ // A watch that waits says so once; a wakeup stays quiet.
700
+ assert.strictEqual(h.sends.length, kind === "wakeup" ? 0 : 1);
701
+ if (kind !== "wakeup") assert.match(h.sends[0], /^⏳ Watch "fixture wakeup" (triggered|timed out) 60 min ago/);
663
702
  h.service.stopAll();
664
703
 
665
704
  // Reopen the real on-disk store and construct a new scheduler, as boot does.
@@ -1043,6 +1082,7 @@ async function main() {
1043
1082
  await detachedHistoricalBranchProbe(schedulerApi.createSchedulerService);
1044
1083
  await busyRestartAndMissedProbe(schedulerApi.createSchedulerService);
1045
1084
  await durablePendingProbe(schedulerApi.createSchedulerService, jobsApi);
1085
+ await busyWatchNoticeProbe(schedulerApi.createSchedulerService);
1046
1086
  await unavailableAndFallbackProbe(schedulerApi.createSchedulerService);
1047
1087
  await cronOccurrenceProbe(schedulerApi.createSchedulerService);
1048
1088
  await watchGateProbe(schedulerApi.createSchedulerService);
package/test-run-lock.js CHANGED
@@ -60,4 +60,35 @@ const { acquireRunLock } = require("./core/state");
60
60
  assert.strictEqual(acquireRunLock(shared), true, "lock is re-acquirable once the turn fully releases");
61
61
  }
62
62
 
63
+ // A /stop flag names the run it stops. Only that run (or a legacy bare `true`)
64
+ // may act on it: a stop that outlived its run cancelled the next message
65
+ // "before the provider started" (24 Sep, three messages lost after one /stop).
66
+ {
67
+ const { cancelFlagFor, cancelTargets } = require("./core/state");
68
+ assert.strictEqual(cancelFlagFor({ activeRunContext: { runId: "run-a" } }), "run-a", "the flag names the active run");
69
+ assert.strictEqual(cancelFlagFor({ activeRunContext: null }), true, "no known run: a bare flag");
70
+ assert.strictEqual(cancelTargets("run-a", { runId: "run-a" }), true, "the stopped run sees its stop");
71
+ assert.strictEqual(cancelTargets("run-a", { runId: "run-b" }), false, "another run never does");
72
+ assert.strictEqual(cancelTargets(true, { runId: "run-b" }), true, "a bare flag still stops whatever is preparing");
73
+ assert.strictEqual(cancelTargets(false, { runId: "run-a" }), false, "no stop, no cancel");
74
+ assert.strictEqual(cancelTargets("run-a", null), false, "no run context, no match");
75
+ }
76
+
77
+ // "Queued." says what the room is busy with once the wait is long enough to
78
+ // look like a dead bot.
79
+ {
80
+ const { queuedNotice } = require("./core/runner");
81
+ const t0 = 1_720_000_000_000;
82
+ assert.strictEqual(queuedNotice(null, t0), "Queued.");
83
+ assert.strictEqual(queuedNotice({ admittedAt: t0, userPrompt: "hi" }, t0 + 60_000), "Queued.", "a short wait stays terse");
84
+ assert.strictEqual(queuedNotice({ admittedAt: t0, userPrompt: "hi" }, t0 + 5 * 60_000),
85
+ "Queued behind an earlier message (running 5 min). /stop cancels it.");
86
+ assert.strictEqual(queuedNotice({ admittedAt: t0, userPrompt: '[Watch triggered "Phone alarm" fired at x; occurrence y]\nbody' }, t0 + 42 * 60_000),
87
+ 'Queued behind the watch "Phone alarm" (running 42 min). /stop cancels it.');
88
+ assert.strictEqual(queuedNotice({ admittedAt: t0, userPrompt: '[Scheduled wakeup "Check CI" fired at x]' }, t0 + 3 * 60_000),
89
+ 'Queued behind the wakeup "Check CI" (running 3 min). /stop cancels it.');
90
+ const long = queuedNotice({ admittedAt: t0, userPrompt: `[Watch triggered "${"x".repeat(90)}" fired at x]` }, t0 + 3 * 60_000);
91
+ assert.ok(long.includes(`"${"x".repeat(59)}…"`), "a long label is shortened to 60 characters");
92
+ }
93
+
63
94
  console.log("run-lock OK");