@inetafrica/open-claudia 3.5.17 → 3.5.19

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/core/scheduler.js CHANGED
@@ -33,6 +33,7 @@ const {
33
33
  getProjectKey,
34
34
  getProviderSession,
35
35
  getProviderSettings,
36
+ getProjectSessions,
36
37
  getUserState,
37
38
  peekLane,
38
39
  laneBusy,
@@ -247,6 +248,7 @@ function createSchedulerService(dependencies = {}) {
247
248
  : dependencies.legacyCronsFile;
248
249
  const runInProvider = dependencies.runAgent || ((...args) => require("./runner").runAgent(...args));
249
250
  const admit = dependencies.admitRunContext || ((...args) => require("./runner").admitRunContext(...args));
251
+ const projectSessions = dependencies.getProjectSessions || getProjectSessions;
250
252
  const timers = new Map();
251
253
  const checksInFlight = new Set();
252
254
  const firesInFlight = new Set();
@@ -335,6 +337,26 @@ function createSchedulerService(dependencies = {}) {
335
337
  return getProviderSession(state, job.provider, roomKey) || null;
336
338
  }
337
339
 
340
+ // previous session -> the session it was compacted into, from the
341
+ // conversation's session history (each compaction's lineage is kept on the
342
+ // new session's record). The completed-compactions index holds only the
343
+ // last 20, under a day of token-diet compactions across every thread, so a
344
+ // job made before them lost its way: 7 Oct the PBX thread's wakeup and
345
+ // watch resumed a 790k conversation compacted that morning ($86).
346
+ function historySuccessors(state, job) {
347
+ const successors = new Map();
348
+ let records = [];
349
+ try { records = projectSessions(state?.userId || job.canonicalUserId, job.project, job.provider) || []; }
350
+ catch (_) { records = []; }
351
+ // Newest first: if a session was ever compacted twice, the latest wins.
352
+ for (const record of records) {
353
+ const lineage = record?.lineage;
354
+ if (lineage?.type !== "compaction" || !lineage.previousSessionId || !lineage.newSessionId) continue;
355
+ if (!successors.has(lineage.previousSessionId)) successors.set(lineage.previousSessionId, lineage.newSessionId);
356
+ }
357
+ return successors;
358
+ }
359
+
338
360
  function resolveCompactionLineage(state, job) {
339
361
  let sessionId = job.sessionId || null;
340
362
  // A thread job never resumes its room's MAIN session: a job made in a
@@ -347,6 +369,7 @@ function createSchedulerService(dependencies = {}) {
347
369
  sessionId = own && own !== mainSession ? own : null;
348
370
  }
349
371
  if (!sessionId) return { sessionId: null, attachSession: true };
372
+ let recorded = null;
350
373
  const seen = new Set();
351
374
  while (sessionId && !seen.has(sessionId)) {
352
375
  seen.add(sessionId);
@@ -355,9 +378,9 @@ function createSchedulerService(dependencies = {}) {
355
378
  && entry?.project === job.project
356
379
  && entry?.previousSessionId === sessionId
357
380
  && entry?.newSessionId
358
- ));
381
+ ))?.newSessionId || (recorded ||= historySuccessors(state, job)).get(sessionId);
359
382
  if (!successor) break;
360
- sessionId = successor.newSessionId;
383
+ sessionId = successor;
361
384
  }
362
385
  const live = getProviderSession(state, job.provider, job.project);
363
386
  return {
@@ -694,6 +717,14 @@ function createSchedulerService(dependencies = {}) {
694
717
  lastProvider: target.provider,
695
718
  fallbackFromProvider: target.fallbackFrom,
696
719
  };
720
+ // A recurring job keeps its own conversation: remember the session this
721
+ // run resumed and ended on (lineage or a compaction may have moved it), so
722
+ // the next run starts there even after the lineage records age out. Only
723
+ // for a run that resumed the job's conversation under its own provider.
724
+ const ranOn = typeof result?.sessionId === "string" && result.sessionId ? result.sessionId : null;
725
+ if (!isOneShot(job) && ranOn && ranOn !== job.sessionId && target.sessionId && !target.fresh && target.provider === job.provider) {
726
+ successPatch.sessionId = ranOn;
727
+ }
697
728
  let marked;
698
729
  try { marked = jobsStore.update(job.id, successPatch); }
699
730
  catch (error) { return { ok: false, status: "failed", error: stableFailure(error), result }; }
package/core/state.js CHANGED
@@ -1671,6 +1671,96 @@ function commitProviderCompaction(state, transaction, options = {}) {
1671
1671
  }
1672
1672
  }
