@inetafrica/open-claudia 3.5.0 → 3.5.4

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +32 -2
  2. package/bin/cli.js +13 -5
  3. package/bin/loopback-client.js +23 -5
  4. package/bin/owner-gate.js +2 -1
  5. package/bin/schedule.js +7 -2
  6. package/bin/tool.js +2 -2
  7. package/bot.js +6 -2
  8. package/channels/kazee/adapter.js +462 -64
  9. package/channels/kazee/state.js +78 -2
  10. package/channels/types.js +11 -5
  11. package/core/actions.js +6 -0
  12. package/core/approvals.js +9 -0
  13. package/core/bot-endpoint.js +194 -0
  14. package/core/context.js +11 -2
  15. package/core/handlers.js +161 -59
  16. package/core/identity.js +33 -3
  17. package/core/io.js +21 -8
  18. package/core/jobs.js +3 -0
  19. package/core/loopback.js +55 -7
  20. package/core/providers/claude.js +6 -2
  21. package/core/router.js +25 -13
  22. package/core/run-context.js +49 -8
  23. package/core/runner.js +313 -126
  24. package/core/scheduler.js +64 -32
  25. package/core/side-chat.js +40 -11
  26. package/core/speaker-attest.js +2 -0
  27. package/core/state.js +317 -50
  28. package/core/system-prompt.js +61 -14
  29. package/core/tool-guard.js +15 -0
  30. package/core/transcripts.js +23 -6
  31. package/package.json +3 -3
  32. package/test-approval-cards.js +24 -0
  33. package/test-call-speech.js +5 -3
  34. package/test-fixtures/fake-agent-cli.js +52 -1
  35. package/test-provider-compaction.js +50 -11
  36. package/test-provider-gateway.js +31 -10
  37. package/test-provider-language.js +15 -1
  38. package/test-provider-parity-e2e.js +8 -2
  39. package/test-provider-scheduler.js +77 -20
  40. package/test-provider-session-commands.js +11 -2
  41. package/test-provider-side-chat.js +30 -2
  42. package/test-run-lock.js +122 -39
  43. package/test-thread-lanes.js +530 -0
  44. package/core/queue-drain.js +0 -25
  45. package/test-queue-routing.js +0 -52
@@ -606,6 +606,19 @@ function mentionsCredentialPath(cmd) {
606
606
  });
607
607
  }
608
608
 
