@inetafrica/open-claudia 3.2.3 → 3.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/bin/loopback-client.js +4 -0
- package/bot.js +10 -1
- package/channels/kazee/adapter.js +5 -0
- package/channels/kazee/call/audio.js +79 -0
- package/channels/kazee/call/manager.js +6 -0
- package/channels/kazee/call/session.js +356 -48
- package/channels/kazee/call/speech-client.js +38 -3
- package/core/actions.js +96 -4
- package/core/enforcer.js +52 -3
- package/core/handlers.js +46 -2
- package/core/intro-flow.js +81 -16
- package/core/loopback.js +37 -0
- package/core/media.js +71 -18
- package/core/pack-review.js +18 -0
- package/core/relationship.js +18 -2
- package/core/router.js +17 -7
- package/core/runner.js +7 -1
- package/core/speaker-attest.js +56 -0
- package/core/system-prompt.js +21 -4
- package/core/verify-contact.js +156 -0
- package/package.json +2 -2
- package/test-enforcer.js +31 -0
- package/test-kazee-call-manager.js +4 -1
- package/test-kazee-call-session.js +100 -4
- package/test-mandate-hygiene.js +23 -5
- package/test-provider-language.js +25 -1
package/core/router.js
CHANGED
|
@@ -63,15 +63,24 @@ async function deliverRouterError(error) {
|
|
|
63
63
|
// person is still caught once the owner's channels are unified.
|
|
64
64
|
const processedMessages = new Set();
|
|
65
65
|
function isDuplicate(envelope) {
|
|
66
|
+
// Memoized per envelope object: the unauthorized path checks dedup before
|
|
67
|
+
// intro handling, then falls through to the normal handlers (which check
|
|
68
|
+
// again) when a known sender's chat was auto-adopted — that second check
|
|
69
|
+
// must not mistake the envelope's own first check for a redelivery.
|
|
70
|
+
if (envelope._dedupSeen !== undefined) return envelope._dedupSeen;
|
|
66
71
|
const canonical = envelope.canonicalUserId || envelope.channelId;
|
|
67
72
|
const key = `${canonical}:${envelope.adapter?.id || envelope.adapter?.type}:${envelope.messageId}`;
|
|
68
|
-
if (processedMessages.has(key))
|
|
73
|
+
if (processedMessages.has(key)) {
|
|
74
|
+
envelope._dedupSeen = true;
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
69
77
|
processedMessages.add(key);
|
|
70
78
|
if (processedMessages.size > 400) {
|
|
71
79
|
const arr = [...processedMessages];
|
|
72
80
|
processedMessages.clear();
|
|
73
81
|
arr.slice(-200).forEach((k) => processedMessages.add(k));
|
|
74
82
|
}
|
|
83
|
+
envelope._dedupSeen = false;
|
|
75
84
|
return false;
|
|
76
85
|
}
|
|
77
86
|
|
|
@@ -141,11 +150,12 @@ async function onMessage(envelope) {
|
|
|
141
150
|
}
|
|
142
151
|
|
|
143
152
|
if (!isChatAuthorized(envelope.channelId, envelope)) {
|
|
144
|
-
if (envelope.type
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
153
|
+
if (envelope.type !== "text") return;
|
|
154
|
+
if (isDuplicate(envelope)) return;
|
|
155
|
+
const consumed = await introFlow.handleInbound(envelope, (text) => send(text));
|
|
156
|
+
if (consumed) return;
|
|
157
|
+
// false = a transport-verified known sender was auto-adopted and the
|
|
158
|
+
// channel is authorized now — fall through and handle the turn.
|
|
149
159
|
}
|
|
150
160
|
|
|
151
161
|
// Auto-contact: an authorized speaker the people store doesn't know yet
|
|
@@ -604,7 +614,7 @@ async function handleText(envelope) {
|
|
|
604
614
|
// ("... for 14d" / "... until 2026-08-30").
|
|
605
615
|
const { text: coreRule, expiresAt } = require("./mandates").extractExpiry(ruleText);
|
|
606
616
|
const r = require("./actions").appendMandateLine(
|
|
607
|
-
{ entitySlug: pend.entitySlug, personName: pend.personName },
|
|
617
|
+
{ entitySlug: pend.entitySlug, personId: pend.personId, personName: pend.personName },
|
|
608
618
|
coreRule,
|
|
609
619
|
"owner added a per-person rule via approval card",
|
|
610
620
|
{ expiresAt },
|
package/core/runner.js
CHANGED
|
@@ -1707,7 +1707,13 @@ function decorateForegroundInvocation(invocation, runContext, bundle) {
|
|
|
1707
1707
|
if (runContext.origin?.adapterId) env.OC_CHANNEL_ADAPTER = runContext.origin.adapterId;
|
|
1708
1708
|
if (runContext.channelId) env.OC_CHANNEL_ID = String(runContext.channelId);
|
|
1709
1709
|
if (runContext.origin?.userId) env.OC_CHANNEL_USER_ID = String(runContext.origin.userId);
|
|
1710
|
-
if (runContext.canonicalUserId)
|
|
1710
|
+
if (runContext.canonicalUserId) {
|
|
1711
|
+
env.OC_CANONICAL_USER_ID = String(runContext.canonicalUserId);
|
|
1712
|
+
// Server-side attestation of the id above: the loopback's owner-gated
|
|
1713
|
+
// kinds only honor a canonicalUserId whose attest verifies, so a shell
|
|
1714
|
+
// that rewrites OC_CANONICAL_USER_ID cannot impersonate the owner.
|
|
1715
|
+
try { env.OC_SPEAKER_ATTEST = require("./speaker-attest").mint(runContext.canonicalUserId); } catch (e) {}
|
|
1716
|
+
}
|
|
1711
1717
|
if (runContext.sessionId) env.OC_LAST_SESSION_ID = String(runContext.sessionId);
|
|
1712
1718
|
if (lb?.url) env.OC_SEND_URL = lb.url;
|
|
1713
1719
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Speaker attestation: lets the loopback server trust OC_CANONICAL_USER_ID.
|
|
2
|
+
//
|
|
3
|
+
// The problem: the loopback's owner-gated kinds (people mutations, auth,
|
|
4
|
+
// intros) identified the caller by payload.canonicalUserId, which the CLI read
|
|
5
|
+
// from env — and env is writable by the agent's shell, so a guarded external
|
|
6
|
+
// turn could forge `OC_CANONICAL_USER_ID=<owner-id> open-claudia people link …`
|
|
7
|
+
// and rewrite the identity map the relationship guard depends on.
|
|
8
|
+
//
|
|
9
|
+
// The fix: the RUNNER (which knows the turn's true speaker from the inbound
|
|
10
|
+
// envelope, server-side) mints an HMAC over the canonical user id with a
|
|
11
|
+
// per-boot secret that lives ONLY in bot-process memory — never on disk, never
|
|
12
|
+
// in the loopback info file, stripped from provider env like all control-plane
|
|
13
|
+
// keys would be (it is simply never exported). The provider spawn env carries
|
|
14
|
+
// OC_SPEAKER_ATTEST next to OC_CANONICAL_USER_ID; the CLI forwards both; the
|
|
15
|
+
// loopback verifies the pair before honoring an owner claim. An agent can
|
|
16
|
+
// still LIE about OC_CANONICAL_USER_ID, but it cannot mint a matching
|
|
17
|
+
// signature for any id other than the one the runner stamped for its turn.
|
|
18
|
+
//
|
|
19
|
+
// Scope and honesty: this authenticates "the runner said this turn speaks as
|
|
20
|
+
// X". It is per-boot (a restart invalidates in-flight attests — same blast
|
|
21
|
+
// radius as the loopback port/token rotating) and deliberately unforgeable
|
|
22
|
+
// rather than merely obscure: no attest, wrong attest, or an attest for a
|
|
23
|
+
// different id all fail closed. It does NOT bind adapter/channel — those stay
|
|
24
|
+
// best-effort inputs to the guarded-speaker checks, whose spoof failure modes
|
|
25
|
+
// are self-limiting (buttons/files land on the spoofed channel, i.e. the
|
|
26
|
+
// owner's own chat, never silently on the attacker's).
|
|
27
|
+
|
|
28
|
+
const crypto = require("crypto");
|
|
29
|
+
|
|
30
|
+
// Lazily created so tests get a fresh secret per process, like production.
|
|
31
|
+
let SECRET = null;
|
|
32
|
+
|
|
33
|
+
function secret() {
|
|
34
|
+
if (!SECRET) SECRET = crypto.randomBytes(32);
|
|
35
|
+
return SECRET;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mint(canonicalUserId) {
|
|
39
|
+
const id = String(canonicalUserId || "");
|
|
40
|
+
if (!id) return "";
|
|
41
|
+
return crypto.createHmac("sha256", secret()).update(id).digest("hex");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Constant-time compare; every malformed shape is a plain false (fail closed).
|
|
45
|
+
function verify(canonicalUserId, attest) {
|
|
46
|
+
const id = String(canonicalUserId || "");
|
|
47
|
+
const sig = String(attest || "");
|
|
48
|
+
if (!id || !sig) return false;
|
|
49
|
+
const expected = mint(id);
|
|
50
|
+
const a = Buffer.from(sig, "utf-8");
|
|
51
|
+
const b = Buffer.from(expected, "utf-8");
|
|
52
|
+
if (a.length !== b.length) return false;
|
|
53
|
+
try { return crypto.timingSafeEqual(a, b); } catch (e) { return false; }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
module.exports = { mint, verify };
|
package/core/system-prompt.js
CHANGED
|
@@ -865,20 +865,29 @@ function buildSpeakerPersonaBlock(opts = {}) {
|
|
|
865
865
|
if (!speaker.isOwner && (s.Mandate || "").trim()) {
|
|
866
866
|
rows.push(`Mandate — what you may do/discuss with them: ${s.Mandate.trim()}`);
|
|
867
867
|
}
|
|
868
|
-
|
|
868
|
+
// A bare note used to render nothing — which hid the guard's policy source
|
|
869
|
+
// exactly when it mattered most (auto-added stubs are born bare, so the
|
|
870
|
+
// agent couldn't see WHICH file governs the speaker, or that a same-named
|
|
871
|
+
// note elsewhere is a different identity). For non-owners the block always
|
|
872
|
+
// renders; an owner with an empty note still gets the quiet no-op.
|
|
873
|
+
if (rows.length === 0 && speaker.isOwner) return "";
|
|
869
874
|
const runIdentity = promptDedupeIdentity(opts);
|
|
870
875
|
const key = `${adapter.id || "?"}:${channelId}`;
|
|
871
876
|
const stamp = `${runIdentity}:${slug}:${ent.updated}`;
|
|
872
877
|
if (speakerPersonaInjectedFor.get(key) === stamp) return "";
|
|
873
878
|
speakerPersonaInjectedFor.set(key, stamp);
|
|
874
879
|
const rel = ent.relationship || (speaker.isOwner ? "owner" : "");
|
|
880
|
+
const unverified = !speaker.isOwner && !!speaker.autoCreated;
|
|
875
881
|
const clip = (str) => (str.length > SPEAKER_PERSONA_MAX_CHARS ? str.slice(0, SPEAKER_PERSONA_MAX_CHARS) + "\n…[truncated — `open-claudia entity show " + slug + "`]" : str);
|
|
876
882
|
return clip([
|
|
877
|
-
`\n\n## Who you're speaking with: ${speaker.name}${rel ? ` (${rel})` : ""}`,
|
|
883
|
+
`\n\n## Who you're speaking with: ${speaker.name}${rel ? ` (${rel})` : ""}${unverified ? " — UNVERIFIED auto-added contact" : ""}`,
|
|
878
884
|
`Their persona pack (source: ${entitiesLib.ENTITIES_DIR}/${slug}.md) — adapt your voice and scope to it. Edit that file to refine how you treat them.`,
|
|
885
|
+
...(speaker.isOwner ? [] : [
|
|
886
|
+
`That file is the ONLY note the relationship guard reads for them — a same-named note anywhere else belongs to a DIFFERENT identity and grants nothing here.${unverified ? " The owner has not yet confirmed who they are (verification happens on the owner's approval card or /people verify)." : ""}`,
|
|
887
|
+
]),
|
|
879
888
|
"",
|
|
880
889
|
rows.join("\n"),
|
|
881
|
-
].join("\n"));
|
|
890
|
+
].join("\n")).trimEnd();
|
|
882
891
|
}
|
|
883
892
|
|
|
884
893
|
// External-mode posture block (Phase 3). For a non-owner (guarded) speaker,
|
|
@@ -896,9 +905,17 @@ function buildExternalModeBlock() {
|
|
|
896
905
|
const who = speaker.name || "this person";
|
|
897
906
|
const mandate = (speaker.mandate || "").trim();
|
|
898
907
|
const unverified = !!(speaker.person && speaker.person.autoCreated);
|
|
908
|
+
let sourceLine = "";
|
|
909
|
+
if (speaker.slug) {
|
|
910
|
+
try {
|
|
911
|
+
const entitiesLib = require("./entities");
|
|
912
|
+
sourceLine = `Guard policy source: ${entitiesLib.ENTITIES_DIR}/${speaker.slug}.md — the ONLY note the guard reads for ${who}. A same-named note elsewhere is a different identity and grants nothing here.`;
|
|
913
|
+
} catch (e) {}
|
|
914
|
+
}
|
|
899
915
|
const lines = [
|
|
900
916
|
`\n\n## External speaker — restricted mode`,
|
|
901
|
-
`You are talking to ${who}, who is NOT the owner. You are operating under a strict, owner-defined scope
|
|
917
|
+
`You are talking to ${who}, who is NOT the owner. You are operating under a strict, owner-defined scope.${unverified ? ` Their identity is UNVERIFIED (auto-added on first contact — only the owner can verify them, via the approval card or /people verify).` : ""}`,
|
|
918
|
+
...(sourceLine ? [sourceLine] : []),
|
|
902
919
|
"",
|
|
903
920
|
mandate
|
|
904
921
|
? `Mandate — the ONLY things you are cleared to do or discuss with ${who}:\n${mandate}`
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
// Owner verification of auto-added contacts — the second half of the
|
|
2
|
+
// ensureContact story. ensureContact deliberately NEVER links a first-time
|
|
3
|
+
// speaker to an existing note by display name (names are claimable, so that
|
|
4
|
+
// would be an impersonation path); it mints an unverified stub instead. This
|
|
5
|
+
// module gives the OWNER the explicit, audited act that closes the loop:
|
|
6
|
+
// confirm who the speaker is, optionally bind them to the existing trusted
|
|
7
|
+
// note, retire the stub, and clear the unverified flag the enforcer surfaces.
|
|
8
|
+
//
|
|
9
|
+
// Security invariants (each refusal below is one of them):
|
|
10
|
+
// - Verification is an OWNER decision arriving via a chat-envelope surface
|
|
11
|
+
// (approval-card button / /people slash command). There is deliberately no
|
|
12
|
+
// CLI or loopback verb: identity decisions never run on agent-forgeable
|
|
13
|
+
// input. Callers must gate on the transport envelope BEFORE calling here.
|
|
14
|
+
// - Verification is NOT trust: it settles who the person is. Relationship
|
|
15
|
+
// upgrades and mandate lines remain separate explicit owner grants.
|
|
16
|
+
// - Never link to an owner-relationship note (R1 defense-in-depth: speakerFor
|
|
17
|
+
// already coerces such a claim, but this surface refuses to create it).
|
|
18
|
+
// - Never link two people to one note: a persona pack carries a Mandate, and
|
|
19
|
+
// sharing one across identities would silently share granted capabilities.
|
|
20
|
+
// - Only auto-added (autoCreated) contacts are verifiable here. Re-pointing an
|
|
21
|
+
// already-verified person is intentionally NOT offered, so this surface can
|
|
22
|
+
// never be used as a general re-identity tool.
|
|
23
|
+
|
|
24
|
+
const fs = require("fs");
|
|
25
|
+
const path = require("path");
|
|
26
|
+
const people = require("./people");
|
|
27
|
+
const entities = require("./entities");
|
|
28
|
+
const audit = require("./audit");
|
|
29
|
+
|
|
30
|
+
// Same-name suggestions for the owner surfaces: existing person-type notes
|
|
31
|
+
// this unverified contact could plausibly BE. Excludes the contact's own stub,
|
|
32
|
+
// owner-relationship notes, and notes already bound to another person record.
|
|
33
|
+
// Suggestion-only — verifyContact re-validates whatever the owner picks.
|
|
34
|
+
function candidateEntitiesFor(person) {
|
|
35
|
+
if (!person) return [];
|
|
36
|
+
const stubSlug = people.resolveEntitySlug(person);
|
|
37
|
+
const wanted = String(person.name || "").trim().toLowerCase();
|
|
38
|
+
if (!wanted) return [];
|
|
39
|
+
const taken = new Set(
|
|
40
|
+
people.list()
|
|
41
|
+
.filter((p) => p.id !== person.id)
|
|
42
|
+
.map((p) => people.resolveEntitySlug(p))
|
|
43
|
+
.filter(Boolean),
|
|
44
|
+
);
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const e of entities.listEntities()) {
|
|
47
|
+
if (e.type !== "person") continue;
|
|
48
|
+
if (e.slug === stubSlug) continue;
|
|
49
|
+
if (e.relationship === "owner") continue;
|
|
50
|
+
if (taken.has(e.slug)) continue;
|
|
51
|
+
const names = [e.name, ...(e.aliases || [])]
|
|
52
|
+
.map((n) => String(n || "").trim().toLowerCase())
|
|
53
|
+
.filter(Boolean);
|
|
54
|
+
if (names.includes(wanted)) out.push(e);
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function refuse(reason, extra = {}) {
|
|
60
|
+
return { ok: false, reason, ...extra };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Verify (and optionally link) one auto-added contact.
|
|
64
|
+
// personId — the people.json record to verify
|
|
65
|
+
// linkSlug — optional existing note to bind them to; omitted = "this stub
|
|
66
|
+
// IS the right note, just mark them verified"
|
|
67
|
+
// by / via — provenance for the audit trail (owner id, surface name)
|
|
68
|
+
function verifyContact({ personId, linkSlug = null, by = "", via = "" } = {}) {
|
|
69
|
+
const person = people.findById(String(personId || ""));
|
|
70
|
+
if (!person) return refuse("person not found");
|
|
71
|
+
if (person.isOwner) return refuse("the owner record is never routed through contact verification");
|
|
72
|
+
if (!person.autoCreated) return refuse(`${person.name} is already verified`, { already: true });
|
|
73
|
+
|
|
74
|
+
const stubSlug = people.resolveEntitySlug(person);
|
|
75
|
+
let targetSlug = stubSlug;
|
|
76
|
+
let supersededStub = false;
|
|
77
|
+
|
|
78
|
+
if (linkSlug) {
|
|
79
|
+
const wantedSlug = entities.slugify(String(linkSlug));
|
|
80
|
+
if (wantedSlug && wantedSlug !== stubSlug) {
|
|
81
|
+
const target = entities.readEntity(wantedSlug);
|
|
82
|
+
if (!target) return refuse(`no note found at ${wantedSlug}`);
|
|
83
|
+
if (target.type !== "person") return refuse(`${wantedSlug} is a ${target.type} note, not a person`);
|
|
84
|
+
if (target.relationship === "owner") {
|
|
85
|
+
return refuse("refusing to link a contact to an owner-relationship note — ownership only ever comes from the people record");
|
|
86
|
+
}
|
|
87
|
+
const holder = people.list().find(
|
|
88
|
+
(p) => p.id !== person.id && people.resolveEntitySlug(p) === target.slug,
|
|
89
|
+
);
|
|
90
|
+
if (holder) {
|
|
91
|
+
return refuse(`that note already belongs to ${holder.name} — two people can never share one persona pack`);
|
|
92
|
+
}
|
|
93
|
+
targetSlug = target.slug;
|
|
94
|
+
|
|
95
|
+
// Provenance onto the surviving note, then retire the stub file as
|
|
96
|
+
// .superseded (kept on disk — the merge is inspectable, not destructive).
|
|
97
|
+
entities.upsertEntity({
|
|
98
|
+
slug: target.slug,
|
|
99
|
+
name: target.name,
|
|
100
|
+
log: `owner verified ${person.name} as this contact (absorbed auto-added stub ${stubSlug})`,
|
|
101
|
+
}, "owner-verify");
|
|
102
|
+
if (stubSlug) {
|
|
103
|
+
const stubFile = path.join(entities.ENTITIES_DIR, `${stubSlug}.md`);
|
|
104
|
+
try {
|
|
105
|
+
if (fs.existsSync(stubFile)) {
|
|
106
|
+
fs.renameSync(stubFile, `${stubFile}.superseded`);
|
|
107
|
+
supersededStub = true;
|
|
108
|
+
}
|
|
109
|
+
} catch (e) { /* best-effort: a stranded stub is inert once unlinked */ }
|
|
110
|
+
try { entities.markIndexDirty(); } catch (e) {}
|
|
111
|
+
}
|
|
112
|
+
people.setEntitySlug(person.id, target.slug);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (targetSlug === stubSlug && stubSlug) {
|
|
117
|
+
// Verify-only: the stub becomes the canonical note for this person. The
|
|
118
|
+
// description keeps its first-seen provenance, minus the unverified label.
|
|
119
|
+
const stub = entities.readEntity(stubSlug);
|
|
120
|
+
const description = stub && stub.description
|
|
121
|
+
? stub.description
|
|
122
|
+
.replace(/^Auto-added contact \(unverified\)/, "Verified contact")
|
|
123
|
+
.replace(/\s*Possible existing note:.*$/, "")
|
|
124
|
+
: "Verified contact — identity confirmed by the owner.";
|
|
125
|
+
entities.upsertEntity({
|
|
126
|
+
slug: stubSlug,
|
|
127
|
+
name: person.name,
|
|
128
|
+
description,
|
|
129
|
+
log: "owner verified this contact; this note is now their canonical persona pack",
|
|
130
|
+
}, "owner-verify");
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const fresh = people.findById(person.id) || person;
|
|
134
|
+
const updated = people.update(person.id, {
|
|
135
|
+
autoCreated: false,
|
|
136
|
+
verifiedAt: new Date().toISOString(),
|
|
137
|
+
verifiedBy: String(by || "owner"),
|
|
138
|
+
verifiedVia: String(via || ""),
|
|
139
|
+
handles: (fresh.handles || []).map((h) => (
|
|
140
|
+
h.approvedBy === "auto-contact" ? { ...h, approvedBy: String(by || "owner") } : h
|
|
141
|
+
)),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
audit.log("people.verify", {
|
|
145
|
+
by: String(by || "owner"),
|
|
146
|
+
via: String(via || ""),
|
|
147
|
+
personId: person.id,
|
|
148
|
+
name: person.name,
|
|
149
|
+
slug: targetSlug,
|
|
150
|
+
supersededStub: supersededStub ? stubSlug : null,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return { ok: true, person: updated, slug: targetSlug, superseded: supersededStub ? stubSlug : null };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
module.exports = { verifyContact, candidateEntitiesFor };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inetafrica/open-claudia",
|
|
3
|
-
"version": "3.2
|
|
3
|
+
"version": "3.3.2",
|
|
4
4
|
"description": "An always-on, provider-agnostic coding-agent harness for Claude Code and OpenAI Codex via chat",
|
|
5
5
|
"main": "bot.js",
|
|
6
6
|
"bin": {
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"setup": "node setup.js",
|
|
12
12
|
"start": "node bot.js",
|
|
13
13
|
"pretest": "node test-provider-fixture.js && node test-provider-core-prompt.js && node test-turn-observer.js && node test-provider-prompt-parity.js && node test-provider-env-isolation.js && node test-provider-resume-parity.js && node test-provider-bootstrap.js && node test-provider-registry.js && node test-provider-stream-decoder.js && node test-provider-events.js && node test-provider-claude.js && node test-provider-codex.js && node test-provider-runner.js && node test-provider-refusal-recovery.js && node test-provider-run-lifecycle.js && node test-provider-gateway.js && node test-provider-side-chat.js && node test-single-runtime.js && node test-provider-migration-backup.js && node test-provider-state-migration.js && node test-provider-session-history.js && node test-provider-session-commands.js && node test-provider-compaction.js && node test-provider-scheduler.js && node test-credential-capture.js && node test-cursor-removal.js && node test-utility-provider-policy.js && node test-provider-subagent.js && node test-provider-recall.js && node test-provider-pack-review.js && node test-memory-mutation-queue.js && node test-provider-dream.js && node test-provider-enforcer.js && node test-provider-tool-hooks.js && node test-provider-boot-matrix.js && node test-provider-capabilities.js && node test-provider-language.js && node test-provider-coupling-audit.js && node test-provider-parity-e2e.js && node test-kazee-channel-health.js && node test-kazee-media-caption.js && node test-socket-reconnect.js && node test-speech-loopback.js && node test-prompt-budget.js && node test-agency-ledger.js && node test-agency-deliberate.js && node test-guardrail.js && node test-spaces-comment-content.js && node test-boot-health.js && node test-lexical-floor.js && node test-safe-mode.js && node test-logout.js && node test-kazee-call-audio.js && node test-kazee-call-janus.js && node test-kazee-call-webrtc.js && node test-kazee-call-speech.js && node test-kazee-call-manager.js && node test-kazee-call-session.js",
|
|
14
|
-
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js"
|
|
14
|
+
"test": "OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node -e \"require('./vault'); console.log('OK')\" && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-runner-watchdog-static.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-usage-accounting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-engine.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-discoverer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-query.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-walker-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-score-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cokept-warming.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-project-transcripts-smoke.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-abilities.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-extraction.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-couse.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-transfer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-tiers.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-ability-merge-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-learning-e2e.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-nesting.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-pack-search.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-extractive-injection.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-dedupe.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-read-signal.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tools.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-graph.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tooling-mode.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-manifest.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-evolution.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-async.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-unified-identity.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-queue-routing.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-packs.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-persona.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-persona-pipeline.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-relationship-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-relationship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-enforcer.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-mandate-hygiene.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-external-rule-scopes.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-cards.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-identity-prune.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-message-id.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-gate.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-kazee-group-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-people-owner-seed.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-delivery-contract.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-run-lock.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-typing-heartbeat.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-web-sessions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-poll-recovery.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-409-grace.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-telegram-conflict-heal-guard.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-single-instance.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-streaming-split.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-spaces-autoenable.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-say-loopback.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-journal-union.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-subst-score.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-section-budgets.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-dedup-adjudicator.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-prospective.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-recall-regressions.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-cost-report.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-speaker-context.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-approval-mining.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-tool-certification.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/tmp/open-claudia-test CLAUDE_PATH=./test-fixtures/fake-agent-cli.js FAKE_AGENT_KIND=claude AGENT_ENV_PASSTHROUGH=FAKE_AGENT_KIND node test-apprenticeship.js && OPEN_CLAUDIA_TEST=1 WORKSPACE=/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-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-intro-adoption.js"
|
|
15
15
|
},
|
|
16
16
|
"files": [
|
|
17
17
|
"bot.js",
|
package/test-enforcer.js
CHANGED
|
@@ -378,5 +378,36 @@ const extSpeaker = relationship.speakerFor("telegram", CH_EXT);
|
|
|
378
378
|
await runInChat({ ...grantCtx, triageApprovalId: escReply.id }, () => enforcer.vetReply(extSpeaker, "reply-kind-grant-payload"));
|
|
379
379
|
assert.ok(!/TURN GRANT/.test(grantPrompt), "only a triage-kind approval grants the turn");
|
|
380
380
|
|
|
381
|
+
// ── stranded-mandate hint (dual-file trap): a grant written to the
|
|
382
|
+
// name-obvious note while the speaker's record reads a hashed slug does
|
|
383
|
+
// nothing — the card must SAY so at the moment it bites, and the stranded
|
|
384
|
+
// text must never be applied or quoted. ──
|
|
385
|
+
const rahil = people.add({ name: "Rahil Malde" });
|
|
386
|
+
people.setEntitySlug(rahil.id, "rahil-malde-abc123");
|
|
387
|
+
people.linkHandle(rahil.id, { adapter: "telegram", channelId: "333" });
|
|
388
|
+
entities.upsertEntity({ slug: "rahil-malde-abc123", name: "Rahil Malde", type: "person", relationship: "external" });
|
|
389
|
+
entities.upsertEntity({ slug: "rahil-malde", name: "Rahil Malde", type: "person", relationship: "external",
|
|
390
|
+
mandate: "- May coordinate ticket triage." });
|
|
391
|
+
const strandedSpeaker = relationship.speakerFor("telegram", "333");
|
|
392
|
+
assert.strictEqual(String(strandedSpeaker.mandate || "").trim(), "",
|
|
393
|
+
"the stranded text never reaches the live mandate");
|
|
394
|
+
sends.length = 0;
|
|
395
|
+
const escStr = await enforcer.escalate({ speaker: strandedSpeaker, kind: "reply", payloadText: "p", summary: "s", reason: "r" });
|
|
396
|
+
assert.strictEqual(escStr.escalated, true, "stranded-mandate escalation still posts");
|
|
397
|
+
const strCard = sends.find((s) => s.channelId === CH_OWNER);
|
|
398
|
+
assert.ok(strCard && /rahil-malde\.md/.test(strCard.text) && /NO effect/.test(strCard.text),
|
|
399
|
+
"the owner card names the file holding the dead grant");
|
|
400
|
+
assert.ok(!/ticket triage/.test(strCard.text), "the stranded text itself stays off the card");
|
|
401
|
+
|
|
402
|
+
// A different person legitimately holding the name slug silences the hint —
|
|
403
|
+
// that mandate is THEIRS, not a stranded copy of this speaker's.
|
|
404
|
+
const rahil2 = people.add({ name: "Rahil M Other" });
|
|
405
|
+
people.setEntitySlug(rahil2.id, "rahil-malde");
|
|
406
|
+
sends.length = 0;
|
|
407
|
+
await enforcer.escalate({ speaker: relationship.speakerFor("telegram", "333"), kind: "reply", payloadText: "p", summary: "s", reason: "r" });
|
|
408
|
+
const strCard2 = sends.find((s) => s.channelId === CH_OWNER);
|
|
409
|
+
assert.ok(strCard2 && !/NO effect/.test(strCard2.text),
|
|
410
|
+
"no hint when the sibling note belongs to a real other person");
|
|
411
|
+
|
|
381
412
|
console.log("enforcer OK");
|
|
382
413
|
})().catch((e) => { console.error(e); process.exit(1); });
|
|
@@ -25,7 +25,10 @@ function fixture() {
|
|
|
25
25
|
assert.strictEqual(f.events[0].payload.reason, "unsupported_media");
|
|
26
26
|
f = fixture();
|
|
27
27
|
await f.manager.incoming({ fromUserId: "owner-1", fromUserName: "Owner", chatId: "chat", roomId: 12, callType: "audio" });
|
|
28
|
-
|
|
28
|
+
// Ringing must go out first (caller UI feedback), then the accept.
|
|
29
|
+
assert.strictEqual(f.events[0].name, "call:ringing");
|
|
30
|
+
assert.deepStrictEqual(f.events[0].payload, { toUserId: "owner-1", chatId: "chat" });
|
|
31
|
+
assert.strictEqual(f.events[1].name, "call:accept");
|
|
29
32
|
f.manager.session.options.onTranscript("What is on my agenda?");
|
|
30
33
|
assert.strictEqual(f.inbound[0].payload.raw.ephemeralAudio, true);
|
|
31
34
|
// Call turns must mute observer banners (spoken otherwise) and flag live-call
|
|
@@ -29,8 +29,9 @@ class FakePC {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
class FakeSpeechStream {
|
|
32
|
-
constructor({ onTranscript } = {}) {
|
|
32
|
+
constructor({ onTranscript, onPartial } = {}) {
|
|
33
33
|
this.onTranscript = onTranscript;
|
|
34
|
+
this.onPartial = onPartial;
|
|
34
35
|
this.audioFrames = [];
|
|
35
36
|
this.commits = 0;
|
|
36
37
|
this.closedStream = false;
|
|
@@ -41,12 +42,12 @@ class FakeSpeechStream {
|
|
|
41
42
|
close() { this.closedStream = true; }
|
|
42
43
|
}
|
|
43
44
|
|
|
44
|
-
function makeSession({ synthesize, maxCallMs, idleMs, asCaller, greeting } = {}) {
|
|
45
|
+
function makeSession({ synthesize, maxCallMs, idleMs, finalWaitMs, asCaller, greeting } = {}) {
|
|
45
46
|
const ended = [];
|
|
46
47
|
const transcripts = [];
|
|
47
48
|
const session = new KazeeCallSession({
|
|
48
49
|
roomId: 7, chatId: "chat", callerId: "owner", botUserId: "bot", janusUrl: "ws://test",
|
|
49
|
-
maxCallMs, idleMs, asCaller, greeting,
|
|
50
|
+
maxCallMs, idleMs, finalWaitMs, asCaller, greeting,
|
|
50
51
|
synthesize: synthesize || (async () => ({ pcm: Buffer.alloc(WEBRTC_SAMPLES * 2 * 2), sampleRate: 48000 })),
|
|
51
52
|
onTranscript: (text, meta) => transcripts.push({ text, meta }),
|
|
52
53
|
onEnded: (reason) => ended.push(reason),
|
|
@@ -127,12 +128,98 @@ async function testPrebufferAndCommit() {
|
|
|
127
128
|
assert.strictEqual(sub.stt.audioFrames.length, 0, "silence stays out of STT");
|
|
128
129
|
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
129
130
|
assert.strictEqual(sub.stt.audioFrames.length, 8, "onset flushes the prebuffer (5 quiet + 2 onset + current)");
|
|
131
|
+
sub.stt.onPartial("hello there"); // recogniser heard words, so the energy close commits directly
|
|
130
132
|
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
131
133
|
assert.strictEqual(sub.stt.commits, 1, "turn commits after sustained silence");
|
|
132
134
|
gen.close();
|
|
133
135
|
await session.close();
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
async function testHoldForWords() {
|
|
139
|
+
const { session } = makeSession({});
|
|
140
|
+
await session._subscribe(101, "caller");
|
|
141
|
+
const sub = session.subscribers.get(101);
|
|
142
|
+
const gen = new OpusAudioCodec();
|
|
143
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
144
|
+
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
145
|
+
assert.strictEqual(sub.stt.commits, 0, "energy close with zero words holds the commit");
|
|
146
|
+
assert(sub.awaitingWordsSince > 0, "hold state armed");
|
|
147
|
+
sub.stt.onPartial("hello there");
|
|
148
|
+
assert.strictEqual(sub.stt.commits, 1, "first words release the held commit");
|
|
149
|
+
assert.strictEqual(sub.awaitingWordsSince, 0);
|
|
150
|
+
assert(session._thinkTimer, "released commit acks and thinks like a normal turn");
|
|
151
|
+
gen.close();
|
|
152
|
+
await session.close();
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function testHoldTimeoutDropsNoiseTurn() {
|
|
156
|
+
const { session, transcripts } = makeSession({});
|
|
157
|
+
await session._subscribe(101, "caller");
|
|
158
|
+
const sub = session.subscribers.get(101);
|
|
159
|
+
const gen = new OpusAudioCodec();
|
|
160
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
161
|
+
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
162
|
+
sub.awaitingWordsSince = Date.now() - 5000; // hold expired
|
|
163
|
+
session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
164
|
+
assert.strictEqual(sub.stt.commits, 1, "expired hold still finalizes the audio");
|
|
165
|
+
assert.strictEqual(sub.noiseCommit, true);
|
|
166
|
+
assert.strictEqual(session._thinkTimer, null, "noise commit stays silent — no ack, no ticking");
|
|
167
|
+
sub.stt.onTranscript("uh"); // babble leaked a stray word into the transcript
|
|
168
|
+
assert.strictEqual(transcripts.length, 0, "sub-threshold noise transcript never dispatches");
|
|
169
|
+
gen.close();
|
|
170
|
+
await session.close();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
async function testWordStallRescue() {
|
|
174
|
+
const { session } = makeSession({});
|
|
175
|
+
await session._subscribe(101, "caller");
|
|
176
|
+
const sub = session.subscribers.get(101);
|
|
177
|
+
const gen = new OpusAudioCodec();
|
|
178
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
179
|
+
sub.stt.onPartial("so the deploy");
|
|
180
|
+
sub.lastWordAt = Date.now() - 4500; // word timeline stalled well past its cadence
|
|
181
|
+
session._onFeedRtp(101, { payload: toneOpus(gen, 12000) }); // background keeps energy loud
|
|
182
|
+
assert.strictEqual(sub.stt.commits, 1, "stalled words commit even while energy stays loud");
|
|
183
|
+
assert.strictEqual(sub.vad.active, false, "rescue resets the energy turn");
|
|
184
|
+
gen.close();
|
|
185
|
+
await session.close();
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async function testInterimFallbackAndLateFinal() {
|
|
189
|
+
const { session, transcripts } = makeSession({ finalWaitMs: 30 });
|
|
190
|
+
await session._subscribe(101, "caller");
|
|
191
|
+
const sub = session.subscribers.get(101);
|
|
192
|
+
const gen = new OpusAudioCodec();
|
|
193
|
+
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
194
|
+
sub.stt.onPartial("here is my answer");
|
|
195
|
+
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
196
|
+
assert.strictEqual(sub.stt.commits, 1);
|
|
197
|
+
assert.strictEqual(transcripts.length, 0, "dispatch waits for the final first");
|
|
198
|
+
await wait(80);
|
|
199
|
+
assert.strictEqual(transcripts.length, 1, "missing final falls back to the interim");
|
|
200
|
+
assert.strictEqual(transcripts[0].text, "here is my answer");
|
|
201
|
+
sub.stt.onTranscript("here is my answer sir");
|
|
202
|
+
assert.strictEqual(transcripts.length, 1, "late final after fallback is dropped, not double-answered");
|
|
203
|
+
gen.close();
|
|
204
|
+
await session.close();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function testTrickleBuffering() {
|
|
208
|
+
const { session } = makeSession({});
|
|
209
|
+
const added = [];
|
|
210
|
+
session.publisher = { pc: { addIceCandidate: async (c) => { added.push(c.candidate); } }, pending: [], remoteSet: false };
|
|
211
|
+
session._onPublisherEvent({ janus: "trickle", candidate: { candidate: "cand-early", sdpMid: "0" } });
|
|
212
|
+
assert.strictEqual(added.length, 0, "candidates before the answer are buffered");
|
|
213
|
+
assert.strictEqual(session.publisher.pending.length, 1);
|
|
214
|
+
session._flushTrickle(session.publisher);
|
|
215
|
+
assert.deepStrictEqual(added, ["cand-early"], "buffered candidates flush once the answer lands");
|
|
216
|
+
session._onPublisherEvent({ janus: "trickle", candidate: { candidate: "cand-late", sdpMid: "0" } });
|
|
217
|
+
assert.deepStrictEqual(added, ["cand-early", "cand-late"], "later candidates apply directly");
|
|
218
|
+
session._onPublisherEvent({ janus: "trickle", candidate: { completed: true } });
|
|
219
|
+
assert.strictEqual(added.length, 2, "end-of-candidates marker is ignored");
|
|
220
|
+
await session.close();
|
|
221
|
+
}
|
|
222
|
+
|
|
136
223
|
async function testTranscriptAttribution() {
|
|
137
224
|
const { session, transcripts } = makeSession({});
|
|
138
225
|
await session._subscribe(101, "alice@kazee");
|
|
@@ -245,8 +332,11 @@ async function testCallerModeRoomLifecycle() {
|
|
|
245
332
|
const spoken = [];
|
|
246
333
|
session.speak = async (text) => { spoken.push(text); return true; };
|
|
247
334
|
await session._subscribe(101, "sumeet@kazee");
|
|
335
|
+
assert.deepStrictEqual(spoken, [], "no greeting while the publisher media leg is still down");
|
|
336
|
+
session._publisherUp = true;
|
|
337
|
+
session._maybeGreet();
|
|
248
338
|
await session._subscribe(102, "second@kazee");
|
|
249
|
-
assert.deepStrictEqual(spoken, ["hi there"], "greeting speaks exactly once,
|
|
339
|
+
assert.deepStrictEqual(spoken, ["hi there"], "greeting speaks exactly once, after media-up + first remote feed");
|
|
250
340
|
await session.close();
|
|
251
341
|
const destroy = messages.find((m) => m?.request === "destroy");
|
|
252
342
|
assert.ok(destroy && destroy.room === 7, "created room is destroyed on close");
|
|
@@ -283,6 +373,7 @@ async function testThinkingCues() {
|
|
|
283
373
|
// plays an audible "heard you" chime, and starts ticking right away — STT
|
|
284
374
|
// finalization takes seconds and must not sound like a dead line.
|
|
285
375
|
for (let i = 0; i < 3; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 12000) });
|
|
376
|
+
sub.stt.onPartial("what is the status");
|
|
286
377
|
for (let i = 0; i < 36; i += 1) session._onFeedRtp(101, { payload: toneOpus(gen, 30) });
|
|
287
378
|
assert.strictEqual(sub.stt.commits, 1);
|
|
288
379
|
await wait(60);
|
|
@@ -335,6 +426,11 @@ async function testCloseResolvesSpeech() {
|
|
|
335
426
|
await testSpeakCancellation();
|
|
336
427
|
await testEchoGateAndBargeIn();
|
|
337
428
|
await testPrebufferAndCommit();
|
|
429
|
+
await testHoldForWords();
|
|
430
|
+
await testHoldTimeoutDropsNoiseTurn();
|
|
431
|
+
await testWordStallRescue();
|
|
432
|
+
await testInterimFallbackAndLateFinal();
|
|
433
|
+
await testTrickleBuffering();
|
|
338
434
|
await testTranscriptAttribution();
|
|
339
435
|
await testIdleTimeout();
|
|
340
436
|
await testIdleBargeInKeepsCall();
|