1673
1673
 
1674
+ // A scheduled job resumes its own conversation: a branch beside its thread's
1675
+ // live session once the thread has moved on. Compacting that branch records
1676
+ // the lineage (on the new session's history record and in the completed-
1677
+ // compactions index) so the job moves on to the new session, but never
1678
+ // touches the thread's live pointer: the thread is not the job's conversation.
1679
+ function commitBranchCompaction(state, transaction, options = {}) {
1680
+ const tx = validateCompactionTransaction(state, transaction);
1681
+ if (state.completedCompactions?.[tx.operationId]) {
1682
+ return {
1683
+ committed: true,
1684
+ duplicate: true,
1685
+ operationId: tx.operationId,
1686
+ sessionId: state.completedCompactions[tx.operationId].newSessionId,
1687
+ };
1688
+ }
1689
+ if (getProviderSession(state, tx.provider, tx.project) === tx.previousSessionId) {
1690
+ throw compactionError("COMPACTION_POINTER_CHANGED", "the branch became the live session before its compaction commit");
1691
+ }
1692
+ const persistState = options.persistState || saveState;
1693
+ const recordHistory = options.recordHistory || recordSession;
1694
+ const nowValue = typeof options.now === "function" ? options.now() : new Date();
1695
+ const now = nowValue instanceof Date ? nowValue : new Date(nowValue);
1696
+ if (!Number.isFinite(now.getTime())) throw compactionError("INVALID_COMPACTION", "compaction clock is invalid");
1697
+ requireCompactionWrite(
1698
+ recordHistory(
1699
+ tx.canonicalUserId,
1700
+ tx.project,
1701
+ tx.provider,
1702
+ tx.newSessionId,
1703
+ tx.title || `Scheduled job conversation, compacted ${now.toLocaleDateString()}`,
1704
+ {
1705
+ strict: true,
1706
+ now: () => now,
1707
+ lineage: {
1708
+ type: "compaction",
1709
+ operationId: tx.operationId,
1710
+ previousSessionId: tx.previousSessionId,
1711
+ newSessionId: tx.newSessionId,
1712
+ branch: true,
1713
+ },
1714
+ },
1715
+ ),
1716
+ "branch compaction history",
1717
+ );
1718
+ const newUsageKey = usageSessionKey(tx.project, tx.provider, tx.newSessionId);
1719
+ if (!isRecord(state.usageBySession)) state.usageBySession = {};
1720
+ const previousUsageBySession = state.usageBySession;
1721
+ const previousCompleted = state.completedCompactions;
1722
+ const nextUsage = {
1723
+ ...freshUsage(),
1724
+ lastCompactedAt: now.getTime(),
1725
+ compactionCheckpoint: {
1726
+ operationId: tx.operationId,
1727
+ previousSessionId: tx.previousSessionId,
1728
+ newSessionId: tx.newSessionId,
1729
+ at: now.toISOString(),
1730
+ branch: true,
1731
+ },
1732
+ };
1733
+ if (tx.provider === "codex" && isRecord(tx.seedUsage)) {
1734
+ nextUsage.providerUsageSnapshot = {
1735
+ input_tokens: Number(tx.seedUsage.input_tokens) || 0,
1736
+ cached_input_tokens: Number(tx.seedUsage.cached_input_tokens) || 0,
1737
+ output_tokens: Number(tx.seedUsage.output_tokens) || 0,
1738
+ };
1739
+ }
1740
+ state.usageBySession = boundUsageBySession({ ...previousUsageBySession, [newUsageKey]: nextUsage });
1741
+ state.completedCompactions = Object.fromEntries(Object.entries({
1742
+ ...cloneRecord(previousCompleted),
1743
+ [tx.operationId]: {
1744
+ project: tx.project,
1745
+ provider: tx.provider,
1746
+ previousSessionId: tx.previousSessionId,
1747
+ newSessionId: tx.newSessionId,
1748
+ committedAt: now.toISOString(),
1749
+ branch: true,
1750
+ },
1751
+ }).slice(-20));
1752
+ try {
1753
+ requireCompactionWrite(persistState({ strict: true }), "branch compaction state");
1754
+ } catch (error) {
1755
+ // The history record stays: the new session IS the branch's compacted
1756
+ // continuation, and the scheduler follows it from there.
1757
+ state.usageBySession = previousUsageBySession;
1758
+ state.completedCompactions = previousCompleted;
1759
+ throw error;
1760
+ }
1761
+ return { committed: true, duplicate: false, branch: true, operationId: tx.operationId, sessionId: tx.newSessionId };
1762
+ }
1763
+
1674
1764
  function migrateUserData(fromUserId, toUserId) {
1675
1765
  const from = normalizeCanonicalUserId(fromUserId);
1676
1766
  const to = normalizeCanonicalUserId(toUserId);
@@ -1828,6 +1918,7 @@ module.exports = {
1828
1918
  createSessionCallbackToken,
1829
1919
  consumeSessionCallbackToken,
1830
1920
  commitProviderCompaction,
1921
+ commitBranchCompaction,
1831
1922
  recoverPendingCompaction,
1832
1923
  migrateUserData,
1833
1924
  linkIdentity,
@@ -498,6 +498,8 @@ function _resetForTests() {
498
498
 
499
499
  module.exports = {
500
500
  journalable, begin, end, enqueue, dropQueued, beat, list, fileFor,
501
+ // Shared with core/limit-pickup.js, which re-runs a message the same way.
502
+ conversationOf, chatScope, findAdapter, defaultDeps,
501
503
  resumeInterrupted, handleResumeAction, RESUME_PROMPT,
502
504
  rememberDropped, droppedKeyboard, handleRunDropped,
503
505
  drain, exitAfterDrain, isDraining, markShuttingDown,
@@ -0,0 +1,321 @@
1
+ "use strict";
2
+
3
+ // Plan usage limits: the 5-hour and weekly windows of a Claude Pro/Max plan
4
+ // and of the ChatGPT plan behind Codex.
5
+ //
6
+ // - recordWindows() keeps the latest use of each window (Claude's
7
+ // rate_limit_event, Codex's rollout snapshot) and says which warning
8
+ // percentages a turn has just crossed, once per window;
9
+ // - limitHitDetails() works out when the limit that stopped a turn resets,
10
+ // from the provider's own reset time, else from the wording of its notice;
11
+ // - the format* helpers word the chat lines.
12
+ //
13
+ // The pick-up job that re-runs a stopped message at the reset lives in
14
+ // core/limit-pickup.js.
15
+
16
+ const path = require("path");
17
+ const { CONFIG_DIR, config } = require("./config");
18
+ const { atomicWriteFileSync, readJsonWithFallback, withFileLock } = require("./fsutil");
19
+
20
+ const DEFAULT_FILE = path.join(CONFIG_DIR, "usage-limits.json");
21
+ const DEFAULT_WARN_PERCENTS = [80, 95];
22
+ // A window counts as new (warnings start over) when its reset time moves by
23
+ // more than this; Claude and Codex both report a fixed reset per window.
24
+ const NEW_WINDOW_SLACK_MS = 10 * 60 * 1000;
25
+ const PROVIDER_LABELS = { claude: "Claude", codex: "Codex" };
26
+ const WINDOW_LABELS = {
27
+ five_hour: "5-hour limit",
28
+ seven_day: "weekly limit",
29
+ seven_day_opus: "weekly Opus limit",
30
+ seven_day_sonnet: "weekly Sonnet limit",
31
+ seven_day_overage_included: "weekly limit",
32
+ overage: "extra-usage limit",
33
+ };
34
+ const MONTHS = ["jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"];
35
+
36
+ const timeZoneDefault = () => process.env.OC_TIMEZONE || config.OC_TIMEZONE || "Africa/Nairobi";
37
+
38
+ // LIMIT_WARN_PERCENTS: comma-separated (default 80,95); "off" for none.
39
+ function warnPercents(raw = process.env.LIMIT_WARN_PERCENTS ?? config.LIMIT_WARN_PERCENTS) {
40
+ if (raw === undefined || raw === null || String(raw).trim() === "") return [...DEFAULT_WARN_PERCENTS];
41
+ if (/^(off|no|none|false|0)$/i.test(String(raw).trim())) return [];
42
+ return String(raw).split(",").map((s) => Number(s.trim()))
43
+ .filter((n) => Number.isFinite(n) && n > 0 && n <= 100)
44
+ .sort((a, b) => a - b);
45
+ }
46
+
47
+ function providerLabel(provider) {
48
+ return PROVIDER_LABELS[provider] || String(provider || "The provider");
49
+ }
50
+
51
+ function windowLabel(id) {
52
+ if (!id) return "usage limit";
53
+ if (WINDOW_LABELS[id]) return WINDOW_LABELS[id];
54
+ const minutes = /^(\d+)m$/.exec(id);
55
+ if (minutes) {
56
+ const n = Number(minutes[1]);
57
+ return n % 60 === 0 ? `${n / 60}-hour limit` : `${n}-minute limit`;
58
+ }
59
+ return "usage limit";
60
+ }
61
+
62
+ // Codex's rollout snapshot ({ primary, secondary } windows with used_percent,
63
+ // window_minutes, resets_at) as windows named like Claude's. Which window is
64
+ // "primary" depends on the plan: on a Pro account it is the weekly one.
65
+ function codexWindows(rateLimits) {
66
+ if (!rateLimits || typeof rateLimits !== "object") return [];
67
+ const windows = [];
68
+ for (const window of [rateLimits.primary, rateLimits.secondary]) {
69
+ if (!window || !Number.isFinite(window.usedPercent)) continue;
70
+ const minutes = Number(window.windowMinutes);
71
+ const id = minutes === 300 ? "five_hour" : (minutes === 10080 ? "seven_day" : (Number.isFinite(minutes) && minutes > 0 ? `${minutes}m` : "plan"));
72
+ const resetsAt = window.resetsAt ? Date.parse(window.resetsAt) : NaN;
73
+ windows.push({ id, usedPercent: window.usedPercent, resetsAt: Number.isFinite(resetsAt) ? resetsAt : null });
74
+ }
75
+ return windows;
76
+ }
77
+
78
+ function readState(file) {
79
+ const state = readJsonWithFallback(file, null);
80
+ return state && typeof state === "object" && state.windows && typeof state.windows === "object"
81
+ ? state
82
+ : { windows: {} };
83
+ }
84
+
85
+ /**
86
+ * Remember the latest use of each window and return the warnings now due:
87
+ * at most one per window, for the highest percentage crossed that this window
88
+ * has not been warned about. A new window (reset time moved) or a drop below a
89
+ * percentage (window reset, or another account) clears its warnings.
90
+ */
91
+ function recordWindows({ provider, windows, file = DEFAULT_FILE, percents = warnPercents(), now = Date.now() } = {}) {
92
+ const usable = (windows || []).filter((w) => w && w.id && Number.isFinite(w.usedPercent));
93
+ if (!provider || !usable.length) return [];
94
+ // A test run that did not point the config dir somewhere else never writes
95
+ // the live bot's file.
96
+ if (file === DEFAULT_FILE && process.env.OPEN_CLAUDIA_TEST === "1" && !process.env.OPEN_CLAUDIA_CONFIG_DIR) return [];
97
+ return withFileLock(file, () => {
98
+ const state = readState(file);
99
+ const due = [];
100
+ for (const window of usable) {
101
+ const key = `${provider}:${window.id}`;
102
+ const previous = state.windows[key] || null;
103
+ const sameWindow = previous
104
+ && (!previous.resetsAt || !window.resetsAt || Math.abs(previous.resetsAt - window.resetsAt) <= NEW_WINDOW_SLACK_MS);
105
+ const warned = (sameWindow ? previous.warned || [] : []).filter((p) => window.usedPercent >= p);
106
+ const crossed = percents.filter((p) => window.usedPercent >= p && !warned.includes(p));
107
+ if (crossed.length) {
108
+ due.push({ provider, id: window.id, percent: crossed[crossed.length - 1], usedPercent: window.usedPercent, resetsAt: window.resetsAt || null });
109
+ }
110
+ state.windows[key] = {
111
+ usedPercent: window.usedPercent,
112
+ resetsAt: window.resetsAt || null,
113
+ updatedAt: new Date(now).toISOString(),
114
+ warned: [...new Set([...warned, ...crossed])].sort((a, b) => a - b),
115
+ };
116
+ }
117
+ atomicWriteFileSync(file, JSON.stringify(state, null, 2) + "\n");
118
+ return due;
119
+ });
120
+ }
121
+
122
+ /** The windows last seen per provider, newest reading first, for /usage. */
123
+ function knownWindows({ file = DEFAULT_FILE, now = Date.now() } = {}) {
124
+ const state = readState(file);
125
+ return Object.entries(state.windows)
126
+ .map(([key, value]) => {
127
+ const [provider, id] = key.split(":");
128
+ // A window whose reset has passed no longer says anything about now.
129
+ const stale = value.resetsAt && value.resetsAt <= now;
130
+ return stale ? null : { provider, id, ...value };
131
+ })
132
+ .filter(Boolean)
133
+ .sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
134
+ }
135
+
136
+ // ---------------------------------------------------------------------------
137
+ // Time zones and wording
138
+
139
+ function zonedParts(t, timeZone) {
140
+ const parts = new Intl.DateTimeFormat("en-US", {
141
+ timeZone, hourCycle: "h23", year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit", second: "2-digit",
142
+ }).formatToParts(new Date(t));
143
+ const p = Object.fromEntries(parts.map((x) => [x.type, Number(x.value)]));
144
+ return { year: p.year, month: p.month - 1, day: p.day, hour: p.hour % 24, minute: p.minute, second: p.second };
145
+ }
146
+
147
+ // The instant a wall-clock time in `timeZone` names (month is 0-based; day
148
+ // may overflow, Date.UTC rolls it). Two passes settle a DST change.
149
+ function zonedInstant({ year, month, day, hour, minute }, timeZone) {
150
+ const wall = Date.UTC(year, month, day, hour, minute);
151
+ let guess = wall;
152
+ for (let i = 0; i < 2; i += 1) {
153
+ const p = zonedParts(guess, timeZone);
154
+ const offset = Date.UTC(p.year, p.month, p.day, p.hour, p.minute, p.second) - Math.floor(guess / 1000) * 1000;
155
+ guess = wall - offset;
156
+ }
157
+ return guess;
158
+ }
159
+
160
+ function validTimeZone(timeZone) {
161
+ try { new Intl.DateTimeFormat("en-US", { timeZone }); return true; } catch (_) { return false; }
162
+ }
163
+
164
+ function whenParts(at, { now = Date.now(), timeZone = timeZoneDefault() } = {}) {
165
+ const tz = validTimeZone(timeZone) ? timeZone : "UTC";
166
+ const time = new Intl.DateTimeFormat("en-GB", { timeZone: tz, hour: "2-digit", minute: "2-digit", hourCycle: "h23" }).format(new Date(at));
167
+ const day = (t) => { const p = zonedParts(t, tz); return Date.UTC(p.year, p.month, p.day); };
168
+ const days = Math.round((day(at) - day(now)) / 86400000);
169
+ const date = new Intl.DateTimeFormat("en-GB", { timeZone: tz, weekday: "short", day: "numeric", month: "short" }).format(new Date(at)).replace(",", "");
170
+ return { time, days, date };
171
+ }
172
+
173
+ /** "17:20" today, "tomorrow 08:00", "yesterday 12:59", else "Fri 9 Oct, 08:00" (local time). */
174
+ function formatResetTime(at, options = {}) {
175
+ const { time, days, date } = whenParts(at, options);
176
+ if (days === 0) return time;
177
+ if (days === 1) return `tomorrow ${time}`;
178
+ if (days === -1) return `yesterday ${time}`;
179
+ return `${date}, ${time}`;
180
+ }
181
+
182
+ /** The same with its preposition: "at 17:20", "tomorrow at 08:00", "on Fri 9 Oct at 08:00". */
183
+ function whenPhrase(at, options = {}) {
184
+ const { time, days, date } = whenParts(at, options);
185
+ if (days === 0) return `at ${time}`;
186
+ if (days === 1) return `tomorrow at ${time}`;
187
+ if (days === -1) return `yesterday at ${time}`;
188
+ return `on ${date} at ${time}`;
189
+ }
190
+
191
+ function formatWarning(due, options = {}) {
192
+ const used = Math.min(100, Math.round(due.usedPercent));
193
+ const resets = due.resetsAt ? ` It resets ${whenPhrase(due.resetsAt, options)}.` : "";
194
+ return `⚠️ ${providerLabel(due.provider)}: ${used}% of the ${windowLabel(due.id)} is used.${resets}`;
195
+ }
196
+
197
+ // ---------------------------------------------------------------------------
198
+ // When does the limit that stopped a turn reset?
199
+
200
+ function hour24(hour, ampm) {
201
+ let h = Number(hour);
202
+ if (!ampm) return h;
203
+ h %= 12;
204
+ return /pm/i.test(ampm) ? h + 12 : h;
205
+ }
206
+
207
+ function monthIndex(name) {
208
+ return MONTHS.indexOf(String(name || "").slice(0, 3).toLowerCase());
209
+ }
210
+
211
+ // Claude Code: "You've hit your session limit · resets 11:30am (Africa/Nairobi)",
212
+ // "… resets Aug 21 at 8am (Africa/Nairobi)", "… resets Oct 9, 5am (UTC)".
213
+ function parseClaudeReset(text, { now, timeZone }) {
214
+ const m = /resets\s+(?:([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(?:at\s+)?)?(\d{1,2})(?::(\d{2}))?\s*(am|pm)?(?:\s*\(([^)]+)\))?/i.exec(text);
215
+ if (!m) return null;
216
+ const tz = m[6] && validTimeZone(m[6].trim()) ? m[6].trim() : timeZone;
217
+ const hour = hour24(m[3], m[5]);
218
+ const minute = Number(m[4] || 0);
219
+ if (hour > 23 || minute > 59) return null;
220
+ const today = zonedParts(now, tz);
221
+ if (m[1]) {
222
+ const month = monthIndex(m[1]);
223
+ if (month < 0) return null;
224
+ let at = zonedInstant({ year: today.year, month, day: Number(m[2]), hour, minute }, tz);
225
+ // "Jan 2" read in late December means next year.
226
+ if (at < now - 180 * 86400000) at = zonedInstant({ year: today.year + 1, month, day: Number(m[2]), hour, minute }, tz);
227
+ return at;
228
+ }
229
+ let at = zonedInstant({ year: today.year, month: today.month, day: today.day, hour, minute }, tz);
230
+ if (at <= now) at = zonedInstant({ year: today.year, month: today.month, day: today.day + 1, hour, minute }, tz);
231
+ return at;
232
+ }
233
+
234
+ // Codex: "… or try again at Sep 7th, 2026 5:28 AM." / "try again at 5:28 AM."
235
+ // (the CLI's local time, which is this process's), or "try again in 2 days 3 hours".
236
+ function parseCodexReset(text, { now }) {
237
+ const at = /try again at\s+(?:([A-Za-z]{3,9})\.?\s+(\d{1,2})(?:st|nd|rd|th)?,?\s+(\d{4})\s+)?(\d{1,2}):(\d{2})\s*(AM|PM)?/i.exec(text);
238
+ if (at) {
239
+ const hour = hour24(at[4], at[6]);
240
+ const minute = Number(at[5]);
241
+ if (at[1]) {
242
+ const month = monthIndex(at[1]);
243
+ if (month < 0) return null;
244
+ return new Date(Number(at[3]), month, Number(at[2]), hour, minute).getTime();
245
+ }
246
+ const base = new Date(now);
247
+ let t = new Date(base.getFullYear(), base.getMonth(), base.getDate(), hour, minute).getTime();
248
+ if (t <= now) t += 86400000;
249
+ return t;
250
+ }
251
+ const inMatch = /try again in\s+([^.]+)/i.exec(text);
252
+ if (!inMatch) return null;
253
+ const units = { day: 86400000, hour: 3600000, minute: 60000, min: 60000, second: 1000, sec: 1000 };
254
+ let total = 0;
255
+ for (const part of inMatch[1].matchAll(/(\d+)\s*(day|hour|minute|min|second|sec)s?/gi)) {
256
+ total += Number(part[1]) * units[part[2].toLowerCase()];
257
+ }
258
+ return total > 0 ? now + total : null;
259
+ }
260
+
261
+ function parseResetText(text, { now = Date.now(), timeZone = timeZoneDefault() } = {}) {
262
+ const s = String(text || "");
263
+ if (/resets\s/i.test(s)) return parseClaudeReset(s, { now, timeZone });
264
+ if (/try again (?:at|in)\s/i.test(s)) return parseCodexReset(s, { now });
265
+ return null;
266
+ }
267
+
268
+ function limitTypeFromText(text) {
269
+ const m = /hit your\s+(session|weekly|opus|sonnet|fable)?\s*limit/i.exec(String(text || ""));
270
+ if (!m || !m[1]) return null;
271
+ return { session: "five_hour", weekly: "seven_day", opus: "seven_day_opus", sonnet: "seven_day_sonnet", fable: "seven_day" }[m[1].toLowerCase()] || null;
272
+ }
273
+
274
+ /**
275
+ * What stopped a turn and when it lifts. `error` is the run's failure (its
276
+ * details carry Claude's limitResetsAt/limitType), `rateLimit` the run's last
277
+ * Claude reading, `windows` Codex's rollout windows. Returns null when the
278
+ * failure was not a usage limit; resetsAt is null when no reset time is known.
279
+ */
280
+ function limitHitDetails({ provider, error, rateLimit = null, windows = [], now = Date.now(), timeZone = timeZoneDefault() } = {}) {
281
+ if (!error || error.code !== "PROVIDER_USAGE_LIMIT") return null;
282
+ const details = error.details || {};
283
+ const text = String(details.message || error.message || "");
284
+ let resetsAt = Number.isFinite(details.limitResetsAt) ? details.limitResetsAt : null;
285
+ let limitType = details.limitType || null;
286
+ if (!resetsAt && rateLimit && rateLimit.status === "rejected" && rateLimit.resetsAt) {
287
+ resetsAt = rateLimit.resetsAt;
288
+ limitType = limitType || rateLimit.limitType || null;
289
+ }
290
+ if (!resetsAt) {
291
+ // Codex: the window the plan used up.
292
+ const spent = (windows || []).filter((w) => w.usedPercent >= 100 && w.resetsAt && w.resetsAt > now)
293
+ .sort((a, b) => b.resetsAt - a.resetsAt)[0];
294
+ if (spent) { resetsAt = spent.resetsAt; limitType = limitType || spent.id; }
295
+ }
296
+ if (!resetsAt) resetsAt = parseResetText(text, { now, timeZone });
297
+ return {
298
+ provider,
299
+ limitType: limitType || limitTypeFromText(text),
300
+ resetsAt: Number.isFinite(resetsAt) ? resetsAt : null,
301
+ text,
302
+ };
303
+ }
304
+
305
+ module.exports = {
306
+ DEFAULT_FILE,
307
+ DEFAULT_WARN_PERCENTS,
308
+ warnPercents,
309
+ providerLabel,
310
+ windowLabel,
311
+ codexWindows,
312
+ recordWindows,
313
+ knownWindows,
314
+ formatResetTime,
315
+ whenPhrase,
316
+ formatWarning,
317
+ parseResetText,
318
+ limitTypeFromText,
319
+ limitHitDetails,
320
+ zonedInstant,
321
+ };
@@ -123,6 +123,13 @@ function validateSchema(value, schema, location = "$", depth = 0) {
123
123
 
124
124
  function prepareSchemaFile(schema) {
125
125
  if (!schema) return null;
126
+ // Claude Code turns the schema into a tool whose input_schema the API
127
+ // requires to be an object, and OpenAI strict mode wants an object root too.
128
+ // Any other root is refused on every call, which a fail-open caller shows
129
+ // only as a warning (the dedup adjudicator's array root, 3.2.2 to 3.5.18).
130
+ if (typeof schema !== "object" || Array.isArray(schema) || schema.type !== "object") {
131
+ throw new UtilityAgentError("INVALID_UTILITY_SCHEMA", 'outputSchema must have top-level type "object"');
132
+ }
126
133
  let serialized;
127
134
  try { serialized = JSON.stringify(schema); }
128
135
  catch (_) { throw new UtilityAgentError("INVALID_UTILITY_SCHEMA", "outputSchema must be JSON-serializable"); }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.5.17",
3
+ "version": "3.5.19",
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-codex-output-schema.js && node test-codex-device-login.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-group-status-notes.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-transcript-room-scope.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-read-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-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-kazee-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-thread-lanes.js && node test-thread-titles.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-thread-stop.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-loopback-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-run-attest.js && node test-sandbox-probe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-bot-endpoint.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-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-cli-owner-gates.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-watch-check-env.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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 && node test-kazee-cards.js && node test-decision-upkeep.js && node test-passive-replies.js && node test-kazee-bot-loops.js && node test-kazee-bot-handoffs.js && node test-placeholder-reply.js && node test-media-speech.js && node test-passive-replies-router.js && node test-loopback-cards.js && node test-task-injection.js && node test-speaker-extras.js && node test-token-diet.js && node test-dream-tasks.js && node test-task-archive.js && node test-cron-tolerance.js && node test-dream-schedule.js && node test-job-failures.js && node test-scheduled-placeholder.js && node test-lessons-since.js && node test-pack-reinject.js && node test-turn-journal.js && node test-browser-view.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-view.js && node test-safe-filename.js && node test-media-filename-paths.js && node test-router-document-guard.js && OPEN_CLAUDIA_TEST=1 node test-web-dashboard-escape.js && OPEN_CLAUDIA_TEST=1 node test-web-bad-requests.js && OPEN_CLAUDIA_TEST=1 node test-kazee-download-limit.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-auth-routes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-otp-link-preview.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-transcript-room-scope.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-read-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-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-kazee-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-thread-lanes.js && node test-thread-titles.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-thread-stop.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-loopback-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-transcript-threads.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-run-attest.js && node test-sandbox-probe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-bot-endpoint.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-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-cli-owner-gates.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-watch-check-env.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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 && node test-kazee-cards.js && node test-decision-upkeep.js && node test-passive-replies.js && node test-kazee-bot-loops.js && node test-kazee-bot-handoffs.js && node test-placeholder-reply.js && node test-media-speech.js && node test-passive-replies-router.js && node test-loopback-cards.js && node test-task-injection.js && node test-speaker-extras.js && node test-token-diet.js && node test-dream-tasks.js && node test-task-archive.js && node test-cron-tolerance.js && node test-dream-schedule.js && node test-job-failures.js && node test-scheduled-placeholder.js && node test-lessons-since.js && node test-pack-reinject.js && node test-turn-journal.js && node test-usage-limits.js && node test-limit-pickup.js && node test-browser-view.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-browser-view.js && node test-safe-filename.js && node test-media-filename-paths.js && node test-router-document-guard.js && OPEN_CLAUDIA_TEST=1 node test-web-dashboard-escape.js && OPEN_CLAUDIA_TEST=1 node test-web-bad-requests.js && OPEN_CLAUDIA_TEST=1 node test-kazee-download-limit.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-auth-routes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test node test-web-otp-link-preview.js"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",