609
+ // Words a guarded turn's command may not contain at all (Phase 0 review,
610
+ // HIGH): the path-anchored rules above only see one spelling of a read, so the
611
+ // names themselves are refused — process environments and argument blocks, the
612
+ // keyring, the bot's own config dir, and the credential stores above as path
613
+ // components — and so is any per-process /proc path, as the read gate already
614
+ // refuses /proc to Read/Grep/Glob. This narrows the shell; it is not a
615
+ // boundary. Phase 2 takes the shell away from staff turns.
616
+ const SECRET_WORD_RE = /\b(?:environ|procargs\d*|keyring)\b/i;
617
+ const PROC_PROCESS_RE = /\/proc\/(?:\d|self\b|thread-self\b|[$*?[{])/;
618
+ const SECRET_COMPONENT_RE = new RegExp(
619
+ `(?:^|[\\s/'"=:(\`])(?:\\.open|${HOME_SECRET_PATHS.map((rel) => rel.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?![A-Za-z0-9_-])`,
620
+ );
621
+
609
622
  // Pure: null, or the id of the secret-read pattern the command matches.
610
623
  function secretReadRule(command) {
611
624
  const cmd = String(command || "");
@@ -614,6 +627,8 @@ function secretReadRule(command) {
614
627
  if (psShowsEnvironment(cmd)) return "ps-environment";
615
628
  if (KEYCHAIN_RE.test(cmd)) return "os-keychain";
616
629
  if (mentionsCredentialPath(cmd)) return "credential-store";
630
+ if (PROC_PROCESS_RE.test(cmd)) return "proc-process";
631
+ if (SECRET_WORD_RE.test(cmd) || SECRET_COMPONENT_RE.test(cmd)) return "secret-word";
617
632
  return null;
618
633
  }
619
634
 
@@ -7,7 +7,7 @@ const {
7
7
  CONFIG_DIR, PROJECT_TRANSCRIPTS, TRANSCRIPTS_DIR, TRANSCRIPT_MAX_ENTRY_CHARS,
8
8
  } = require("./config");
9
9
  const { redactSensitive } = require("./redact");
10
- const { currentChannelId } = require("./context");
10
+ const { currentChannelId, currentThreadId } = require("./context");
11
11
  const { currentState } = require("./state");
12
12
  const transcriptIndex = require("./transcript-index");
13
13
 
@@ -61,11 +61,16 @@ function appendProjectTranscript(role, text, metadata = {}, state = currentState
61
61
  if (!state.currentSession) return null;
62
62
  try {
63
63
  const transport = (state.userId || "").split(":")[0] || "telegram";
64
+ // A chat thread's turns carry its root id; main-timeline entries stay as
65
+ // they always were (no threadId key).
66
+ const threadId = currentThreadId();
67
+ const chat = { transport, id: String(currentChannelId() || "") };
68
+ if (threadId) chat.threadId = String(threadId);
64
69
  const result = projectTranscripts.append({
65
70
  role,
66
71
  text,
67
72
  userId: state.userId,
68
- chat: { transport, id: String(currentChannelId() || "") },
73
+ chat,
69
74
  projectName: state.currentSession.name,
70
75
  projectPath: state.currentSession.dir,
71
76
  backend: state.settings.backend,
@@ -84,7 +89,12 @@ function appendProjectTranscript(role, text, metadata = {}, state = currentState
84
89
  }
85
90
  }
86
91
 
87
- // Last few user/assistant turns on THIS channel, for recall context.
92
+ // Last few user/assistant turns on THIS channel (and, in a chat thread, this
93
+ // thread only; on the main timeline, the main timeline only), for recall
94
+ // context.
95
+ // How far back recentTranscriptText looks for its conversation's entries.
96
+ const RECENT_SCAN_LINES = 4000;
97
+
88
98
  // The current turn's user message is already appended by the time the
89
99
  // prompt is built, so the trailing user entry is dropped — the live
90
100
  // message drives recall directly as origin "user".
@@ -96,14 +106,21 @@ function recentTranscriptText({ limit = 6, perEntryChars = 280 } = {}, state = c
96
106
  const raw = fs.readFileSync(info.transcriptPath, "utf-8").trim();
97
107
  if (!raw) return "";
98
108
  const channel = String(currentChannelId() || "");
109
+ const thread = currentThreadId() ? String(currentThreadId()) : null;
110
+ // This conversation's last 80 entries, not this conversation's share of
111
+ // the file's last 80 lines: busy threads writing to the same transcript
112
+ // would otherwise leave a quiet one (or the main timeline) almost nothing.
113
+ // Newest first, bounded so a huge file is never parsed whole.
99
114
  const entries = [];
100
- for (const line of raw.split("\n").slice(-80)) {
115
+ const lines = raw.split("\n");
116
+ for (let i = lines.length - 1; i >= Math.max(0, lines.length - RECENT_SCAN_LINES) && entries.length < 80; i -= 1) {
101
117
  try {
102
- const obj = JSON.parse(line);
118
+ const obj = JSON.parse(lines[i]);
103
119
  if (obj.role !== "user" && obj.role !== "assistant") continue;
104
120
  if (channel && obj.chat?.id && String(obj.chat.id) !== channel) continue;
121
+ if (channel && (obj.chat?.threadId ? String(obj.chat.threadId) : null) !== thread) continue;
105
122
  if (typeof obj.text !== "string" || !obj.text.trim()) continue;
106
- entries.push(obj);
123
+ entries.unshift(obj);
107
124
  } catch (e) {}
108
125
  }
109
126
  if (entries.length > 0 && entries[entries.length - 1].role === "user") entries.pop();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inetafrica/open-claudia",
3
- "version": "3.5.0",
3
+ "version": "3.5.4",
4
4
  "description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
5
5
  "main": "bot.js",
6
6
  "bin": {
@@ -11,7 +11,7 @@
11
11
  "setup": "node setup.js",
12
12
  "start": "node bot.js",
13
13
  "pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-upgrade-path.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-kazee-context-media.js && node test-kazee-typing-join.js && node test-kazee-inbound-dedup.js && node test-typing-phase-push.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-call-speech.js && node test-kazee-call-session.js",
14
- "test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && node test-codex-rollout.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-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-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-stream-preview-purpose.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-attest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-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-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"
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 && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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 && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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"
15
15
  },
16
16
  "files": [
17
17
  "bot.js",
@@ -62,7 +62,6 @@
62
62
  "test-approval-async.js",
63
63
  "test-recall-evolution.js",
64
64
  "test-unified-identity.js",
65
- "test-queue-routing.js",
66
65
  "test-persona-packs.js",
67
66
  "test-speaker-persona.js",
68
67
  "test-persona-pipeline.js",
@@ -88,6 +87,7 @@
88
87
  "test-guardrail.js",
89
88
  "test-delivery-contract.js",
90
89
  "test-run-lock.js",
90
+ "test-thread-lanes.js",
91
91
  "test-web-sessions.js",
92
92
  "test-provider-fixture.js",
93
93
  "test-provider-core-prompt.js",
@@ -216,6 +216,30 @@ const lastSend = () => sends[sends.length - 1]?.text || "";
216
216
  assert.strictEqual(issued[0].principal.channelId, "user-lucy");
217
217
  assert.strictEqual(issued[0].room, "kazee:ext-channel");
218
218
 
219
+ // ── chat threads: an approval raised in a thread turn re-runs in that thread ──
220
+ {
221
+ const ROOT = "66f0a1b2c3d4e5f600000001";
222
+ const { runInChat } = require("./core/context");
223
+ // Raised inside the thread turn (the enforcer's case): taken from the context.
224
+ const inThread = await runInChat({ channelId: "ext-channel", threadId: ROOT }, async () =>
225
+ approvals.create({ ...base, kind: "triage", inboundText: "Check invoice 7?" }));
226
+ assert.strictEqual(inThread.threadId, ROOT, "the origin thread is recorded");
227
+ // A turn in ANOTHER channel never lends its thread to the record.
228
+ const elsewhere = await runInChat({ channelId: "owner-chat", threadId: ROOT }, async () =>
229
+ approvals.create({ ...base, kind: "triage", inboundText: "x" }));
230
+ assert.strictEqual(elsewhere.threadId, null, "no thread when the origin is another channel");
231
+ assert.strictEqual(approvals.create({ ...base, kind: "triage", inboundText: "y" }).threadId, null, "main timeline: null");
232
+ const before = jobsAdded.length;
233
+ await handleAction(envelopeFor(`apr:${inThread.id}:ok`));
234
+ assert.strictEqual(jobsAdded.length, before + 1);
235
+ assert.strictEqual(jobsAdded.at(-1).threadId, ROOT, "the triage wake-up fires in the thread");
236
+ // External action approval (explicit threadId) re-runs in the thread too.
237
+ const act = approvals.create({ ...base, kind: "action", tier: "write", command: "open-claudia tool run kticket create", threadId: ROOT });
238
+ await handleAction(envelopeFor(`apr:${act.id}:ok`));
239
+ assert.strictEqual(jobsAdded.at(-1).threadId, ROOT, "the approval re-run fires in the thread");
240
+ assert.strictEqual(jobsAdded.at(-1).source, "approval");
241
+ }
242
+
219
243
  // ── expired card: nothing to decide ──
220
244
  await handleAction(envelopeFor("apr:apr_0000_dead:ok"));
221
245
  assert.ok(/expired|cleaned up/.test(lastEdit()), "stale card gets a clear ack");
@@ -206,11 +206,13 @@ if (process.env.OC_CALL_SPEECH_FIXTURE === "1") {
206
206
  const f = fixture();
207
207
  const state = { userId: "kazee:caller", currentSession: { name: "call-fixture", dir: process.env.WORKSPACE },
208
208
  settings: { backend: "claude", model: "fixture-model", permissionMode: "default" },
209
- providerSettings: { claude: { model: "fixture-model" } }, activeSessions: {}, messageQueue: [] };
209
+ providerSettings: { claude: { model: "fixture-model" } }, activeSessions: {} };
210
+ // The running process is recorded on the run's conversation lane.
211
+ const runningLane = () => [...(state.lanes?.values() || [])].find((lane) => lane.runningProcess) || null;
210
212
  const spoken = [];
211
213
  f.adapter.suppressStreamPreview = true; f.adapter.suppressChatVisibility = true;
212
214
  f.adapter.speakSentence = async text => {
213
- assert.ok(state.runningProcess, "sentence is spoken before provider completion");
215
+ assert.ok(runningLane(), "sentence is spoken before provider completion");
214
216
  spoken.push(text); return true;
215
217
  };
216
218
  f.adapter.send = async (_channel, text) => {
@@ -224,7 +226,7 @@ if (process.env.OC_CALL_SPEECH_FIXTURE === "1") {
224
226
  assert.deepStrictEqual(spoken, ["The first result is ready.", "The second result is ready."]);
225
227
  const token = lastLease.token;
226
228
  assert.strictEqual((await post(info, { ...f.payload, callSpeechToken: token, originRunId: result.runId })).status, 409);
227
- assert.strictEqual(state.runningProcess, null);
229
+ assert.strictEqual(runningLane(), null);
228
230
  const capturedService = createForegroundService({ state, store: f.store, stopTyping() {}, capture: true, ownsAdmissionLock: false, persistCapture: false });
229
231
  const captured = await context.runInChat(f.store, () => capturedService.runAgentCapture("Capture only.", process.env.WORKSPACE));
230
232
  assert.strictEqual(captured.ok, true, JSON.stringify(captured.error));
@@ -226,7 +226,10 @@ function inferSessionId() {
226
226
  if (process.env.FAKE_AGENT_SESSION_ID) return process.env.FAKE_AGENT_SESSION_ID;
227
227
  if (kind === "claude") {
228
228
  const resumeAt = argv.indexOf("--resume");
229
+ // --fork-session resumes into a NEW session, as Claude Code does.
230
+ if (resumeAt >= 0 && argv[resumeAt + 1] && argv.includes("--fork-session")) return `${argv[resumeAt + 1]}-fork-${process.pid}`;
229
231
  if (resumeAt >= 0 && argv[resumeAt + 1]) return argv[resumeAt + 1];
232
+ if (boolControl("FAKE_AGENT_UNIQUE_SESSIONS")) return `claude-fresh-session-${process.pid}`;
230
233
  if (argv.includes("--continue")) return "claude-continued-session";
231
234
  return "claude-fresh-session";
232
235
  }
@@ -421,7 +424,7 @@ async function emitEvents(events) {
421
424
  line + (index < lines.length - 1 || finalNewline ? ending : "")
422
425
  ));
423
426
  const mode = process.env.FAKE_AGENT_CHUNK_MODE || "lines";
424
- const delayMs = integerControl("FAKE_AGENT_CHUNK_DELAY_MS", 0);
427
+ const delayMs = chunkDelayOverride ?? integerControl("FAKE_AGENT_CHUNK_DELAY_MS", 0);
425
428
 
426
429
  if (mode === "coalesce") {
427
430
  const payload = framed.join("");
@@ -469,6 +472,48 @@ async function emitEvents(events) {
469
472
  }
470
473
  }
471
474
 
475
+ // Concurrency probes. FAKE_AGENT_DELAY_MS holds the run before it answers; a
476
+ // prompt containing [[delay:<ms>]] overrides it for that run only, and
477
+ // [[chunkdelay:<ms>]] overrides FAKE_AGENT_CHUNK_DELAY_MS (the pause between
478
+ // streamed events) for that run.
479
+ // FAKE_AGENT_RUN_LOG appends one JSON line when the run starts and one when it
480
+ // answers ({event, at, pid, project, threadId, runId, tag, fork, resume,
481
+ // sessionEnv, lastSessionEnv}), where tag
482
+ // comes from a [[tag:<word>]] marker in the prompt, so a test can prove which
483
+ // runs overlapped and where each one belonged.
484
+ let chunkDelayOverride = null;
485
+ function promptChunkDelay(stdin) {
486
+ const match = /\[\[chunkdelay:(\d{1,5})\]\]/.exec(String(stdin || ""));
487
+ return match ? Math.min(60000, Number(match[1])) : null;
488
+ }
489
+
490
+ function runDelayMs(stdin) {
491
+ const match = /\[\[delay:(\d{1,5})\]\]/.exec(String(stdin || ""));
492
+ if (match) return Math.min(60000, Number(match[1]));
493
+ return integerControl("FAKE_AGENT_DELAY_MS", 0);
494
+ }
495
+
496
+ function logRun(event, stdin) {
497
+ const logPath = process.env.FAKE_AGENT_RUN_LOG;
498
+ if (!logPath) return;
499
+ const tag = /\[\[tag:([\w-]{1,40})\]\]/.exec(String(stdin || ""));
500
+ const record = {
501
+ event,
502
+ at: Date.now(),
503
+ pid: process.pid,
504
+ project: process.env.OC_PROJECT || null,
505
+ threadId: process.env.OC_THREAD_ID || null,
506
+ runId: process.env.OC_RUN_ID || null,
507
+ tag: tag ? tag[1] : null,
508
+ fork: argv.includes("--fork-session"),
509
+ resume: argv.indexOf("--resume") >= 0 ? argv[argv.indexOf("--resume") + 1] || null : null,
510
+ sessionEnv: process.env.OC_SESSION_ID || null,
511
+ lastSessionEnv: process.env.OC_LAST_SESSION_ID || null,
512
+ };
513
+ fs.mkdirSync(path.dirname(logPath), { recursive: true });
514
+ fs.appendFileSync(logPath, `${JSON.stringify(record)}\n`);
515
+ }
516
+
472
517
  function installSignalControls() {
473
518
  const signalCapture = process.env.FAKE_AGENT_SIGNAL_CAPTURE;
474
519
  const onSignal = (signal) => {
@@ -502,9 +547,15 @@ async function main() {
502
547
  return;
503
548
  }
504
549
 
550
+ // The foreground prompt rides argv, utility prompts ride stdin.
551
+ const promptText = `${argv.join(" ")}\n${stdin}`;
552
+ chunkDelayOverride = promptChunkDelay(promptText);
553
+ logRun("start", promptText);
554
+ await sleep(runDelayMs(promptText));
505
555
  const sessionId = inferSessionId();
506
556
  const events = kind === "claude" ? claudeEvents(sessionId) : codexEvents(sessionId);
507
557
  await emitEvents(events);
558
+ logRun("end", promptText);
508
559
 
509
560
  if (scenario === "auth-failure") {
510
561
  process.stderr.write(`${kind} authentication failed\n`);
@@ -26,6 +26,9 @@ const stateApi = require("./core/state");
26
26
  const runner = require("./core/runner");
27
27
 
28
28
  const state = stateApi.currentState();
29
+ // Compaction runs in ONE conversation lane: outside a chat turn that is the
30
+ // state's project ("alpha" here).
31
+ const alphaLane = () => stateApi.laneFor(state, "alpha");
29
32
 
30
33
  function result(overrides = {}) {
31
34
  return {
@@ -58,9 +61,7 @@ function resetFixture(provider = "claude") {
58
61
  state.pendingCompaction = null;
59
62
  state.completedCompactions = {};
60
63
  state.lastCompactedAt = 0;
61
- state.isCompacting = false;
62
- state.runningProcess = null;
63
- state.preparingRun = false;
64
+ state.lanes?.clear();
64
65
  state.isFirstMessage = false;
65
66
  stateApi.replaceProjectSessions(state.userId, "alpha", [
66
67
  {
@@ -92,9 +93,9 @@ function serviceWith(runCapture, extra = {}) {
92
93
 
93
94
  async function immutableSuccessProbe() {
94
95
  resetFixture("claude");
95
- state.isCompacting = true;
96
+ alphaLane().isCompacting = true;
96
97
  assert.strictEqual(stateApi.acquireRunLock(state), false, "foreground admission stays closed between compaction stages");
97
- state.isCompacting = false;
98
+ alphaLane().isCompacting = false;
98
99
  const contexts = [];
99
100
  const service = serviceWith(async (_prompt, _cwd, options) => {
100
101
  assert.strictEqual(options.persist, false, "compaction provider stages have no ordinary capture persistence");
@@ -152,17 +153,17 @@ async function immutableSuccessProbe() {
152
153
  const usage = stateApi.getUsageForSession(state, "alpha", "claude", "new-claude");
153
154
  assert.strictEqual(usage.turns, 0, "seed is not billed as a user turn");
154
155
  assert.strictEqual(usage.compactionCheckpoint.previousSessionId, "old-claude");
155
- assert.strictEqual(state.isCompacting, false);
156
+ assert.strictEqual(alphaLane().isCompacting, false);
156
157
  }
157
158
 
158
159
  async function failureProbe() {
159
160
  resetFixture("claude");
160
- state.runningProcess = { pid: 123 };
161
+ alphaLane().runningProcess = { pid: 123 };
161
162
  let busyCalls = 0;
162
163
  const busy = await serviceWith(async () => { busyCalls += 1; return result(); }).compact(alphaDir);
163
164
  assert.strictEqual(busy.compacted, false);
164
165
  assert.strictEqual(busyCalls, 0, "manual compaction never overlaps an active provider run");
165
- state.runningProcess = null;
166
+ alphaLane().runningProcess = null;
166
167
 
167
168
  resetFixture("claude");
168
169
  let summaryCalls = 0;
@@ -354,15 +355,15 @@ async function deferredWatchAcrossCompactionProbe() {
354
355
  },
355
356
  });
356
357
 
357
- state.runningProcess = { pid: 4242 };
358
+ alphaLane().runningProcess = { pid: 4242 };
358
359
  assert.strictEqual((await scheduler.fireJob("watch-long-turn")).deferred, true, "a watch that passes mid-turn is queued");
359
360
  for (let i = 0; i < 3; i++) await timers.at(-1).fn();
360
- state.runningProcess = null;
361
+ alphaLane().runningProcess = null;
361
362
  let calls = 0;
362
363
  const compaction = serviceWith(async () => {
363
364
  calls += 1;
364
365
  if (calls === 1) {
365
- assert.strictEqual(state.isCompacting, true);
366
+ assert.strictEqual(alphaLane().isCompacting, true);
366
367
  assert.strictEqual((await timers.at(-1).fn()).deferred, true, "the delivery waits out the compaction");
367
368
  return result({ sessionId: "old-claude" });
368
369
  }
@@ -381,6 +382,43 @@ async function deferredWatchAcrossCompactionProbe() {
381
382
  }
382
383
  }
383
384
 
385
+ // Compaction is per conversation: compacting one lane keeps only THAT lane
386
+ // closed, and the recent turns it seeds from come from that conversation's
387
+ // transcript — not the state's legacy "Workspace" session.
388
+ async function perLaneProbe() {
389
+ resetFixture("claude");
390
+ const { runInChat } = require("./core/context");
391
+ const convo = "telegram:fixture";
392
+ const thread = "telegram:fixture#thread:eeeeeeeeeeeeeeeeeeeeeeee";
393
+ state.currentSession = stateApi.rootSession();
394
+ stateApi.setProviderSession(state, "claude", convo, "old-claude");
395
+ const views = [];
396
+ let lanesDuringCompaction = null;
397
+ const service = serviceWith(async () => {
398
+ if (!lanesDuringCompaction) {
399
+ lanesDuringCompaction = {
400
+ own: stateApi.acquireRunLock(state, convo),
401
+ thread: stateApi.acquireRunLock(state, thread),
402
+ compacting: stateApi.laneFor(state, convo).isCompacting,
403
+ };
404
+ stateApi.laneFor(state, thread).preparingRun = false;
405
+ return result({ sessionId: "old-claude" });
406
+ }
407
+ return result({ sessionId: "lane-new-claude" });
408
+ }, { collectRecentTurns: (view) => { views.push(view); return null; } });
409
+ const compacted = await runInChat({ channelId: "fixture", conversationKey: convo, roomKey: convo, canonicalUserId: state.userId },
410
+ () => service.compact(workspace, { operationId: "compact-lane" }));
411
+ assert.strictEqual(compacted.compacted, true, compacted.reason);
412
+ assert.deepStrictEqual(lanesDuringCompaction, { own: false, thread: true, compacting: true },
413
+ "only the compacting conversation is closed; a thread of the same chat can start");
414
+ assert.strictEqual(stateApi.laneFor(state, convo).isCompacting, false);
415
+ assert.strictEqual(views.length, 1);
416
+ assert.strictEqual(views[0].currentSession.name, convo, "recent turns are read from the conversation's own transcript");
417
+ assert.strictEqual(stateApi.getProviderSession(state, "claude", convo), "lane-new-claude");
418
+ assert.strictEqual(runner.shouldAutoCompact(state, { laneKey: thread }), false, "a thread with no session never auto-compacts");
419
+ state.lanes.clear();
420
+ }
421
+
384
422
  function duplicateCommitProbe() {
385
423
  resetFixture("claude");
386
424
  const transaction = {
@@ -457,6 +495,7 @@ function restartRecoveryProbe() {
457
495
  await failureProbe();
458
496
  await codexRestrictionProbe();
459
497
  await deferredWatchAcrossCompactionProbe();
498
+ await perLaneProbe();
460
499
  duplicateCommitProbe();
461
500
  restartRecoveryProbe();
462
501
  console.log("provider compaction OK");
@@ -37,13 +37,12 @@ async function routerProbe() {
37
37
  claude: { model: "claude-fixture" },
38
38
  codex: { model: "codex-fixture" },
39
39
  },
40
- messageQueue: [],
41
40
  pendingCodexAuthProcess: null,
42
41
  pendingClaudeAuthProcess: null,
43
42
  pendingVaultUnlock: false,
44
43
  onboardingStep: null,
45
- lastInputWasVoice: false,
46
44
  };
45
+ const lane = { messageQueue: [], lastInputWasVoice: false, pendingVoiceClone: null };
47
46
  const admissions = [];
48
47
  const runs = [];
49
48
  const sends = [];
@@ -55,6 +54,10 @@ async function routerProbe() {
55
54
  autoLinkOwnerChannel: () => null,
56
55
  getActiveProvider: (s) => (s || state).settings?.backend || null,
57
56
  getProjectKey: (s) => (s || state).currentSession?.name || "",
57
+ laneFor: () => lane,
58
+ peekLane: () => lane,
59
+ laneBusy: (l) => !!(l && (l.runningProcess || l.preparingRun)),
60
+ slotBlocker: () => null,
58
61
  });
59
62
  // The up-front triage gate is pinned in test-enforcer.js; here it must only
60
63
  // be CONSULTED with the routed text + provider/project pinning, then let the
@@ -327,12 +330,16 @@ async function schedulerProbe() {
327
330
  getProviderSession: (_state, provider, project) => (
328
331
  state.activeSessions?.[project]?.[provider] || null
329
332
  ),
333
+ peekLane: () => null,
334
+ laneBusy: () => false,
335
+ slotBlocker: () => null,
330
336
  userStates: new Map(),
331
337
  });
332
338
  installStub("./core/identity", {
333
339
  canonicalForTelegram: () => "fixture-user",
334
340
  canonicalForChannel: () => "fixture-user",
335
341
  conversationKeyFor: (transport, channelId) => `${transport}:${channelId}`,
342
+ parseConversationKey: (key) => ({ threadId: null, roomKey: String(key || "") }),
336
343
  });
337
344
  installStub("./core/io", {
338
345
  send: async () => {
@@ -420,10 +427,15 @@ async function queueProbe() {
420
427
  claude: { model: "claude-fixture", effort: "high" },
421
428
  codex: { model: "codex-fixture", effort: "low" },
422
429
  },
430
+ };
431
+ // The conversation's lane is busy preparing a turn, so new work queues in it.
432
+ const lane = {
433
+ key: "project-a",
423
434
  messageQueue: [],
424
435
  preparingRun: true,
425
436
  runningProcess: null,
426
437
  isCompacting: false,
438
+ activeRunContext: null,
427
439
  lastInputWasVoice: false,
428
440
  };
429
441
  const store = Object.freeze({
@@ -441,7 +453,15 @@ async function queueProbe() {
441
453
  recordSession: () => {},
442
454
  userOwnsProviderSession: () => false,
443
455
  resetSessionUsage: () => {},
444
- acquireRunLock: () => false,
456
+ admitLane: () => ({ ok: false, reason: "busy", lane }),
457
+ laneFor: () => lane,
458
+ laneForRun: () => lane,
459
+ peekLane: () => lane,
460
+ laneBusy: () => true,
461
+ runLaneKey: (runContext) => runContext?.laneKey || runContext?.project?.name || null,
462
+ waitingLanes: () => [],
463
+ slotBlocker: () => null,
464
+ pruneIdleLanes: () => {},
445
465
  });
446
466
  installStub("./core/context", {
447
467
  chatContext: { getStore: () => store, run: (_store, fn) => fn() },
@@ -449,6 +469,7 @@ async function queueProbe() {
449
469
  currentAdapter: () => store.adapter,
450
470
  currentUserId: () => store.userId,
451
471
  currentConversationKey: () => store.conversationKey || null,
472
+ currentThreadId: () => null,
452
473
  });
453
474
  installStub("./core/io", {
454
475
  send: async () => ({ ok: true, messageId: "queued", editable: true }),
@@ -462,8 +483,8 @@ async function queueProbe() {
462
483
  const { runAgent } = require("./core/runner");
463
484
  const pending = runAgent("queued prompt", projectA, "message-queued", { runId: "queued-run" });
464
485
  assert.strictEqual(typeof pending.then, "function");
465
- assert.strictEqual(state.messageQueue.length, 1);
466
- const queued = state.messageQueue[0];
486
+ assert.strictEqual(lane.messageQueue.length, 1);
487
+ const queued = lane.messageQueue[0];
467
488
  assert.ok(Object.isFrozen(queued.runContext));
468
489
  assert.ok(Object.isFrozen(queued.runContext.project));
469
490
  assert.ok(Object.isFrozen(queued.runContext.providerSettings));
@@ -483,14 +504,14 @@ async function queueProbe() {
483
504
  store.adapter.liveCall = true;
484
505
  store.adapter.callTurn = { signal: obsoleteCall.signal };
485
506
  const callPending = runAgent("obsolete call question", projectA, null, { runId: "obsolete-call" });
486
- assert.strictEqual(state.messageQueue.length, 2);
507
+ assert.strictEqual(lane.messageQueue.length, 2);
487
508
  obsoleteCall.abort();
488
- assert.strictEqual(state.messageQueue.length, 1, "superseded call entry is removed immediately, ordinary queued work is preserved");
509
+ assert.strictEqual(lane.messageQueue.length, 1, "superseded call entry is removed immediately, ordinary queued work is preserved");
489
510
  assert.strictEqual((await callPending).status, "cancelled");
490
- assert.strictEqual(state.messageQueue[0], queued);
511
+ assert.strictEqual(lane.messageQueue[0], queued);
491
512
  const alreadyEnded = await runAgent("late call dispatch", projectA, null, { runId: "late-call" });
492
513
  assert.strictEqual(alreadyEnded.status, "cancelled");
493
- assert.strictEqual(state.messageQueue.length, 1, "an already-ended call never joins the queue");
514
+ assert.strictEqual(lane.messageQueue.length, 1, "an already-ended call never joins the queue");
494
515
  delete store.adapter.callTurn;
495
516
  delete store.adapter.liveCall;
496
517
  console.log("provider gateway immutable queue OK");
@@ -521,7 +542,7 @@ function staticChecks() {
521
542
  }
522
543
  assert.match(source["core/router.js"], /\badmitRunContext\b/, "media/text ingress captures before preparation awaits");
523
544
  assert.match(source["core/scheduler.js"], /result\.ok/, "scheduler consumes terminal RunResult success");
524
- assert.match(source["core/runner.js"], /messageQueue\.push\(\{ runContext,/, "queued work stores its immutable run context");
545
+ assert.match(source["core/runner.js"], /queueInLane\(lane, \{ runContext,/, "queued work stores its immutable run context in its lane");
525
546
  assert.match(source["core/runner.js"], /executeAdmittedRun\(item\.runContext,/, "queue drain executes the admitted context");
526
547
  assert.match(source["bin/cli.js"], /Open Claudia agent task/);
527
548
  assert.ok(!source["bin/cli.js"].includes("inside a running Claude task"));
@@ -514,7 +514,21 @@ assert.match(pkg.description, /provider-agnostic coding-agent harness/i);
514
514
  // setter's level; notes/tasks owner-only and search stays in the room; reads
515
515
  // screened and the hook fails closed on staff turns; no standing destructive
516
516
  // or anonymous grants; canary release tags. Shipped first as canary-v3.5.0.
517
- assert.strictEqual(pkg.version, "3.5.0", "release version must remain unchanged without explicit approval");
517
+ // 3.5.1 under the same approval (Sumeet 2026-09-25 14:43 EAT, phase 0 hardening
518
+ // from the adversarial review before any fleet roll): a CLI believes only its
519
+ // own bot (ancestor listener, not started in a staff turn); staff watch checks
520
+ // get an allowlist env and owner checks the bot's env again; a partial chat
521
+ // origin is a non-owner; wider staff shell screen, documented as no boundary.
522
+ // 3.5.4 approved by Sumeet 2026-09-29 12:22 EAT ("work through end to end till I
523
+ // can test multiple threads with you on mobile and web"): a Kazee thread is its
524
+ // own conversation, running beside the main chat and other threads (4 per
525
+ // person, 8 in all); replies, files, voice, typing, jobs and approvals stay in
526
+ // their thread; /stop per conversation and /stop all, and /stop drops the
527
+ // messages queued before it (his 26 Sep ask); a new thread also sees the main
528
+ // chat just before it; plus the independent review's fixes. Canary channel
529
+ // only (npm next). 3.5.2 failed this pin and 3.5.3 was cancelled: neither
530
+ // was published.
531
+ assert.strictEqual(pkg.version, "3.5.4", "release version must remain unchanged without explicit approval");
518
532
  for (const keyword of ["claude", "codex", "coding-agent", "provider-agnostic"]) {
519
533
  assert.ok(pkg.keywords.includes(keyword), `package keyword missing: ${keyword}`);
520
534
  }