@inetafrica/open-claudia 3.5.1 → 3.5.7
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 +13 -0
- package/CHANGELOG.md +36 -0
- package/bin/cli.js +13 -5
- package/bin/loopback-client.js +4 -0
- package/bin/schedule.js +7 -2
- package/bot.js +6 -2
- package/channels/kazee/adapter.js +519 -64
- package/channels/kazee/state.js +88 -2
- package/channels/types.js +11 -5
- package/core/actions.js +6 -0
- package/core/approvals.js +9 -0
- package/core/context.js +11 -2
- package/core/handlers.js +161 -59
- package/core/identity.js +33 -3
- package/core/io.js +21 -8
- package/core/jobs.js +3 -0
- package/core/loopback.js +55 -7
- package/core/providers/claude.js +6 -2
- package/core/providers/utility-policy.js +3 -1
- package/core/router.js +25 -13
- package/core/run-context.js +49 -8
- package/core/runner.js +318 -122
- package/core/sandbox-probe.js +78 -0
- package/core/scheduler.js +45 -3
- package/core/side-chat.js +40 -11
- package/core/speaker-attest.js +2 -0
- package/core/state.js +317 -50
- package/core/system-prompt.js +61 -14
- package/core/thread-title.js +237 -0
- package/core/transcripts.js +23 -6
- package/package.json +3 -3
- package/test-approval-cards.js +24 -0
- package/test-call-speech.js +5 -3
- package/test-fixtures/fake-agent-cli.js +52 -1
- package/test-provider-compaction.js +50 -11
- package/test-provider-gateway.js +31 -10
- package/test-provider-language.js +7 -1
- package/test-provider-scheduler.js +77 -20
- package/test-provider-session-commands.js +11 -2
- package/test-provider-side-chat.js +30 -2
- package/test-run-lock.js +122 -39
- package/test-thread-lanes.js +530 -0
- package/core/queue-drain.js +0 -25
- package/test-queue-routing.js +0 -52
package/test-run-lock.js
CHANGED
|
@@ -1,63 +1,146 @@
|
|
|
1
|
-
// P0-3: single-flight run-lock regression test
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
// "is a run in progress?" check
|
|
5
|
-
// files behind it. acquireRunLock() closes that window by
|
|
6
|
-
// SYNCHRONOUSLY (no await between the check and the arm)
|
|
7
|
-
//
|
|
1
|
+
// P0-3: single-flight run-lock regression test, per conversation LANE. A lane
|
|
2
|
+
// is one conversation (the channel's main timeline, or one chat thread): turns
|
|
3
|
+
// inside a lane are strictly one at a time, so two turns of one conversation
|
|
4
|
+
// can never both pass the "is a run in progress?" check and race its provider
|
|
5
|
+
// session and the JSON files behind it. acquireRunLock() closes that window by
|
|
6
|
+
// arming the lane SYNCHRONOUSLY (no await between the check and the arm).
|
|
7
|
+
//
|
|
8
|
+
// Deliberate change (parallel conversations): under unified identity the
|
|
9
|
+
// owner's Telegram DM and Kazee chat still resolve to ONE canonical state, but
|
|
10
|
+
// they are DIFFERENT conversations (conversation keys), so they are separate
|
|
11
|
+
// lanes and may now run at the same time, bounded by the per-speaker cap
|
|
12
|
+
// (OC_MAX_PARALLEL_TURNS, default 4) and the global cap. The old test pinned
|
|
13
|
+
// that the second channel was refused; what must hold now is that the SAME
|
|
14
|
+
// conversation is refused, and that the caps bound the rest.
|
|
8
15
|
|
|
9
16
|
const assert = require("assert");
|
|
10
17
|
process.env.OPEN_CLAUDIA_TEST = "1";
|
|
11
18
|
|
|
12
|
-
const
|
|
19
|
+
const stateApi = require("./core/state");
|
|
20
|
+
const { acquireRunLock, admitLane, laneFor, peekLane, anyTurnActive, waitingLanes } = stateApi;
|
|
21
|
+
|
|
22
|
+
const MAIN = "telegram:owner";
|
|
23
|
+
const KAZEE = "kazee:owner-chat";
|
|
24
|
+
const THREAD = "kazee:owner-chat#thread:aaaaaaaaaaaaaaaaaaaaaaaa";
|
|
25
|
+
// Fixture states join the global slot count once they hold a lane; each block
|
|
26
|
+
// releases its own so the global cap never carries over between blocks.
|
|
27
|
+
const fixtures = [];
|
|
28
|
+
const fresh = () => { const s = { userId: `owner-${Math.random()}` }; fixtures.push(s); return s; };
|
|
29
|
+
const releaseAll = () => { for (const s of fixtures) { s.lanes?.clear(); stateApi.pruneIdleLanes(s); } };
|
|
13
30
|
|
|
14
31
|
// A run-lock must be synchronous — its whole point is that no await can
|
|
15
32
|
// interleave between the check and the arm. If this ever returned a Promise the
|
|
16
33
|
// guarantee would silently evaporate.
|
|
17
34
|
{
|
|
18
|
-
const r = acquireRunLock(
|
|
35
|
+
const r = acquireRunLock(fresh(), MAIN);
|
|
19
36
|
assert.strictEqual(typeof r, "boolean", "acquireRunLock must be synchronous (return a boolean, not a Promise)");
|
|
20
37
|
}
|
|
21
38
|
|
|
22
|
-
// Idle → acquires and arms preparingRun.
|
|
39
|
+
// Idle lane → acquires and arms the lane's preparingRun, nothing on the state.
|
|
23
40
|
{
|
|
24
|
-
const s =
|
|
25
|
-
assert.strictEqual(acquireRunLock(s), true, "idle
|
|
26
|
-
assert.strictEqual(s.preparingRun, true, "acquiring arms preparingRun");
|
|
41
|
+
const s = fresh();
|
|
42
|
+
assert.strictEqual(acquireRunLock(s, MAIN), true, "idle lane acquires");
|
|
43
|
+
assert.strictEqual(laneFor(s, MAIN).preparingRun, true, "acquiring arms the lane's preparingRun");
|
|
44
|
+
assert.strictEqual(s.preparingRun, undefined, "the speaker's state holds no run slot of its own");
|
|
45
|
+
assert.ok(!Object.keys(s).includes("lanes"), "lanes are not enumerable (never cloned or serialized)");
|
|
27
46
|
}
|
|
28
47
|
|
|
29
48
|
// Already preparing (armed but not yet spawned) → refused, lock left intact.
|
|
30
49
|
{
|
|
31
|
-
const s =
|
|
32
|
-
|
|
33
|
-
|
|
50
|
+
const s = fresh();
|
|
51
|
+
laneFor(s, MAIN).preparingRun = true;
|
|
52
|
+
const refused = admitLane(s, MAIN);
|
|
53
|
+
assert.strictEqual(refused.ok, false, "preparing lane is refused");
|
|
54
|
+
assert.strictEqual(refused.reason, "busy");
|
|
55
|
+
assert.strictEqual(laneFor(s, MAIN).preparingRun, true, "refusal leaves the existing lock armed");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// A process is actively running, or the lane is settling/compacting → refused.
|
|
59
|
+
for (const field of ["runningProcess", "settling", "isCompacting"]) {
|
|
60
|
+
const s = fresh();
|
|
61
|
+
laneFor(s, MAIN)[field] = field === "runningProcess" ? { pid: 123 } : true;
|
|
62
|
+
assert.strictEqual(acquireRunLock(s, MAIN), false, `${field} lane is refused`);
|
|
34
63
|
}
|
|
35
64
|
|
|
36
|
-
//
|
|
65
|
+
// THE guarantee: two turns of ONE conversation. Exactly one may acquire; the
|
|
66
|
+
// other must be refused until the first fully releases.
|
|
37
67
|
{
|
|
38
|
-
const
|
|
39
|
-
|
|
68
|
+
const shared = fresh();
|
|
69
|
+
const first = acquireRunLock(shared, KAZEE);
|
|
70
|
+
const second = acquireRunLock(shared, KAZEE);
|
|
71
|
+
assert.strictEqual(first, true, "first turn wins the lane");
|
|
72
|
+
assert.strictEqual(second, false, "second turn of the same conversation is refused (no double-acquire)");
|
|
73
|
+
const lane = laneFor(shared, KAZEE);
|
|
74
|
+
lane.preparingRun = false;
|
|
75
|
+
lane.runningProcess = { pid: 1 };
|
|
76
|
+
assert.strictEqual(acquireRunLock(shared, KAZEE), false, "still refused while the process runs");
|
|
77
|
+
lane.runningProcess = null;
|
|
78
|
+
assert.strictEqual(acquireRunLock(shared, KAZEE), true, "lane is re-acquirable once the turn fully releases");
|
|
40
79
|
}
|
|
41
80
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
81
|
+
releaseAll();
|
|
82
|
+
|
|
83
|
+
// Unified identity: the owner's channels and a thread are separate lanes of
|
|
84
|
+
// ONE state and run in parallel.
|
|
45
85
|
{
|
|
46
|
-
const
|
|
86
|
+
const owner = fresh();
|
|
87
|
+
assert.strictEqual(acquireRunLock(owner, MAIN), true, "Telegram DM lane");
|
|
88
|
+
assert.strictEqual(acquireRunLock(owner, KAZEE), true, "Kazee chat lane runs beside it");
|
|
89
|
+
assert.strictEqual(acquireRunLock(owner, THREAD), true, "a thread of that chat is its own lane too");
|
|
90
|
+
assert.strictEqual(laneFor(owner, THREAD).threadId, "aaaaaaaaaaaaaaaaaaaaaaaa", "a thread lane knows its thread");
|
|
91
|
+
assert.strictEqual(laneFor(owner, KAZEE).threadId, null, "the main timeline has no thread");
|
|
92
|
+
assert.strictEqual(anyTurnActive(), true, "anyTurnActive sees every lane of every state");
|
|
93
|
+
releaseAll();
|
|
94
|
+
assert.strictEqual(anyTurnActive(), false, "and nothing once they are released");
|
|
95
|
+
}
|
|
47
96
|
|
|
48
|
-
|
|
49
|
-
const second = acquireRunLock(shared); // Kazee turn arrives before the first releases
|
|
50
|
-
assert.strictEqual(first, true, "first turn wins the lock");
|
|
51
|
-
assert.strictEqual(second, false, "second concurrent turn is refused (no double-acquire)");
|
|
97
|
+
releaseAll();
|
|
52
98
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
99
|
+
// Caps: a speaker may run OC_MAX_PARALLEL_TURNS lanes; the next waits ("cap"),
|
|
100
|
+
// and a lane that waits keeps new messages of its conversation behind it.
|
|
101
|
+
{
|
|
102
|
+
const prior = process.env.OC_MAX_PARALLEL_TURNS;
|
|
103
|
+
process.env.OC_MAX_PARALLEL_TURNS = "2";
|
|
104
|
+
try {
|
|
105
|
+
const s = fresh();
|
|
106
|
+
assert.strictEqual(acquireRunLock(s, "k:1"), true);
|
|
107
|
+
assert.strictEqual(acquireRunLock(s, "k:2"), true);
|
|
108
|
+
const third = admitLane(s, "k:3");
|
|
109
|
+
assert.strictEqual(third.ok, false, "the third lane of the speaker waits");
|
|
110
|
+
assert.strictEqual(third.reason, "cap");
|
|
111
|
+
assert.strictEqual(third.running, 2, "the notice can say how many are running");
|
|
112
|
+
assert.strictEqual(third.lane.preparingRun, false, "a waiting lane is not armed");
|
|
113
|
+
third.lane.messageQueue.push({ queuedAt: 1, seq: 1 });
|
|
114
|
+
assert.strictEqual(admitLane(s, "k:3").reason, "waiting", "a waiting lane queues its next message behind the first");
|
|
115
|
+
assert.deepStrictEqual(waitingLanes().filter((w) => w.state === s).map((w) => w.lane.key), ["k:3"]);
|
|
116
|
+
const other = fresh();
|
|
117
|
+
assert.strictEqual(acquireRunLock(other, "k:1"), true, "another speaker has slots of its own");
|
|
118
|
+
laneFor(s, "k:1").preparingRun = false;
|
|
119
|
+
assert.strictEqual(admitLane(s, "k:4").reason, "cap", "a lane that did not wait still sees the cap while k:3 waits for the freed slot");
|
|
120
|
+
} finally {
|
|
121
|
+
releaseAll();
|
|
122
|
+
if (prior === undefined) delete process.env.OC_MAX_PARALLEL_TURNS;
|
|
123
|
+
else process.env.OC_MAX_PARALLEL_TURNS = prior;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
58
126
|
|
|
59
|
-
|
|
60
|
-
|
|
127
|
+
// The global cap bounds every speaker together.
|
|
128
|
+
{
|
|
129
|
+
const prior = process.env.OC_MAX_PARALLEL_TURNS_GLOBAL;
|
|
130
|
+
process.env.OC_MAX_PARALLEL_TURNS_GLOBAL = "1";
|
|
131
|
+
try {
|
|
132
|
+
const a = fresh();
|
|
133
|
+
const b = fresh();
|
|
134
|
+
assert.strictEqual(acquireRunLock(a, "g:1"), true);
|
|
135
|
+
const blocked = admitLane(b, "g:2");
|
|
136
|
+
assert.strictEqual(blocked.ok, false, "another speaker waits when the bot-wide cap is reached");
|
|
137
|
+
assert.strictEqual(blocked.scope, "global");
|
|
138
|
+
assert.strictEqual(anyTurnActive(), true, "anyTurnActive sees a running lane of any state");
|
|
139
|
+
} finally {
|
|
140
|
+
releaseAll();
|
|
141
|
+
if (prior === undefined) delete process.env.OC_MAX_PARALLEL_TURNS_GLOBAL;
|
|
142
|
+
else process.env.OC_MAX_PARALLEL_TURNS_GLOBAL = prior;
|
|
143
|
+
}
|
|
61
144
|
}
|
|
62
145
|
|
|
63
146
|
// A /stop flag names the run it stops. Only that run (or a legacy bare `true`)
|
|
@@ -65,7 +148,7 @@ const { acquireRunLock } = require("./core/state");
|
|
|
65
148
|
// "before the provider started" (24 Sep, three messages lost after one /stop).
|
|
66
149
|
{
|
|
67
150
|
const { cancelFlagFor, cancelTargets } = require("./core/state");
|
|
68
|
-
assert.strictEqual(cancelFlagFor({ activeRunContext: { runId: "run-a" } }), "run-a", "the flag names the active run");
|
|
151
|
+
assert.strictEqual(cancelFlagFor({ activeRunContext: { runId: "run-a" } }), "run-a", "the flag names the lane's active run");
|
|
69
152
|
assert.strictEqual(cancelFlagFor({ activeRunContext: null }), true, "no known run: a bare flag");
|
|
70
153
|
assert.strictEqual(cancelTargets("run-a", { runId: "run-a" }), true, "the stopped run sees its stop");
|
|
71
154
|
assert.strictEqual(cancelTargets("run-a", { runId: "run-b" }), false, "another run never does");
|
|
@@ -82,13 +165,13 @@ const { acquireRunLock } = require("./core/state");
|
|
|
82
165
|
assert.strictEqual(queuedNotice(null, t0), "Queued.");
|
|
83
166
|
assert.strictEqual(queuedNotice({ admittedAt: t0, userPrompt: "hi" }, t0 + 60_000), "Queued.", "a short wait stays terse");
|
|
84
167
|
assert.strictEqual(queuedNotice({ admittedAt: t0, userPrompt: "hi" }, t0 + 5 * 60_000),
|
|
85
|
-
"Queued behind an earlier message (running 5 min). /stop cancels it.");
|
|
168
|
+
"Queued behind an earlier message (running 5 min). /stop cancels it (and drops this message).");
|
|
86
169
|
assert.strictEqual(queuedNotice({ admittedAt: t0, userPrompt: '[Watch triggered "Phone alarm" fired at x; occurrence y]\nbody' }, t0 + 42 * 60_000),
|
|
87
|
-
'Queued behind the watch "Phone alarm" (running 42 min). /stop cancels it.');
|
|
170
|
+
'Queued behind the watch "Phone alarm" (running 42 min). /stop cancels it (and drops this message).');
|
|
88
171
|
assert.strictEqual(queuedNotice({ admittedAt: t0, userPrompt: '[Scheduled wakeup "Check CI" fired at x]' }, t0 + 3 * 60_000),
|
|
89
|
-
'Queued behind the wakeup "Check CI" (running 3 min). /stop cancels it.');
|
|
172
|
+
'Queued behind the wakeup "Check CI" (running 3 min). /stop cancels it (and drops this message).');
|
|
90
173
|
assert.strictEqual(queuedNotice({ admittedAt: t0, userPrompt: '[Watch check timing out "Phone health" fired at x; occurrence y]' }, t0 + 20 * 60_000),
|
|
91
|
-
'Queued behind the watch "Phone health" (running 20 min). /stop cancels it.', "a check-timeout alarm is a watch run too");
|
|
174
|
+
'Queued behind the watch "Phone health" (running 20 min). /stop cancels it (and drops this message).', "a check-timeout alarm is a watch run too");
|
|
92
175
|
const long = queuedNotice({ admittedAt: t0, userPrompt: `[Watch triggered "${"x".repeat(90)}" fired at x]` }, t0 + 3 * 60_000);
|
|
93
176
|
assert.ok(long.includes(`"${"x".repeat(59)}…"`), "a long label is shortened to 60 characters");
|
|
94
177
|
}
|