@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.
Files changed (128) hide show
  1. package/dist/__tests__/brand-port-uniqueness.test.js +74 -0
  2. package/dist/known-brands.js +1 -0
  3. package/package.json +1 -1
  4. package/payload/platform/config/brand-registry.json +8 -0
  5. package/payload/platform/lib/active-rules/dist/index.d.ts +35 -0
  6. package/payload/platform/lib/active-rules/dist/index.d.ts.map +1 -0
  7. package/payload/platform/lib/active-rules/dist/index.js +94 -0
  8. package/payload/platform/lib/active-rules/dist/index.js.map +1 -0
  9. package/payload/platform/lib/active-rules/package.json +7 -0
  10. package/payload/platform/lib/active-rules/src/index.test.ts +89 -0
  11. package/payload/platform/lib/active-rules/src/index.ts +133 -0
  12. package/payload/platform/lib/active-rules/tsconfig.json +9 -0
  13. package/payload/platform/package.json +4 -3
  14. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +14 -2
  15. package/payload/platform/plugins/docs/references/internals.md +10 -0
  16. package/payload/platform/plugins/docs/references/memory-guide.md +2 -0
  17. package/payload/platform/plugins/docs/references/outlook-guide.md +1 -1
  18. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.d.ts +2 -0
  19. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.d.ts.map +1 -0
  20. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js +49 -0
  21. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-read-active-rules.test.js.map +1 -0
  22. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.d.ts +2 -0
  23. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.d.ts.map +1 -0
  24. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.js +99 -0
  25. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/profile-update-reconcile.test.js.map +1 -0
  26. package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.d.ts +3 -0
  27. package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.d.ts.map +1 -1
  28. package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js +11 -3
  29. package/payload/platform/plugins/memory/mcp/dist/tools/profile-read.js.map +1 -1
  30. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts +21 -0
  31. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +106 -14
  33. package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js.map +1 -1
  34. package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js +24 -0
  35. package/payload/platform/plugins/outlook/mcp/dist/__tests__/calendar-write.test.js.map +1 -1
  36. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.d.ts +15 -2
  37. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.d.ts.map +1 -1
  38. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.js +16 -7
  39. package/payload/platform/plugins/outlook/mcp/dist/tools/calendar-create.js.map +1 -1
  40. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +10 -5
  41. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  42. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +9 -1
  43. package/payload/platform/scripts/__tests__/provision-role-stamp.test.sh +3 -0
  44. package/payload/platform/scripts/lib/provision-account-dir.sh +9 -1
  45. package/payload/platform/services/claude-session-manager/dist/agent-identity-locator.d.ts +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/agent-identity-locator.d.ts.map +1 -1
  47. package/payload/platform/services/claude-session-manager/dist/agent-identity-locator.js +5 -1
  48. package/payload/platform/services/claude-session-manager/dist/agent-identity-locator.js.map +1 -1
  49. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts +1 -0
  50. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  51. package/payload/platform/services/claude-session-manager/dist/http-server.js +29 -2
  52. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  53. package/payload/platform/services/claude-session-manager/dist/index.js +14 -0
  54. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  55. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts +46 -0
  56. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.d.ts.map +1 -0
  57. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js +96 -0
  58. package/payload/platform/services/claude-session-manager/dist/preference-reconciliation-audit.js.map +1 -0
  59. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts +33 -0
  60. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  61. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +54 -5
  62. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  63. package/payload/platform/services/claude-session-manager/dist/public-agent-reachability.d.ts.map +1 -1
  64. package/payload/platform/services/claude-session-manager/dist/public-agent-reachability.js +5 -1
  65. package/payload/platform/services/claude-session-manager/dist/public-agent-reachability.js.map +1 -1
  66. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +6 -0
  67. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  68. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +20 -0
  69. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  70. package/payload/platform/services/telegram-channel/dist/instructions.d.ts +9 -0
  71. package/payload/platform/services/telegram-channel/dist/instructions.d.ts.map +1 -0
  72. package/payload/platform/services/telegram-channel/dist/instructions.js +100 -0
  73. package/payload/platform/services/telegram-channel/dist/instructions.js.map +1 -0
  74. package/payload/platform/services/telegram-channel/dist/notification.d.ts +93 -0
  75. package/payload/platform/services/telegram-channel/dist/notification.d.ts.map +1 -0
  76. package/payload/platform/services/telegram-channel/dist/notification.js +119 -0
  77. package/payload/platform/services/telegram-channel/dist/notification.js.map +1 -0
  78. package/payload/platform/services/telegram-channel/dist/server.d.ts +40 -0
  79. package/payload/platform/services/telegram-channel/dist/server.d.ts.map +1 -0
  80. package/payload/platform/services/telegram-channel/dist/server.js +291 -0
  81. package/payload/platform/services/telegram-channel/dist/server.js.map +1 -0
  82. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts +28 -0
  83. package/payload/platform/services/telegram-channel/dist/turn-follow.d.ts.map +1 -0
  84. package/payload/platform/services/telegram-channel/dist/turn-follow.js +188 -0
  85. package/payload/platform/services/telegram-channel/dist/turn-follow.js.map +1 -0
  86. package/payload/platform/services/webchat-channel/dist/notification.d.ts +5 -0
  87. package/payload/platform/services/webchat-channel/dist/notification.d.ts.map +1 -1
  88. package/payload/platform/services/webchat-channel/dist/notification.js +6 -1
  89. package/payload/platform/services/webchat-channel/dist/notification.js.map +1 -1
  90. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts +6 -0
  91. package/payload/platform/services/whatsapp-channel/dist/notification.d.ts.map +1 -1
  92. package/payload/platform/services/whatsapp-channel/dist/notification.js +8 -1
  93. package/payload/platform/services/whatsapp-channel/dist/notification.js.map +1 -1
  94. package/payload/platform/templates/agents/passive/IDENTITY.md +21 -0
  95. package/payload/server/public/assets/{AdminLoginScreens-CBTx7ZPK.js → AdminLoginScreens-B1PkOF66.js} +1 -1
  96. package/payload/server/public/assets/AdminShell-D8wNArGQ.js +2 -0
  97. package/payload/server/public/assets/{Checkbox-BHTxekRB.js → Checkbox-Bm9Er726.js} +1 -1
  98. package/payload/server/public/assets/{Transcript-5kLbBInN.js → Transcript-CBIyQ8T5.js} +1 -1
  99. package/payload/server/public/assets/{admin-BWCLE32N.js → admin-CWM9G_yA.js} +1 -1
  100. package/payload/server/public/assets/{browser-CRvCPsvV.js → browser-D8QhNffV.js} +1 -1
  101. package/payload/server/public/assets/{calendar-BI-kd-QP.js → calendar-qob5K4t_.js} +1 -1
  102. package/payload/server/public/assets/chat-BNrxVUF6.js +1 -0
  103. package/payload/server/public/assets/chevron-left-BQThFVrd.js +1 -0
  104. package/payload/server/public/assets/data-C-EqNbRi.js +1 -0
  105. package/payload/server/public/assets/{graph-BFSyW_5i.js → graph-DZ2HMpIB.js} +1 -1
  106. package/payload/server/public/assets/{graph-labels-DEjD_qJh.js → graph-labels-BFG4bjXQ.js} +1 -1
  107. package/payload/server/public/assets/{maximize-2-BcMJNeCB.js → maximize-2-DkZ40lSl.js} +1 -1
  108. package/payload/server/public/assets/{operator-Dtvm9ero.js → operator-FrXrSsBj.js} +1 -1
  109. package/payload/server/public/assets/page-CHWMI9Tm.js +32 -0
  110. package/payload/server/public/assets/{page-TXi9dmaB.js → page-CXKV01gT.js} +1 -1
  111. package/payload/server/public/assets/{public-DBx3U0r8.js → public-2p1pJ4WF.js} +1 -1
  112. package/payload/server/public/assets/{rotate-ccw-8ti3QkeY.js → rotate-ccw-D3o_bMBA.js} +1 -1
  113. package/payload/server/public/assets/useSubAccountSwitcher-8dLvF3Ux.js +9 -0
  114. package/payload/server/public/browser.html +3 -3
  115. package/payload/server/public/calendar.html +4 -4
  116. package/payload/server/public/chat.html +8 -8
  117. package/payload/server/public/data.html +7 -7
  118. package/payload/server/public/graph.html +8 -8
  119. package/payload/server/public/index.html +10 -10
  120. package/payload/server/public/operator.html +10 -10
  121. package/payload/server/public/public.html +8 -8
  122. package/payload/server/server.js +237 -38
  123. package/payload/server/public/assets/AdminShell-DPHUeXea.js +0 -2
  124. package/payload/server/public/assets/chat-DFy8hb1y.js +0 -1
  125. package/payload/server/public/assets/chevron-left-Bggm5B5U.js +0 -1
  126. package/payload/server/public/assets/data-HvZ69Ya1.js +0 -1
  127. package/payload/server/public/assets/page-C_pm3hl3.js +0 -32
  128. 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
+ });
@@ -18,6 +18,7 @@ export const KNOWN_BRAND_HOSTNAMES = [
18
18
  "realagent",
19
19
  "realagent-code",
20
20
  "sitedesk-code",
21
+ "property-administrators",
21
22
  "maxy-2",
22
23
  "maxy-3",
23
24
  "maxy-4",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.417",
3
+ "version": "0.1.419",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -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":"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"}
@@ -0,0 +1,7 @@
1
+ {
2
+ "name": "@maxy/active-rules",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "commonjs",
6
+ "main": "dist/index.js"
7
+ }
@@ -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
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "dist",
5
+ "rootDir": "src"
6
+ },
7
+ "include": ["src"],
8
+ "exclude": ["src/**/*.test.ts"]
9
+ }
@@ -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:c17392a180e59049412359b379894e93bb588c79b83553554e3b8f1b80dff94d
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` (or `event-create-failed ... status=<N>`) |
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` (or `event-create-failed ... status=<N>`) |
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=profile-read-active-rules.test.d.ts.map
@@ -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":""}