@inetafrica/open-claudia 3.5.19 → 3.5.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +8 -3
- package/CHANGELOG.md +37 -0
- package/README.md +5 -4
- package/bin/cli.js +6 -0
- package/bin/teach.js +78 -0
- package/bot.js +7 -0
- package/channels/kazee/adapter.js +30 -0
- package/core/actions.js +4 -0
- package/core/auth-flow.js +7 -2
- package/core/bot-apps.js +133 -0
- package/core/browser-control.js +540 -0
- package/core/browser-lock.js +51 -0
- package/core/browser-teach.js +1012 -0
- package/core/browser-view.js +5 -2
- package/core/cards.js +4 -2
- package/core/dream.js +132 -10
- package/core/handlers.js +16 -2
- package/core/limit-pickup.js +24 -20
- package/core/providers/claude.js +15 -8
- package/core/providers/events.js +25 -1
- package/core/providers/utility-policy.js +31 -6
- package/core/runner.js +203 -10
- package/core/scheduler.js +57 -0
- package/core/state.js +14 -0
- package/core/system-prompt.js +12 -2
- package/core/tool-guard.js +343 -0
- package/core/turn-journal.js +55 -2
- package/core/usage-limits.js +110 -0
- package/core/utility-agent.js +109 -15
- package/package.json +2 -2
- package/test-fixtures/fake-agent-cli.js +46 -2
- package/test-provider-boot-matrix.js +6 -2
- package/test-provider-capabilities.js +1 -1
- package/test-provider-claude.js +7 -5
- package/test-provider-events.js +43 -1
- package/test-provider-language.js +15 -1
- package/web.js +68 -0
package/.env.example
CHANGED
|
@@ -74,12 +74,17 @@ USAGE_ALERT_COOLDOWN_MS=1800000
|
|
|
74
74
|
# when a window's use passes these percentages, once per window (off = never).
|
|
75
75
|
LIMIT_WARN_PERCENTS=80,95
|
|
76
76
|
# A message a usage limit stopped is picked up when the limit resets, unless
|
|
77
|
-
# the person writes again first: on (default) | off.
|
|
77
|
+
# the person writes again first: on (default) | off. Scheduled runs and the
|
|
78
|
+
# dream's AI step always wait for the reset.
|
|
78
79
|
LIMIT_PICKUP=on
|
|
79
|
-
# Seconds after the reset before
|
|
80
|
-
# than LIMIT_PICKUP_MAX_WAIT_H hours (default 192) is not waited
|
|
80
|
+
# Seconds after the reset before waiting work runs (default 120); a reset
|
|
81
|
+
# further out than LIMIT_PICKUP_MAX_WAIT_H hours (default 192) is not waited
|
|
82
|
+
# for. One rule for chat messages, scheduled runs and the dream.
|
|
81
83
|
LIMIT_PICKUP_DELAY_S=120
|
|
82
84
|
LIMIT_PICKUP_MAX_WAIT_H=192
|
|
85
|
+
# Messages sent while a conversation is busy run together as one turn when it
|
|
86
|
+
# ends (on, default); off runs them one at a time.
|
|
87
|
+
QUEUE_BATCH=on
|
|
83
88
|
MEMORY_RECALL_MAX_CHARS=9000
|
|
84
89
|
# Default recall engine when a chat hasn't picked one with /engine: classic | discoverer
|
|
85
90
|
RECALL_ENGINE=classic
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 3.5.23
|
|
4
|
+
|
|
5
|
+
Usage limits as one rule, /stop as a real cancel, the queue's ✕ and run-together, and background checks on Haiku 5.5 and Sonnet 5.5 (Sumeet, 9 Oct 15:56 EAT: "yeh release and upgrade all the bots, include rahil for the updates going forward"). 3.5.20 below, the Browser app, only ever went out as a canary build, so this is the first release that brings it to everyone. 3.5.21 and 3.5.22 were tagged with the same changes but never published: 3.5.21's build stopped on the new /stop and queue test, which needed the fake agent its neighbours run on (CI has no Claude or Codex installed), and 3.5.22's builds stopped on two timing checks that busy build machines pushed past their limits (the boot matrix's 30 s per boot, the /stop test's 3 s and 2.5 s windows); those checks now allow more time.
|
|
6
|
+
|
|
7
|
+
Usage limits as one runtime rule (Sumeet, 9 Oct 2026 after the 07:00 dream failed on his bots: "it should use whatever the active model is … if the Claude model's out of usage … we can't assume that the other will be available … do the standard wake up, the delay until the bot's session has been replenished … the handling of session limits working as a foundational concept rather than just being hacks"). On 9 Oct the 07:00 dream's AI step failed on 18 of 24 fleet bots: Codex bots dreamed on a signed-out Claude, used-up limits failed instead of waiting, and Claude Code's refresh-lock race was final. No chats failed.
|
|
8
|
+
|
|
9
|
+
- **Work a usage limit stops waits for the reset, the same way everywhere.** One rule (`core/usage-limits.js` `limitRetryAt`: 2 minutes after the reset, `LIMIT_PICKUP_DELAY_S`; never a reset more than `LIMIT_PICKUP_MAX_WAIT_H`, 192 h, away) now serves a chat message (the 3.5.18 pick-up), a scheduled run and the dream. Never on the other provider: it may not be signed in, or be used up too.
|
|
10
|
+
- **A scheduled run a limit stops waits for the reset.** A wake-up, watch or cron that hit a limit used to retry every 30 s and give up after its attempts ("gave up and will not fire"). It now waits in the jobs store until the reset ("⏳ Claude's weekly limit is used up, so this scheduled run waits. I'll run it at 11:02, when it resets."), without using up an attempt; a cron's ticks meanwhile start nothing; the run after the reset is told it waited, and a reset that is used up again waits again, at most 5 times.
|
|
11
|
+
- **The dream's AI step waits for the reset.** The automatic filing and graph upkeep run on time; the AI-led merges and self-review run after the reset ("⏳ Codex's weekly limit is used up, so the AI-led merges wait for its reset: I'll run them tomorrow at 11:02."), on a timer kept in the dream state, so a restart keeps it. A sign-in that died says so plainly ("Claude's sign-in on this bot has expired (send /login to sign in again)").
|
|
12
|
+
- **Background work with nobody behind it runs on the bot's own provider.** The dream, introspection and other checks outside any chat now use the provider the owner chats on (`/backend`, `/login`), else the one a new chat starts on. They used to take the first provider that did not look signed out, which on a Codex bot was a signed-out Claude.
|
|
13
|
+
- **A signed-out Claude reads as signed out.** `claude auth status` prints `{"loggedIn": false}` (Claude Code 2.1), which no check matched, so the bot read it as "unknown" and still chose Claude.
|
|
14
|
+
- **One reading of a provider's failure for chats and background calls** (`providerFailureCode`). Background calls now see a plan limit as `PROVIDER_USAGE_LIMIT` with its reset time (it was a plain `UTILITY_PROVIDER_ERROR`). A dead sign-in ("Failed to authenticate: OAuth session expired and could not be refreshed", "OAuth access token is invalid", Codex's "Your authentication token has expired") is a sign-in error in chats and background calls alike, so a chat gets the /login help instead of a raw error. Claude Code's refresh-lock race ("another Claude Code process is refreshing it") gets one more try, 20 s later, in work nobody waits on (the dream, memory review, sub-agents); a chat's own checks go without rather than hold the reply up.
|
|
15
|
+
- **A limit is recorded for every waiter.** Any run a limit stops (a chat turn, a scheduled run, a background call) records the provider as used up until its reset (`usage-limits.json` `blocks`); background calls do not start it until then, and any run on it that works clears the record. Chat turns never read the record: they always try, so the provider stays the judge. A weekly Opus limit stops only Opus calls.
|
|
16
|
+
- **Tests:** `test-limit-waits.js` (new: background calls through the fake Claude CLI, scheduled runs, the dream and its timer, provider choice with nobody behind a call, the signed-out status), `test-usage-limits.js` (the rule and the record), `test-provider-events.js` (Claude Code 2.1.280's own sign-in and refresh-race wording); the fake agent CLI gains a `provider-error` scenario (`FAKE_AGENT_ERROR_TEXT`, `FAKE_AGENT_ERROR_ONCE_FILE`).
|
|
17
|
+
|
|
18
|
+
/stop and the queue (Sumeet, 9 Oct 14:18 EAT, with a screenshot of home pi: "it doesn't seem to understand i cancelled something … one for a queued message a small card with delete queued … i can delete message 2 and 4 arbitrarily from the queue … when the turn finishes … get all the messages that are queued at once? no point of them coming in series is there?").
|
|
19
|
+
|
|
20
|
+
- **A /stop is a cancel, not a cut.** /stop kills the provider mid-turn. On the next message Claude Code resumes the session and writes the stopped turn into the history the way a restart's cut reads ("[Request interrupted by user for tool use]", then "Continue from where you left off."), and the system prompt (since 3.5.11) says to finish work a restart cut off. So the stopped request came back: home pi answered the message after "/stop" on "ok play big bang" with "Big Bang didn't start … Just say if you still want it." /stop now marks the conversation in its turn-journal file (kept across a restart), and the conversation's next turn opens with a note that the person stopped that request and it is cancelled; the system prompt says so too. A stop before the provider started, or of a compaction, leaves no note; `/new`, `/end` and a project change drop it.
|
|
21
|
+
- **Each "Queued." notice has a ✕ Remove from queue button.** It takes that one message out of the queue, so of four queued messages the second and fourth can go while the others stay ("Removed from the queue: "…""). Only the person who sent it, or the owner, can remove it; a message that already started says so.
|
|
22
|
+
- **Queued messages run together.** When a turn ends, the messages the person sent meanwhile run as one turn ("3 messages arrived while you were busy … where a later one changes or cancels an earlier one, the later one wins"), in order, with every image and file, the reply answering the newest. They used to run one turn each, in series, each without the others. Only the same person's plain chat messages in the same session, project, provider and model join (a scheduled run, a side chat's follow-up, a limit pick-up and a live call run on their own), at most 10 per turn. `QUEUE_BATCH=off` runs them one at a time again.
|
|
23
|
+
- **Tests:** `test-stop-queue.js` (new: the note after a /stop, once, kept on disk, not after `/new` or a stop before the provider started; three queued messages as one turn with no journal leftovers; ✕ on two of four; who may press; what joins a batch), `test-turn-journal.js` (the stop survives a restart's boot pass); the fake agent CLI logs every `[[tag:…]]` in a prompt and `FAKE_AGENT_PROMPT_PROBES` matches.
|
|
24
|
+
|
|
25
|
+
Background checks on Claude Haiku 5.5 and Sonnet 5.5 (Sumeet, 9 Oct 14:51 EAT: "check if anything using haiku and have it shifted to the new haiku 5.5", then 15:36: "sonnet should also be just 5.5").
|
|
26
|
+
|
|
27
|
+
- **The low tier is Claude Haiku 5.5** (`claude-haiku-5-5`, was `claude-haiku-4-5-20251001`): the recall router and its warm walker, the recall filter, passive group replies, thread titles, the memory duplicate check and the agency deliberation. List price $0.10 / $0.50 per million tokens for prompts up to 100,000 tokens ($0.50 / $2.50 above), against $1 / $5 for Haiku 4.5. Checked live through the utility agent and Claude Code 2.1.280: a structured duplicate-check reply and a plain thread title, about 5 s each. The /model picker offers Haiku 5.5 instead of 4.5; a typed `/model claude-haiku-4-5-20251001` still works.
|
|
28
|
+
- **The medium tier is Claude Sonnet 5.5** (`claude-sonnet-5-5`, was `claude-sonnet-4-6`): the post-turn memory reviewer, the tool-rule guard and `open-claudia agent` sub-agents. List price $2 / $10 per million tokens, cache reads $0.10, against $3 / $15 and $0.30 for Sonnet 4.6; Sonnet 5.5 uses the newer tokenizer (about 30% more tokens for the same text), so a call costs roughly 15% less on fresh input and output and over half less on cache reads. The /model picker offers Sonnet 5.5 instead of 4.6; a typed `/model claude-sonnet-4-6` still works. A weekly Sonnet limit still stops only Sonnet work.
|
|
29
|
+
|
|
30
|
+
## 3.5.20
|
|
31
|
+
|
|
32
|
+
The Browser app in Kazee Chat, bot apps v1 (Sumeet, 9 Oct 12:02 EAT: "fine go ahead and push but also ensure you clear the images after they all done"). Kazee Chat opens it through Agent Space, which calls these pod routes with the control token.
|
|
33
|
+
|
|
34
|
+
- **A person can open the bot's browser from their direct chat.** `POST /api/apps/check` lets the Kazee owner or a chat on auth.json's authorized list open the `browser` app. After its slash commands, the Kazee adapter registers the app with chat-central at `AGENTSPACE_API_URL` + `/apps/browser`. A non-https or in-cluster address is skipped. `OC_BROWSER_APP=off` withdraws the app.
|
|
35
|
+
- **Take over and Hand back.** A driver lock (`browser-driver.json`, 0600) lasts 120 s from the last input, and a second person gets 409. Input events are checked field by field and relayed to the agent-browser stream. A hand-back wakes the bot once in that person's chat. While someone drives, the tool guard refuses the bot's own `agent-browser` commands, except reads.
|
|
36
|
+
- **Teach me.** It records clicks (with the element and a picture), typing per field, keys and scrolls. Characters typed into password or one-time-code fields are never kept. On Stop the bot is woken to post the steps, do one watched run, save the skill, and then run `open-claudia teach finish <id>`, which deletes the pictures and keeps the text. `teach cancel` deletes everything. A sweeper runs at boot and hourly: it deletes pictures after 24 h, stops abandoned recordings after 2 h, and removes recordings after 7 days.
|
|
37
|
+
- **Cards** pass `open: { app, label }` through. The system prompt tells bots that offer the app to send an Open Browser card when they need a person, and never to ask for passwords in chat.
|
|
38
|
+
- **Tests:** `test-browser-control.js`, `test-browser-teach.js`, `test-kazee-bot-apps.js` and `test-web-browser-app.js`, all in `npm test`.
|
|
39
|
+
|
|
3
40
|
## 3.5.19
|
|
4
41
|
|
|
5
42
|
The memory duplicate-check fix as its own release (Sumeet, 8 Oct 13:08 EAT: "I'd rather just push out another release now rather than waiting for another day or so"). Everything in 3.5.18 below comes with it.
|
package/README.md
CHANGED
|
@@ -346,7 +346,7 @@ Unsupported optional controls return a provider-specific explanation and do not
|
|
|
346
346
|
|
|
347
347
|
## Utility provider policy
|
|
348
348
|
|
|
349
|
-
Background intelligence—sub-agents, recall, memory review, dream/introspection, and the external-person enforcer—uses one provider-selection policy. A per-purpose override is checked first, then `UTILITY_PROVIDER` (default `active`)
|
|
349
|
+
Background intelligence—sub-agents, recall, memory review, dream/introspection, and the external-person enforcer—uses one provider-selection policy. A per-purpose override is checked first, then `UTILITY_PROVIDER` (default `active`): the speaker's provider when a chat exists, and for work with nobody behind it (the dream, introspection) the provider the owner chats on, else `DEFAULT_PROVIDER`, else the one a new chat starts on. A provider a usage limit has used up is not started for background work until its reset; such work waits for the reset (the dream) or goes without (a recall check), and never moves to the other provider. Providers translate the neutral `low`, `medium`, and `high` tiers to their own models; exact per-provider model overrides remain available.
|
|
350
350
|
|
|
351
351
|
Foreground turns never silently fall back to a different provider. Utility work uses `PROVIDER_FALLBACKS` only when the operator explicitly supplies an ordered list, starts a fresh provider session, and records the provider/model used. The relationship enforcer may try that configured list, but total failure always fails closed and asks the owner to decide.
|
|
352
352
|
|
|
@@ -455,7 +455,8 @@ All stored in `~/.open-claudia/`:
|
|
|
455
455
|
| `USAGE_ALERT_BASELINE_TURNS` / `USAGE_ALERT_MIN_BASELINE_TURNS` / `USAGE_ALERT_COOLDOWN_MS` | No | Tune token-rate baseline size, minimum sample size, and alert cooldown |
|
|
456
456
|
| `LIMIT_WARN_PERCENTS` | No | Plan-limit warnings in the chat: comma-separated percentages of a 5-hour or weekly window (default `80,95`, `off` disables). Each is said once per window, after the reply that crossed it; never to an outside contact |
|
|
457
457
|
| `LIMIT_PICKUP` | No | `on` (default) or `off`. A message a Claude or Codex usage limit stopped is picked up when the limit resets; the person writing again in that conversation, `/stop`, `/new` or `/end` drops it, and a new limit hit arms it again |
|
|
458
|
-
| `LIMIT_PICKUP_DELAY_S` / `LIMIT_PICKUP_MAX_WAIT_H` | No | Seconds after the reset before
|
|
458
|
+
| `LIMIT_PICKUP_DELAY_S` / `LIMIT_PICKUP_MAX_WAIT_H` | No | Seconds after the reset before waiting work runs (default `120`); resets further out than this many hours (default `192`) are not waited for. One rule for a picked-up message, a scheduled run a limit stopped (it waits instead of failing) and the dream's AI step |
|
|
459
|
+
| `QUEUE_BATCH` | No | `on` (default) or `off`. Messages a person sends while their conversation is busy run together as one turn when it ends (the same person's plain chat messages, at most 10); `off` runs them one at a time. Each "Queued." notice has a ✕ Remove from queue button either way |
|
|
459
460
|
| `MEMORY_RECALL_MAX_CHARS` | No | Hard cap for auto-injected pack/entity memory per turn (default `9000`, `off` disables auto recall injection) |
|
|
460
461
|
| `RECALL_ENGINE` | No | Default recall engine when a chat hasn't set one via `/engine` (`classic` or `discoverer`, default `discoverer`) |
|
|
461
462
|
| `RECALL_GRAPH_DB` / `RECALL_METRICS` | No | Override the discoverer graph DB path; `off` on metrics disables per-turn recall logging |
|
|
@@ -468,11 +469,11 @@ All stored in `~/.open-claudia/`:
|
|
|
468
469
|
| `TRANSCRIPT_MAX_ENTRY_CHARS` | No | Max chars per transcript entry (default `12000`) |
|
|
469
470
|
| `TRANSCRIPTS_DIR` / `PACKS_DIR` / `ENTITIES_DIR` | No | Override storage directories |
|
|
470
471
|
| `PACK_REVIEW` | No | `off` disables the post-turn pack/entity reviewer (default on) |
|
|
471
|
-
| `PACK_REVIEW_MODEL` | No | Model for the reviewer (default `
|
|
472
|
+
| `PACK_REVIEW_MODEL` | No | Model for the reviewer (default: the provider's medium tier, `claude-sonnet-5-5` on Claude). Background checks without a model of their own use the low tier: `claude-haiku-5-5` on Claude |
|
|
472
473
|
| `PACK_MATCH_THRESHOLD` / `ENTITY_MATCH_THRESHOLD` | No | Router match score thresholds (default `2`) |
|
|
473
474
|
| `DREAM` | No | `off` disables the nightly memory consolidation pass (default on) |
|
|
474
475
|
| `DREAM_CRON` | No | Schedule for the dream pass (default `0 4 * * *`) |
|
|
475
|
-
| `DREAM_PROVIDER` | No | Provider for dream and introspection (`claude` or `codex`); otherwise
|
|
476
|
+
| `DREAM_PROVIDER` | No | Provider for dream and introspection (`claude` or `codex`); otherwise the provider the owner chats on, then `DEFAULT_PROVIDER` |
|
|
476
477
|
| `DREAM_MODEL_CLAUDE` / `DREAM_MODEL_CODEX` | No | Exact per-provider dream model override; legacy `DREAM_MODEL` remains a Claude-only compatibility alias |
|
|
477
478
|
| `DREAM_TIER` | No | Provider-owned model tier for the dream pass: `low`, `medium`, or `high` (default) |
|
|
478
479
|
| `DREAM_FULL_SWEEP_DAYS` | No | Days between full-corpus dream sweeps; other nights are cheap deltas over recently-touched packs (default `30`) |
|
package/bin/cli.js
CHANGED
|
@@ -452,6 +452,11 @@ switch (command) {
|
|
|
452
452
|
require("./cards").runDecide(args.slice(1));
|
|
453
453
|
break;
|
|
454
454
|
}
|
|
455
|
+
case "teach": {
|
|
456
|
+
Promise.resolve(require("./teach").run(args.slice(1)))
|
|
457
|
+
.catch((e) => { console.error(`teach command failed: ${e.message}`); process.exitCode = 1; });
|
|
458
|
+
break;
|
|
459
|
+
}
|
|
455
460
|
case "schedule-wakeup": {
|
|
456
461
|
require("./schedule").runScheduleWakeup(args.slice(1));
|
|
457
462
|
break;
|
|
@@ -623,6 +628,7 @@ Send tools (only work inside an active bot-spawned task):
|
|
|
623
628
|
open-claudia card send <spec|-> Send a Kazee Card (decision, choice, rating, slider, report); answers wake this conversation
|
|
624
629
|
open-claudia card update <id> ... Close/open a card you sent, add a note, or replace its spec (card example <kind> for specs)
|
|
625
630
|
open-claudia decide "<q>" --option id="label" ... Queue a decision card for the owner (one at a time; decide list|cancel)
|
|
631
|
+
open-claudia teach list|show|finish|cancel <id> Browser recordings from Teach me; finish deletes the pictures, cancel the whole recording
|
|
626
632
|
|
|
627
633
|
Memory tools:
|
|
628
634
|
open-claudia transcript-search <query> Ranked FTS5 search over indexed transcripts
|
package/bin/teach.js
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// `open-claudia teach ...`: the bot's side of Teach me recordings (core/browser-teach.js). The
|
|
2
|
+
// bot is woken with a recording, posts its steps, checks them with the person, saves the skill,
|
|
3
|
+
// and then deletes the pictures with `teach finish`.
|
|
4
|
+
//
|
|
5
|
+
// Whose recordings a call may use is decided by the RUNNING BOT (loopback whoami over the run's
|
|
6
|
+
// attestation), never by this process's env: an owner turn sees every recording, anyone else's
|
|
7
|
+
// turn only their own, and a process with no live run behind it none.
|
|
8
|
+
|
|
9
|
+
const { whoami } = require("./loopback-client");
|
|
10
|
+
|
|
11
|
+
const HELP = `Usage:
|
|
12
|
+
open-claudia teach list Browser recordings (Teach me) you may see, newest first
|
|
13
|
+
open-claudia teach show <id> The steps as text
|
|
14
|
+
open-claudia teach finish <id> Delete the pictures, keep the text steps (once the skill is saved)
|
|
15
|
+
open-claudia teach cancel <id> Delete the whole recording
|
|
16
|
+
An owner turn sees every recording; anyone else's turn sees only their own.`;
|
|
17
|
+
|
|
18
|
+
function fail(message, code = 1) {
|
|
19
|
+
console.error(message);
|
|
20
|
+
process.exitCode = code;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function run(args, { caller = null } = {}) {
|
|
24
|
+
const teach = require("../core/browser-teach");
|
|
25
|
+
const sub = args[0];
|
|
26
|
+
if (!sub || sub === "help" || sub === "--help" || sub === "-h") {
|
|
27
|
+
console.log(HELP);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (!["list", "show", "finish", "cancel"].includes(sub)) return fail(HELP, 2);
|
|
31
|
+
let who = caller;
|
|
32
|
+
if (!who) {
|
|
33
|
+
try { who = await whoami(); } catch (_) { who = null; }
|
|
34
|
+
}
|
|
35
|
+
if (!who || who.attributed !== true) {
|
|
36
|
+
return fail("⛔ teach: this process is not attributed to a chat turn of this bot, so it cannot tell whose recordings it may use. Run it from the bot's turn.", 3);
|
|
37
|
+
}
|
|
38
|
+
const userId = who.owner === true ? null : String(who.userId || "");
|
|
39
|
+
if (userId === "") return fail("⛔ teach: this turn has no person to match recordings to.", 3);
|
|
40
|
+
|
|
41
|
+
if (sub === "list") {
|
|
42
|
+
const list = teach.listRecordings({ userId });
|
|
43
|
+
if (!list.length) {
|
|
44
|
+
console.log("No recordings.");
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
for (const r of list) {
|
|
48
|
+
const pictures = r.picturesDeleted ? "pictures deleted" : "pictures kept";
|
|
49
|
+
console.log(`${r.id} ${String(r.status).padEnd(9)} ${String(r.steps).padStart(3)} steps ${pictures.padEnd(16)} ${r.startedAt} "${teach.cleanText(r.title, 80).replace(/"/g, "'")}"`);
|
|
50
|
+
}
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const id = args[1];
|
|
55
|
+
if (!teach.validId(id)) return fail(`teach ${sub}: give a recording id (open-claudia teach list)`, 2);
|
|
56
|
+
const rec = teach.getRecording(id, { userId });
|
|
57
|
+
if (!rec) return fail(`teach ${sub}: no recording ${id} that this turn may use`);
|
|
58
|
+
|
|
59
|
+
if (sub === "show") {
|
|
60
|
+
console.log(teach.formatRecording(rec));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (sub === "finish") {
|
|
64
|
+
const result = teach.finishRecording(id);
|
|
65
|
+
if (!result.ok) {
|
|
66
|
+
return fail(result.error === "still-recording"
|
|
67
|
+
? `teach finish: ${id} is still recording; it can be finished once it is stopped`
|
|
68
|
+
: `teach finish: no recording ${id}`);
|
|
69
|
+
}
|
|
70
|
+
console.log(`Finished ${id}: ${result.removed} picture${result.removed === 1 ? "" : "s"} deleted, ${result.recording.steps.length} text steps kept.`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const result = teach.cancelRecording(id);
|
|
74
|
+
if (!result.ok) return fail(`teach cancel: no recording ${id}`);
|
|
75
|
+
console.log(`Cancelled ${id}: the recording and its pictures are deleted.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { run, HELP };
|
package/bot.js
CHANGED
|
@@ -275,6 +275,8 @@ async function safeModeBoot() {
|
|
|
275
275
|
console.error(`Adapter ${adapter.id} failed to start in safe mode:`, e.message);
|
|
276
276
|
}
|
|
277
277
|
}
|
|
278
|
+
// Teach-me pictures are still deleted on schedule: it only removes files, never runs a turn.
|
|
279
|
+
try { require("./core/browser-teach").startRetentionSweeper(); } catch (e) { console.error("Teach-me retention sweeper start failed:", e.message); }
|
|
278
280
|
const alert = safeBoot.formatSafeModeAlert({ version: CURRENT_VERSION });
|
|
279
281
|
console.error(alert);
|
|
280
282
|
const tg = adapters.find((a) => a.type === "telegram");
|
|
@@ -391,6 +393,11 @@ async function safeModeBoot() {
|
|
|
391
393
|
// Kazee Cards: snoozed owner decisions are asked again when due.
|
|
392
394
|
try { require("./core/cards").startDecisionPump(registry); } catch (e) { console.error("Decision pump start failed:", e.message); }
|
|
393
395
|
|
|
396
|
+
// Browser app: a driver whose panel went quiet hands the browser back (30 s sweep), and
|
|
397
|
+
// Teach-me pictures and old recordings are deleted on schedule (boot + hourly).
|
|
398
|
+
try { require("./core/browser-control").startSweeper(); } catch (e) { console.error("Browser lock sweeper start failed:", e.message); }
|
|
399
|
+
try { require("./core/browser-teach").startRetentionSweeper(); } catch (e) { console.error("Teach-me retention sweeper start failed:", e.message); }
|
|
400
|
+
|
|
394
401
|
// This incarnation counts as healthy once it survives the stable window —
|
|
395
402
|
// resets the crash-loop streak the safe-mode gate counts at boot.
|
|
396
403
|
setTimeout(() => { try { safeBoot.markBootStable(); } catch (e) {} }, safeBoot.STABLE_UPTIME_MS).unref();
|
|
@@ -1750,6 +1750,36 @@ class KazeeAdapter {
|
|
|
1750
1750
|
} catch (e) {
|
|
1751
1751
|
console.error("Kazee registerCommands failed:", e.message);
|
|
1752
1752
|
}
|
|
1753
|
+
await this.registerApps();
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
// Bot apps (chat-central PUT /bot/:botId/apps, the same shape as commands): with Agent Space
|
|
1757
|
+
// configured, the Browser app, opened from the Apps button of a direct chat with this bot or a
|
|
1758
|
+
// card's Open button (core/bot-apps.js). OC_BROWSER_APP=off sends an empty list. An
|
|
1759
|
+
// AGENTSPACE_API_URL that is not a public https address is skipped with one log line (chat-central
|
|
1760
|
+
// would refuse it). An older chat-central answers 404; that, or any failure, is logged once and
|
|
1761
|
+
// otherwise ignored.
|
|
1762
|
+
async registerApps(env = process.env) {
|
|
1763
|
+
const { apps, skip } = require("../../core/bot-apps").registrationPlan(env);
|
|
1764
|
+
if (skip && !this._appsSkipLogged) {
|
|
1765
|
+
this._appsSkipLogged = true;
|
|
1766
|
+
console.error(`Kazee registerApps skipped: ${skip}. The Browser app stays hidden.`);
|
|
1767
|
+
}
|
|
1768
|
+
if (!apps || !this.botUserId) return null;
|
|
1769
|
+
try {
|
|
1770
|
+
const res = await this._request("PUT", `/bot/${this.botUserId}/apps`, { apps });
|
|
1771
|
+
if (apps.length) console.log(`Kazee bot apps registered: ${apps.map((a) => a.key).join(", ")}`);
|
|
1772
|
+
return res;
|
|
1773
|
+
} catch (e) {
|
|
1774
|
+
if (!this._appsFailureLogged) {
|
|
1775
|
+
this._appsFailureLogged = true;
|
|
1776
|
+
const old = / → 404:/.test(String(e.message || ""));
|
|
1777
|
+
console.error(old
|
|
1778
|
+
? "Kazee registerApps: this chat-central has no bot apps yet (404); the Browser app stays hidden."
|
|
1779
|
+
: `Kazee registerApps failed (the Browser app stays hidden): ${String(e.message || e).slice(0, 300)}`);
|
|
1780
|
+
}
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1753
1783
|
}
|
|
1754
1784
|
|
|
1755
1785
|
// ── HTTP helpers ────────────────────────────────────────────────
|
package/core/actions.js
CHANGED
|
@@ -918,6 +918,10 @@ async function handleAction(envelope) {
|
|
|
918
918
|
if (d.startsWith("rq:")) {
|
|
919
919
|
return require("./turn-journal").handleRunDropped(d, envelope, { isOwner: isChatOwner(envelope.channelId) });
|
|
920
920
|
}
|
|
921
|
+
// The ✕ on a "Queued." notice: take that message out of the queue.
|
|
922
|
+
if (d.startsWith("qx:")) {
|
|
923
|
+
return require("./runner").handleQueueRemove(d, envelope, { isOwner: isChatOwner(envelope.channelId) });
|
|
924
|
+
}
|
|
921
925
|
if (d.startsWith("grd:")) {
|
|
922
926
|
// Guardrail capture buttons (Track C). Owner-only: standing rules layer
|
|
923
927
|
// above lessons and are never editable by an external speaker.
|
package/core/auth-flow.js
CHANGED
|
@@ -109,7 +109,12 @@ function looksLikeOpenAIKey(value) {
|
|
|
109
109
|
|
|
110
110
|
function isClaudeAuthErrorText(text) {
|
|
111
111
|
const lower = String(text || "").toLowerCase();
|
|
112
|
-
|
|
112
|
+
// `claude auth status` prints JSON in Claude Code 2.1: signed out it is
|
|
113
|
+
// {"loggedIn": false, "authMethod": "none"} (exit 1), which no phrase below
|
|
114
|
+
// matched, so a signed-out Claude read as "unknown" and was still chosen
|
|
115
|
+
// (9 Oct 2026: the dream of every Codex-only bot ran on Claude).
|
|
116
|
+
return /"loggedin"\s*:\s*false/.test(lower) ||
|
|
117
|
+
lower.includes("unauthorized") ||
|
|
113
118
|
lower.includes("not logged in") ||
|
|
114
119
|
lower.includes("login required") ||
|
|
115
120
|
lower.includes("reauthenticate") ||
|
|
@@ -145,7 +150,7 @@ function summarizeAuthOutput(output, ok) {
|
|
|
145
150
|
function summarizeClaudeAuthStatus(output, exitCode, tokenInfo) {
|
|
146
151
|
const text = String(output || "");
|
|
147
152
|
const lower = text.toLowerCase();
|
|
148
|
-
const loggedOut = /not (logged in|authenticated)|unauthenticated|no auth|login required/.test(lower);
|
|
153
|
+
const loggedOut = /not (logged in|authenticated)|unauthenticated|no auth|login required|"loggedin"\s*:\s*false/.test(lower);
|
|
149
154
|
const loggedIn = !loggedOut && (exitCode === 0 || /logged in|authenticated|claude\.ai|anthropic/.test(lower));
|
|
150
155
|
const email = (text.match(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/i) || [null])[0];
|
|
151
156
|
const provider = /claude\.ai|claudeai/.test(lower) ? "Claude.ai" : (/anthropic/.test(lower) ? "Anthropic" : "unknown");
|
package/core/bot-apps.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// Bot apps in Kazee Chat (SPEC "Bot apps v1", contracts C1 and C8).
|
|
2
|
+
//
|
|
3
|
+
// The bot registers the apps it offers with chat-central (PUT /bot/:botId/apps, done by the
|
|
4
|
+
// Kazee adapter after its slash commands), and Kazee Chat shows them behind an Apps button in
|
|
5
|
+
// a direct chat with the bot. Opening one goes through Agent Space (the front door), which asks
|
|
6
|
+
// this bot, over the control token, whether that person may use it: checkAccess() below.
|
|
7
|
+
//
|
|
8
|
+
// v1 has one app, "browser": the bot's own agent-browser, shown live, with Take over and
|
|
9
|
+
// Teach me (core/browser-control.js, core/browser-teach.js). OC_BROWSER_APP=off withdraws it:
|
|
10
|
+
// the next registration sends an empty list and every check answers "not allowed".
|
|
11
|
+
|
|
12
|
+
const BROWSER_APP_KEY = "browser";
|
|
13
|
+
const ID_RE = /^[A-Za-z0-9._:-]{1,100}$/;
|
|
14
|
+
|
|
15
|
+
function browserAppEnabled(env = process.env) {
|
|
16
|
+
return String(env.OC_BROWSER_APP || "").trim().toLowerCase() !== "off";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// An address only reachable inside the cluster or this machine (a Service name such as
|
|
20
|
+
// agentspace-backend.<ns>.svc, localhost, an IP): no person's Kazee Chat can open a page there.
|
|
21
|
+
function internalHost(hostname) {
|
|
22
|
+
const host = String(hostname || "").toLowerCase().replace(/\.$/, "");
|
|
23
|
+
if (!host.includes(".") || host.startsWith("[")) return true; // a bare Service name, or IPv6
|
|
24
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host)) return true;
|
|
25
|
+
return /(^|\.)(localhost|local|internal|svc|cluster\.local)$/.test(host);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Why AGENTSPACE_API_URL cannot carry the page, or "" when it can. chat-central accepts only
|
|
29
|
+
// https addresses on its allowed origins, so anything else would be refused there anyway.
|
|
30
|
+
function browserAppUrlProblem(apiUrl) {
|
|
31
|
+
let u;
|
|
32
|
+
try { u = new URL(String(apiUrl || "").trim()); } catch (_) { return "it is not a URL"; }
|
|
33
|
+
if (u.protocol !== "https:") return "it is not https";
|
|
34
|
+
if (internalHost(u.hostname)) return "it is an in-cluster address";
|
|
35
|
+
return "";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The page lives on Agent Space's API host: <AGENTSPACE_API_URL without trailing slash> +
|
|
39
|
+
// "/apps/browser". Every live pod has AGENTSPACE_API_URL=https://api.agentspace.kazee.africa/api,
|
|
40
|
+
// so the page is https://api.agentspace.kazee.africa/api/apps/browser. A bare origin means
|
|
41
|
+
// "/api", the same path base the pod's other Agent Space calls use (core/cluster-client.js).
|
|
42
|
+
function browserAppUrl(apiUrl) {
|
|
43
|
+
if (browserAppUrlProblem(apiUrl)) return null;
|
|
44
|
+
const u = new URL(String(apiUrl).trim());
|
|
45
|
+
const base = u.pathname.replace(/\/+$/, "") || "/api";
|
|
46
|
+
return `${u.origin}${base}/apps/browser`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// What to do with chat-central, and why not:
|
|
50
|
+
// { apps: null } nothing (no Agent Space, so no page to open)
|
|
51
|
+
// { apps: null, skip: <why> } nothing: AGENTSPACE_API_URL is not a public https address
|
|
52
|
+
// { apps: [] } withdraw (OC_BROWSER_APP=off)
|
|
53
|
+
// { apps: [<browser>] } offer the Browser app
|
|
54
|
+
function registrationPlan(env = process.env) {
|
|
55
|
+
const raw = String(env.AGENTSPACE_API_URL || "").trim();
|
|
56
|
+
if (!raw) return { apps: null, skip: null };
|
|
57
|
+
if (!browserAppEnabled(env)) return { apps: [], skip: null };
|
|
58
|
+
const problem = browserAppUrlProblem(raw);
|
|
59
|
+
if (problem) {
|
|
60
|
+
let origin = "";
|
|
61
|
+
try { origin = new URL(raw).origin; } catch (_) { origin = ""; }
|
|
62
|
+
const shown = origin && origin !== "null" ? ` (${origin})` : "";
|
|
63
|
+
return { apps: null, skip: `AGENTSPACE_API_URL${shown} cannot carry the Browser page: ${problem}, and chat-central only accepts public https app addresses` };
|
|
64
|
+
}
|
|
65
|
+
return { apps: [{ key: BROWSER_APP_KEY, title: "Browser", url: browserAppUrl(raw), icon: "globe" }], skip: null };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// What to register with chat-central: null = do nothing, [] = withdraw, else the Browser app.
|
|
69
|
+
function appsToRegister(env = process.env) {
|
|
70
|
+
return registrationPlan(env).apps;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function ownerUserId() {
|
|
74
|
+
try {
|
|
75
|
+
const value = require("./config").config.KAZEE_OWNER_USER_ID;
|
|
76
|
+
if (value) return String(value);
|
|
77
|
+
} catch (_) { /* fall back to the process env */ }
|
|
78
|
+
return String(process.env.KAZEE_OWNER_USER_ID || "");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function ownerName() {
|
|
82
|
+
try {
|
|
83
|
+
const owner = (require("./people").owners() || [])[0];
|
|
84
|
+
return owner && owner.name ? String(owner.name) : "";
|
|
85
|
+
} catch (_) {
|
|
86
|
+
return "";
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The authorized list of auth.json, read under its store lock so a roster change in flight is
|
|
91
|
+
// never half seen (core/access.js mutateAuth holds the same lock).
|
|
92
|
+
function authorizedEntries() {
|
|
93
|
+
const { loadAuth } = require("./access");
|
|
94
|
+
const { withFileLock } = require("./fsutil");
|
|
95
|
+
const { AUTH_FILE } = require("./config");
|
|
96
|
+
return withFileLock(AUTH_FILE, () => loadAuth().authorized);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// C1: may this person open this app in this direct chat? Agent Space has already checked with
|
|
100
|
+
// chat-central that `userId` is the person in the direct chat `chatId` with this bot, so:
|
|
101
|
+
// - the Kazee owner (KAZEE_OWNER_USER_ID) is allowed, role "owner";
|
|
102
|
+
// - a chat in auth.json's authorized list is allowed, role "user" ("owner" when that entry is
|
|
103
|
+
// an owner entry), with the name the roster has for it;
|
|
104
|
+
// - anyone else, and any app other than "browser", is not.
|
|
105
|
+
function checkAccess({ app, chatId, userId } = {}) {
|
|
106
|
+
const no = { allowed: false, role: null, name: null };
|
|
107
|
+
if (app !== BROWSER_APP_KEY || !browserAppEnabled()) return no;
|
|
108
|
+
const chat = String(chatId || "");
|
|
109
|
+
const user = String(userId || "");
|
|
110
|
+
if (!ID_RE.test(chat) || !ID_RE.test(user)) return no;
|
|
111
|
+
let entries = [];
|
|
112
|
+
try { entries = authorizedEntries(); } catch (_) { entries = []; }
|
|
113
|
+
const entry = entries.find((a) => a && String(a.chatId) === chat) || null;
|
|
114
|
+
const owner = ownerUserId();
|
|
115
|
+
if (owner && user === owner) {
|
|
116
|
+
return { allowed: true, role: "owner", name: String((entry && entry.name) || ownerName() || "") };
|
|
117
|
+
}
|
|
118
|
+
if (entry) {
|
|
119
|
+
return { allowed: true, role: entry.isOwner === true ? "owner" : "user", name: String(entry.name || "") };
|
|
120
|
+
}
|
|
121
|
+
return no;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = {
|
|
125
|
+
BROWSER_APP_KEY,
|
|
126
|
+
ID_RE,
|
|
127
|
+
appsToRegister,
|
|
128
|
+
browserAppEnabled,
|
|
129
|
+
browserAppUrl,
|
|
130
|
+
browserAppUrlProblem,
|
|
131
|
+
checkAccess,
|
|
132
|
+
registrationPlan,
|
|
133
|
+
};
|