@rubytech/create-maxy-code 0.1.417 → 0.1.419
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/dist/__tests__/brand-port-uniqueness.test.js +74 -0
- package/dist/known-brands.js +1 -0
- package/package.json +1 -1
- package/payload/platform/config/brand-registry.json +8 -0
- package/payload/platform/lib/active-rules/dist/index.d.ts +35 -0
- package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/active-rules/dist/index.js +94 -0
- package/payload/platform/lib/active-rules/dist/index.js.map +1 -0
- package/payload/platform/lib/active-rules/package.json +7 -0
- package/payload/platform/lib/active-rules/src/index.test.ts +89 -0
- package/payload/platform/lib/active-rules/src/index.ts +133 -0
- package/payload/platform/lib/active-rules/tsconfig.json +9 -0
- package/payload/platform/package.json +4 -3
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +14 -2
- package/payload/platform/plugins/docs/references/internals.md +10 -0
- package/payload/platform/plugins/docs/references/memory-guide.md +2 -0
- package/payload/platform/plugins/docs/references/outlook-guide.md +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js +49 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.d.ts +2 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.d.ts.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.js +99 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.js.map +1 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.d.ts +3 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +11 -3
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +21 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +106 -14
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js +24 -0
- package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.d.ts +15 -2
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.d.ts.map +1 -1
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.js +16 -7
- package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.js.map +1 -1
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js +10 -5
- package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +9 -1
- package/payload/platform/scripts/__tests__/provision-role-stamp.test.sh +3 -0
- package/payload/platform/scripts/lib/provision-account-dir.sh +9 -1
- package/payload/platform/services/claude-session-manager/dist/agent-identity-locator.d.ts +1 -1
- package/payload/platform/services/claude-session-manager/dist/agent-identity-locator.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/agent-identity-locator.js +5 -1
- package/payload/platform/services/claude-session-manager/dist/agent-identity-locator.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +1 -0
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +29 -2
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/index.js +14 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts +46 -0
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js +96 -0
- package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +33 -0
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +54 -5
- package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/public-agent-reachability.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/public-agent-reachability.js +5 -1
- package/payload/platform/services/claude-session-manager/dist/public-agent-reachability.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +6 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js +20 -0
- package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
- package/payload/platform/services/telegram-channel/dist/instructions.d.ts +9 -0
- package/payload/platform/services/telegram-channel/dist/instructions.d.ts.map +1 -0
- package/payload/platform/services/telegram-channel/dist/instructions.js +100 -0
- package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -0
- package/payload/platform/services/telegram-channel/dist/notification.d.ts +93 -0
- package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -0
- package/payload/platform/services/telegram-channel/dist/notification.js +119 -0
- package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -0
- package/payload/platform/services/telegram-channel/dist/server.d.ts +40 -0
- package/payload/platform/services/telegram-channel/dist/server.d.ts.map +1 -0
- package/payload/platform/services/telegram-channel/dist/server.js +291 -0
- package/payload/platform/services/telegram-channel/dist/server.js.map +1 -0
- package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts +28 -0
- package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts.map +1 -0
- package/payload/platform/services/telegram-channel/dist/turn-follow.js +188 -0
- package/payload/platform/services/telegram-channel/dist/turn-follow.js.map +1 -0
- package/payload/platform/services/webchat-channel/dist/notification.d.ts +5 -0
- package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/webchat-channel/dist/notification.js +6 -1
- package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -0
- package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js +8 -1
- package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
- package/payload/platform/templates/agents/passive/IDENTITY.md +21 -0
- package/payload/server/public/assets/{AdminLoginScreens-CBTx7ZPK.js → AdminLoginScreens-B1PkOF66.js} +1 -1
- package/payload/server/public/assets/AdminShell-D8wNArGQ.js +2 -0
- package/payload/server/public/assets/{Checkbox-BHTxekRB.js → Checkbox-Bm9Er726.js} +1 -1
- package/payload/server/public/assets/{Transcript-5kLbBInN.js → Transcript-CBIyQ8T5.js} +1 -1
- package/payload/server/public/assets/{admin-BWCLE32N.js → admin-CWM9G_yA.js} +1 -1
- package/payload/server/public/assets/{browser-CRvCPsvV.js → browser-D8QhNffV.js} +1 -1
- package/payload/server/public/assets/{calendar-BI-kd-QP.js → calendar-qob5K4t_.js} +1 -1
- package/payload/server/public/assets/chat-BNrxVUF6.js +1 -0
- package/payload/server/public/assets/chevron-left-BQThFVrd.js +1 -0
- package/payload/server/public/assets/data-C-EqNbRi.js +1 -0
- package/payload/server/public/assets/{graph-BFSyW_5i.js → graph-DZ2HMpIB.js} +1 -1
- package/payload/server/public/assets/{graph-labels-DEjD_qJh.js → graph-labels-BFG4bjXQ.js} +1 -1
- package/payload/server/public/assets/{maximize-2-BcMJNeCB.js → maximize-2-DkZ40lSl.js} +1 -1
- package/payload/server/public/assets/{operator-Dtvm9ero.js → operator-FrXrSsBj.js} +1 -1
- package/payload/server/public/assets/page-CHWMI9Tm.js +32 -0
- package/payload/server/public/assets/{page-TXi9dmaB.js → page-CXKV01gT.js} +1 -1
- package/payload/server/public/assets/{public-DBx3U0r8.js → public-2p1pJ4WF.js} +1 -1
- package/payload/server/public/assets/{rotate-ccw-8ti3QkeY.js → rotate-ccw-D3o_bMBA.js} +1 -1
- package/payload/server/public/assets/useSubAccountSwitcher-8dLvF3Ux.js +9 -0
- package/payload/server/public/browser.html +3 -3
- package/payload/server/public/calendar.html +4 -4
- package/payload/server/public/chat.html +8 -8
- package/payload/server/public/data.html +7 -7
- package/payload/server/public/graph.html +8 -8
- package/payload/server/public/index.html +10 -10
- package/payload/server/public/operator.html +10 -10
- package/payload/server/public/public.html +8 -8
- package/payload/server/server.js +237 -38
- package/payload/server/public/assets/AdminShell-DPHUeXea.js +0 -2
- package/payload/server/public/assets/chat-DFy8hb1y.js +0 -1
- package/payload/server/public/assets/chevron-left-Bggm5B5U.js +0 -1
- package/payload/server/public/assets/data-HvZ69Ya1.js +0 -1
- package/payload/server/public/assets/page-C_pm3hl3.js +0 -32
- package/payload/server/public/assets/useSubAccountSwitcher-Bc5dOBdI.js +0 -9
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Standing check: no two brands may fight for the same host resource when
|
|
2
|
+
// co-resident on one device. Two namespaces are at stake:
|
|
3
|
+
//
|
|
4
|
+
// 1. Host TCP ports. neo4jPort / ttydPort / rfbPort / websockifyPort /
|
|
5
|
+
// cdpPort / claudeSessionManagerPort all bind TCP on the same host, so they
|
|
6
|
+
// must be globally unique across every (brand, field) pair — not merely
|
|
7
|
+
// unique within each field. A future brand whose ttydPort equals another
|
|
8
|
+
// brand's neo4jPort would EADDRINUSE at runtime with no build event, even
|
|
9
|
+
// though each field is individually unique.
|
|
10
|
+
// 2. X display numbers. vncDisplay is an Xvnc display index (:NN and its
|
|
11
|
+
// /tmp/.X<NN>-lock), a namespace distinct from TCP ports, so it must be
|
|
12
|
+
// unique across brands on its own.
|
|
13
|
+
//
|
|
14
|
+
// This reconciles all brands/*/brand.json on every CI run. Path resolution
|
|
15
|
+
// mirrors known-brand-hostnames.test.ts.
|
|
16
|
+
import test from "node:test";
|
|
17
|
+
import assert from "node:assert/strict";
|
|
18
|
+
import { readdirSync, readFileSync, existsSync } from "node:fs";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import { dirname, resolve, join } from "node:path";
|
|
21
|
+
// dist/__tests__/brand-port-uniqueness.test.js → maxy-code/brands
|
|
22
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
23
|
+
const BRANDS_DIR = resolve(here, "../../../../brands");
|
|
24
|
+
// Fields that bind a host TCP port (one shared namespace across all fields).
|
|
25
|
+
const TCP_PORT_FIELDS = [
|
|
26
|
+
"neo4jPort",
|
|
27
|
+
"ttydPort",
|
|
28
|
+
"rfbPort",
|
|
29
|
+
"websockifyPort",
|
|
30
|
+
"cdpPort",
|
|
31
|
+
"claudeSessionManagerPort",
|
|
32
|
+
];
|
|
33
|
+
// X display index — its own namespace, unique per brand.
|
|
34
|
+
const DISPLAY_FIELD = "vncDisplay";
|
|
35
|
+
const ALL_FIELDS = [...TCP_PORT_FIELDS, DISPLAY_FIELD];
|
|
36
|
+
function brandManifests() {
|
|
37
|
+
return readdirSync(BRANDS_DIR, { withFileTypes: true })
|
|
38
|
+
.filter((e) => e.isDirectory())
|
|
39
|
+
.map((e) => ({ name: e.name, path: join(BRANDS_DIR, e.name, "brand.json") }))
|
|
40
|
+
.filter((b) => existsSync(b.path))
|
|
41
|
+
.map((b) => ({ name: b.name, manifest: JSON.parse(readFileSync(b.path, "utf-8")) }));
|
|
42
|
+
}
|
|
43
|
+
test("brands/ is discoverable and every brand declares all port fields", () => {
|
|
44
|
+
const brands = brandManifests();
|
|
45
|
+
assert.ok(brands.length > 0, `no brand.json found under ${BRANDS_DIR}`);
|
|
46
|
+
for (const { name, manifest } of brands) {
|
|
47
|
+
for (const field of ALL_FIELDS) {
|
|
48
|
+
assert.equal(typeof manifest[field], "number", `brands/${name}/brand.json is missing numeric ${field}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
test("host TCP ports are globally unique across every (brand, field)", () => {
|
|
53
|
+
const brands = brandManifests();
|
|
54
|
+
const seen = new Map(); // port -> "brand.field" that claimed it
|
|
55
|
+
for (const { name, manifest } of brands) {
|
|
56
|
+
for (const field of TCP_PORT_FIELDS) {
|
|
57
|
+
const value = manifest[field];
|
|
58
|
+
const owner = `${name}.${field}`;
|
|
59
|
+
const prior = seen.get(value);
|
|
60
|
+
assert.equal(prior, undefined, `TCP port collision: ${prior} and ${owner} both bind ${value} — they would EADDRINUSE when co-resident`);
|
|
61
|
+
seen.set(value, owner);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
test("vncDisplay is unique across brands", () => {
|
|
66
|
+
const brands = brandManifests();
|
|
67
|
+
const seen = new Map();
|
|
68
|
+
for (const { name, manifest } of brands) {
|
|
69
|
+
const value = manifest[DISPLAY_FIELD];
|
|
70
|
+
const prior = seen.get(value);
|
|
71
|
+
assert.equal(prior, undefined, `vncDisplay collision: brands "${prior}" and "${name}" both use :${value}`);
|
|
72
|
+
seen.set(value, name);
|
|
73
|
+
}
|
|
74
|
+
});
|
package/dist/known-brands.js
CHANGED
package/package.json
CHANGED
|
@@ -8,6 +8,14 @@
|
|
|
8
8
|
"websockifyPort": 6080,
|
|
9
9
|
"cdpPort": 9222
|
|
10
10
|
},
|
|
11
|
+
{
|
|
12
|
+
"hostname": "property-administrators",
|
|
13
|
+
"configDir": ".property-administrators",
|
|
14
|
+
"vncDisplay": 102,
|
|
15
|
+
"rfbPort": 5903,
|
|
16
|
+
"websockifyPort": 6083,
|
|
17
|
+
"cdpPort": 9225
|
|
18
|
+
},
|
|
11
19
|
{
|
|
12
20
|
"hostname": "realagent-code",
|
|
13
21
|
"configDir": ".realagent-code",
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export declare const SAME_SUBJECT_COSINE = 0.85;
|
|
2
|
+
export declare const INJECTION_RULE_CAP = 12;
|
|
3
|
+
export declare const ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
4
|
+
export declare const ACTIVE_CANDIDATE_LIMIT = 30;
|
|
5
|
+
export interface RuleCandidate {
|
|
6
|
+
preferenceId: string;
|
|
7
|
+
category: string;
|
|
8
|
+
key: string;
|
|
9
|
+
value: string;
|
|
10
|
+
confidence: number;
|
|
11
|
+
observedAt: string;
|
|
12
|
+
embedding: number[] | null;
|
|
13
|
+
}
|
|
14
|
+
export interface ActiveRule {
|
|
15
|
+
preferenceId: string;
|
|
16
|
+
category: string;
|
|
17
|
+
key: string;
|
|
18
|
+
value: string;
|
|
19
|
+
confidence: number;
|
|
20
|
+
observedAt: string;
|
|
21
|
+
}
|
|
22
|
+
export declare function cosine(a: number[], b: number[]): number;
|
|
23
|
+
export declare function deconflictBySubject(records: RuleCandidate[], cosineThreshold?: number): ActiveRule[];
|
|
24
|
+
export declare function formatStandingRulesBlock(rules: ActiveRule[], cap?: number): string;
|
|
25
|
+
interface MinimalSession {
|
|
26
|
+
run: (q: string, p: Record<string, unknown>) => Promise<{
|
|
27
|
+
records: Array<{
|
|
28
|
+
get(k: string): unknown;
|
|
29
|
+
}>;
|
|
30
|
+
}>;
|
|
31
|
+
}
|
|
32
|
+
export declare function queryActiveRuleCandidates(session: MinimalSession, accountId: string): Promise<RuleCandidate[]>;
|
|
33
|
+
export declare function activeRuleSet(session: MinimalSession, accountId: string, cap?: number): Promise<ActiveRule[]>;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAMA,eAAO,MAAM,mBAAmB,OAAO,CAAC;AACxC,eAAO,MAAM,kBAAkB,KAAK,CAAC;AACrC,eAAO,MAAM,sBAAsB,MAAM,CAAC;AAC1C,eAAO,MAAM,sBAAsB,KAAK,CAAC;AAEzC,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;AAED,wBAAgB,MAAM,CAAC,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,EAAE,GAAG,MAAM,CAYvD;AAOD,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,aAAa,EAAE,EACxB,eAAe,GAAE,MAA4B,GAC5C,UAAU,EAAE,CAgBd;AAED,wBAAgB,wBAAwB,CACtC,KAAK,EAAE,UAAU,EAAE,EACnB,GAAG,GAAE,MAA2B,GAC/B,MAAM,CAIR;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;AAOD,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,EAAE,CAAC,CA6B1B;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,cAAc,EACvB,SAAS,EAAE,MAAM,EACjB,GAAG,GAAE,MAA2B,GAC/B,OAAO,CAAC,UAAU,EAAE,CAAC,CAGvB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// The one definition of "this account's active, de-conflicted, bounded
|
|
3
|
+
// rule-set" (Task 1486). Consumed by the memory plugin's profile-read
|
|
4
|
+
// (agent-callable) and by the three inbound gateways (per-turn injection). Kept
|
|
5
|
+
// free of neo4j-driver types so both an MCP process and the UI can import the
|
|
6
|
+
// built dist.
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.ACTIVE_CANDIDATE_LIMIT = exports.ACTIVE_CANDIDATE_FLOOR = exports.INJECTION_RULE_CAP = exports.SAME_SUBJECT_COSINE = void 0;
|
|
9
|
+
exports.cosine = cosine;
|
|
10
|
+
exports.deconflictBySubject = deconflictBySubject;
|
|
11
|
+
exports.formatStandingRulesBlock = formatStandingRulesBlock;
|
|
12
|
+
exports.queryActiveRuleCandidates = queryActiveRuleCandidates;
|
|
13
|
+
exports.activeRuleSet = activeRuleSet;
|
|
14
|
+
exports.SAME_SUBJECT_COSINE = 0.85;
|
|
15
|
+
exports.INJECTION_RULE_CAP = 12;
|
|
16
|
+
exports.ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
17
|
+
exports.ACTIVE_CANDIDATE_LIMIT = 30;
|
|
18
|
+
function cosine(a, b) {
|
|
19
|
+
if (!a.length || !b.length || a.length !== b.length)
|
|
20
|
+
return 0;
|
|
21
|
+
let dot = 0;
|
|
22
|
+
let na = 0;
|
|
23
|
+
let nb = 0;
|
|
24
|
+
for (let i = 0; i < a.length; i++) {
|
|
25
|
+
dot += a[i] * b[i];
|
|
26
|
+
na += a[i] * a[i];
|
|
27
|
+
nb += b[i] * b[i];
|
|
28
|
+
}
|
|
29
|
+
if (na === 0 || nb === 0)
|
|
30
|
+
return 0;
|
|
31
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
32
|
+
}
|
|
33
|
+
// records MUST be confidence-desc on entry. Greedy: the first record of a
|
|
34
|
+
// cluster is the winner; any later record within cosineThreshold of an
|
|
35
|
+
// already-kept winner is a rephrasing and is dropped. A record with no usable
|
|
36
|
+
// embedding is never folded — it is kept as its own subject (we cannot prove it
|
|
37
|
+
// is a duplicate).
|
|
38
|
+
function deconflictBySubject(records, cosineThreshold = exports.SAME_SUBJECT_COSINE) {
|
|
39
|
+
const winners = [];
|
|
40
|
+
for (const r of records) {
|
|
41
|
+
const emb = r.embedding;
|
|
42
|
+
const isDup = Array.isArray(emb) &&
|
|
43
|
+
emb.length > 0 &&
|
|
44
|
+
winners.some((w) => Array.isArray(w.embedding) &&
|
|
45
|
+
w.embedding.length > 0 &&
|
|
46
|
+
cosine(emb, w.embedding) >= cosineThreshold);
|
|
47
|
+
if (!isDup)
|
|
48
|
+
winners.push(r);
|
|
49
|
+
}
|
|
50
|
+
return winners.map(({ embedding: _embedding, ...rest }) => rest);
|
|
51
|
+
}
|
|
52
|
+
function formatStandingRulesBlock(rules, cap = exports.INJECTION_RULE_CAP) {
|
|
53
|
+
if (rules.length === 0)
|
|
54
|
+
return "";
|
|
55
|
+
const lines = rules.slice(0, cap).map((r) => `- ${r.value}`);
|
|
56
|
+
return `## Standing rules\n${lines.join("\n")}`;
|
|
57
|
+
}
|
|
58
|
+
// The candidate query: this account's admin-scope preferences, not trashed, not
|
|
59
|
+
// a "does not apply" declination, above the injection floor, ordered so the
|
|
60
|
+
// highest-confidence / most-recent wins de-confliction. Returns `embedding` for
|
|
61
|
+
// the clustering step. The trashed predicate mirrors graph-trash's
|
|
62
|
+
// `notTrashed()` exactly (label + deletedAt) so this lib stays dependency-free.
|
|
63
|
+
async function queryActiveRuleCandidates(session, accountId) {
|
|
64
|
+
// ACTIVE_CANDIDATE_LIMIT is inlined as a Cypher literal rather than bound as a
|
|
65
|
+
// param: a plain JS number bound to `LIMIT $p` serializes as a FLOAT and Neo4j
|
|
66
|
+
// rejects it ("not a valid value. Must be a non-negative integer"). This lib
|
|
67
|
+
// is dependency-free (no neo4j-driver), so it cannot call `neo4j.int()`; the
|
|
68
|
+
// constant is a hardcoded integer, so inlining it is safe and gate-clean.
|
|
69
|
+
const result = await session.run(`
|
|
70
|
+
MATCH (pref:Preference {accountId: $accountId, scope: 'admin'})
|
|
71
|
+
WHERE NOT pref:Trashed AND pref.deletedAt IS NULL
|
|
72
|
+
AND coalesce(pref.notApplicable, false) = false
|
|
73
|
+
AND pref.confidence >= $floor
|
|
74
|
+
RETURN pref.preferenceId AS preferenceId, pref.category AS category,
|
|
75
|
+
pref.key AS key, pref.value AS value, pref.confidence AS confidence,
|
|
76
|
+
pref.observedAt AS observedAt, pref.embedding AS embedding
|
|
77
|
+
ORDER BY pref.confidence DESC, pref.observedAt DESC
|
|
78
|
+
LIMIT ${exports.ACTIVE_CANDIDATE_LIMIT}
|
|
79
|
+
`, { accountId, floor: exports.ACTIVE_CANDIDATE_FLOOR });
|
|
80
|
+
return result.records.map((r) => ({
|
|
81
|
+
preferenceId: r.get("preferenceId"),
|
|
82
|
+
category: r.get("category"),
|
|
83
|
+
key: r.get("key"),
|
|
84
|
+
value: r.get("value"),
|
|
85
|
+
confidence: r.get("confidence"),
|
|
86
|
+
observedAt: r.get("observedAt") ?? "",
|
|
87
|
+
embedding: r.get("embedding") ?? null,
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
async function activeRuleSet(session, accountId, cap = exports.INJECTION_RULE_CAP) {
|
|
91
|
+
const candidates = await queryActiveRuleCandidates(session, accountId);
|
|
92
|
+
return deconflictBySubject(candidates).slice(0, cap);
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA,uEAAuE;AACvE,sEAAsE;AACtE,gFAAgF;AAChF,8EAA8E;AAC9E,cAAc;;;AA0Bd,wBAYC;AAOD,kDAmBC;AAED,4DAOC;AAcD,8DAgCC;AAED,sCAOC;AA9HY,QAAA,mBAAmB,GAAG,IAAI,CAAC;AAC3B,QAAA,kBAAkB,GAAG,EAAE,CAAC;AACxB,QAAA,sBAAsB,GAAG,GAAG,CAAC;AAC7B,QAAA,sBAAsB,GAAG,EAAE,CAAC;AAqBzC,SAAgB,MAAM,CAAC,CAAW,EAAE,CAAW;IAC7C,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,CAAC,CAAC;IAC9D,IAAI,GAAG,GAAG,CAAC,CAAC;IACZ,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,IAAI,EAAE,GAAG,CAAC,CAAC;IACX,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QAClC,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QACnB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;QAClB,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC;IACpB,CAAC;IACD,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC;QAAE,OAAO,CAAC,CAAC;IACnC,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;AAC/C,CAAC;AAED,0EAA0E;AAC1E,uEAAuE;AACvE,8EAA8E;AAC9E,gFAAgF;AAChF,mBAAmB;AACnB,SAAgB,mBAAmB,CACjC,OAAwB,EACxB,kBAA0B,2BAAmB;IAE7C,MAAM,OAAO,GAAoB,EAAE,CAAC;IACpC,KAAK,MAAM,CAAC,IAAI,OAAO,EAAE,CAAC;QACxB,MAAM,GAAG,GAAG,CAAC,CAAC,SAAS,CAAC;QACxB,MAAM,KAAK,GACT,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC;YAClB,GAAG,CAAC,MAAM,GAAG,CAAC;YACd,OAAO,CAAC,IAAI,CACV,CAAC,CAAC,EAAE,EAAE,CACJ,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC;gBAC1B,CAAC,CAAC,SAAS,CAAC,MAAM,GAAG,CAAC;gBACtB,MAAM,CAAC,GAAG,EAAE,CAAC,CAAC,SAAS,CAAC,IAAI,eAAe,CAC9C,CAAC;QACJ,IAAI,CAAC,KAAK;YAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC9B,CAAC;IACD,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,SAAS,EAAE,UAAU,EAAE,GAAG,IAAI,EAAE,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC;AACnE,CAAC;AAED,SAAgB,wBAAwB,CACtC,KAAmB,EACnB,MAAc,0BAAkB;IAEhC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAClC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC;IAC7D,OAAO,sBAAsB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;AAClD,CAAC;AASD,gFAAgF;AAChF,4EAA4E;AAC5E,gFAAgF;AAChF,mEAAmE;AACnE,gFAAgF;AACzE,KAAK,UAAU,yBAAyB,CAC7C,OAAuB,EACvB,SAAiB;IAEjB,+EAA+E;IAC/E,+EAA+E;IAC/E,6EAA6E;IAC7E,6EAA6E;IAC7E,0EAA0E;IAC1E,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,GAAG,CAC9B;;;;;;;;;YASQ,8BAAsB;KAC7B,EACD,EAAE,SAAS,EAAE,KAAK,EAAE,8BAAsB,EAAE,CAC7C,CAAC;IACF,OAAO,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAChC,YAAY,EAAE,CAAC,CAAC,GAAG,CAAC,cAAc,CAAW;QAC7C,QAAQ,EAAE,CAAC,CAAC,GAAG,CAAC,UAAU,CAAW;QACrC,GAAG,EAAE,CAAC,CAAC,GAAG,CAAC,KAAK,CAAW;QAC3B,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAW;QAC/B,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,CAAW;QACzC,UAAU,EAAG,CAAC,CAAC,GAAG,CAAC,YAAY,CAAY,IAAI,EAAE;QACjD,SAAS,EAAG,CAAC,CAAC,GAAG,CAAC,WAAW,CAAqB,IAAI,IAAI;KAC3D,CAAC,CAAC,CAAC;AACN,CAAC;AAEM,KAAK,UAAU,aAAa,CACjC,OAAuB,EACvB,SAAiB,EACjB,MAAc,0BAAkB;IAEhC,MAAM,UAAU,GAAG,MAAM,yBAAyB,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC;IACvE,OAAO,mBAAmB,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
cosine,
|
|
4
|
+
deconflictBySubject,
|
|
5
|
+
formatStandingRulesBlock,
|
|
6
|
+
INJECTION_RULE_CAP,
|
|
7
|
+
type RuleCandidate,
|
|
8
|
+
} from "./index.js";
|
|
9
|
+
|
|
10
|
+
const vec = (n: number, fill: number): number[] => Array(n).fill(fill);
|
|
11
|
+
|
|
12
|
+
function cand(over: Partial<RuleCandidate>): RuleCandidate {
|
|
13
|
+
return {
|
|
14
|
+
preferenceId: "p",
|
|
15
|
+
category: "communication",
|
|
16
|
+
key: "k",
|
|
17
|
+
value: "v",
|
|
18
|
+
confidence: 0.5,
|
|
19
|
+
observedAt: "2026-07-01T00:00:00Z",
|
|
20
|
+
embedding: vec(4, 1),
|
|
21
|
+
...over,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("cosine", () => {
|
|
26
|
+
it("is 1 for identical vectors and 0 for an empty one", () => {
|
|
27
|
+
expect(cosine([1, 0, 0], [1, 0, 0])).toBeCloseTo(1, 6);
|
|
28
|
+
expect(cosine([], [1, 2, 3])).toBe(0);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("deconflictBySubject", () => {
|
|
33
|
+
it("folds a near-duplicate into the higher-confidence winner", () => {
|
|
34
|
+
const a = cand({
|
|
35
|
+
preferenceId: "a",
|
|
36
|
+
key: "header_rule",
|
|
37
|
+
value: "use header A",
|
|
38
|
+
confidence: 0.8,
|
|
39
|
+
embedding: [1, 0, 0, 0],
|
|
40
|
+
});
|
|
41
|
+
const b = cand({
|
|
42
|
+
preferenceId: "b",
|
|
43
|
+
key: "doc_header",
|
|
44
|
+
value: "use header A restated",
|
|
45
|
+
confidence: 0.5,
|
|
46
|
+
embedding: [0.99, 0.01, 0, 0],
|
|
47
|
+
});
|
|
48
|
+
const out = deconflictBySubject([a, b], 0.85);
|
|
49
|
+
expect(out.map((r) => r.preferenceId)).toEqual(["a"]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("keeps genuinely different subjects", () => {
|
|
53
|
+
const a = cand({ preferenceId: "a", embedding: [1, 0, 0, 0] });
|
|
54
|
+
const b = cand({ preferenceId: "b", embedding: [0, 1, 0, 0] });
|
|
55
|
+
expect(deconflictBySubject([a, b], 0.85).map((r) => r.preferenceId)).toEqual([
|
|
56
|
+
"a",
|
|
57
|
+
"b",
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it("never folds a candidate with no embedding", () => {
|
|
62
|
+
const a = cand({ preferenceId: "a", embedding: [1, 0, 0, 0] });
|
|
63
|
+
const b = cand({ preferenceId: "b", embedding: null });
|
|
64
|
+
expect(deconflictBySubject([a, b], 0.85).map((r) => r.preferenceId)).toEqual([
|
|
65
|
+
"a",
|
|
66
|
+
"b",
|
|
67
|
+
]);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe("formatStandingRulesBlock", () => {
|
|
72
|
+
it("returns empty string for no rules", () => {
|
|
73
|
+
expect(formatStandingRulesBlock([], INJECTION_RULE_CAP)).toBe("");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("caps the number of rendered rules", () => {
|
|
77
|
+
const rules = Array.from({ length: 20 }, (_, i) => ({
|
|
78
|
+
preferenceId: `p${i}`,
|
|
79
|
+
category: "communication",
|
|
80
|
+
key: `k${i}`,
|
|
81
|
+
value: `rule ${i}`,
|
|
82
|
+
confidence: 1 - i * 0.01,
|
|
83
|
+
observedAt: "2026-07-01T00:00:00Z",
|
|
84
|
+
}));
|
|
85
|
+
const block = formatStandingRulesBlock(rules, 12);
|
|
86
|
+
expect(block).toContain("## Standing rules");
|
|
87
|
+
expect((block.match(/^- /gm) ?? []).length).toBe(12);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
// The one definition of "this account's active, de-conflicted, bounded
|
|
2
|
+
// rule-set" (Task 1486). Consumed by the memory plugin's profile-read
|
|
3
|
+
// (agent-callable) and by the three inbound gateways (per-turn injection). Kept
|
|
4
|
+
// free of neo4j-driver types so both an MCP process and the UI can import the
|
|
5
|
+
// built dist.
|
|
6
|
+
|
|
7
|
+
export const SAME_SUBJECT_COSINE = 0.85;
|
|
8
|
+
export const INJECTION_RULE_CAP = 12;
|
|
9
|
+
export const ACTIVE_CANDIDATE_FLOOR = 0.4;
|
|
10
|
+
export const ACTIVE_CANDIDATE_LIMIT = 30;
|
|
11
|
+
|
|
12
|
+
export interface RuleCandidate {
|
|
13
|
+
preferenceId: string;
|
|
14
|
+
category: string;
|
|
15
|
+
key: string;
|
|
16
|
+
value: string;
|
|
17
|
+
confidence: number;
|
|
18
|
+
observedAt: string;
|
|
19
|
+
embedding: number[] | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ActiveRule {
|
|
23
|
+
preferenceId: string;
|
|
24
|
+
category: string;
|
|
25
|
+
key: string;
|
|
26
|
+
value: string;
|
|
27
|
+
confidence: number;
|
|
28
|
+
observedAt: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function cosine(a: number[], b: number[]): number {
|
|
32
|
+
if (!a.length || !b.length || a.length !== b.length) return 0;
|
|
33
|
+
let dot = 0;
|
|
34
|
+
let na = 0;
|
|
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 {
|
|
75
|
+
if (rules.length === 0) return "";
|
|
76
|
+
const lines = rules.slice(0, cap).map((r) => `- ${r.value}`);
|
|
77
|
+
return `## Standing rules\n${lines.join("\n")}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface MinimalSession {
|
|
81
|
+
run: (
|
|
82
|
+
q: string,
|
|
83
|
+
p: Record<string, unknown>,
|
|
84
|
+
) => Promise<{ records: Array<{ get(k: string): unknown }> }>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// The candidate query: this account's admin-scope preferences, not trashed, not
|
|
88
|
+
// a "does not apply" declination, above the injection floor, ordered so the
|
|
89
|
+
// highest-confidence / most-recent wins de-confliction. Returns `embedding` for
|
|
90
|
+
// the clustering step. The trashed predicate mirrors graph-trash's
|
|
91
|
+
// `notTrashed()` exactly (label + deletedAt) so this lib stays dependency-free.
|
|
92
|
+
export async function queryActiveRuleCandidates(
|
|
93
|
+
session: MinimalSession,
|
|
94
|
+
accountId: string,
|
|
95
|
+
): Promise<RuleCandidate[]> {
|
|
96
|
+
// ACTIVE_CANDIDATE_LIMIT is inlined as a Cypher literal rather than bound as a
|
|
97
|
+
// param: a plain JS number bound to `LIMIT $p` serializes as a FLOAT and Neo4j
|
|
98
|
+
// rejects it ("not a valid value. Must be a non-negative integer"). This lib
|
|
99
|
+
// is dependency-free (no neo4j-driver), so it cannot call `neo4j.int()`; the
|
|
100
|
+
// constant is a hardcoded integer, so inlining it is safe and gate-clean.
|
|
101
|
+
const result = await session.run(
|
|
102
|
+
`
|
|
103
|
+
MATCH (pref:Preference {accountId: $accountId, scope: 'admin'})
|
|
104
|
+
WHERE NOT pref:Trashed AND pref.deletedAt IS NULL
|
|
105
|
+
AND coalesce(pref.notApplicable, false) = false
|
|
106
|
+
AND pref.confidence >= $floor
|
|
107
|
+
RETURN pref.preferenceId AS preferenceId, pref.category AS category,
|
|
108
|
+
pref.key AS key, pref.value AS value, pref.confidence AS confidence,
|
|
109
|
+
pref.observedAt AS observedAt, pref.embedding AS embedding
|
|
110
|
+
ORDER BY pref.confidence DESC, pref.observedAt DESC
|
|
111
|
+
LIMIT ${ACTIVE_CANDIDATE_LIMIT}
|
|
112
|
+
`,
|
|
113
|
+
{ accountId, floor: ACTIVE_CANDIDATE_FLOOR },
|
|
114
|
+
);
|
|
115
|
+
return result.records.map((r) => ({
|
|
116
|
+
preferenceId: r.get("preferenceId") as string,
|
|
117
|
+
category: r.get("category") as string,
|
|
118
|
+
key: r.get("key") as string,
|
|
119
|
+
value: r.get("value") as string,
|
|
120
|
+
confidence: r.get("confidence") as number,
|
|
121
|
+
observedAt: (r.get("observedAt") as string) ?? "",
|
|
122
|
+
embedding: (r.get("embedding") as number[] | null) ?? null,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export async function activeRuleSet(
|
|
127
|
+
session: MinimalSession,
|
|
128
|
+
accountId: string,
|
|
129
|
+
cap: number = INJECTION_RULE_CAP,
|
|
130
|
+
): Promise<ActiveRule[]> {
|
|
131
|
+
const candidates = await queryActiveRuleCandidates(session, accountId);
|
|
132
|
+
return deconflictBySubject(candidates).slice(0, cap);
|
|
133
|
+
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"services/*"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && tsc -p services/whatsapp-channel/tsconfig.json && tsc -p services/webchat-channel/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json",
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/active-rules/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json && tsc -p services/claude-session-manager/tsconfig.json && tsc -p services/whatsapp-channel/tsconfig.json && tsc -p services/webchat-channel/tsconfig.json && tsc -p services/telegram-channel/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/mcp-lifeline/tsconfig.json && tsc -p lib/mcp-eager/tsconfig.json && tsc -p lib/account-enumeration/tsconfig.json && tsc -p lib/account-scope/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/embed-client/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/active-rules/tsconfig.json && tsc -p lib/admin-conversation-purge/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/graph-style/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && tsc -p lib/entitlement/tsconfig.json && tsc -p lib/task-secrets/tsconfig.json && tsc -p lib/admins-write/tsconfig.json && tsc -p lib/admin-access-password/tsconfig.json && tsc -p lib/persistent-components/tsconfig.json && tsc -p lib/require-port-env/tsconfig.json && tsc -p lib/aeo-llms-txt-writer/tsconfig.json && tsc -p lib/obsidian-parser/tsconfig.json",
|
|
11
11
|
"gen:canonical-tools": "node scripts/generate-canonical-tool-names.mjs",
|
|
12
12
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
13
13
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
"build:graph-viewer": "tsc -p plugins/graph-viewer/mcp/tsconfig.json",
|
|
24
24
|
"build:claude-session-manager": "tsc -p services/claude-session-manager/tsconfig.json",
|
|
25
25
|
"build:whatsapp-channel": "tsc -p services/whatsapp-channel/tsconfig.json",
|
|
26
|
-
"build:webchat-channel": "tsc -p services/webchat-channel/tsconfig.json"
|
|
26
|
+
"build:webchat-channel": "tsc -p services/webchat-channel/tsconfig.json",
|
|
27
|
+
"build:telegram-channel": "tsc -p services/telegram-channel/tsconfig.json"
|
|
27
28
|
},
|
|
28
29
|
"overrides": {
|
|
29
30
|
"zod": "3.x"
|
|
@@ -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:055029b708dab719a426f768edcdd3de24d7aafc49219af6f8978495f4f50fe2
|
|
5
5
|
brand: maxy-code
|
|
6
6
|
product-name: Maxy
|
|
7
7
|
---
|
|
@@ -1493,6 +1493,8 @@ Maxy will confirm and store it.
|
|
|
1493
1493
|
|
|
1494
1494
|
Maxy also learns how you work without you having to teach it deliberately. Six broad areas cover the way most operators run a business — communication, scheduling, decisions, workflow, content, and interaction. Inside each area sits a small set of concrete fields (Maxy tracks around 28 in total) such as your preferred channel, quiet hours, workday start time, risk tolerance, content tonality, or address form. Maxy tracks which of these specific fields you have spoken into and which are still empty. While any are empty, it folds one organic question per turn into the conversation aimed at the next gap — never a list, never a form, never the same question twice. If you tell Maxy a field doesn't apply to you ("I work weekends, weekend availability isn't a thing for me"), it marks that field as covered and never re-asks. Once every field is either set or marked not-applicable, the proactive questions stop and Maxy answers what you ask without volunteering more. This is why session 300 should feel sharper than session 3: the longer you work together, the less Maxy needs to ask.
|
|
1495
1495
|
|
|
1496
|
+
The standing rules you give it over a channel are held the same way, and two things keep them working. When you restate a rule in different words, Maxy recognises it as the same rule and updates the one you already set instead of keeping two; a correction wins over the older version rather than sitting next to it. And on every channel message the rules that are currently in force are put back in front of Maxy automatically, so a rule you set last week is applied this week without you repeating it, even after a long conversation. You do not have to re-teach a rule you have already given.
|
|
1497
|
+
|
|
1496
1498
|
## Telling Maxy to Forget Something
|
|
1497
1499
|
|
|
1498
1500
|
Be direct:
|
|
@@ -1993,7 +1995,7 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
|
|
|
1993
1995
|
| Mail search | `mail-search account=<id> query=<trunc-32> count=<N> elapsedMs=<N>` |
|
|
1994
1996
|
| Calendar list | `calendar-list account=<id> rangeDays=<N> count=<N> elapsedMs=<N>` |
|
|
1995
1997
|
| Calendar event | `calendar-event account=<id> eventId=<trunc-12> elapsedMs=<N>` |
|
|
1996
|
-
| Event created | `cal-create-request account=<id> subj=<trunc-64>` then `event-created account=<id> eventId=<trunc-16> status=201
|
|
1998
|
+
| Event created | `cal-create-request account=<id> subj=<trunc-64>` then `event-created account=<id> eventId=<trunc-16> status=201`. A non-201 logs `event-create-failed ... status=<N>` and throws. A 201 whose body carried no readable id logs `event-created-id-unknown account=<id> status=201` and returns a created-but-id-unknown result (the event exists; do not retry) rather than a failure. |
|
|
1997
1999
|
| Event updated | `cal-update-request account=<id> eventId=<trunc-16>` then `event-updated ... status=200` |
|
|
1998
2000
|
| Event cancelled | `cal-cancel-request account=<id> eventId=<trunc-16> mode=<cancel-or-delete>` then `event-cancelled ... status=<N>` |
|
|
1999
2001
|
| Event responded | `cal-respond-request account=<id> eventId=<trunc-16> action=<a>` then `event-responded ... status=202` |
|
|
@@ -3785,6 +3787,16 @@ to a `<previous-context>` block, and threads it through the public spawn body so
|
|
|
3785
3787
|
appends it to the agent's system prompt. Only the visitor's own slice is read; open-mode
|
|
3786
3788
|
visitors, admins, and first visits inject nothing.
|
|
3787
3789
|
|
|
3790
|
+
### Standing-rules reconciliation and per-turn injection
|
|
3791
|
+
|
|
3792
|
+
Standing rules the operator gives over a channel are `Preference` nodes. Two mechanisms keep them effective across a long-lived session that auto-compacts.
|
|
3793
|
+
|
|
3794
|
+
**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.
|
|
3795
|
+
|
|
3796
|
+
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: a bounded, floor-filtered (`confidence >= 0.4`), confidence-ordered candidate query, then semantic de-confliction (one winner per subject at the same 0.85 cosine), capped to `INJECTION_RULE_CAP` (12). `profile-read` exposes this 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`. 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.
|
|
3797
|
+
|
|
3798
|
+
**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>` — `injected=0` on an account with stored rules is the write-only regression re-appearing. `[preference-audit] op=drift accountId=… storedActive=… injectedActive=… duplicateClusters=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose store holds more active rows than injection would surface, the no-event detector for drift.
|
|
3799
|
+
|
|
3788
3800
|
---
|
|
3789
3801
|
|
|
3790
3802
|
## Output Formatting and Budget
|
|
@@ -394,6 +394,16 @@ to a `<previous-context>` block, and threads it through the public spawn body so
|
|
|
394
394
|
appends it to the agent's system prompt. Only the visitor's own slice is read; open-mode
|
|
395
395
|
visitors, admins, and first visits inject nothing.
|
|
396
396
|
|
|
397
|
+
### Standing-rules reconciliation and per-turn injection
|
|
398
|
+
|
|
399
|
+
Standing rules the operator gives over a channel are `Preference` nodes. Two mechanisms keep them effective across a long-lived session that auto-compacts.
|
|
400
|
+
|
|
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
|
+
|
|
403
|
+
**Inject the active rule-set every turn (channel gateways).** The one definition of "this account's active rules" is `@maxy/active-rules`: a bounded, floor-filtered (`confidence >= 0.4`), confidence-ordered candidate query, then semantic de-confliction (one winner per subject at the same 0.85 cosine), capped to `INJECTION_RULE_CAP` (12). `profile-read` exposes this 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`. 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
|
+
|
|
405
|
+
**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>` — `injected=0` on an account with stored rules is the write-only regression re-appearing. `[preference-audit] op=drift accountId=… storedActive=… injectedActive=… duplicateClusters=…` — the standing reconciliation audit (claude-session-manager, 5-minute tick) flags any account whose store holds more active rows than injection would surface, the no-event detector for drift.
|
|
406
|
+
|
|
397
407
|
---
|
|
398
408
|
|
|
399
409
|
## Output Formatting and Budget
|
|
@@ -37,6 +37,8 @@ Just say it naturally:
|
|
|
37
37
|
|
|
38
38
|
{{productName}} also learns how you work without you having to teach it deliberately. Six broad areas cover the way most operators run a business — communication, scheduling, decisions, workflow, content, and interaction. Inside each area sits a small set of concrete fields ({{productName}} tracks around 28 in total) such as your preferred channel, quiet hours, workday start time, risk tolerance, content tonality, or address form. {{productName}} tracks which of these specific fields you have spoken into and which are still empty. While any are empty, it folds one organic question per turn into the conversation aimed at the next gap — never a list, never a form, never the same question twice. If you tell {{productName}} a field doesn't apply to you ("I work weekends, weekend availability isn't a thing for me"), it marks that field as covered and never re-asks. Once every field is either set or marked not-applicable, the proactive questions stop and {{productName}} answers what you ask without volunteering more. This is why session 300 should feel sharper than session 3: the longer you work together, the less {{productName}} needs to ask.
|
|
39
39
|
|
|
40
|
+
The standing rules you give it over a channel are held the same way, and two things keep them working. When you restate a rule in different words, {{productName}} recognises it as the same rule and updates the one you already set instead of keeping two; a correction wins over the older version rather than sitting next to it. And on every channel message the rules that are currently in force are put back in front of {{productName}} automatically, so a rule you set last week is applied this week without you repeating it, even after a long conversation. You do not have to re-teach a rule you have already given.
|
|
41
|
+
|
|
40
42
|
## Telling {{productName}} to Forget Something
|
|
41
43
|
|
|
42
44
|
Be direct:
|
|
@@ -50,7 +50,7 @@ All log lines start with `[outlook-mcp]` and write to `server.log`. They are key
|
|
|
50
50
|
| Mail search | `mail-search account=<id> query=<trunc-32> count=<N> elapsedMs=<N>` |
|
|
51
51
|
| Calendar list | `calendar-list account=<id> rangeDays=<N> count=<N> elapsedMs=<N>` |
|
|
52
52
|
| Calendar event | `calendar-event account=<id> eventId=<trunc-12> elapsedMs=<N>` |
|
|
53
|
-
| Event created | `cal-create-request account=<id> subj=<trunc-64>` then `event-created account=<id> eventId=<trunc-16> status=201
|
|
53
|
+
| Event created | `cal-create-request account=<id> subj=<trunc-64>` then `event-created account=<id> eventId=<trunc-16> status=201`. A non-201 logs `event-create-failed ... status=<N>` and throws. A 201 whose body carried no readable id logs `event-created-id-unknown account=<id> status=201` and returns a created-but-id-unknown result (the event exists; do not retry) rather than a failure. |
|
|
54
54
|
| Event updated | `cal-update-request account=<id> eventId=<trunc-16>` then `event-updated ... status=200` |
|
|
55
55
|
| Event cancelled | `cal-cancel-request account=<id> eventId=<trunc-16> mode=<cancel-or-delete>` then `event-cancelled ... status=<N>` |
|
|
56
56
|
| Event responded | `cal-respond-request account=<id> eventId=<trunc-16> action=<a>` then `event-responded ... status=202` |
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"profile-read-active-rules.test.d.ts","sourceRoot":"","sources":["../../../src/tools/__tests__/profile-read-active-rules.test.ts"],"names":[],"mappings":""}
|