@inetafrica/open-claudia 3.4.20 → 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 +20 -1
- package/core/handlers.js +124 -32
- package/core/providers/claude.js +8 -3
- package/core/runner.js +38 -8
- package/core/scheduler.js +19 -0
- package/core/state.js +12 -0
- package/package.json +3 -3
- package/test-provider-claude.js +18 -2
- package/test-provider-language.js +14 -1
- package/test-provider-scheduler.js +41 -1
- package/test-run-lock.js +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
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
|
+
|
|
10
|
+
## 3.4.22
|
|
11
|
+
|
|
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.
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
- Fix `/upgrade` reporting success while upgrading a copy of the bot that nothing runs. It ran `npm install -g`, then read the resulting version back from `npm root -g` — the one directory guaranteed to agree with itself. Found on a host where the bot executes from `/usr/local/lib/node_modules` while `npm config get prefix` is `~/.local`: three releases (3.4.18, 3.4.19, 3.4.20) were each announced as installed while the running process stayed on 3.4.17.
|
|
16
|
+
- The layout check no longer string-matches the docker `/app` path. It compares the directory the running process executes from against the directory npm would write to, so any prefix drift routes to the in-place overlay, not just the container case. An unqueryable `npm root -g` is not treated as a mismatch — the post-install check is the backstop there.
|
|
17
|
+
- `/upgrade` and `/downgrade` now read the resulting version back from the directory the process actually executes from. A mismatch reports "Upgrade did NOT take", names both directories, and restarts nothing; the previous behaviour was to restart and claim success.
|
|
18
|
+
- Drop the superseded `claude-opus-4-8`, `claude-opus-4-7` and `claude-opus-4-6` buttons from the Claude model picker, which had grown long enough to bury the current models.
|
|
19
|
+
- Removal is cosmetic by design. `/model` takes a typed name verbatim without consulting the list, so `/model claude-opus-4-8` still works as a rollback; only the buttons go. The list is still checked on tapped callbacks, whose job is to reject a stale button from an old message.
|
|
20
|
+
- `claude-sonnet-4-6` and `claude-haiku-4-5-20251001` deliberately stay: they are the tier models for medium and low utility work, not picker garnish, and dropping them would push recall, pack review and the enforcer onto opus.
|
|
21
|
+
|
|
3
22
|
## 3.4.20
|
|
4
23
|
|
|
5
24
|
- Make `claude-opus-5-5` the default Claude model. 3.4.19 only added it to the picker; the default stayed on `claude-opus-4-8`, so a room that had never run `/model` kept the old model.
|
|
@@ -261,7 +280,7 @@ Three approved plans land together — memory adoption A1-A4 (from the TencentDB
|
|
|
261
280
|
## v3.1.21 — Kazee public participation: owner seeding, group tag-gating, guarded external turns
|
|
262
281
|
|
|
263
282
|
- **Owner seeding for kazee-only pods.** A pod whose only owner signal is `KAZEE_OWNER_USER_ID` (empty `auth.json`) never got a people-store owner record, so every unknown inbound — group or DM — was refused with a false "no owner configured" (observed on network-mon, 2026-07-23). `seedOwnerFromLegacy` now falls back to the kazee env owner; legacy `auth.json` owners keep precedence and seeding stays idempotent.
|
|
264
|
-
- **Group chats engage only when addressed.** The kazee adapter fetches chat meta (5-min cache) and, in group chats, engages only on an @-mention of the bot or a reply to the bot's own message — matching chat-central's own `assistantAction` default `assistant_tagged_reply`. Chats set to `assistant_free_reply` engage on everything. Untagged group chatter is dropped silently (no roster prompts, no refusals — owner included). 1:1 chats are unchanged;
|
|
283
|
+
- **Group chats engage only when addressed.** The kazee adapter fetches chat meta (5-min cache) and, in group chats, engages only on an @-mention of the bot or a reply to the bot's own message — matching chat-central's own `assistantAction` default `assistant_tagged_reply`. Chats set to `assistant_free_reply` engage on everything. Untagged group chatter is dropped silently (no roster prompts, no refusals — owner included). 1:1 chats are unchanged; an unresolved metadata fetch is not cached and fails closed because the room cannot safely be classified as a DM.
|
|
265
284
|
- **Tagged externals get the guarded turn.** An engaged group message authorizes the turn via adapter-attested `envelope.engaged` (never derived from message content), but the speaker still classifies external: replies are vetted by the enforcer and escalate to the owner's approve-once / deny / make-a-rule card — the same pattern as Spaces. Slash commands stay closed to externals.
|
|
266
285
|
- **Escalation cards reach the kazee owner.** `ownerTarget` used to return env-seeded kazee owner handles as the raw user id, but kazee sends need a chat-document id, so approval cards would 404. User-id handles now resolve through `openDirectChat`, falling back to the configured owner target.
|
|
267
286
|
- New release-chain tests: `test-people-owner-seed.js` (env seed, legacy precedence, no-signal refusal) and `test-kazee-group-gate.js` (mention/reply engagement, free-reply, 1:1, meta failure, meta cache, engaged access pass-through).
|
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");
|
|
@@ -643,21 +643,70 @@ async function requestAgentSpaceUpgrade() {
|
|
|
643
643
|
});
|
|
644
644
|
}
|
|
645
645
|
|
|
646
|
-
//
|
|
647
|
-
//
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
646
|
+
// The directory the RUNNING bot's source actually lives in. Everything about
|
|
647
|
+
// upgrading has to be judged against this, never against `npm root -g`.
|
|
648
|
+
function runningInstallRoot() {
|
|
649
|
+
return path.resolve(path.join(__dirname, ".."));
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
// Where `npm install -g` would place our package, or null when npm cannot be
|
|
653
|
+
// asked. Null means "cannot tell" and callers fall back to the npm path.
|
|
654
|
+
function globalInstallRoot() {
|
|
655
|
+
try {
|
|
656
|
+
const root = execSync("npm root -g", {
|
|
657
|
+
encoding: "utf-8", timeout: 15000,
|
|
658
|
+
cwd: process.env.HOME || require("os").homedir(),
|
|
659
|
+
env: { ...process.env, PATH: FULL_PATH },
|
|
660
|
+
}).trim();
|
|
661
|
+
if (!root) return null;
|
|
662
|
+
return path.resolve(path.join(root, "@inetafrica", "open-claudia"));
|
|
663
|
+
} catch (e) {
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// True when `npm install -g` cannot upgrade THIS process, because the code it
|
|
669
|
+
// executes does not live where npm would write. Two shapes seen in the wild:
|
|
670
|
+
//
|
|
671
|
+
// 1. the docker /app layout from our Dockerfile - the process reads code
|
|
672
|
+
// from /app and uid 1001 cannot write the global root anyway;
|
|
673
|
+
// 2. a host whose npm prefix has moved away from where the bot was
|
|
674
|
+
// installed. Observed 2026-09-23 on Sumeet's Mac: launchd runs
|
|
675
|
+
// /usr/local/lib/node_modules/@inetafrica/open-claudia/bot.js while
|
|
676
|
+
// `npm config get prefix` is ~/.local, so `npm install -g` faithfully
|
|
677
|
+
// installed a copy nothing executes. Three releases (3.4.18, 3.4.19,
|
|
678
|
+
// 3.4.20) landed in that copy and /upgrade reported success each time
|
|
679
|
+
// while the running bot stayed on 3.4.17 from 7 Sep.
|
|
680
|
+
//
|
|
681
|
+
// Case 2 is why this is a directory comparison and not a string match on
|
|
682
|
+
// "/app": the failure is general, and it is silent, which is the worst part.
|
|
683
|
+
// Version recorded in a given install directory, or null if unreadable. The
|
|
684
|
+
// ONLY honest way to answer "did the upgrade take": read it back from the
|
|
685
|
+
// directory the process executes from.
|
|
686
|
+
function installedVersionAt(dir) {
|
|
652
687
|
try {
|
|
653
|
-
|
|
654
|
-
|
|
688
|
+
return JSON.parse(fs.readFileSync(path.join(dir, "package.json"), "utf-8")).version || null;
|
|
689
|
+
} catch (e) {
|
|
690
|
+
return null;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
function npmGlobalUpgradeWouldMiss(probe = {}) {
|
|
695
|
+
try {
|
|
696
|
+
const running = probe.running !== undefined ? probe.running : runningInstallRoot();
|
|
697
|
+
if (running === "/app") return true;
|
|
698
|
+
const global = probe.global !== undefined ? probe.global : globalInstallRoot();
|
|
699
|
+
// Cannot tell where npm would write - keep the historical behaviour and
|
|
700
|
+
// let `npm install -g` run. The post-install version check below is what
|
|
701
|
+
// catches it if that guess was wrong.
|
|
702
|
+
if (!global) return false;
|
|
703
|
+
return global !== running;
|
|
655
704
|
} catch (e) {
|
|
656
705
|
return false;
|
|
657
706
|
}
|
|
658
707
|
}
|
|
659
708
|
|
|
660
|
-
function inPlaceTarballUpgrade(targetVersion) {
|
|
709
|
+
function inPlaceTarballUpgrade(targetVersion, targetDir = "/app") {
|
|
661
710
|
const tmpDir = `/tmp/oc-upgrade-${process.pid}-${Date.now()}`;
|
|
662
711
|
fs.mkdirSync(tmpDir, { recursive: true });
|
|
663
712
|
const env = { ...process.env, PATH: FULL_PATH, HOME: process.env.HOME || require("os").homedir() };
|
|
@@ -670,10 +719,10 @@ function inPlaceTarballUpgrade(targetVersion) {
|
|
|
670
719
|
execSync(`tar -xzf "${tgzPath}" -C "${tmpDir}"`, { encoding: "utf-8", timeout: 60000 });
|
|
671
720
|
// npm pack tarballs always extract into a top-level "package/" directory.
|
|
672
721
|
// cp -a preserves perms and symlinks; the trailing /. copies contents only.
|
|
673
|
-
execSync(`cp -a "${tmpDir}/package/." /
|
|
674
|
-
try { fs.chmodSync("
|
|
722
|
+
execSync(`cp -a "${tmpDir}/package/." "${targetDir}/"`, { encoding: "utf-8", timeout: 60000 });
|
|
723
|
+
try { fs.chmodSync(path.join(targetDir, "bin", "cli.js"), 0o755); } catch (e) {}
|
|
675
724
|
execSync(`npm install --omit=dev --no-audit --no-fund`, {
|
|
676
|
-
encoding: "utf-8", cwd:
|
|
725
|
+
encoding: "utf-8", cwd: targetDir, timeout: 240000, env,
|
|
677
726
|
});
|
|
678
727
|
try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (e) {}
|
|
679
728
|
}
|
|
@@ -731,11 +780,12 @@ register({
|
|
|
731
780
|
return;
|
|
732
781
|
}
|
|
733
782
|
}
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
// brings us back on the new source.
|
|
738
|
-
if (
|
|
783
|
+
// The running source does not live where `npm install -g` writes - docker
|
|
784
|
+
// /app, or a host whose npm prefix has drifted. Overlay the tarball onto
|
|
785
|
+
// the directory we actually execute from and exit; whatever supervises us
|
|
786
|
+
// (k8s, docker restart=always, launchd) brings us back on the new source.
|
|
787
|
+
if (npmGlobalUpgradeWouldMiss()) {
|
|
788
|
+
const overlayDir = runningInstallRoot();
|
|
739
789
|
try { process.chdir("/tmp"); } catch (e) {}
|
|
740
790
|
let latest = null;
|
|
741
791
|
try {
|
|
@@ -751,19 +801,27 @@ register({
|
|
|
751
801
|
await send(`Already on the latest version (v${CURRENT_VERSION}).`);
|
|
752
802
|
return;
|
|
753
803
|
}
|
|
754
|
-
await send(`Upgrading v${CURRENT_VERSION} → v${latest} (in-place
|
|
804
|
+
await send(`Upgrading v${CURRENT_VERSION} → v${latest} (in-place overlay into ${overlayDir})...`);
|
|
755
805
|
try {
|
|
756
|
-
inPlaceTarballUpgrade(latest);
|
|
806
|
+
inPlaceTarballUpgrade(latest, overlayDir);
|
|
757
807
|
} catch (e) {
|
|
758
808
|
const errOutput = (e.stdout || e.stderr || e.message || String(e)).slice(-700);
|
|
759
809
|
await send(`Upgrade failed:\n${errOutput}`);
|
|
760
810
|
return;
|
|
761
811
|
}
|
|
812
|
+
// Verify against the directory we RUN from. Reporting a version read
|
|
813
|
+
// from anywhere else is how three releases were announced as installed
|
|
814
|
+
// while the running bot never moved.
|
|
815
|
+
const landed = installedVersionAt(overlayDir);
|
|
816
|
+
if (landed !== latest) {
|
|
817
|
+
await send(`Upgrade did NOT take: ${overlayDir} still reports v${landed || "unknown"}, expected v${latest}. Nothing was restarted.`);
|
|
818
|
+
return;
|
|
819
|
+
}
|
|
762
820
|
let newVersion = latest;
|
|
763
821
|
let whatsNew = "";
|
|
764
822
|
try {
|
|
765
|
-
newVersion = JSON.parse(fs.readFileSync("
|
|
766
|
-
const changelog = fs.readFileSync("
|
|
823
|
+
newVersion = JSON.parse(fs.readFileSync(path.join(overlayDir, "package.json"), "utf-8")).version;
|
|
824
|
+
const changelog = fs.readFileSync(path.join(overlayDir, "CHANGELOG.md"), "utf-8");
|
|
767
825
|
let versionHeader = `## v${newVersion}`;
|
|
768
826
|
let start = changelog.indexOf(versionHeader);
|
|
769
827
|
if (start < 0) { versionHeader = `## ${newVersion}`; start = changelog.indexOf(versionHeader); }
|
|
@@ -809,11 +867,20 @@ register({
|
|
|
809
867
|
const { ensureSetup: postUpgradeSetup } = require("../health");
|
|
810
868
|
postUpgradeSetup();
|
|
811
869
|
} catch (e) {}
|
|
812
|
-
|
|
813
|
-
|
|
870
|
+
// Read the result back from the directory we EXECUTE from. Reading it
|
|
871
|
+
// from `npm root -g` only proves npm wrote somewhere, which is exactly
|
|
872
|
+
// the bug that let 3.4.18/19/20 be announced as installed on a host
|
|
873
|
+
// whose prefix had drifted while the running bot stayed on 3.4.17.
|
|
874
|
+
const runRoot = runningInstallRoot();
|
|
875
|
+
const landed = installedVersionAt(runRoot);
|
|
876
|
+
if (installTarget !== "latest" && landed !== installTarget) {
|
|
877
|
+
await send(`Upgrade did NOT take: ${runRoot} still reports v${landed || "unknown"}, expected v${installTarget}. npm installed elsewhere (prefix ${globalInstallRoot() || "unknown"}). Nothing was restarted.`);
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
const newPkg = { version: landed || installTarget };
|
|
814
881
|
let whatsNew = "";
|
|
815
882
|
try {
|
|
816
|
-
const changelog = fs.readFileSync(path.join(
|
|
883
|
+
const changelog = fs.readFileSync(path.join(runRoot, "CHANGELOG.md"), "utf-8");
|
|
817
884
|
let versionHeader = `## v${newPkg.version}`;
|
|
818
885
|
let start = changelog.indexOf(versionHeader);
|
|
819
886
|
if (start < 0) { versionHeader = `## ${newPkg.version}`; start = changelog.indexOf(versionHeader); }
|
|
@@ -882,14 +949,14 @@ register({
|
|
|
882
949
|
return;
|
|
883
950
|
}
|
|
884
951
|
const direction = isNewerVersion(resolved, CURRENT_VERSION) ? "Moving" : "Rolling back";
|
|
885
|
-
if (
|
|
952
|
+
if (npmGlobalUpgradeWouldMiss()) {
|
|
886
953
|
try { process.chdir("/tmp"); } catch (e) {}
|
|
887
954
|
const podNote = process.env.AGENTSPACE_POD_TOKEN
|
|
888
955
|
? "\nNote: this is an in-place overlay — a pod recreate returns to the deployment image version. Roll the image for a permanent downgrade."
|
|
889
956
|
: "";
|
|
890
957
|
await send(`${direction} v${CURRENT_VERSION} → v${resolved} (in-place tarball overlay into /app)...${podNote}`);
|
|
891
958
|
try {
|
|
892
|
-
inPlaceTarballUpgrade(resolved);
|
|
959
|
+
inPlaceTarballUpgrade(resolved, runningInstallRoot());
|
|
893
960
|
} catch (e) {
|
|
894
961
|
const errOutput = (e.stdout || e.stderr || e.message || String(e)).slice(-700);
|
|
895
962
|
await send(`Downgrade failed:\n${errOutput}`);
|
|
@@ -1575,6 +1642,17 @@ register({
|
|
|
1575
1642
|
},
|
|
1576
1643
|
});
|
|
1577
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
|
+
|
|
1578
1656
|
register({
|
|
1579
1657
|
name: "stop", description: "Cancel running task",
|
|
1580
1658
|
handler: async (env) => {
|
|
@@ -1588,14 +1666,23 @@ register({
|
|
|
1588
1666
|
return n ? ` ${n} queued message${n === 1 ? "" : "s"} will run next.` : "";
|
|
1589
1667
|
};
|
|
1590
1668
|
if (state.runningProcess) {
|
|
1591
|
-
const
|
|
1669
|
+
const proc = state.runningProcess;
|
|
1670
|
+
const pid = proc.pid;
|
|
1592
1671
|
const { killProcessTree } = require("./process-tree");
|
|
1593
1672
|
// Quiet unwind: deliverForegroundResult consumes this flag to suppress
|
|
1594
|
-
// the "run failed" diagnostic the SIGTERM exit would otherwise emit.
|
|
1595
|
-
|
|
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;
|
|
1596
1677
|
killProcessTree(pid, "SIGTERM");
|
|
1597
1678
|
setTimeout(() => killProcessTree(pid, "SIGKILL"), 3000);
|
|
1598
|
-
|
|
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?.();
|
|
1599
1686
|
// Stop pending preview flushes but keep statusMessageId/Channel: the
|
|
1600
1687
|
// killed run's delivery unwind edits the preview to drop its footer.
|
|
1601
1688
|
if (state.streamInterval) { clearTimeout(state.streamInterval); state.streamInterval = null; }
|
|
@@ -1608,7 +1695,7 @@ register({
|
|
|
1608
1695
|
} else if (state.preparingRun) {
|
|
1609
1696
|
// Turn is mid-recall/prompt-build — no process to kill yet. Flag it so
|
|
1610
1697
|
// the runner's pre-spawn shouldAbort checkpoint bails before spawning.
|
|
1611
|
-
state.cancelRequested =
|
|
1698
|
+
state.cancelRequested = cancelFlagFor(state);
|
|
1612
1699
|
if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; state.thinkingPhase = false; state.typingTick = null; }
|
|
1613
1700
|
try { currentAdapter()?.typingStop?.(currentChannelId()); } catch (_) {}
|
|
1614
1701
|
await sideCancellation;
|
|
@@ -2687,3 +2774,8 @@ register({
|
|
|
2687
2774
|
send("Usage: /intros | /intros approve <id> | /intros reject <id> [reason]");
|
|
2688
2775
|
},
|
|
2689
2776
|
});
|
|
2777
|
+
|
|
2778
|
+
module.exports.runningInstallRoot = runningInstallRoot;
|
|
2779
|
+
module.exports.globalInstallRoot = globalInstallRoot;
|
|
2780
|
+
module.exports.npmGlobalUpgradeWouldMiss = npmGlobalUpgradeWouldMiss;
|
|
2781
|
+
module.exports.installedVersionAt = installedVersionAt;
|
package/core/providers/claude.js
CHANGED
|
@@ -12,14 +12,19 @@ const { createClaudeHookTransport } = require("./claude-hook");
|
|
|
12
12
|
// them. Verified 2026-09-23 on 2.1.280: the API reported claude-opus-5-5 as the
|
|
13
13
|
// serving model (canonicalModel claude-opus-5-5, 1M context), while 2.1.258
|
|
14
14
|
// returned "does not support this model; version 2.1.280 or newer is required".
|
|
15
|
+
// The opus 4.x entries (4-8, 4-7, 4-6) were dropped from the picker in 3.4.21 —
|
|
16
|
+
// superseded, and the menu had grown long enough to bury the current models.
|
|
17
|
+
// This removes the BUTTONS only: /model takes a typed name verbatim without
|
|
18
|
+
// consulting this list, so `/model claude-opus-4-8` still works as a rollback.
|
|
19
|
+
// The list is consulted on tapped mb: callbacks (core/actions.js), whose job is
|
|
20
|
+
// to reject a stale button from an old message — that is the intended effect.
|
|
21
|
+
// claude-sonnet-4-6 and claude-haiku-4-5 STAY: they are the tier models for
|
|
22
|
+
// utility work (see tierModel below), not just picker entries.
|
|
15
23
|
const CLAUDE_MODELS = Object.freeze([
|
|
16
24
|
"claude-fable-5-1",
|
|
17
25
|
"claude-fable-5",
|
|
18
26
|
"claude-opus-5-5",
|
|
19
27
|
"claude-opus-5",
|
|
20
|
-
"claude-opus-4-8",
|
|
21
|
-
"claude-opus-4-7",
|
|
22
|
-
"claude-opus-4-6",
|
|
23
28
|
"claude-sonnet-4-6",
|
|
24
29
|
"claude-haiku-4-5-20251001",
|
|
25
30
|
]);
|
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
|
-
|
|
1703
|
-
|
|
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
|
|
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…" :
|
|
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.
|
|
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": {
|
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
"test:kazee-call": "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",
|
|
11
11
|
"setup": "node setup.js",
|
|
12
12
|
"start": "node bot.js",
|
|
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-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"
|
|
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-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",
|
package/test-provider-claude.js
CHANGED
|
@@ -49,6 +49,15 @@ assert.ok(provider.modelChoices().includes("claude-fable-5-1"));
|
|
|
49
49
|
assert.ok(provider.modelChoices().includes("claude-opus-5-5"), "opus 5.5 is offered in the model picker");
|
|
50
50
|
assert.ok(provider.modelChoices().includes("claude-fable-5"));
|
|
51
51
|
assert.ok(provider.modelChoices().includes("claude-sonnet-4-6"));
|
|
52
|
+
// 3.4.21: the superseded opus 4.x entries are gone from the picker. They are
|
|
53
|
+
// BUTTONS only - /model takes a typed name verbatim (core/handlers.js reads
|
|
54
|
+
// tail without consulting this list), so the rollback path survives removal.
|
|
55
|
+
for (const retired of ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"]) {
|
|
56
|
+
assert.ok(!provider.modelChoices().includes(retired), `retired model still offered: ${retired}`);
|
|
57
|
+
}
|
|
58
|
+
// These two are NOT picker garnish - they are what tierModel returns for
|
|
59
|
+
// utility work, so they must survive any picker trim.
|
|
60
|
+
assert.ok(provider.modelChoices().includes("claude-haiku-4-5-20251001"), "haiku stays: it is the low tier");
|
|
52
61
|
|
|
53
62
|
// The SHIPPED default, asserted on the real module rather than the fixture above,
|
|
54
63
|
// so changing the constant cannot pass unnoticed. Read from source because the
|
|
@@ -65,8 +74,15 @@ assert.match(
|
|
|
65
74
|
// stop a default bump from silently repricing recall, pack review and the enforcer.
|
|
66
75
|
assert.strictEqual(claudeProvider.tierModel("low"), "claude-haiku-4-5-20251001", "low-tier utility work stays on haiku");
|
|
67
76
|
assert.strictEqual(claudeProvider.tierModel("medium"), "claude-sonnet-4-6", "medium-tier utility work stays on sonnet");
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
// Guard on the RESOLVED config, not process.env: loadEnv folds ~/.open-claudia/.env
|
|
78
|
+
// over process.env, so an operator pin lives in config.config and never appears in
|
|
79
|
+
// process.env. The 3.4.20 version of this check tested the wrong one and passed
|
|
80
|
+
// locally on a machine that was in fact pinned - fixed in 3.4.21.
|
|
81
|
+
if (!require("./core/config").config.CLAUDE_MODEL) {
|
|
82
|
+
assert.ok(
|
|
83
|
+
CLAUDE_MODELS.includes(claudeProvider.defaultModel()),
|
|
84
|
+
"with no operator override, the resolved default must be a model the picker offers",
|
|
85
|
+
);
|
|
70
86
|
}
|
|
71
87
|
assert.deepStrictEqual(provider.compatibilityStatus(), {
|
|
72
88
|
state: "compatible",
|
|
@@ -487,7 +487,20 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
487
487
|
// entrypoint never writes one, so the constant is what pods resolve; the pod
|
|
488
488
|
// image installs Claude Code unpinned, which must be >= 2.1.280 or the model is
|
|
489
489
|
// refused with an API 400 rather than degrading.
|
|
490
|
-
|
|
490
|
+
// 3.4.21 approved by Sumeet 2026-09-23 ("I think we can get rid of the 4th gen",
|
|
491
|
+
// then "Your call" when asked to confirm the scope): drop claude-opus-4-8, 4-7
|
|
492
|
+
// and 4-6 from the Claude picker. Deliberately NOT dropped: claude-sonnet-4-6
|
|
493
|
+
// and claude-haiku-4-5, which tierModel returns for medium/low utility work -
|
|
494
|
+
// removing those would push recall, pack review and the enforcer onto opus.
|
|
495
|
+
// Removal is cosmetic by design: /model accepts a typed name without checking
|
|
496
|
+
// the list, so `/model claude-opus-4-8` remains a working rollback.
|
|
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");
|
|
491
504
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
492
505
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
493
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
|
-
|
|
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");
|