@rubytech/create-maxy-code 0.1.498 → 0.1.499
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/package.json +1 -1
- package/payload/platform/lib/active-rules/dist/index.d.ts +3 -7
- package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/active-rules/dist/index.js +20 -52
- package/payload/platform/lib/active-rules/dist/index.js.map +1 -1
- package/payload/platform/lib/active-rules/src/index.test.ts +35 -69
- package/payload/platform/lib/active-rules/src/index.ts +17 -58
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +3 -3
- package/payload/platform/plugins/docs/references/internals.md +2 -2
- package/payload/platform/plugins/email/mcp/dist/__tests__/compose-signature.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/compose-signature.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/compose-signature.test.js +53 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/compose-signature.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.d.ts +2 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.js +50 -0
- package/payload/platform/plugins/email/mcp/dist/__tests__/signature.test.js.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/compose.d.ts.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js +15 -2
- package/payload/platform/plugins/email/mcp/dist/lib/compose.js.map +1 -1
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts +29 -0
- package/payload/platform/plugins/email/mcp/dist/lib/signature.d.ts.map +1 -0
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js +55 -0
- package/payload/platform/plugins/email/mcp/dist/lib/signature.js.map +1 -0
- package/payload/platform/plugins/email/references/email-reference.md +10 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js +10 -9
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js.map +1 -1
- package/payload/platform/plugins/memory/skills/conversational-memory/SKILL.md +2 -0
- package/payload/platform/plugins/memory/skills/preference-audit/SKILL.md +16 -0
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts +19 -19
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js +28 -34
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js.map +1 -1
- package/payload/server/public/assets/{chat-qtOiJX-Y.js → chat-MvnxPJZ5.js} +1 -1
- package/payload/server/public/assets/{operator-icJwMkZF.js → operator-DCNWUKP_.js} +1 -1
- package/payload/server/public/assets/{page-iG57nKOg.js → page-C4TrTU6r.js} +1 -1
- package/payload/server/public/assets/{public-CkOumzpf.js → public-DXJXQPwe.js} +1 -1
- package/payload/server/public/chat.html +2 -2
- package/payload/server/public/operator.html +2 -2
- package/payload/server/public/public.html +2 -2
- package/payload/server/server.js +47 -57
package/package.json
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
export declare const SAME_SUBJECT_COSINE = 0.85;
|
|
2
|
-
export declare const INJECTION_RULE_CAP = 12;
|
|
3
2
|
export declare const ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
4
|
-
export declare const ACTIVE_CANDIDATE_LIMIT = 30;
|
|
5
3
|
export interface RuleCandidate {
|
|
6
4
|
preferenceId: string;
|
|
7
5
|
category: string;
|
|
@@ -19,9 +17,7 @@ export interface ActiveRule {
|
|
|
19
17
|
confidence: number;
|
|
20
18
|
observedAt: string;
|
|
21
19
|
}
|
|
22
|
-
export declare function
|
|
23
|
-
export declare function deconflictBySubject(records: RuleCandidate[], cosineThreshold?: number): ActiveRule[];
|
|
24
|
-
export declare function formatStandingRulesBlock(rules: ActiveRule[], cap?: number): string;
|
|
20
|
+
export declare function formatStandingRulesBlock(rules: ActiveRule[]): string;
|
|
25
21
|
interface MinimalSession {
|
|
26
22
|
run: (q: string, p: Record<string, unknown>) => Promise<{
|
|
27
23
|
records: Array<{
|
|
@@ -43,7 +39,7 @@ export interface StandingRulesInjection {
|
|
|
43
39
|
}
|
|
44
40
|
export declare function resolveOwnerUserId(session: MinimalSession, accountId: string): Promise<string | null>;
|
|
45
41
|
export declare function queryActiveRuleCandidates(session: MinimalSession, accountId: string): Promise<RuleCandidate[]>;
|
|
46
|
-
export declare function resolveActiveRules(session: MinimalSession, accountId: string
|
|
47
|
-
export declare function activeRuleSet(session: MinimalSession, accountId: string
|
|
42
|
+
export declare function resolveActiveRules(session: MinimalSession, accountId: string): Promise<ActiveRuleResolution>;
|
|
43
|
+
export declare function activeRuleSet(session: MinimalSession, accountId: string): Promise<ActiveRule[]>;
|
|
48
44
|
export {};
|
|
49
45
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB;AAKD,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,CAIpE;AAED,UAAU,cAAc;IACtB,GAAG,EAAE,CACH,CAAC,EAAE,MAAM,EACT,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KACvB,OAAO,CAAC;QAAE,OAAO,EAAE,KAAK,CAAC;YAAE,GAAG,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAA;SAAE,CAAC,CAAA;KAAE,CAAC,CAAC;CAC/D;AAMD,MAAM,MAAM,gBAAgB,GAAG,OAAO,GAAG,UAAU,GAAG,aAAa,CAAC;AACpE,MAAM,MAAM,eAAe,GAAG,gBAAgB,GAAG,aAAa,CAAC;AAE/D,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,gBAAgB,CAAC;CAC1B;AAID,MAAM,WAAW,sBAAsB;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,eAAe,CAAC;CACzB;AAOD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAaxB;AAgED,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,EAAE,CAAC,CAE1B;AAKD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,oBAAoB,CAAC,CAI/B;AAID,wBAAsB,aAAa,CACjC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,UAAU,EAAE,CAAC,CAEvB"}
|
|
@@ -5,56 +5,25 @@
|
|
|
5
5
|
// free of neo4j-driver types so both an MCP process and the UI can import the
|
|
6
6
|
// built dist.
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.
|
|
9
|
-
exports.cosine = cosine;
|
|
10
|
-
exports.deconflictBySubject = deconflictBySubject;
|
|
8
|
+
exports.ACTIVE_CANDIDATE_FLOOR = exports.SAME_SUBJECT_COSINE = void 0;
|
|
11
9
|
exports.formatStandingRulesBlock = formatStandingRulesBlock;
|
|
12
10
|
exports.resolveOwnerUserId = resolveOwnerUserId;
|
|
13
11
|
exports.queryActiveRuleCandidates = queryActiveRuleCandidates;
|
|
14
12
|
exports.resolveActiveRules = resolveActiveRules;
|
|
15
13
|
exports.activeRuleSet = activeRuleSet;
|
|
14
|
+
// Write-time only: profile-update routes a same-subject write (a neighbour at
|
|
15
|
+
// or above this raw-cosine threshold) into the existing node instead of minting
|
|
16
|
+
// a duplicate. Injection no longer dedups — it surfaces every stored active
|
|
17
|
+
// preference verbatim (Task 1951) — so this constant has no read-side use.
|
|
16
18
|
exports.SAME_SUBJECT_COSINE = 0.85;
|
|
17
|
-
exports.INJECTION_RULE_CAP = 12;
|
|
18
19
|
exports.ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
let dot = 0;
|
|
24
|
-
let na = 0;
|
|
25
|
-
let nb = 0;
|
|
26
|
-
for (let i = 0; i < a.length; i++) {
|
|
27
|
-
dot += a[i] * b[i];
|
|
28
|
-
na += a[i] * a[i];
|
|
29
|
-
nb += b[i] * b[i];
|
|
30
|
-
}
|
|
31
|
-
if (na === 0 || nb === 0)
|
|
32
|
-
return 0;
|
|
33
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
34
|
-
}
|
|
35
|
-
// records MUST be confidence-desc on entry. Greedy: the first record of a
|
|
36
|
-
// cluster is the winner; any later record within cosineThreshold of an
|
|
37
|
-
// already-kept winner is a rephrasing and is dropped. A record with no usable
|
|
38
|
-
// embedding is never folded — it is kept as its own subject (we cannot prove it
|
|
39
|
-
// is a duplicate).
|
|
40
|
-
function deconflictBySubject(records, cosineThreshold = exports.SAME_SUBJECT_COSINE) {
|
|
41
|
-
const winners = [];
|
|
42
|
-
for (const r of records) {
|
|
43
|
-
const emb = r.embedding;
|
|
44
|
-
const isDup = Array.isArray(emb) &&
|
|
45
|
-
emb.length > 0 &&
|
|
46
|
-
winners.some((w) => Array.isArray(w.embedding) &&
|
|
47
|
-
w.embedding.length > 0 &&
|
|
48
|
-
cosine(emb, w.embedding) >= cosineThreshold);
|
|
49
|
-
if (!isDup)
|
|
50
|
-
winners.push(r);
|
|
51
|
-
}
|
|
52
|
-
return winners.map(({ embedding: _embedding, ...rest }) => rest);
|
|
53
|
-
}
|
|
54
|
-
function formatStandingRulesBlock(rules, cap = exports.INJECTION_RULE_CAP) {
|
|
20
|
+
// Render every rule verbatim. Injection has no cap (Task 1951): concision is
|
|
21
|
+
// enforced at write time by the conversational-memory skill so each stored
|
|
22
|
+
// value is already short, not by truncating the block here.
|
|
23
|
+
function formatStandingRulesBlock(rules) {
|
|
55
24
|
if (rules.length === 0)
|
|
56
25
|
return "";
|
|
57
|
-
const lines = rules.
|
|
26
|
+
const lines = rules.map((r) => `- ${r.value}`);
|
|
58
27
|
return `## Standing rules\n${lines.join("\n")}`;
|
|
59
28
|
}
|
|
60
29
|
// The account's owner identity (Task 1461). One `AdminUser {role:'owner'}` is
|
|
@@ -80,11 +49,8 @@ async function resolveOwnerUserId(session, accountId) {
|
|
|
80
49
|
// most-recent wins de-confliction. The trashed predicate mirrors graph-trash's
|
|
81
50
|
// `notTrashed()` exactly (label + deletedAt) so this lib stays dependency-free.
|
|
82
51
|
async function queryOwnerCandidates(session, accountId, ownerUserId) {
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// rejects it ("not a valid value. Must be a non-negative integer"). This lib
|
|
86
|
-
// is dependency-free (no neo4j-driver), so it cannot call `neo4j.int()`; the
|
|
87
|
-
// constant is a hardcoded integer, so inlining it is safe and gate-clean.
|
|
52
|
+
// No LIMIT — every owner admin preference above the floor is a candidate
|
|
53
|
+
// (Task 1951). The result is bounded by how many the owner has, not by a cap.
|
|
88
54
|
const result = await session.run(`
|
|
89
55
|
MATCH (up:UserProfile {accountId: $accountId, userId: $ownerUserId})-[:HAS_PREFERENCE]->(pref:Preference {scope: 'admin'})
|
|
90
56
|
WHERE NOT pref:Trashed AND pref.deletedAt IS NULL
|
|
@@ -94,7 +60,6 @@ async function queryOwnerCandidates(session, accountId, ownerUserId) {
|
|
|
94
60
|
pref.key AS key, pref.value AS value, pref.confidence AS confidence,
|
|
95
61
|
pref.observedAt AS observedAt, pref.embedding AS embedding
|
|
96
62
|
ORDER BY pref.confidence DESC, pref.observedAt DESC
|
|
97
|
-
LIMIT ${exports.ACTIVE_CANDIDATE_LIMIT}
|
|
98
63
|
`, { accountId, ownerUserId, floor: exports.ACTIVE_CANDIDATE_FLOOR });
|
|
99
64
|
return result.records.map((r) => ({
|
|
100
65
|
preferenceId: r.get("preferenceId"),
|
|
@@ -128,14 +93,17 @@ async function resolveCandidates(session, accountId) {
|
|
|
128
93
|
async function queryActiveRuleCandidates(session, accountId) {
|
|
129
94
|
return (await resolveCandidates(session, accountId)).candidates;
|
|
130
95
|
}
|
|
131
|
-
// The account's active
|
|
132
|
-
|
|
96
|
+
// The account's active rule-set plus its provenance. Every owner candidate is
|
|
97
|
+
// returned verbatim, in `confidence DESC, observedAt DESC` order — no cap, no
|
|
98
|
+
// de-confliction (Task 1951). Only the read-facing `embedding` is dropped.
|
|
99
|
+
async function resolveActiveRules(session, accountId) {
|
|
133
100
|
const { candidates, ownerUserId, source } = await resolveCandidates(session, accountId);
|
|
134
|
-
|
|
101
|
+
const rules = candidates.map(({ embedding: _embedding, ...rest }) => rest);
|
|
102
|
+
return { rules, ownerUserId, source };
|
|
135
103
|
}
|
|
136
104
|
// The rule array alone — the unchanged call boundary for profile-read and the
|
|
137
105
|
// ui/server injection wrapper.
|
|
138
|
-
async function activeRuleSet(session, accountId
|
|
139
|
-
return (await resolveActiveRules(session, accountId
|
|
106
|
+
async function activeRuleSet(session, accountId) {
|
|
107
|
+
return (await resolveActiveRules(session, accountId)).rules;
|
|
140
108
|
}
|
|
141
109
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,uEAAuE;AACvE,sEAAsE;AACtE,gFAAgF;AAChF,8EAA8E;AAC9E,cAAc;;;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,76 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import {
|
|
3
|
-
cosine,
|
|
4
|
-
deconflictBySubject,
|
|
5
3
|
formatStandingRulesBlock,
|
|
6
4
|
resolveActiveRules,
|
|
7
5
|
activeRuleSet,
|
|
8
6
|
queryActiveRuleCandidates,
|
|
9
|
-
INJECTION_RULE_CAP,
|
|
10
7
|
type RuleCandidate,
|
|
11
8
|
} from "./index.js";
|
|
12
9
|
|
|
13
|
-
const vec = (n: number, fill: number): number[] => Array(n).fill(fill);
|
|
14
|
-
|
|
15
|
-
function cand(over: Partial<RuleCandidate>): RuleCandidate {
|
|
16
|
-
return {
|
|
17
|
-
preferenceId: "p",
|
|
18
|
-
category: "communication",
|
|
19
|
-
key: "k",
|
|
20
|
-
value: "v",
|
|
21
|
-
confidence: 0.5,
|
|
22
|
-
observedAt: "2026-07-01T00:00:00Z",
|
|
23
|
-
embedding: vec(4, 1),
|
|
24
|
-
...over,
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("cosine", () => {
|
|
29
|
-
it("is 1 for identical vectors and 0 for an empty one", () => {
|
|
30
|
-
expect(cosine([1, 0, 0], [1, 0, 0])).toBeCloseTo(1, 6);
|
|
31
|
-
expect(cosine([], [1, 2, 3])).toBe(0);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
describe("deconflictBySubject", () => {
|
|
36
|
-
it("folds a near-duplicate into the higher-confidence winner", () => {
|
|
37
|
-
const a = cand({
|
|
38
|
-
preferenceId: "a",
|
|
39
|
-
key: "header_rule",
|
|
40
|
-
value: "use header A",
|
|
41
|
-
confidence: 0.8,
|
|
42
|
-
embedding: [1, 0, 0, 0],
|
|
43
|
-
});
|
|
44
|
-
const b = cand({
|
|
45
|
-
preferenceId: "b",
|
|
46
|
-
key: "doc_header",
|
|
47
|
-
value: "use header A restated",
|
|
48
|
-
confidence: 0.5,
|
|
49
|
-
embedding: [0.99, 0.01, 0, 0],
|
|
50
|
-
});
|
|
51
|
-
const out = deconflictBySubject([a, b], 0.85);
|
|
52
|
-
expect(out.map((r) => r.preferenceId)).toEqual(["a"]);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it("keeps genuinely different subjects", () => {
|
|
56
|
-
const a = cand({ preferenceId: "a", embedding: [1, 0, 0, 0] });
|
|
57
|
-
const b = cand({ preferenceId: "b", embedding: [0, 1, 0, 0] });
|
|
58
|
-
expect(deconflictBySubject([a, b], 0.85).map((r) => r.preferenceId)).toEqual([
|
|
59
|
-
"a",
|
|
60
|
-
"b",
|
|
61
|
-
]);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it("never folds a candidate with no embedding", () => {
|
|
65
|
-
const a = cand({ preferenceId: "a", embedding: [1, 0, 0, 0] });
|
|
66
|
-
const b = cand({ preferenceId: "b", embedding: null });
|
|
67
|
-
expect(deconflictBySubject([a, b], 0.85).map((r) => r.preferenceId)).toEqual([
|
|
68
|
-
"a",
|
|
69
|
-
"b",
|
|
70
|
-
]);
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
10
|
// A fake Neo4j session that dispatches on query text: the owner-resolution
|
|
75
11
|
// query (matches `role: 'owner'`) returns a configurable owner userId or none;
|
|
76
12
|
// the candidate query (matches HAS_PREFERENCE) returns configurable pref rows
|
|
@@ -115,7 +51,7 @@ const prefRow = (over: Partial<RuleCandidate>): Record<string, unknown> => ({
|
|
|
115
51
|
...over,
|
|
116
52
|
});
|
|
117
53
|
|
|
118
|
-
describe("resolveActiveRules — owner-scoped
|
|
54
|
+
describe("resolveActiveRules — owner-scoped, uncapped, verbatim", () => {
|
|
119
55
|
it("returns the owner's prefs with source=owner when an owner exists", async () => {
|
|
120
56
|
const s = fakeSession({
|
|
121
57
|
ownerId: "owner-35fed05e",
|
|
@@ -132,6 +68,36 @@ describe("resolveActiveRules — owner-scoped selection", () => {
|
|
|
132
68
|
expect(s.candidateRuns).toBe(1);
|
|
133
69
|
});
|
|
134
70
|
|
|
71
|
+
it("returns MORE than 12 candidates with no cap and no truncation", async () => {
|
|
72
|
+
const rows = Array.from({ length: 20 }, (_, i) =>
|
|
73
|
+
prefRow({
|
|
74
|
+
preferenceId: `p${i}`,
|
|
75
|
+
value: `rule ${i}`,
|
|
76
|
+
confidence: 1 - i * 0.01,
|
|
77
|
+
}),
|
|
78
|
+
);
|
|
79
|
+
const s = fakeSession({ ownerId: "owner-x", prefRows: rows });
|
|
80
|
+
const res = await resolveActiveRules(s, "acct-1");
|
|
81
|
+
expect(res.rules.length).toBe(20);
|
|
82
|
+
expect(res.rules.map((r) => r.preferenceId)).toEqual(
|
|
83
|
+
rows.map((r) => r.preferenceId),
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("preserves the candidate order verbatim (no reordering, no dedup)", async () => {
|
|
88
|
+
// Two rows that a cosine dedup would once have folded (identical embeddings)
|
|
89
|
+
// are BOTH returned now — injection never dedups.
|
|
90
|
+
const s = fakeSession({
|
|
91
|
+
ownerId: "owner-1",
|
|
92
|
+
prefRows: [
|
|
93
|
+
prefRow({ preferenceId: "a", value: "use header A", embedding: [1, 0, 0, 0] }),
|
|
94
|
+
prefRow({ preferenceId: "b", value: "use header A restated", embedding: [1, 0, 0, 0] }),
|
|
95
|
+
],
|
|
96
|
+
});
|
|
97
|
+
const res = await resolveActiveRules(s, "acct-1");
|
|
98
|
+
expect(res.rules.map((r) => r.preferenceId)).toEqual(["a", "b"]);
|
|
99
|
+
});
|
|
100
|
+
|
|
135
101
|
it("returns empty with source=no-owner and never runs the candidate query", async () => {
|
|
136
102
|
const s = fakeSession({ ownerId: null });
|
|
137
103
|
const res = await resolveActiveRules(s, "acct-1");
|
|
@@ -176,10 +142,10 @@ describe("resolveActiveRules — owner-scoped selection", () => {
|
|
|
176
142
|
|
|
177
143
|
describe("formatStandingRulesBlock", () => {
|
|
178
144
|
it("returns empty string for no rules", () => {
|
|
179
|
-
expect(formatStandingRulesBlock([]
|
|
145
|
+
expect(formatStandingRulesBlock([])).toBe("");
|
|
180
146
|
});
|
|
181
147
|
|
|
182
|
-
it("
|
|
148
|
+
it("renders every rule, with no cap", () => {
|
|
183
149
|
const rules = Array.from({ length: 20 }, (_, i) => ({
|
|
184
150
|
preferenceId: `p${i}`,
|
|
185
151
|
category: "communication",
|
|
@@ -188,8 +154,8 @@ describe("formatStandingRulesBlock", () => {
|
|
|
188
154
|
confidence: 1 - i * 0.01,
|
|
189
155
|
observedAt: "2026-07-01T00:00:00Z",
|
|
190
156
|
}));
|
|
191
|
-
const block = formatStandingRulesBlock(rules
|
|
157
|
+
const block = formatStandingRulesBlock(rules);
|
|
192
158
|
expect(block).toContain("## Standing rules");
|
|
193
|
-
expect((block.match(/^- /gm) ?? []).length).toBe(
|
|
159
|
+
expect((block.match(/^- /gm) ?? []).length).toBe(20);
|
|
194
160
|
});
|
|
195
161
|
});
|
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
// free of neo4j-driver types so both an MCP process and the UI can import the
|
|
5
5
|
// built dist.
|
|
6
6
|
|
|
7
|
+
// Write-time only: profile-update routes a same-subject write (a neighbour at
|
|
8
|
+
// or above this raw-cosine threshold) into the existing node instead of minting
|
|
9
|
+
// a duplicate. Injection no longer dedups — it surfaces every stored active
|
|
10
|
+
// preference verbatim (Task 1951) — so this constant has no read-side use.
|
|
7
11
|
export const SAME_SUBJECT_COSINE = 0.85;
|
|
8
|
-
export const INJECTION_RULE_CAP = 12;
|
|
9
12
|
export const ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
10
|
-
export const ACTIVE_CANDIDATE_LIMIT = 30;
|
|
11
13
|
|
|
12
14
|
export interface RuleCandidate {
|
|
13
15
|
preferenceId: string;
|
|
@@ -28,52 +30,12 @@ export interface ActiveRule {
|
|
|
28
30
|
observedAt: string;
|
|
29
31
|
}
|
|
30
32
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
let nb = 0;
|
|
36
|
-
for (let i = 0; i < a.length; i++) {
|
|
37
|
-
dot += a[i] * b[i];
|
|
38
|
-
na += a[i] * a[i];
|
|
39
|
-
nb += b[i] * b[i];
|
|
40
|
-
}
|
|
41
|
-
if (na === 0 || nb === 0) return 0;
|
|
42
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// records MUST be confidence-desc on entry. Greedy: the first record of a
|
|
46
|
-
// cluster is the winner; any later record within cosineThreshold of an
|
|
47
|
-
// already-kept winner is a rephrasing and is dropped. A record with no usable
|
|
48
|
-
// embedding is never folded — it is kept as its own subject (we cannot prove it
|
|
49
|
-
// is a duplicate).
|
|
50
|
-
export function deconflictBySubject(
|
|
51
|
-
records: RuleCandidate[],
|
|
52
|
-
cosineThreshold: number = SAME_SUBJECT_COSINE,
|
|
53
|
-
): ActiveRule[] {
|
|
54
|
-
const winners: RuleCandidate[] = [];
|
|
55
|
-
for (const r of records) {
|
|
56
|
-
const emb = r.embedding;
|
|
57
|
-
const isDup =
|
|
58
|
-
Array.isArray(emb) &&
|
|
59
|
-
emb.length > 0 &&
|
|
60
|
-
winners.some(
|
|
61
|
-
(w) =>
|
|
62
|
-
Array.isArray(w.embedding) &&
|
|
63
|
-
w.embedding.length > 0 &&
|
|
64
|
-
cosine(emb, w.embedding) >= cosineThreshold,
|
|
65
|
-
);
|
|
66
|
-
if (!isDup) winners.push(r);
|
|
67
|
-
}
|
|
68
|
-
return winners.map(({ embedding: _embedding, ...rest }) => rest);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export function formatStandingRulesBlock(
|
|
72
|
-
rules: ActiveRule[],
|
|
73
|
-
cap: number = INJECTION_RULE_CAP,
|
|
74
|
-
): string {
|
|
33
|
+
// Render every rule verbatim. Injection has no cap (Task 1951): concision is
|
|
34
|
+
// enforced at write time by the conversational-memory skill so each stored
|
|
35
|
+
// value is already short, not by truncating the block here.
|
|
36
|
+
export function formatStandingRulesBlock(rules: ActiveRule[]): string {
|
|
75
37
|
if (rules.length === 0) return "";
|
|
76
|
-
const lines = rules.
|
|
38
|
+
const lines = rules.map((r) => `- ${r.value}`);
|
|
77
39
|
return `## Standing rules\n${lines.join("\n")}`;
|
|
78
40
|
}
|
|
79
41
|
|
|
@@ -139,11 +101,8 @@ async function queryOwnerCandidates(
|
|
|
139
101
|
accountId: string,
|
|
140
102
|
ownerUserId: string,
|
|
141
103
|
): Promise<RuleCandidate[]> {
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
// rejects it ("not a valid value. Must be a non-negative integer"). This lib
|
|
145
|
-
// is dependency-free (no neo4j-driver), so it cannot call `neo4j.int()`; the
|
|
146
|
-
// constant is a hardcoded integer, so inlining it is safe and gate-clean.
|
|
104
|
+
// No LIMIT — every owner admin preference above the floor is a candidate
|
|
105
|
+
// (Task 1951). The result is bounded by how many the owner has, not by a cap.
|
|
147
106
|
const result = await session.run(
|
|
148
107
|
`
|
|
149
108
|
MATCH (up:UserProfile {accountId: $accountId, userId: $ownerUserId})-[:HAS_PREFERENCE]->(pref:Preference {scope: 'admin'})
|
|
@@ -154,7 +113,6 @@ async function queryOwnerCandidates(
|
|
|
154
113
|
pref.key AS key, pref.value AS value, pref.confidence AS confidence,
|
|
155
114
|
pref.observedAt AS observedAt, pref.embedding AS embedding
|
|
156
115
|
ORDER BY pref.confidence DESC, pref.observedAt DESC
|
|
157
|
-
LIMIT ${ACTIVE_CANDIDATE_LIMIT}
|
|
158
116
|
`,
|
|
159
117
|
{ accountId, ownerUserId, floor: ACTIVE_CANDIDATE_FLOOR },
|
|
160
118
|
);
|
|
@@ -201,14 +159,16 @@ export async function queryActiveRuleCandidates(
|
|
|
201
159
|
return (await resolveCandidates(session, accountId)).candidates;
|
|
202
160
|
}
|
|
203
161
|
|
|
204
|
-
// The account's active
|
|
162
|
+
// The account's active rule-set plus its provenance. Every owner candidate is
|
|
163
|
+
// returned verbatim, in `confidence DESC, observedAt DESC` order — no cap, no
|
|
164
|
+
// de-confliction (Task 1951). Only the read-facing `embedding` is dropped.
|
|
205
165
|
export async function resolveActiveRules(
|
|
206
166
|
session: MinimalSession,
|
|
207
167
|
accountId: string,
|
|
208
|
-
cap: number = INJECTION_RULE_CAP,
|
|
209
168
|
): Promise<ActiveRuleResolution> {
|
|
210
169
|
const { candidates, ownerUserId, source } = await resolveCandidates(session, accountId);
|
|
211
|
-
|
|
170
|
+
const rules = candidates.map(({ embedding: _embedding, ...rest }) => rest);
|
|
171
|
+
return { rules, ownerUserId, source };
|
|
212
172
|
}
|
|
213
173
|
|
|
214
174
|
// The rule array alone — the unchanged call boundary for profile-read and the
|
|
@@ -216,7 +176,6 @@ export async function resolveActiveRules(
|
|
|
216
176
|
export async function activeRuleSet(
|
|
217
177
|
session: MinimalSession,
|
|
218
178
|
accountId: string,
|
|
219
|
-
cap: number = INJECTION_RULE_CAP,
|
|
220
179
|
): Promise<ActiveRule[]> {
|
|
221
|
-
return (await resolveActiveRules(session, accountId
|
|
180
|
+
return (await resolveActiveRules(session, accountId)).rules;
|
|
222
181
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: platform-architecture
|
|
3
3
|
description: Use when grounding any documented-surface claim about what Maxy ships — plugins, skills, specialists, install/deploy flows, internals. This is the install catalogue, not evidence of what is enabled on the current account. For install state on this account, call `capabilities-here`; for documented surface, cite the `Source:` URL inline.
|
|
4
|
-
content-hash: sha256:
|
|
4
|
+
content-hash: sha256:de872279f0c40b8dee0a8ef51dba6534e2452d6034ca2765d6ab912447bc95b3
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -4290,13 +4290,13 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
|
|
|
4290
4290
|
|
|
4291
4291
|
**Reconcile by meaning on write (`profile-update`).** The existence check is two-tier. First an exact `(category, key)` match — the classic fast path, unchanged. When that misses, a semantic-neighbour lookup runs over the `preference_embedding` vector index, scoped to the same `accountId` and `scope:'admin'`, non-trashed. If the nearest neighbour is at or above `SAME_SUBJECT_COSINE` (0.85), the write lands on that node through the existing reinforce/update/contradict curve instead of minting a duplicate; the matched neighbours are returned in the tool result so the agent can classify future writes against real prior state. The confidence constants are unchanged — only the join key widens, so a rephrasing under a new `key` no longer forks the rule and a correction demotes the prior node rather than adding a contradictory one.
|
|
4292
4292
|
|
|
4293
|
-
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a
|
|
4293
|
+
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a floor-filtered (`confidence >= 0.4`), confidence-ordered query that returns every such preference verbatim — no cap, no de-confliction, no candidate limit. Concision is enforced at write time by the `conversational-memory` skill (each stored value is a single minimal imperative) and by the write-time semantic reconcile (a same-subject write at or above `SAME_SUBJECT_COSINE` folds into the existing node rather than minting a duplicate), so the injected block stays small because each stored preference is short and non-redundant, not because injection truncates. A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context`. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
|
|
4294
4294
|
|
|
4295
4295
|
**Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). A sender has exactly one session: every inbound, real or scheduled, resolves to the deterministic `adminSessionIdFor(effectiveAccount, destination)`. An earlier change gave each firing its own salted seat so distrust could not accrete across firings, but the inbound hub keys on `senderId` and replaces its subscriber outright, so a seat's channel server attached under the sender's own key and swallowed the sender's REAL messages until it detached — splitting one conversation across two sessions that each denied what the other held. The seat is removed; the authoritative `agentPrompt`, this provenance marker, and the injected standing rules ground each firing without one. Self-cancel attribution is disarmed while that model beds in: the guard used to compare the cancelling turn's session id to a dispatcher stamp, which discriminated only because each firing ran its own seat, so with one session per sender it would report an autonomous loop on every operator cancel. It now always reports false — under-counting a real loop rather than inventing one — until the discriminator is rebuilt from the turn's own dispatched eventId. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker`, `§ Session model — one session per sender`, and `§ Self-cancel attribution — disarmed`.
|
|
4296
4296
|
|
|
4297
4297
|
**Carry what a reply points at (WhatsApp reply quote).** A WhatsApp sender replying to an earlier message produces a quote that `app/lib/whatsapp/inbound/extract.ts` resolves to `{ id, text?, sender? }` and `manager.ts` assigns to the route payload's `replyContext`. That field had no reader: the `server/index.ts` gateway argument omitted it, `AdminInbound` and the hub frame had no such field, and the channel service's `composePayload` emitted the caption and media notes only, so a message saying "prepare an email confirmation for this booking" reached the agent with nothing to resolve "this booking" against. It now rides both WhatsApp admin doors. On the persistent door it travels `server/index.ts` -> `AdminInbound` -> the hub frame the SSE route stringifies -> the channel service's `InboundPayload`, where `composeReplyNote` renders it as a bracketed note at the top of the payload, inside `## Context` and above the caption and any woven media notes: the quoted body is sender-controlled and belongs in the reframed untrusted zone, never in the platform preamble. Both channel-server inbound paths carry it, through the same `doorInboundPayload` seam that carries `standingRules` and the schedule provenance marker: the single-target SSE loop parses the whole frame, and the unified multi-target door reader rebuilds the payload field by field, so a field that seam omits is dropped on multi-target sessions alone. That seam is guarded by a test which reads the `InboundPayload` declaration and the rebuild function rather than a fixture, because the pre-existing byte-identity guard compared one hardcoded payload and so stayed green for every field added after it was written. On the passive account-manager door there is no channel server and no `## Context` frame, so the one-shot's whole initial prompt is read at face value; `composePassivePrompt` therefore puts the BARE sentence above the requirement and never the quoted body. The body arrives from the sender's own client and is never checked against the message store, so quoting it in a platform-voiced bracket on the door with no untrusted zone would let a modified client put words the agent never said into a `work-create` filing. The empty-text drop gate still tests the requirement alone, so a quote never rescues an inbound carrying no requirement of its own. The quoted body is capped at `QUOTED_TEXT_CAP` (500 characters, `…` appended when cut) because it is unbounded sender-controlled input, and the cut lands on whole characters so a body whose 500th boundary falls inside a surrogate pair cannot emit a lone surrogate at a position the sender chose. Only the channel service renders a body, so the cap lives there alone; platform/ui's `app/lib/whatsapp/reply-quote.ts` holds just the bare sentence for the passive door, and both packages hardcode that sentence in their own tests so the two doors cannot drift apart on the wording (the packages never cross-import, the same arrangement `REFRAME_INSTRUCTION` and `MediaRef` use). Telegram and webchat extract no quote at all, so neither carries one. `payloadBytes` measures the whole inner payload, so a reply carrying a quote produces a visibly larger `[reframe] op=applied … payloadBytes=<n>` than the same text without one, and a bare-text-length `payloadBytes` on an inbound whose store row has a non-null `quotedId` is the signature of this defect returning. Because the field is optional at every hop, dropping it from either `server/index.ts` argument would be silent (tsc stays green, and no unit test executes that wiring file), so the `check-reply-context-threaded` build gate asserts both argument sites.
|
|
4298
4298
|
|
|
4299
|
-
**Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N> owner=<8> source=<owner|no-owner|owner-error|fetch-error>` — injection selects only the account owner's admin preferences, so `source=owner injected=0` is a genuinely empty owner, `source=no-owner` is an account with no seeded owner (injects nothing, a missed owner seeding), and `injected=0 source=owner` on an account whose owner holds rules is the write-only regression re-appearing. `[preference-
|
|
4299
|
+
**Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N> owner=<8> source=<owner|no-owner|owner-error|fetch-error>` — injection selects only the account owner's admin preferences, so `source=owner injected=0` is a genuinely empty owner, `source=no-owner` is an account with no seeded owner (injects nothing, a missed owner seeding), and `injected=0 source=owner` on an account whose owner holds rules is the write-only regression re-appearing. `[preference-inject] op=reconcile accountId=… stored=… injected=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose stored active count diverges from what injection surfaces; injection now surfaces every stored active preference verbatim, so `stored == injected` is the healthy signal and any gap is a dropped-row regression (a reintroduced cap, filter, or truncation). A clean pass logs `op=reconcile drifted=0`, the no-event detector.
|
|
4300
4300
|
|
|
4301
4301
|
**Consult-before-send gate.** Injection (above) puts the owner's active rules in front of every channel turn, but the growing layer-2 `Preference` set is read on demand with `profile-read`, a per-turn judgement the agent can skip, the miss that dropped a customer signature on a delivered document. `hooks/preference-consult-gate.sh` (admin plugin) is the deliverable-boundary enforcement: a PreToolUse gate on customer-facing document deliverables (`browser-pdf-save`/`SendUserFile` to `memory/users/<phone>/documents/`, and email/Outlook sends carrying attachments) that blocks (exit 2) when no `profile-read` ran after the last user message in the transcript. It fires on documents only, never on casual replies. **Observability:** `[preference-gate] op=allow tool=<name> consulted=true` on an allowed document deliverable, `op=bypass tool=<name> detail=no profile-read this turn` on a block; a stream of `op=bypass` is the un-consulted-send signature. The advisory companion `hooks/preference-consult-directive.sh` injects the two-layer architecture reminder every turn (`[pref-wrapper] op=inject`). The placement half, promoting finished deliverables out of `output/` scratch, is counted by the `data-manager` reconcile audit (`scratch-refs`, `stranded`); making that audit run on a standing periodic cadence is a filed follow-up.
|
|
4302
4302
|
|
|
@@ -400,13 +400,13 @@ Standing rules the operator gives over a channel are `Preference` nodes. Two mec
|
|
|
400
400
|
|
|
401
401
|
**Reconcile by meaning on write (`profile-update`).** The existence check is two-tier. First an exact `(category, key)` match — the classic fast path, unchanged. When that misses, a semantic-neighbour lookup runs over the `preference_embedding` vector index, scoped to the same `accountId` and `scope:'admin'`, non-trashed. If the nearest neighbour is at or above `SAME_SUBJECT_COSINE` (0.85), the write lands on that node through the existing reinforce/update/contradict curve instead of minting a duplicate; the matched neighbours are returned in the tool result so the agent can classify future writes against real prior state. The confidence constants are unchanged — only the join key widens, so a rephrasing under a new `key` no longer forks the rule and a correction demotes the prior node rather than adding a contradictory one.
|
|
402
402
|
|
|
403
|
-
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a
|
|
403
|
+
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: the account owner's admin-scope preferences (resolve the seeded `AdminUser {role:'owner'}`, then select through that owner's `UserProfile` via `HAS_PREFERENCE`), a floor-filtered (`confidence >= 0.4`), confidence-ordered query that returns every such preference verbatim — no cap, no de-confliction, no candidate limit. Concision is enforced at write time by the `conversational-memory` skill (each stored value is a single minimal imperative) and by the write-time semantic reconcile (a same-subject write at or above `SAME_SUBJECT_COSINE` folds into the existing node rather than minting a duplicate), so the injected block stays small because each stored preference is short and non-redundant, not because injection truncates. A preference owned by a non-owner profile (an operator's, stranded on the operator's own profile while managing the account) is never injected; an account with no seeded owner injects nothing. `profile-read` exposes this same owner-scoped set as `activeRules`, and the three inbound gateways (`wa-gateway`, `telegram-gateway`, `webchat-gateway`) fetch it for the effective session account and attach it as `standingRules` on the inbound frame; the channel builders prepend it as a `## Standing rules` block above `## Context`. Both channel-server inbound paths carry the block: the single-target SSE loop parses the whole frame, and the unified multi-target door reader (`runDoorStream`,) reconstructs it through the shared `doorInboundPayload` seam whose `parseFrame` closures read `standingRules` off the frame. The read is by code, not agent discretion, so a rule set last week is applied this week regardless of how many times the session compacted. Telegram and webchat inject for `role:'admin'` only — a public visitor never receives operator rules.
|
|
404
404
|
|
|
405
405
|
**Trust a scheduled turn's provenance, never an inbound's claim (channel-agent grounding).** A cron-dispatched agent turn reaches the same channel builders as a real inbound, so its own authorisation text would otherwise land inside `## Context` — the zone `REFRAME_INSTRUCTION` demotes to untrusted — and a security-cautious agent would distrust its own automation (the self-cancel loop). The dispatcher therefore stamps a `ScheduleProvenance` marker (owning account, event, owner userId, dispatch time) that the channel builders render as a `## Scheduled automation (platform-verified)` preamble **above** `## Context` (and above the rules block); the whatsapp wrap tag names the door `scheduler`. Both channel-server paths carry the marker — the single-target loop and the unified multi-target door reader (via the same `doorInboundPayload` seam that carries `standingRules`); the scheduler only dispatches to the whatsapp/telegram door, so a webchat frame never bears one. Because the marker is dispatcher-stamped and never derived from payload text, an inbound message cannot forge it — placement above `## Context` alone is not the trust signal, the dispatcher provenance is. The grounding binds to it in two places: one recipient-neutral clause added byte-identical to the three reframe instructions (a marker-bearing turn is authorised standing automation; a reconfiguration or re-enable is not intrusion), and the admin `IDENTITY.md` "Untrusted input" rule that the agent never states a platform-level authenticity or impersonation verdict it was not given (an unsourced "this is not from you" is its own inference, flagged, never asserted as the platform's). A sender has exactly one session: every inbound, real or scheduled, resolves to the deterministic `adminSessionIdFor(effectiveAccount, destination)`. An earlier change gave each firing its own salted seat so distrust could not accrete across firings, but the inbound hub keys on `senderId` and replaces its subscriber outright, so a seat's channel server attached under the sender's own key and swallowed the sender's REAL messages until it detached — splitting one conversation across two sessions that each denied what the other held. The seat is removed; the authoritative `agentPrompt`, this provenance marker, and the injected standing rules ground each firing without one. Self-cancel attribution is disarmed while that model beds in: the guard used to compare the cancelling turn's session id to a dispatcher stamp, which discriminated only because each firing ran its own seat, so with one session per sender it would report an autonomous loop on every operator cancel. It now always reports false — under-counting a real loop rather than inventing one — until the discriminator is rebuilt from the turn's own dispatched eventId. See `.docs/scheduling-agent-dispatch.md § Operator-provenance marker`, `§ Session model — one session per sender`, and `§ Self-cancel attribution — disarmed`.
|
|
406
406
|
|
|
407
407
|
**Carry what a reply points at (WhatsApp reply quote).** A WhatsApp sender replying to an earlier message produces a quote that `app/lib/whatsapp/inbound/extract.ts` resolves to `{ id, text?, sender? }` and `manager.ts` assigns to the route payload's `replyContext`. That field had no reader: the `server/index.ts` gateway argument omitted it, `AdminInbound` and the hub frame had no such field, and the channel service's `composePayload` emitted the caption and media notes only, so a message saying "prepare an email confirmation for this booking" reached the agent with nothing to resolve "this booking" against. It now rides both WhatsApp admin doors. On the persistent door it travels `server/index.ts` -> `AdminInbound` -> the hub frame the SSE route stringifies -> the channel service's `InboundPayload`, where `composeReplyNote` renders it as a bracketed note at the top of the payload, inside `## Context` and above the caption and any woven media notes: the quoted body is sender-controlled and belongs in the reframed untrusted zone, never in the platform preamble. Both channel-server inbound paths carry it, through the same `doorInboundPayload` seam that carries `standingRules` and the schedule provenance marker: the single-target SSE loop parses the whole frame, and the unified multi-target door reader rebuilds the payload field by field, so a field that seam omits is dropped on multi-target sessions alone. That seam is guarded by a test which reads the `InboundPayload` declaration and the rebuild function rather than a fixture, because the pre-existing byte-identity guard compared one hardcoded payload and so stayed green for every field added after it was written. On the passive account-manager door there is no channel server and no `## Context` frame, so the one-shot's whole initial prompt is read at face value; `composePassivePrompt` therefore puts the BARE sentence above the requirement and never the quoted body. The body arrives from the sender's own client and is never checked against the message store, so quoting it in a platform-voiced bracket on the door with no untrusted zone would let a modified client put words the agent never said into a `work-create` filing. The empty-text drop gate still tests the requirement alone, so a quote never rescues an inbound carrying no requirement of its own. The quoted body is capped at `QUOTED_TEXT_CAP` (500 characters, `…` appended when cut) because it is unbounded sender-controlled input, and the cut lands on whole characters so a body whose 500th boundary falls inside a surrogate pair cannot emit a lone surrogate at a position the sender chose. Only the channel service renders a body, so the cap lives there alone; platform/ui's `app/lib/whatsapp/reply-quote.ts` holds just the bare sentence for the passive door, and both packages hardcode that sentence in their own tests so the two doors cannot drift apart on the wording (the packages never cross-import, the same arrangement `REFRAME_INSTRUCTION` and `MediaRef` use). Telegram and webchat extract no quote at all, so neither carries one. `payloadBytes` measures the whole inner payload, so a reply carrying a quote produces a visibly larger `[reframe] op=applied … payloadBytes=<n>` than the same text without one, and a bare-text-length `payloadBytes` on an inbound whose store row has a non-null `quotedId` is the signature of this defect returning. Because the field is optional at every hop, dropping it from either `server/index.ts` argument would be silent (tsc stays green, and no unit test executes that wiring file), so the `check-reply-context-threaded` build gate asserts both argument sites.
|
|
408
408
|
|
|
409
|
-
**Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N> owner=<8> source=<owner|no-owner|owner-error|fetch-error>` — injection selects only the account owner's admin preferences, so `source=owner injected=0` is a genuinely empty owner, `source=no-owner` is an account with no seeded owner (injects nothing, a missed owner seeding), and `injected=0 source=owner` on an account whose owner holds rules is the write-only regression re-appearing. `[preference-
|
|
409
|
+
**Observability.** `[preference-write] op=reconcile … mode=<reinforce|update|contradict|new> similarity=…` — a stream of `mode=new` for subjects that already exist is the duplicate-minting signature. `[preference-inject] op=inject channel=<wa|tg|web> injected=<N> owner=<8> source=<owner|no-owner|owner-error|fetch-error>` — injection selects only the account owner's admin preferences, so `source=owner injected=0` is a genuinely empty owner, `source=no-owner` is an account with no seeded owner (injects nothing, a missed owner seeding), and `injected=0 source=owner` on an account whose owner holds rules is the write-only regression re-appearing. `[preference-inject] op=reconcile accountId=… stored=… injected=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose stored active count diverges from what injection surfaces; injection now surfaces every stored active preference verbatim, so `stored == injected` is the healthy signal and any gap is a dropped-row regression (a reintroduced cap, filter, or truncation). A clean pass logs `op=reconcile drifted=0`, the no-event detector.
|
|
410
410
|
|
|
411
411
|
**Consult-before-send gate.** Injection (above) puts the owner's active rules in front of every channel turn, but the growing layer-2 `Preference` set is read on demand with `profile-read`, a per-turn judgement the agent can skip, the miss that dropped a customer signature on a delivered document. `hooks/preference-consult-gate.sh` (admin plugin) is the deliverable-boundary enforcement: a PreToolUse gate on customer-facing document deliverables (`browser-pdf-save`/`SendUserFile` to `memory/users/<phone>/documents/`, and email/Outlook sends carrying attachments) that blocks (exit 2) when no `profile-read` ran after the last user message in the transcript. It fires on documents only, never on casual replies. **Observability:** `[preference-gate] op=allow tool=<name> consulted=true` on an allowed document deliverable, `op=bypass tool=<name> detail=no profile-read this turn` on a block; a stream of `op=bypass` is the un-consulted-send signature. The advisory companion `hooks/preference-consult-directive.sh` injects the two-layer architecture reminder every turn (`[pref-wrapper] op=inject`). The placement half, promoting finished deliverables out of `output/` scratch, is counted by the `data-manager` reconcile audit (`scratch-refs`, `stranded`); making that audit run on a standing periodic cadence is a filed follow-up.
|
|
412
412
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-signature.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/compose-signature.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { composeDraft } from '../lib/compose.js';
|
|
6
|
+
const config = { email: 'agent@gls.test', agentAddress: 'agent@gls.test' };
|
|
7
|
+
describe('Task 1952 — composeDraft applies signature', () => {
|
|
8
|
+
let root = '';
|
|
9
|
+
let accountDir = '';
|
|
10
|
+
const accountId = 'gls12345-aaaa';
|
|
11
|
+
const saved = { ...process.env };
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
root = realpathSync(mkdtempSync(join(tmpdir(), 'cmp-')));
|
|
14
|
+
const platformRoot = join(root, 'platform');
|
|
15
|
+
mkdirSync(platformRoot, { recursive: true });
|
|
16
|
+
accountDir = join(root, 'data', 'accounts', accountId);
|
|
17
|
+
mkdirSync(accountDir, { recursive: true });
|
|
18
|
+
process.env.PLATFORM_ROOT = platformRoot;
|
|
19
|
+
delete process.env.ACCOUNT_DIR;
|
|
20
|
+
});
|
|
21
|
+
afterEach(() => {
|
|
22
|
+
process.env = { ...saved };
|
|
23
|
+
if (root)
|
|
24
|
+
rmSync(root, { recursive: true, force: true });
|
|
25
|
+
});
|
|
26
|
+
it('appends the text signature to the composed body', async () => {
|
|
27
|
+
writeFileSync(join(accountDir, 'email-signature.txt'), 'Regards,\nDale');
|
|
28
|
+
const { raw } = await composeDraft(config, 'pw', accountId, {
|
|
29
|
+
to: 'x@y.test', subject: 'Hi', body: 'Quote attached.',
|
|
30
|
+
});
|
|
31
|
+
const text = raw.toString('utf8');
|
|
32
|
+
expect(text).toContain('Quote attached.');
|
|
33
|
+
expect(text).toMatch(/Regards,\r?\nDale/);
|
|
34
|
+
});
|
|
35
|
+
it('logs applied=true with the account id prefix and asset path', async () => {
|
|
36
|
+
writeFileSync(join(accountDir, 'email-signature.txt'), 'Regards,\nDale');
|
|
37
|
+
const err = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
38
|
+
await composeDraft(config, 'pw', accountId, { to: 'x@y.test', subject: 'Hi', body: 'Body.' });
|
|
39
|
+
const line = err.mock.calls.map((c) => String(c[0])).find((s) => s.includes('op=signature'));
|
|
40
|
+
expect(line).toContain('accountId=gls12345');
|
|
41
|
+
expect(line).toContain('applied=true');
|
|
42
|
+
expect(line).toContain(join(accountDir, 'email-signature'));
|
|
43
|
+
err.mockRestore();
|
|
44
|
+
});
|
|
45
|
+
it('logs applied=false when no signature file is present', async () => {
|
|
46
|
+
const err = vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
47
|
+
await composeDraft(config, 'pw', accountId, { to: 'x@y.test', subject: 'Hi', body: 'Body.' });
|
|
48
|
+
const line = err.mock.calls.map((c) => String(c[0])).find((s) => s.includes('op=signature'));
|
|
49
|
+
expect(line).toContain('applied=false');
|
|
50
|
+
err.mockRestore();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
//# sourceMappingURL=compose-signature.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose-signature.test.js","sourceRoot":"","sources":["../../src/__tests__/compose-signature.test.ts"],"names":[],"mappings":"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"}
|