@inetafrica/open-claudia 3.2.1 → 3.2.3
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 +16 -0
- package/bin/cli.js +1 -1
- package/bin/schedule.js +24 -4
- package/channels/kazee/call/session.js +3 -0
- package/core/handlers.js +6 -2
- package/core/loopback.js +4 -0
- package/core/providers/utility-policy.js +4 -1
- package/core/router.js +28 -4
- package/core/runner.js +14 -3
- package/core/scheduler.js +60 -21
- package/core/system-prompt.js +1 -0
- package/package.json +2 -2
- package/test-provider-language.js +15 -1
- package/test-provider-scheduler.js +80 -0
- package/test-utility-provider-policy.js +22 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.2.3 — No watch can end silently: every watch carries a timeout that fires as a wake-up
|
|
4
|
+
|
|
5
|
+
- **The silent-failure hole in watches.** A watch whose check never exits 0 polled forever without a sound — if the watched thing hung, was misconfigured (the unauthenticated-`glab` watch from 2026-08-12 would never have fired), or simply never happened, the agent slept through it and the user had to notice the silence themselves. The working pattern was to pair every watch with a manual backup wakeup; that safety net is now built in.
|
|
6
|
+
- **Every new watch gets a hard expiry.** `watch-add` grows `--timeout <duration|ISO>` (90m, 6h, 1d, or an ISO datetime), default **2h**, stamped as `expiresAt` on the job. On the first poll tick at or past the deadline where the check *still* fails, the watch FIRES anyway: the woken prompt opens with `[Watch expired ...]` plus an explicit "the watched condition did NOT happen — this is a deadline wake-up, not a trigger" brief, and the chat advisory reads `⏰ Watch "label" timed out without triggering — waking anyway`. A check that passes exactly on the deadline tick still reports as a normal trigger.
|
|
7
|
+
- **Expiry ends the watch's window.** An expiry fire is one-shot even for `--repeat` watches (the woken agent re-arms if still wanted), and the expiry mark is persisted so a failed expiry fire retries with the same semantics across restarts — while a retry whose condition came true in the meantime reports as a real trigger, not an expiry. `watch-list` now shows each watch's expiry (or `EXPIRED (fires on next poll)`); the global `/watch off` master switch still pauses everything, expiry included, until re-enabled.
|
|
8
|
+
- **Backward compatible by choice.** Watches persisted before this release carry no `expiresAt` and keep their old poll-forever semantics rather than being retro-expired on upgrade (a pre-upgrade watch older than the default would otherwise fire the instant the bot restarts). `watch-list` flags these as `no expiry (pre-timeout watch)`.
|
|
9
|
+
- Regression coverage in `test-provider-scheduler.js`: future-expiry idle stays silent, lapsed-expiry fire (header, brief, advisory, removal despite `--repeat`), deadline-tick pass is a trigger, default and explicit expiry stamping, and static end-to-end wiring (scheduler default → loopback passthrough → CLI flag).
|
|
10
|
+
|
|
11
|
+
## v3.2.2 — The chip thinks from first contact, keeps thinking through the tools, and the dedup gate actually runs
|
|
12
|
+
|
|
13
|
+
- **The dead minute before the chip appeared.** The typing heartbeat only started inside `executeAdmittedRun` — but a turn-bound message first crosses the enforcer's inbound triage (an LLM call), and voice notes add a media download plus STT before that. All of it ran with zero presence: on a slow triage the chat sat silent looking ignored, then "thinking" popped up long after the work had begun. The router now starts the heartbeat the moment a turn-bound message arrives (`startPreTurnTyping` in all five inbound handlers — text, voice, audio, photo, document), covering triage, download, and transcription. Non-turn traffic (wizards, vault prompts, credential pastes, commands) never shows a false chip because the start sits after those gates.
|
|
14
|
+
- **The chip decayed to a stale "typing" during tool work.** Any `tool_start` permanently cleared `thinkingPhase`, so from the first tool call onward every 4s tick sent a stateless `typing:start` — clients rendered "typing…" through minutes of tool execution, and "thinking" never came back within a turn. Terminal and text events (`text_delta`, `text_final`, `result`, `error`) still end the thinking phase, but `tool_start` now re-enters it: streaming text reads as "typing", tool stretches read as "thinking" — which is what they are. (Kazee mobile renders a stateless tick as "typing" and auto-clears 5s after the last tick, so the 4s cadence keeps the chip alive and the state accurate on old and new clients alike.)
|
|
15
|
+
- **Handoff without a scramble.** Two starts can now coexist safely: `startTypingHeartbeat` is a singleton per chat state — a new start supersedes the old interval, and every stopper is ownership-aware, acting only while its own interval is current. The router's pre-turn heartbeat hands off to the run's own heartbeat at `executeAdmittedRun` with no gap and no double interval; a stale stopper (a router `finally` after handoff, a superseded run) is a structural no-op. `/stop`, which clears the interval directly and thereby orphans the run's stopper, now emits the explicit `typing:stop` itself in both branches. `test-typing-heartbeat.js` joins the release chain: immediate thinking tick, supersede/ownership semantics, no orphaned timers across a full tick window, and the event-steering contract pinned.
|
|
16
|
+
- **The journal dedup gate never actually ran.** v3.2.0's write-time dedup adjudicator (A1) calls its utility with `purpose: "dedup"` — a purpose `utility-policy` never registered, so every invocation died at validation with `INVALID_UTILITY_PURPOSE` and failed open, silently, by design. Log-proven on the local install. "dedup" is now a first-class purpose riding the review pipeline: `MEMORY_PROVIDER` steers it and `PACK_DEDUP_MODEL` is honored as its model override (both the prefix and the legacy Claude alias, matching what the adjudicator already passes). Pinned in `test-utility-provider-policy.js`.
|
|
17
|
+
- **Not changed, on purpose:** the 5× duplicate voice notes from 2026-08-09 were client multi-tap creating *distinct* messages (fixed client-side in kazee-chat-mobile v3.25.23's optimistic-upload guard); the router's existing `isDuplicate` LRU already covers true socket redeliveries by message id, so no bot-side dedup was added.
|
|
18
|
+
|
|
3
19
|
## v3.2.1 — Speech as a general capability, sockets that survive rejection, and calls that greet back
|
|
4
20
|
|
|
5
21
|
- **A rejected handshake no longer strands a bot forever.** socket.io-client permanently stops reconnecting when the *server* rejects a handshake (auth middleware `next(new Error())`): `connect_error` fires with `socket.active === false` and the manager never dials again. During the 2026-08-08 outage chat-central booted before Central's MongoDB and rejected every handshake for a few minutes — terminally stranding the whole bot fleet, pods "Running" but deaf until manually restarted. New `core/socket-reconnect.js` `wireTerminalReconnect()` watches for exactly that terminal state and drives a manual `connect()` with 2s→30s capped backoff; transport-level failures (where the manager retries on its own, `socket.active === true`) are left alone. A deliberate client-side `disconnect()` is intercepted — a wedged socket emits no event for it — and cancels retries until `connect()` re-arms them. Wired into both the kazee and spaces sockets; `test-socket-reconnect.js` joins the pretest chain.
|
package/bin/cli.js
CHANGED
|
@@ -588,7 +588,7 @@ Background work (only inside an active bot-spawned task):
|
|
|
588
588
|
open-claudia cron-add "<sched>" "<prompt>" Recurring; each fire uses your current provider/model selection
|
|
589
589
|
open-claudia cron-list List all wakeups + crons on this channel
|
|
590
590
|
open-claudia cron-remove <id> Remove a scheduled job
|
|
591
|
-
open-claudia watch-add "<check>" "<prompt>" Silent monitor: poll a shell check, wake
|
|
591
|
+
open-claudia watch-add "<check>" "<prompt>" Silent monitor: poll a shell check, wake when it exits 0 or on --timeout expiry (default 2h)
|
|
592
592
|
open-claudia watch-list List watches on this channel + master switch state
|
|
593
593
|
open-claudia watch-remove <id> Remove a watch
|
|
594
594
|
open-claudia watch-on | watch-off Global master switch for all watches (owner)
|
package/bin/schedule.js
CHANGED
|
@@ -16,11 +16,15 @@ Crons (recurring):
|
|
|
16
16
|
open-claudia cron-remove <id> # remove by id from cron-list
|
|
17
17
|
|
|
18
18
|
Watches (silent background monitor — polls a shell check, wakes you only on a hit):
|
|
19
|
-
open-claudia watch-add "<check-cmd>" "<wake-prompt>" [--label "<label>"] [--every "<cron>"] [--repeat]
|
|
19
|
+
open-claudia watch-add "<check-cmd>" "<wake-prompt>" [--label "<label>"] [--every "<cron>"] [--repeat] [--timeout <when>]
|
|
20
20
|
<check-cmd>: a shell command polled on a schedule (default every minute).
|
|
21
21
|
exit 0 = condition met → wake the agent with <wake-prompt>.
|
|
22
22
|
non-zero = still idle → nothing happens, nothing hits the chat.
|
|
23
23
|
--repeat: keep polling after it fires (default: fire once, then remove).
|
|
24
|
+
--timeout: how long to keep polling — a duration (90m, 6h, 1d) or ISO
|
|
25
|
+
datetime. Default 2h. A watch that reaches its timeout without
|
|
26
|
+
the check ever passing FIRES anyway on the next poll, with the
|
|
27
|
+
prompt marked "Watch expired" — no watch can end silently.
|
|
24
28
|
open-claudia watch-list # watches on this channel + master switch state
|
|
25
29
|
open-claudia watch-remove <id> # remove a watch by id
|
|
26
30
|
open-claudia watch-on / watch-off # global master switch for ALL watches (owner)
|
|
@@ -141,18 +145,29 @@ async function runWatchAdd(args) {
|
|
|
141
145
|
const label = takeFlag(rest, "label");
|
|
142
146
|
const schedule = takeFlag(rest, "every");
|
|
143
147
|
const project = takeFlag(rest, "project");
|
|
148
|
+
const timeout = takeFlag(rest, "timeout");
|
|
144
149
|
const repeatIdx = rest.indexOf("--repeat");
|
|
145
150
|
const repeat = repeatIdx >= 0;
|
|
146
151
|
if (repeat) rest.splice(repeatIdx, 1);
|
|
152
|
+
let expiresAt = null;
|
|
153
|
+
if (timeout !== null) {
|
|
154
|
+
expiresAt = parseWhen(timeout);
|
|
155
|
+
if (!expiresAt) {
|
|
156
|
+
console.error(`Could not parse --timeout: "${timeout}". Use a duration (90m, 6h, 1d) or ISO datetime.`);
|
|
157
|
+
process.exit(2);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
147
160
|
const check = rest[0];
|
|
148
161
|
const prompt = rest.slice(1).join(" ");
|
|
149
|
-
if (!check || !prompt) { console.error("Usage: watch-add \"<check-cmd>\" \"<wake-prompt>\" [--every \"<cron>\"] [--repeat]"); process.exit(2); }
|
|
162
|
+
if (!check || !prompt) { console.error("Usage: watch-add \"<check-cmd>\" \"<wake-prompt>\" [--every \"<cron>\"] [--repeat] [--timeout <when>]"); process.exit(2); }
|
|
150
163
|
try {
|
|
151
164
|
const request = { check, prompt, repeat, label: label || prompt.slice(0, 60) };
|
|
152
165
|
if (schedule) request.schedule = schedule;
|
|
153
166
|
if (project) request.project = project;
|
|
167
|
+
if (expiresAt) request.expiresAt = expiresAt;
|
|
154
168
|
const res = await postJson("watch-add", request);
|
|
155
|
-
|
|
169
|
+
const expiry = res.job.expiresAt ? `, expires ${new Date(res.job.expiresAt).toISOString()}` : "";
|
|
170
|
+
console.log(`Added watch ${res.job.id} (${repeat ? "repeating" : "one-shot"}, polls "${res.job.schedule}"${expiry}). Silent until the check passes; fires as "Watch expired" if the timeout lapses first.`);
|
|
156
171
|
process.exit(0);
|
|
157
172
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
158
173
|
}
|
|
@@ -165,7 +180,12 @@ async function runWatchList() {
|
|
|
165
180
|
if (list.length === 0) { console.log("No watches on this channel."); process.exit(0); }
|
|
166
181
|
for (const w of list) {
|
|
167
182
|
const mode = w.repeat ? "repeating" : "one-shot";
|
|
168
|
-
|
|
183
|
+
const expiry = Number(w.expiresAt) > 0
|
|
184
|
+
? (Date.now() >= Number(w.expiresAt)
|
|
185
|
+
? " EXPIRED (fires on next poll)"
|
|
186
|
+
: ` expires ${new Date(Number(w.expiresAt)).toISOString()}`)
|
|
187
|
+
: " no expiry (pre-timeout watch)";
|
|
188
|
+
console.log(`${w.id} ${mode} polls "${w.schedule}"${expiry} check: ${w.check} → ${w.label || "(no label)"}`);
|
|
169
189
|
}
|
|
170
190
|
process.exit(0);
|
|
171
191
|
} catch (e) { console.error(e.message); process.exit(1); }
|
|
@@ -107,6 +107,9 @@ class KazeeCallSession {
|
|
|
107
107
|
const created = await this.janus.message(this.publisherHandle, {
|
|
108
108
|
request: "create", room: this.roomId, publishers: 6, audiocodec: "opus",
|
|
109
109
|
description: `Call room ${this.roomId}`, permanent: false,
|
|
110
|
+
// Hidden from videoroom "list": the control plane is internet-reachable,
|
|
111
|
+
// so a listable room is eavesdroppable. Join-by-roomId is unaffected.
|
|
112
|
+
is_private: true,
|
|
110
113
|
});
|
|
111
114
|
const cdata = created?.plugindata?.data || {};
|
|
112
115
|
const exists = Number(cdata.error_code) === 427 || /already exists/i.test(String(cdata.error || ""));
|
package/core/handlers.js
CHANGED
|
@@ -1611,14 +1611,18 @@ register({
|
|
|
1611
1611
|
// Stop pending preview flushes but keep statusMessageId/Channel: the
|
|
1612
1612
|
// killed run's delivery unwind edits the preview to drop its footer.
|
|
1613
1613
|
if (state.streamInterval) { clearTimeout(state.streamInterval); state.streamInterval = null; }
|
|
1614
|
-
|
|
1614
|
+
// Clearing the interval here orphans the run's own stopper (ownership
|
|
1615
|
+
// check makes it a no-op), so the explicit typing:stop must go out now.
|
|
1616
|
+
if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; state.thinkingPhase = false; }
|
|
1617
|
+
try { currentAdapter()?.typingStop?.(currentChannelId()); } catch (_) {}
|
|
1615
1618
|
await sideCancellation;
|
|
1616
1619
|
await send(`Cancelled.${queuedNote()}`);
|
|
1617
1620
|
} else if (state.preparingRun) {
|
|
1618
1621
|
// Turn is mid-recall/prompt-build — no process to kill yet. Flag it so
|
|
1619
1622
|
// the runner's pre-spawn shouldAbort checkpoint bails before spawning.
|
|
1620
1623
|
state.cancelRequested = true;
|
|
1621
|
-
if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; }
|
|
1624
|
+
if (state.typingHeartbeat) { clearInterval(state.typingHeartbeat); state.typingHeartbeat = null; state.thinkingPhase = false; }
|
|
1625
|
+
try { currentAdapter()?.typingStop?.(currentChannelId()); } catch (_) {}
|
|
1622
1626
|
await sideCancellation;
|
|
1623
1627
|
await send(`Cancelled.${queuedNote()}`);
|
|
1624
1628
|
} else if (await sideCancellation) await send("Cancelled side response.");
|
package/core/loopback.js
CHANGED
|
@@ -347,6 +347,10 @@ async function handleJson(req, res, url, kind) {
|
|
|
347
347
|
label: String(payload.label || payload.prompt).slice(0, 60),
|
|
348
348
|
schedule: payload.schedule ? String(payload.schedule) : undefined,
|
|
349
349
|
repeat: !!payload.repeat,
|
|
350
|
+
// Absent/invalid → the scheduler stamps its default watch timeout.
|
|
351
|
+
expiresAt: Number.isFinite(Number(payload.expiresAt)) && Number(payload.expiresAt) > 0
|
|
352
|
+
? Number(payload.expiresAt)
|
|
353
|
+
: undefined,
|
|
350
354
|
source: payload.source || "agent",
|
|
351
355
|
sessionId: payload.sessionId || null,
|
|
352
356
|
previousSessionId: payload.previousSessionId || null,
|
|
@@ -4,7 +4,7 @@ const config = require("../config");
|
|
|
4
4
|
|
|
5
5
|
const PROVIDER_IDS = Object.freeze(["claude", "codex"]);
|
|
6
6
|
const PROVIDER_ID_SET = new Set(PROVIDER_IDS);
|
|
7
|
-
const PURPOSES = Object.freeze(["subagent", "recall", "review", "dream", "introspection", "enforcer"]);
|
|
7
|
+
const PURPOSES = Object.freeze(["subagent", "recall", "review", "dedup", "dream", "introspection", "enforcer"]);
|
|
8
8
|
const PURPOSE_SET = new Set(PURPOSES);
|
|
9
9
|
const TIERS = Object.freeze(["low", "medium", "high"]);
|
|
10
10
|
const TIER_SET = new Set(TIERS);
|
|
@@ -12,6 +12,7 @@ const PURPOSE_PROVIDER_ENV = Object.freeze({
|
|
|
12
12
|
subagent: "SUBAGENT_PROVIDER",
|
|
13
13
|
recall: "RECALL_PROVIDER",
|
|
14
14
|
review: "MEMORY_PROVIDER",
|
|
15
|
+
dedup: "MEMORY_PROVIDER",
|
|
15
16
|
dream: "DREAM_PROVIDER",
|
|
16
17
|
introspection: "DREAM_PROVIDER",
|
|
17
18
|
enforcer: "ENFORCER_PROVIDER",
|
|
@@ -20,12 +21,14 @@ const PURPOSE_MODEL_PREFIX = Object.freeze({
|
|
|
20
21
|
subagent: "SUBAGENT_MODEL",
|
|
21
22
|
recall: "RECALL_MODEL",
|
|
22
23
|
review: "PACK_REVIEW_MODEL",
|
|
24
|
+
dedup: "PACK_DEDUP_MODEL",
|
|
23
25
|
dream: "DREAM_MODEL",
|
|
24
26
|
introspection: "DREAM_MODEL",
|
|
25
27
|
enforcer: "ENFORCER_MODEL",
|
|
26
28
|
});
|
|
27
29
|
const LEGACY_CLAUDE_MODEL_ENV = Object.freeze({
|
|
28
30
|
review: "PACK_REVIEW_MODEL",
|
|
31
|
+
dedup: "PACK_DEDUP_MODEL",
|
|
29
32
|
dream: "DREAM_MODEL",
|
|
30
33
|
introspection: "DREAM_MODEL",
|
|
31
34
|
enforcer: "ENFORCER_MODEL",
|
package/core/router.js
CHANGED
|
@@ -12,7 +12,7 @@ const { isChatAuthorized } = require("./access");
|
|
|
12
12
|
const introFlow = require("./intro-flow");
|
|
13
13
|
const { send, deleteMessage } = require("./io");
|
|
14
14
|
const { currentState, saveState, autoLinkOwnerChannel, getActiveProvider, getProjectKey } = require("./state");
|
|
15
|
-
const { admitRunContext, runAgent } = require("./runner");
|
|
15
|
+
const { admitRunContext, runAgent, startTypingHeartbeat } = require("./runner");
|
|
16
16
|
const { transcribeAudio, cloneVoiceFromClip } = require("./media");
|
|
17
17
|
const { vault } = require("./vault-store");
|
|
18
18
|
const { isOnboarded, handleOnboarding } = require("./onboarding");
|
|
@@ -203,9 +203,20 @@ async function admitGuardedTurn(askText, envelope, state = currentState()) {
|
|
|
203
203
|
return !(triage && triage.held);
|
|
204
204
|
}
|
|
205
205
|
|
|
206
|
+
// Chip-on-receipt: enforcer triage, media download, and STT all run before
|
|
207
|
+
// runAgent — many seconds with no feedback. Start the "thinking" heartbeat as
|
|
208
|
+
// soon as a turn-bound message arrives; the run's own heartbeat supersedes it
|
|
209
|
+
// at executeAdmittedRun, after which this stopper is a no-op. Skipped while a
|
|
210
|
+
// run is active — the chip is already owned by that turn.
|
|
211
|
+
function startPreTurnTyping(state) {
|
|
212
|
+
if (state.runningProcess || state.preparingRun) return () => {};
|
|
213
|
+
try { return startTypingHeartbeat(state); } catch (_) { return () => {}; }
|
|
214
|
+
}
|
|
215
|
+
|
|
206
216
|
async function handleVoice(envelope) {
|
|
207
217
|
if (isDuplicate(envelope)) return;
|
|
208
218
|
const state = currentState();
|
|
219
|
+
let stopPreTurnTyping = () => {};
|
|
209
220
|
try {
|
|
210
221
|
const media = envelope.media?.[0];
|
|
211
222
|
if (!media) return;
|
|
@@ -223,7 +234,7 @@ async function handleVoice(envelope) {
|
|
|
223
234
|
purpose: "foreground",
|
|
224
235
|
attachments: [{ type: "voice", mimeType: media.mimeType || "audio/ogg", fileName: media.fileName || null }],
|
|
225
236
|
});
|
|
226
|
-
|
|
237
|
+
stopPreTurnTyping = startPreTurnTyping(state);
|
|
227
238
|
const tStart = Date.now();
|
|
228
239
|
const oggPath = await envelope.adapter.downloadMedia(media);
|
|
229
240
|
const tAfterDownload = Date.now();
|
|
@@ -243,11 +254,13 @@ async function handleVoice(envelope) {
|
|
|
243
254
|
voiceStream: envelope.adapter?.type === "voice",
|
|
244
255
|
});
|
|
245
256
|
} catch (err) { await send(`Voice failed: ${err.message}`); }
|
|
257
|
+
finally { stopPreTurnTyping(); }
|
|
246
258
|
}
|
|
247
259
|
|
|
248
260
|
async function handleAudio(envelope) {
|
|
249
261
|
if (isDuplicate(envelope)) return;
|
|
250
262
|
const state = currentState();
|
|
263
|
+
let stopPreTurnTyping = () => {};
|
|
251
264
|
try {
|
|
252
265
|
const media = envelope.media?.[0];
|
|
253
266
|
if (!media) return;
|
|
@@ -260,7 +273,7 @@ async function handleAudio(envelope) {
|
|
|
260
273
|
purpose: "foreground",
|
|
261
274
|
attachments: [{ type: "audio", mimeType: media.mimeType || null, fileName: media.fileName || null }],
|
|
262
275
|
});
|
|
263
|
-
|
|
276
|
+
stopPreTurnTyping = startPreTurnTyping(state);
|
|
264
277
|
const p = await envelope.adapter.downloadMedia(media);
|
|
265
278
|
const t = await transcribeAudio(p);
|
|
266
279
|
try { fs.unlinkSync(p); } catch (e) {}
|
|
@@ -272,6 +285,7 @@ async function handleAudio(envelope) {
|
|
|
272
285
|
attachments: [{ type: "audio", path: p, mimeType: media.mimeType || null, fileName: media.fileName || null }],
|
|
273
286
|
});
|
|
274
287
|
} catch (err) { await send(`Audio failed: ${err.message}`); }
|
|
288
|
+
finally { stopPreTurnTyping(); }
|
|
275
289
|
}
|
|
276
290
|
|
|
277
291
|
// A clip the owner sent right after /voice clone. Upload it to the speech
|
|
@@ -297,6 +311,7 @@ async function captureVoiceClone(envelope, media) {
|
|
|
297
311
|
async function handlePhoto(envelope) {
|
|
298
312
|
if (isDuplicate(envelope)) return;
|
|
299
313
|
const state = currentState();
|
|
314
|
+
let stopPreTurnTyping = () => {};
|
|
300
315
|
try {
|
|
301
316
|
const items = envelope.media || [];
|
|
302
317
|
if (!items.length) return;
|
|
@@ -304,6 +319,7 @@ async function handlePhoto(envelope) {
|
|
|
304
319
|
purpose: "foreground",
|
|
305
320
|
attachments: items.map((item) => ({ type: "image", mimeType: item.mimeType || "image/*", fileName: item.fileName || null })),
|
|
306
321
|
});
|
|
322
|
+
stopPreTurnTyping = startPreTurnTyping(state);
|
|
307
323
|
const paths = [];
|
|
308
324
|
for (const m of items) paths.push(await envelope.adapter.downloadMedia(m));
|
|
309
325
|
const caption = envelope.caption || "Describe this image. If code/UI/error — explain and fix.";
|
|
@@ -327,11 +343,13 @@ async function handlePhoto(envelope) {
|
|
|
327
343
|
})),
|
|
328
344
|
});
|
|
329
345
|
} catch (err) { await send(`Image failed: ${err.message}`); }
|
|
346
|
+
finally { stopPreTurnTyping(); }
|
|
330
347
|
}
|
|
331
348
|
|
|
332
349
|
async function handleDocument(envelope) {
|
|
333
350
|
if (isDuplicate(envelope)) return;
|
|
334
351
|
const state = currentState();
|
|
352
|
+
let stopPreTurnTyping = () => {};
|
|
335
353
|
try {
|
|
336
354
|
const media = envelope.media?.[0];
|
|
337
355
|
if (!media) return;
|
|
@@ -342,6 +360,7 @@ async function handleDocument(envelope) {
|
|
|
342
360
|
purpose: "foreground",
|
|
343
361
|
attachments: [{ type: "document", mimeType: media.mimeType || null, fileName: media.fileName || null }],
|
|
344
362
|
});
|
|
363
|
+
stopPreTurnTyping = startPreTurnTyping(state);
|
|
345
364
|
const savePath = await envelope.adapter.downloadMedia(media);
|
|
346
365
|
const fileName = media.fileName || path.basename(savePath);
|
|
347
366
|
const mime = media.mimeType || "";
|
|
@@ -361,6 +380,7 @@ async function handleDocument(envelope) {
|
|
|
361
380
|
attachments: [{ type: isImage ? "image" : "document", path: savePath, mimeType: mime, fileName }],
|
|
362
381
|
});
|
|
363
382
|
} catch (err) { await send(`Failed: ${err.message}`); }
|
|
383
|
+
finally { stopPreTurnTyping(); }
|
|
364
384
|
}
|
|
365
385
|
|
|
366
386
|
// Provider-agnostic capture of a credential pasted into chat with no auth
|
|
@@ -605,7 +625,11 @@ async function handleText(envelope) {
|
|
|
605
625
|
// stop, instead of forwarding a secret to the model (Codex refuses those).
|
|
606
626
|
if (await captureLooseCredential(text.trim(), envelope)) return;
|
|
607
627
|
|
|
608
|
-
|
|
628
|
+
const stopPreTurnTyping = startPreTurnTyping(state);
|
|
629
|
+
let admitted;
|
|
630
|
+
try { admitted = await admitGuardedTurn(text, envelope, state); }
|
|
631
|
+
catch (e) { stopPreTurnTyping(); throw e; }
|
|
632
|
+
if (!admitted) { stopPreTurnTyping(); return; }
|
|
609
633
|
|
|
610
634
|
const runContext = admitRunContext(text, state.currentSession.dir, envelope.messageId, { purpose: "foreground" });
|
|
611
635
|
const attachments = [];
|
package/core/runner.js
CHANGED
|
@@ -1725,17 +1725,24 @@ function decorateForegroundInvocation(invocation, runContext, bundle) {
|
|
|
1725
1725
|
return { ...invocation, env };
|
|
1726
1726
|
}
|
|
1727
1727
|
|
|
1728
|
+
// Singleton per chat state: a new start supersedes any running heartbeat
|
|
1729
|
+
// (pre-turn "thinking" started at message receipt hands off to the run's own
|
|
1730
|
+
// heartbeat here). A stopper only acts while its own interval is current, so
|
|
1731
|
+
// a stale stopper from a superseded start can never kill a newer heartbeat.
|
|
1728
1732
|
function startTypingHeartbeat(state, store) {
|
|
1729
1733
|
const adapter = store?.adapter || currentAdapter();
|
|
1730
1734
|
const channelId = store?.channelId || currentChannelId();
|
|
1731
1735
|
if (!adapter || !channelId) return () => {};
|
|
1736
|
+
if (state.typingHeartbeat) clearInterval(state.typingHeartbeat);
|
|
1732
1737
|
const tick = () => adapter.typing(channelId, state.thinkingPhase ? "thinking" : undefined).catch(() => {});
|
|
1733
1738
|
state.thinkingPhase = true;
|
|
1734
1739
|
tick();
|
|
1735
|
-
|
|
1740
|
+
const interval = setInterval(tick, 4000);
|
|
1741
|
+
state.typingHeartbeat = interval;
|
|
1736
1742
|
return () => {
|
|
1743
|
+
if (state.typingHeartbeat !== interval) return;
|
|
1737
1744
|
state.thinkingPhase = false;
|
|
1738
|
-
|
|
1745
|
+
clearInterval(interval);
|
|
1739
1746
|
state.typingHeartbeat = null;
|
|
1740
1747
|
try { adapter.typingStop?.(channelId); } catch (_) {}
|
|
1741
1748
|
};
|
|
@@ -1880,7 +1887,10 @@ function createForegroundService({ state, store, stopTyping, capture, ownsAdmiss
|
|
|
1880
1887
|
observer.onRecallSurfaced(bundle.recallMetadata);
|
|
1881
1888
|
},
|
|
1882
1889
|
onEvent(event, runContext) {
|
|
1883
|
-
|
|
1890
|
+
// Streaming text reads as "typing"; tool work re-enters "thinking" so
|
|
1891
|
+
// the chip doesn't decay to a stale "typing" through long tool stretches.
|
|
1892
|
+
if (["text_delta", "text_final", "result", "error"].includes(event.type)) state.thinkingPhase = false;
|
|
1893
|
+
else if (event.type === "tool_start") state.thinkingPhase = true;
|
|
1884
1894
|
if (event.type === "tool_start" && ["Shell", "Bash"].includes(event.name)) {
|
|
1885
1895
|
const command = typeof event.detail === "string" ? event.detail : event.detail?.command;
|
|
1886
1896
|
if (command) { try { require("./tool-guard").noteRawOp(command); } catch (_) {} }
|
|
@@ -2248,6 +2258,7 @@ module.exports = {
|
|
|
2248
2258
|
runAgent,
|
|
2249
2259
|
runAgentCapture,
|
|
2250
2260
|
runAgentSilent,
|
|
2261
|
+
startTypingHeartbeat,
|
|
2251
2262
|
shouldRetryRefusedRun,
|
|
2252
2263
|
quarantineRefusedRun,
|
|
2253
2264
|
refusalRetryContext,
|
package/core/scheduler.js
CHANGED
|
@@ -43,6 +43,10 @@ const RETRY_DELAY_MS = 30 * 1000;
|
|
|
43
43
|
const BUSY_DELAY_MS = 30 * 1000;
|
|
44
44
|
// A watch's `check` command is capped so a hung poll can't wedge the tick.
|
|
45
45
|
const WATCH_CHECK_TIMEOUT_MS = 30 * 1000;
|
|
46
|
+
// Every watch carries a hard expiry so none can idle-poll forever: when the
|
|
47
|
+
// deadline lapses with the check still failing, the next poll tick fires the
|
|
48
|
+
// wake anyway (marked expired) instead of skipping silently.
|
|
49
|
+
const DEFAULT_WATCH_TIMEOUT_MS = 2 * 60 * 60 * 1000;
|
|
46
50
|
const WATCH_LOG_FILE = process.env.OPEN_CLAUDIA_WATCH_LOG_FILE || path.join(CONFIG_DIR, "watch.log");
|
|
47
51
|
const WATCHES_ENABLED_FILE = process.env.OPEN_CLAUDIA_WATCHES_ENABLED_FILE || path.join(CONFIG_DIR, "watches-enabled");
|
|
48
52
|
|
|
@@ -107,6 +111,7 @@ function createSchedulerService(dependencies = {}) {
|
|
|
107
111
|
const retryDelayMs = Number.isFinite(dependencies.retryDelayMs) ? dependencies.retryDelayMs : RETRY_DELAY_MS;
|
|
108
112
|
const busyDelayMs = Number.isFinite(dependencies.busyDelayMs) ? dependencies.busyDelayMs : BUSY_DELAY_MS;
|
|
109
113
|
const missedGraceMs = Number.isFinite(dependencies.missedGraceMs) ? dependencies.missedGraceMs : MISSED_WAKEUP_GRACE_MS;
|
|
114
|
+
const watchTimeoutMs = Number.isFinite(dependencies.watchTimeoutMs) ? dependencies.watchTimeoutMs : DEFAULT_WATCH_TIMEOUT_MS;
|
|
110
115
|
const occurrenceIdFactory = dependencies.occurrenceIdFactory || (() => uniqueId("occ"));
|
|
111
116
|
const legacyCronsFile = dependencies.legacyCronsFile === undefined
|
|
112
117
|
? (dependencies.jobsStore ? null : CRONS_FILE)
|
|
@@ -273,22 +278,21 @@ function createSchedulerService(dependencies = {}) {
|
|
|
273
278
|
|
|
274
279
|
function wrappedPrompt(job, target) {
|
|
275
280
|
const firedAt = new Date(now()).toISOString();
|
|
276
|
-
const
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
281
|
+
const expired = !!(job.check && job.watchExpired);
|
|
282
|
+
const header = job.check
|
|
283
|
+
? (expired ? "Watch expired" : "Watch triggered")
|
|
284
|
+
: job.kind === "cron" ? "Scheduled cron" : "Scheduled wakeup";
|
|
285
|
+
const lines = [`[${header} "${job.label}" fired at ${firedAt}; occurrence ${job.occurrenceId}]`];
|
|
286
|
+
if (expired) {
|
|
287
|
+
lines.push("[This watch reached its timeout without its check ever passing — the watched condition did NOT happen. This is a deadline wake-up, not a trigger: investigate why the condition never became true, and arm a new watch if it is still worth waiting for.]");
|
|
283
288
|
}
|
|
284
|
-
if (target.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
job.prompt,
|
|
289
|
-
].join("\n");
|
|
289
|
+
if (target.fallbackFrom) {
|
|
290
|
+
lines.push(`[Archived provider-neutral brief: the originating ${target.fallbackFrom} provider is unavailable. Its native session is not being resumed; this explicitly configured fallback starts fresh.]`);
|
|
291
|
+
} else if (target.switchedFrom) {
|
|
292
|
+
lines.push(`[Provider now tracks your current selection (${target.provider}); this was scheduled under ${target.switchedFrom}, whose native session cannot cross providers, so this run starts a fresh thread.]`);
|
|
290
293
|
}
|
|
291
|
-
|
|
294
|
+
lines.push(job.prompt);
|
|
295
|
+
return lines.join("\n");
|
|
292
296
|
}
|
|
293
297
|
|
|
294
298
|
function unscheduleJob(id) {
|
|
@@ -394,7 +398,7 @@ function createSchedulerService(dependencies = {}) {
|
|
|
394
398
|
|
|
395
399
|
async function fireJob(idOrJob, options = {}) {
|
|
396
400
|
const requested = typeof idOrJob === "string" ? jobsStore.get(idOrJob) : idOrJob;
|
|
397
|
-
|
|
401
|
+
let job = requested?.id && jobsStore.get(requested.id) ? jobsStore.get(requested.id) : requested;
|
|
398
402
|
if (!job) return { ok: false, status: "missing" };
|
|
399
403
|
if (job.disabled || job.status === "dead-letter" || job.status === "archived") {
|
|
400
404
|
return { ok: false, status: job.status, skipped: true };
|
|
@@ -424,10 +428,35 @@ function createSchedulerService(dependencies = {}) {
|
|
|
424
428
|
catch (_) { check = { code: 1, timedOut: false }; }
|
|
425
429
|
finally { checksInFlight.delete(job.id); }
|
|
426
430
|
if (!check || check.code !== 0) {
|
|
427
|
-
|
|
428
|
-
|
|
431
|
+
// Expiry backstop: a watch whose deadline has lapsed fires the wake
|
|
432
|
+
// anyway (marked expired) so no watch can ever end silently. Watches
|
|
433
|
+
// persisted before expiry existed (no expiresAt) keep polling forever.
|
|
434
|
+
const expiresAt = Number(job.expiresAt || 0);
|
|
435
|
+
if (!(expiresAt > 0 && now() >= expiresAt)) {
|
|
436
|
+
logWatch(job, check && check.timedOut ? "idle (check timed out)" : `idle (check exit ${check ? check.code : "?"})`);
|
|
437
|
+
return { ok: false, status: "watch-idle", skipped: true };
|
|
438
|
+
}
|
|
439
|
+
logWatch(job, `expired without firing (check exit ${check ? check.code : "?"}) — waking agent`);
|
|
440
|
+
// Expiry ends the watch's window: one-shot removal even for --repeat
|
|
441
|
+
// (the woken agent re-arms if still wanted). Persisted so a retry of a
|
|
442
|
+
// failed expiry fire keeps the same semantics across restarts.
|
|
443
|
+
try {
|
|
444
|
+
const marked = jobsStore.update(job.id, { repeat: false, watchExpired: true });
|
|
445
|
+
if (marked) job = marked;
|
|
446
|
+
else { job.repeat = false; job.watchExpired = true; }
|
|
447
|
+
} catch (_) { job.repeat = false; job.watchExpired = true; }
|
|
448
|
+
} else {
|
|
449
|
+
logWatch(job, "triggered (check passed) — waking agent");
|
|
450
|
+
if (job.watchExpired) {
|
|
451
|
+
// A prior expiry fire failed and the condition came true before the
|
|
452
|
+
// retry: report it as a real trigger, not an expiry.
|
|
453
|
+
try {
|
|
454
|
+
const cleared = jobsStore.update(job.id, { watchExpired: false });
|
|
455
|
+
if (cleared) job = cleared;
|
|
456
|
+
else job.watchExpired = false;
|
|
457
|
+
} catch (_) { job.watchExpired = false; }
|
|
458
|
+
}
|
|
429
459
|
}
|
|
430
|
-
logWatch(job, "triggered (check passed) — waking agent");
|
|
431
460
|
}
|
|
432
461
|
|
|
433
462
|
const adapter = resolveAdapter(job);
|
|
@@ -504,7 +533,10 @@ function createSchedulerService(dependencies = {}) {
|
|
|
504
533
|
return { ok: false, status: "failed", error: stableFailure(error) };
|
|
505
534
|
}
|
|
506
535
|
|
|
507
|
-
|
|
536
|
+
const advisory = job.check
|
|
537
|
+
? (job.watchExpired ? `⏰ Watch "${job.label}" timed out without triggering — waking anyway` : `🔔 Watch "${job.label}" triggered`)
|
|
538
|
+
: `Wakeup: ${job.label}`;
|
|
539
|
+
try { await send(advisory); } catch (_) { /* advisory only */ }
|
|
508
540
|
let result;
|
|
509
541
|
try { result = await runInProvider(prompt, runContext.cwd, null, { ...runOptions, runContext }); }
|
|
510
542
|
catch (error) {
|
|
@@ -655,15 +687,21 @@ function createSchedulerService(dependencies = {}) {
|
|
|
655
687
|
}
|
|
656
688
|
|
|
657
689
|
// A watch is a cron carrying a shell `check`; it rides the migration-verified
|
|
658
|
-
// jobs store untouched (check/repeat survive normalizeRunnableJob
|
|
659
|
-
// fields). Default schedule polls every minute; default is one-shot.
|
|
690
|
+
// jobs store untouched (check/repeat/expiresAt survive normalizeRunnableJob
|
|
691
|
+
// as extra fields). Default schedule polls every minute; default is one-shot.
|
|
692
|
+
// Every new watch gets an expiry (caller's expiresAt or the default timeout)
|
|
693
|
+
// so it can never idle-poll silently forever.
|
|
660
694
|
function addWatch(spec) {
|
|
695
|
+
const requestedExpiry = Number(spec.expiresAt);
|
|
661
696
|
return addJob({
|
|
662
697
|
...spec,
|
|
663
698
|
kind: "cron",
|
|
664
699
|
schedule: spec.schedule || "* * * * *",
|
|
665
700
|
check: String(spec.check || "").trim(),
|
|
666
701
|
repeat: !!spec.repeat,
|
|
702
|
+
expiresAt: Number.isFinite(requestedExpiry) && requestedExpiry > 0
|
|
703
|
+
? requestedExpiry
|
|
704
|
+
: now() + watchTimeoutMs,
|
|
667
705
|
label: spec.label || String(spec.prompt || "watch").slice(0, 60),
|
|
668
706
|
});
|
|
669
707
|
}
|
|
@@ -710,6 +748,7 @@ module.exports = {
|
|
|
710
748
|
MISSED_WAKEUP_GRACE_MS,
|
|
711
749
|
RETRY_DELAY_MS,
|
|
712
750
|
BUSY_DELAY_MS,
|
|
751
|
+
DEFAULT_WATCH_TIMEOUT_MS,
|
|
713
752
|
createSchedulerService,
|
|
714
753
|
parseFallbacks,
|
|
715
754
|
terminalRunSucceeded,
|
package/core/system-prompt.js
CHANGED
|
@@ -255,6 +255,7 @@ Wake-ups and crons (real schedulers, not hallucinations — use them instead of
|
|
|
255
255
|
|
|
256
256
|
Watches (a silent background monitor — poll a condition WITHOUT waking every tick; wake only when it actually happens). Use this instead of a minute-by-minute cron whenever you'd otherwise fire a turn just to check "is it done yet?" and stay quiet most times:
|
|
257
257
|
- \`open-claudia watch-add "<check-cmd>" "<wake-prompt>"\` — the scheduler polls \`<check-cmd>\` as a shell command (default every minute). The poll is SILENT: nothing reaches the chat while the check keeps failing. The instant \`<check-cmd>\` exits 0, you wake in this same conversation with \`<wake-prompt>\`, exactly like a wakeup. Non-zero exit = still waiting = nothing happens. Default is ONE-SHOT (fires once, then removes itself); add \`--repeat\` to keep polling after each hit. \`--every "<5-field cron>"\` changes the poll cadence.
|
|
258
|
+
- Every watch has a hard timeout — \`--timeout <duration|ISO>\` (default 2h). A watch that reaches its timeout without the check ever passing FIRES anyway on the next poll, with the prompt marked "Watch expired" so you know the condition did NOT happen. No watch can end silently, so you no longer need a separate backup wakeup for every watch — but size \`--timeout\` honestly to the work being watched.
|
|
258
259
|
- \`open-claudia watch-list\` / \`open-claudia watch-remove <id>\` — inspect/cancel watches on this channel.
|
|
259
260
|
- The owner has a global master switch: \`/watch off\` pauses ALL watch polling (and \`/watch on\` resumes). If \`watch-list\` shows the switch OFF, watches won't fire until it's back on.
|
|
260
261
|
- Example: after kicking off a long external job, \`open-claudia watch-add "test -f /tmp/build-done || exit 1" "the build finished — verify and report"\` beats a wakeup guess, because it fires the moment it's actually done, not on a fixed timer. (Never use a watch to poll harness-tracked work you started — that notifies you automatically. Watches are for EXTERNAL state the harness can't see.)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.2.
|
|
3
|
+
"version": "3.2.3",
|
|
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-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.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-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 && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-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"
|
|
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 && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bot.js",
|
|
@@ -351,7 +351,21 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
|
|
|
351
351
|
// fix from the chat-central outage, the call-platform batch (outgoing calls,
|
|
352
352
|
// greetings, TURN via /webrtc/config, ring-phase teardown fixes), the
|
|
353
353
|
// received-media .bin extension fix, and the tts/transcribe agent verbs.
|
|
354
|
-
|
|
354
|
+
// 3.2.2 approved by Sumeet 2026-08-09 ("Yeh", answering "Want me to build the
|
|
355
|
+
// chip fix now and fold it in? Then a single /restart live-verifies everything
|
|
356
|
+
// and I cut"): the presence-chip batch — thinking-on-receipt (heartbeat starts
|
|
357
|
+
// the moment a turn-bound message arrives, covering enforcer triage/download/
|
|
358
|
+
// STT dead air), tool stretches re-enter "thinking" instead of decaying the
|
|
359
|
+
// chip, /stop emits an explicit typing:stop, and the dedup adjudicator's
|
|
360
|
+
// "dedup" utility purpose registered so the journal dedup gate actually runs.
|
|
361
|
+
// 3.2.3 approved by Sumeet 2026-08-13 ("While we wait can we update open
|
|
362
|
+
// Claudia so a watcher requires a timeout or defaults to something so that
|
|
363
|
+
// when it expires it acts as a wake up so no watcher can silently fail?"):
|
|
364
|
+
// watch expiry — every new watch carries expiresAt (--timeout flag, default
|
|
365
|
+
// 2h); a lapsed deadline with a still-failing check fires the wake anyway as
|
|
366
|
+
// a marked "Watch expired" prompt and removes the watch, so a watch whose
|
|
367
|
+
// condition never comes true can no longer end silently.
|
|
368
|
+
assert.strictEqual(pkg.version, "3.2.3", "release version must remain unchanged without explicit approval");
|
|
355
369
|
for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
|
|
356
370
|
assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
|
|
357
371
|
}
|
|
@@ -826,6 +826,40 @@ async function watchGateProbe(createSchedulerService) {
|
|
|
826
826
|
assert.strictEqual(repeatJob.status, "pending");
|
|
827
827
|
assert.strictEqual(repeatJob.occurrenceId, "next-occ-1", "repeat watch advances to a new occurrence");
|
|
828
828
|
|
|
829
|
+
// Failing check with a FUTURE expiry → still the normal silent idle skip.
|
|
830
|
+
const pending = schedulerHarness(createSchedulerService, [watchJob({ id: "watch-pending", expiresAt: 1_720_000_100_000 })], {
|
|
831
|
+
runWatchCheck: async () => ({ code: 1, timedOut: false }),
|
|
832
|
+
outcomes: [successOutcome()],
|
|
833
|
+
});
|
|
834
|
+
const pendingResult = await pending.service.fireJob("watch-pending");
|
|
835
|
+
assert.strictEqual(pendingResult.status, "watch-idle");
|
|
836
|
+
assert.strictEqual(pending.runCalls.length, 0, "unexpired failing check stays silent");
|
|
837
|
+
|
|
838
|
+
// Failing check with a LAPSED expiry → fires anyway as an expiry wake-up:
|
|
839
|
+
// distinct header, advisory says timed out, removed even though repeat=true.
|
|
840
|
+
const expired = schedulerHarness(createSchedulerService, [watchJob({ id: "watch-expired", repeat: true, expiresAt: 1_720_000_000_000 })], {
|
|
841
|
+
runWatchCheck: async () => ({ code: 1, timedOut: false }),
|
|
842
|
+
outcomes: [successOutcome()],
|
|
843
|
+
});
|
|
844
|
+
const expiredResult = await expired.service.fireJob("watch-expired");
|
|
845
|
+
assert.strictEqual(expiredResult.ok, true);
|
|
846
|
+
assert.strictEqual(expiredResult.status, "succeeded");
|
|
847
|
+
assert.strictEqual(expired.runCalls.length, 1, "expired watch wakes the agent exactly once");
|
|
848
|
+
assert.match(expired.runCalls[0].prompt, /Watch expired/, "expiry fire carries the Watch expired header");
|
|
849
|
+
assert.match(expired.runCalls[0].prompt, /did NOT happen/, "expiry fire tells the agent the condition never came true");
|
|
850
|
+
assert.ok(expired.sends.some((text) => /timed out without triggering/.test(text)), "expiry advisory says it timed out");
|
|
851
|
+
assert.strictEqual(expired.jobsStore.get("watch-expired"), null, "expired watch is removed even when repeat was requested");
|
|
852
|
+
|
|
853
|
+
// Check that passes ON the expiry tick → a normal trigger, not an expiry.
|
|
854
|
+
const lastGasp = schedulerHarness(createSchedulerService, [watchJob({ id: "watch-lastgasp", expiresAt: 1_720_000_000_000 })], {
|
|
855
|
+
runWatchCheck: async () => ({ code: 0, timedOut: false }),
|
|
856
|
+
outcomes: [successOutcome()],
|
|
857
|
+
});
|
|
858
|
+
await lastGasp.service.fireJob("watch-lastgasp");
|
|
859
|
+
assert.strictEqual(lastGasp.runCalls.length, 1);
|
|
860
|
+
assert.match(lastGasp.runCalls[0].prompt, /Watch triggered/, "a passing check at the deadline is a real trigger");
|
|
861
|
+
assert.ok(!/Watch expired/.test(lastGasp.runCalls[0].prompt), "a passing check never reports as expired");
|
|
862
|
+
|
|
829
863
|
// Global master switch OFF → skip before the check even runs.
|
|
830
864
|
fs.writeFileSync(watchesEnabledFile, "off");
|
|
831
865
|
let checkRan = false;
|
|
@@ -841,6 +875,45 @@ async function watchGateProbe(createSchedulerService) {
|
|
|
841
875
|
fs.rmSync(watchesEnabledFile, { force: true });
|
|
842
876
|
}
|
|
843
877
|
|
|
878
|
+
function watchExpiryStampProbe(createSchedulerService, DEFAULT_WATCH_TIMEOUT_MS) {
|
|
879
|
+
assert.strictEqual(typeof DEFAULT_WATCH_TIMEOUT_MS, "number", "scheduler must export the default watch timeout");
|
|
880
|
+
const state = {
|
|
881
|
+
userId: "fixture-user",
|
|
882
|
+
currentSession: { name: "alpha", dir: "/workspace/alpha" },
|
|
883
|
+
settings: { backend: "claude" },
|
|
884
|
+
activeSessions: { alpha: { claude: "thread-alpha" } },
|
|
885
|
+
providerSettings: { claude: {} },
|
|
886
|
+
};
|
|
887
|
+
const service = createSchedulerService({
|
|
888
|
+
jobsStore: memoryJobsStore([]),
|
|
889
|
+
currentState: () => state,
|
|
890
|
+
now: () => 1_720_000_000_500,
|
|
891
|
+
setTimer: () => ({}),
|
|
892
|
+
clearTimer: () => {},
|
|
893
|
+
scheduleCron: () => ({ stop() {} }),
|
|
894
|
+
});
|
|
895
|
+
const stamped = service.addWatch({
|
|
896
|
+
adapter: "telegram-fixture",
|
|
897
|
+
adapterType: "telegram",
|
|
898
|
+
channelId: "fixture-channel",
|
|
899
|
+
canonicalUserId: "fixture-user",
|
|
900
|
+
check: "true",
|
|
901
|
+
prompt: "watch prompt",
|
|
902
|
+
});
|
|
903
|
+
assert.strictEqual(stamped.expiresAt, 1_720_000_000_500 + DEFAULT_WATCH_TIMEOUT_MS, "watch-add stamps the default expiry when none is given");
|
|
904
|
+
const explicit = service.addWatch({
|
|
905
|
+
adapter: "telegram-fixture",
|
|
906
|
+
adapterType: "telegram",
|
|
907
|
+
channelId: "fixture-channel",
|
|
908
|
+
canonicalUserId: "fixture-user",
|
|
909
|
+
check: "true",
|
|
910
|
+
prompt: "watch prompt 2",
|
|
911
|
+
expiresAt: 1_720_000_999_999,
|
|
912
|
+
});
|
|
913
|
+
assert.strictEqual(explicit.expiresAt, 1_720_000_999_999, "an explicit --timeout expiry passes through unchanged");
|
|
914
|
+
service.stopAll();
|
|
915
|
+
}
|
|
916
|
+
|
|
844
917
|
function staticSchedulingProbe() {
|
|
845
918
|
const files = [
|
|
846
919
|
"core/jobs.js",
|
|
@@ -863,6 +936,12 @@ function staticSchedulingProbe() {
|
|
|
863
936
|
assert.match(source["core/loopback.js"], /watch-add/, "loopback must route watch-add");
|
|
864
937
|
assert.match(source["bin/schedule.js"], /runWatchAdd/, "schedule CLI must expose watch-add");
|
|
865
938
|
assert.match(source["bin/cli.js"], /watch-add/, "cli must dispatch watch-add");
|
|
939
|
+
|
|
940
|
+
// Watch expiry must be wired end to end: default stamp → gate → CLI flag.
|
|
941
|
+
assert.match(source["core/scheduler.js"], /DEFAULT_WATCH_TIMEOUT_MS/, "scheduler must define the default watch timeout");
|
|
942
|
+
assert.match(source["core/scheduler.js"], /watchExpired/, "scheduler must mark expiry fires distinctly");
|
|
943
|
+
assert.match(source["core/loopback.js"], /expiresAt/, "loopback must pass the watch expiry through");
|
|
944
|
+
assert.match(source["bin/schedule.js"], /--timeout/, "watch-add CLI must expose --timeout");
|
|
866
945
|
}
|
|
867
946
|
|
|
868
947
|
async function main() {
|
|
@@ -884,6 +963,7 @@ async function main() {
|
|
|
884
963
|
await unavailableAndFallbackProbe(schedulerApi.createSchedulerService);
|
|
885
964
|
await cronOccurrenceProbe(schedulerApi.createSchedulerService);
|
|
886
965
|
await watchGateProbe(schedulerApi.createSchedulerService);
|
|
966
|
+
watchExpiryStampProbe(schedulerApi.createSchedulerService, schedulerApi.DEFAULT_WATCH_TIMEOUT_MS);
|
|
887
967
|
creationEnrichmentProbe(schedulerApi.createSchedulerService);
|
|
888
968
|
loopbackMetadataProbe(require("./core/runner"), require("./bin/loopback-client"));
|
|
889
969
|
staticSchedulingProbe();
|
|
@@ -185,6 +185,28 @@ async function main() {
|
|
|
185
185
|
"legacy Claude model aliases never leak into Codex",
|
|
186
186
|
);
|
|
187
187
|
|
|
188
|
+
// v3.2.2: "dedup" (journal dedup adjudicator) used to be rejected outright
|
|
189
|
+
// with INVALID_UTILITY_PURPOSE, silently disabling the dedup gate on every
|
|
190
|
+
// memory review. It rides the review pipeline: MEMORY_PROVIDER steers it,
|
|
191
|
+
// PACK_DEDUP_MODEL is its legacy Claude model alias.
|
|
192
|
+
assert.ok(
|
|
193
|
+
createUtilityPolicy({ registry: allReady, env: { UTILITY_PROVIDER: "claude" } })
|
|
194
|
+
.resolve({ purpose: "dedup", tier: "low" }).providerId,
|
|
195
|
+
"dedup is a valid utility purpose",
|
|
196
|
+
);
|
|
197
|
+
assert.strictEqual(
|
|
198
|
+
createUtilityPolicy({ registry: allReady, env: { UTILITY_PROVIDER: "claude", MEMORY_PROVIDER: "codex" } })
|
|
199
|
+
.resolve({ purpose: "dedup", tier: "low" }).providerId,
|
|
200
|
+
"codex",
|
|
201
|
+
"dedup follows the review pipeline's MEMORY_PROVIDER override",
|
|
202
|
+
);
|
|
203
|
+
assert.strictEqual(
|
|
204
|
+
createUtilityPolicy({ registry: allReady, env: { UTILITY_PROVIDER: "claude", PACK_DEDUP_MODEL: "legacy-claude-dedup" } })
|
|
205
|
+
.resolve({ purpose: "dedup", tier: "low" }).model,
|
|
206
|
+
"legacy-claude-dedup",
|
|
207
|
+
"dedup honors its PACK_DEDUP_MODEL legacy alias",
|
|
208
|
+
);
|
|
209
|
+
|
|
188
210
|
const fallbackRegistry = registry(map, {
|
|
189
211
|
claude: { availability: { state: "missing", executable: null, reason: "fixture unavailable" } },
|
|
190
212
|
});
|