@rubytech/create-sitedesk-code 0.1.530 → 0.1.532

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 (99) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  3. package/payload/platform/lib/graph-write/dist/index.js +4 -0
  4. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  5. package/payload/platform/lib/graph-write/src/index.ts +4 -0
  6. package/payload/platform/lib/learning-constants/dist/index.d.ts +35 -0
  7. package/payload/platform/lib/learning-constants/dist/index.d.ts.map +1 -0
  8. package/payload/platform/lib/learning-constants/dist/index.js +50 -0
  9. package/payload/platform/lib/learning-constants/dist/index.js.map +1 -0
  10. package/payload/platform/lib/learning-constants/package.json +7 -0
  11. package/payload/platform/lib/learning-constants/src/index.test.ts +47 -0
  12. package/payload/platform/lib/learning-constants/src/index.ts +57 -0
  13. package/payload/platform/lib/learning-constants/tsconfig.json +9 -0
  14. package/payload/platform/neo4j/schema.cypher +24 -0
  15. package/payload/platform/package.json +1 -1
  16. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  17. package/payload/platform/plugins/admin/PLUGIN.md +1 -1
  18. package/payload/platform/plugins/admin/mcp/dist/index.js +2 -2
  19. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  20. package/payload/platform/plugins/admin/skills/insight/SKILL.md +1 -1
  21. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +58 -10
  22. package/payload/platform/plugins/admin/skills/whats-new/SKILL.md +12 -0
  23. package/payload/platform/plugins/docs/references/linkedin-extension.md +18 -9
  24. package/payload/platform/plugins/docs/references/session-retrospective.md +39 -0
  25. package/payload/platform/plugins/linkedin-extension/.claude-plugin/plugin.json +1 -1
  26. package/payload/platform/plugins/linkedin-extension/PLUGIN.md +7 -10
  27. package/payload/platform/plugins/linkedin-extension/extension/README.md +20 -17
  28. package/payload/platform/plugins/linkedin-extension/extension/background/sw.js +96 -13
  29. package/payload/platform/plugins/linkedin-extension/extension/manifest.json +3 -18
  30. package/payload/platform/plugins/linkedin-extension/extension/options/options.html +2 -2
  31. package/payload/platform/plugins/linkedin-extension/extension/options/options.js +1 -1
  32. package/payload/platform/plugins/memory/.claude-plugin/plugin.json +1 -1
  33. package/payload/platform/plugins/memory/PLUGIN.md +15 -1
  34. package/payload/platform/plugins/memory/mcp/dist/index.js +169 -0
  35. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  36. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/session-jsonl.test.d.ts +2 -0
  37. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/session-jsonl.test.d.ts.map +1 -0
  38. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/session-jsonl.test.js +95 -0
  39. package/payload/platform/plugins/memory/mcp/dist/lib/__tests__/session-jsonl.test.js.map +1 -0
  40. package/payload/platform/plugins/memory/mcp/dist/lib/session-jsonl.d.ts +60 -0
  41. package/payload/platform/plugins/memory/mcp/dist/lib/session-jsonl.d.ts.map +1 -0
  42. package/payload/platform/plugins/memory/mcp/dist/lib/session-jsonl.js +153 -0
  43. package/payload/platform/plugins/memory/mcp/dist/lib/session-jsonl.js.map +1 -0
  44. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js +6 -4
  45. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/cross-account.test.js.map +1 -1
  46. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-derive.test.d.ts +2 -0
  47. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-derive.test.d.ts.map +1 -0
  48. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-derive.test.js +160 -0
  49. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-derive.test.js.map +1 -0
  50. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-recall.test.d.ts +2 -0
  51. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-recall.test.d.ts.map +1 -0
  52. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-recall.test.js +64 -0
  53. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-recall.test.js.map +1 -0
  54. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-scan.test.d.ts +2 -0
  55. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-scan.test.d.ts.map +1 -0
  56. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-scan.test.js +78 -0
  57. package/payload/platform/plugins/memory/mcp/dist/tools/__tests__/learnings-scan.test.js.map +1 -0
  58. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-derive.d.ts +41 -0
  59. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-derive.d.ts.map +1 -0
  60. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-derive.js +225 -0
  61. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-derive.js.map +1 -0
  62. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-recall.d.ts +31 -0
  63. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-recall.d.ts.map +1 -0
  64. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-recall.js +67 -0
  65. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-recall.js.map +1 -0
  66. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-scan.d.ts +40 -0
  67. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-scan.d.ts.map +1 -0
  68. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-scan.js +55 -0
  69. package/payload/platform/plugins/memory/mcp/dist/tools/learnings-scan.js.map +1 -0
  70. package/payload/platform/plugins/memory/references/schema-base.md +3 -0
  71. package/payload/platform/plugins/memory/skills/learnings-recall/SKILL.md +27 -0
  72. package/payload/platform/plugins/scheduling/PLUGIN.md +1 -1
  73. package/payload/platform/plugins/whatsapp/PLUGIN.md +1 -1
  74. package/payload/platform/plugins/whatsapp/mcp/dist/index.js +5 -5
  75. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  76. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +6 -6
  77. package/payload/platform/plugins/whatsapp/skills/connect-whatsapp/SKILL.md +6 -2
  78. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +6 -2
  79. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  80. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +3 -0
  81. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  82. package/payload/platform/services/claude-session-manager/dist/learning-index.d.ts +28 -0
  83. package/payload/platform/services/claude-session-manager/dist/learning-index.d.ts.map +1 -0
  84. package/payload/platform/services/claude-session-manager/dist/learning-index.js +70 -0
  85. package/payload/platform/services/claude-session-manager/dist/learning-index.js.map +1 -0
  86. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  87. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +18 -0
  88. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  89. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +6 -0
  90. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  91. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +17 -0
  92. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  93. package/payload/platform/templates/specialists/agents/librarian.md +1 -1
  94. package/payload/server/{chunk-2YMGNYA3.js → chunk-UH6GCQO4.js} +78 -38
  95. package/payload/server/{manager-XPRN5UF5.js → manager-H34562SN.js} +5 -1
  96. package/payload/server/server.js +121 -38
  97. package/payload/platform/plugins/linkedin-extension/extension/assets/pill.css +0 -52
  98. package/payload/platform/plugins/linkedin-extension/extension/content/profile.js +0 -100
  99. package/payload/platform/plugins/linkedin-extension/extension/content/thread.js +0 -102
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learnings-scan.test.js","sourceRoot":"","sources":["../../../src/tools/__tests__/learnings-scan.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,41 @@
1
+ /**
2
+ * learnings-derive — turn the turn's per-chunk claims into :Learning writes,
3
+ * then decay what nobody re-observed.
4
+ *
5
+ * Reconcile is the two-tier shape profile-update.ts:708-755 already uses for
6
+ * preferences: embed the claim, take the nearest same-account neighbour over
7
+ * the vector index, convert the normalised index score back to raw cosine, and
8
+ * compare against SAME_SUBJECT_COSINE. The claim's stance is explicit
9
+ * (`reinforce` | `contradict`) exactly as profile-update takes `mode` — the
10
+ * tool never infers a stance from prose.
11
+ *
12
+ * Decay runs here, inside the routine's own pass, NOT at session start.
13
+ * Preference decay fires only inside loadUserProfile (neo4j-store.ts:2704),
14
+ * which means an account nobody opens never decays; this does not inherit that.
15
+ */
16
+ export type ClaimMode = "reinforce" | "contradict";
17
+ export interface Claim {
18
+ title: string;
19
+ body: string;
20
+ mode: ClaimMode;
21
+ }
22
+ export interface LearningsDeriveParams {
23
+ accountId: string;
24
+ sessionId: string;
25
+ claims: Claim[];
26
+ /** Run the decay pass after the claims land. The overnight routine sets it. */
27
+ runDecay?: boolean;
28
+ }
29
+ export interface DecaySummary {
30
+ examined: number;
31
+ decayed: number;
32
+ floored: number;
33
+ }
34
+ export interface LearningsDeriveResult {
35
+ created: number;
36
+ amplified: number;
37
+ relegated: number;
38
+ decay: DecaySummary | null;
39
+ }
40
+ export declare function learningsDerive(params: LearningsDeriveParams): Promise<LearningsDeriveResult>;
41
+ //# sourceMappingURL=learnings-derive.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learnings-derive.d.ts","sourceRoot":"","sources":["../../src/tools/learnings-derive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAaH,MAAM,MAAM,SAAS,GAAG,WAAW,GAAG,YAAY,CAAC;AAGnD,MAAM,WAAW,KAAK;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,SAAS,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,KAAK,EAAE,CAAC;IAChB,+EAA+E;IAC/E,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,YAAY,GAAG,IAAI,CAAC;CAC5B;AAkMD,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CA8DhC"}
@@ -0,0 +1,225 @@
1
+ /**
2
+ * learnings-derive — turn the turn's per-chunk claims into :Learning writes,
3
+ * then decay what nobody re-observed.
4
+ *
5
+ * Reconcile is the two-tier shape profile-update.ts:708-755 already uses for
6
+ * preferences: embed the claim, take the nearest same-account neighbour over
7
+ * the vector index, convert the normalised index score back to raw cosine, and
8
+ * compare against SAME_SUBJECT_COSINE. The claim's stance is explicit
9
+ * (`reinforce` | `contradict`) exactly as profile-update takes `mode` — the
10
+ * tool never infers a stance from prose.
11
+ *
12
+ * Decay runs here, inside the routine's own pass, NOT at session start.
13
+ * Preference decay fires only inside loadUserProfile (neo4j-store.ts:2704),
14
+ * which means an account nobody opens never decays; this does not inherit that.
15
+ */
16
+ import { randomUUID } from "node:crypto";
17
+ import { getSession } from "../lib/neo4j.js";
18
+ import { embed } from "../lib/embeddings.js";
19
+ import { SAME_SUBJECT_COSINE } from "../../../../../lib/active-rules/dist/index.js";
20
+ import { LEARNING_CONTRADICTION_DECREMENT, LEARNING_INITIAL_CONFIDENCE, LEARNING_REINFORCEMENT_INCREMENT, decayedConfidence, } from "../../../../../lib/learning-constants/dist/index.js";
21
+ const VALID_MODES = new Set(["reinforce", "contradict"]);
22
+ async function nearestNeighbour(accountId, embedding) {
23
+ const session = getSession();
24
+ try {
25
+ // Over-fetch 25 then filter to this account: the vector index cannot
26
+ // pre-filter by accountId, and a house install co-resides many
27
+ // sub-accounts' learnings in one index, so a small topK could let another
28
+ // account's rows crowd out this account's real same-subject neighbour.
29
+ const res = await session.run(`
30
+ CALL db.index.vector.queryNodes('learning_embedding', 25, $embedding)
31
+ YIELD node AS l, score
32
+ WHERE l.accountId = $accountId
33
+ RETURN l.learningId AS learningId, l.title AS title,
34
+ l.confidence AS confidence, score
35
+ ORDER BY score DESC
36
+ LIMIT 1
37
+ `, { embedding, accountId });
38
+ const r = res.records[0];
39
+ if (!r)
40
+ return null;
41
+ return {
42
+ learningId: r.get("learningId"),
43
+ title: r.get("title"),
44
+ confidence: r.get("confidence"),
45
+ // Neo4j's cosine vector index returns a NORMALIZED score (1 + cosine)/2,
46
+ // not raw cosine. Comparing the raw score directly would fire at raw
47
+ // cosine ~0.70 and over-merge distinct learnings.
48
+ similarity: 2 * r.get("score") - 1,
49
+ };
50
+ }
51
+ finally {
52
+ await session.close();
53
+ }
54
+ }
55
+ async function createLearning(accountId, sessionId, claim, embedding, nowIso) {
56
+ const session = getSession();
57
+ try {
58
+ // The :Learning is CREATEd unconditionally and the parent edge is attached
59
+ // only if a UserProfile exists. Neither of the obvious shapes is safe:
60
+ // `MATCH (p:UserProfile …) CREATE …` silently no-ops the whole write when
61
+ // the profile is absent (the hazard profile-update.ts:309-311 documents for
62
+ // preferences), and `MERGE (p:UserProfile { accountId })` would mint a
63
+ // profile missing its required `userId` (schema-base.md:32). Account scope
64
+ // is the `accountId` property, not the edge, so the edge is a convenience
65
+ // and its absence costs nothing.
66
+ //
67
+ // `embedding` is omitted entirely rather than written as [] when the
68
+ // embedder returned nothing. :Learning is vector-indexed, and a zero-length
69
+ // vector makes the node permanently invisible to the neighbour query — it
70
+ // could never be amplified, so every re-observation would mint a duplicate.
71
+ // That is exactly the rising-total/flat-amplified signature, so it is
72
+ // logged rather than left to be inferred.
73
+ await session.run(`
74
+ CREATE (l:Learning {
75
+ accountId: $accountId,
76
+ learningId: $learningId,
77
+ title: $title,
78
+ body: $body,
79
+ confidence: $confidence,
80
+ observedAt: $observedAt,
81
+ observationCount: 1,
82
+ sourceSessionIds: [$sessionId]
83
+ })
84
+ WITH l
85
+ FOREACH (_ IN CASE WHEN $embedding IS NULL THEN [] ELSE [1] END |
86
+ SET l.embedding = $embedding
87
+ )
88
+ WITH l
89
+ OPTIONAL MATCH (p:UserProfile { accountId: $accountId })
90
+ FOREACH (_ IN CASE WHEN p IS NULL THEN [] ELSE [1] END |
91
+ CREATE (p)-[:HAS_LEARNING]->(l)
92
+ )
93
+ `, {
94
+ accountId,
95
+ learningId: randomUUID(),
96
+ title: claim.title,
97
+ body: claim.body,
98
+ confidence: LEARNING_INITIAL_CONFIDENCE,
99
+ observedAt: nowIso,
100
+ sessionId,
101
+ embedding,
102
+ });
103
+ if (embedding === null) {
104
+ process.stderr.write(`[learnings] op=derive-embed-missing accountId=${accountId} ` +
105
+ `title=${JSON.stringify(claim.title)} reason=embedder-returned-nothing\n`);
106
+ }
107
+ }
108
+ finally {
109
+ await session.close();
110
+ }
111
+ }
112
+ async function applyToExisting(accountId, sessionId, match, mode, nowIso) {
113
+ const confidence = mode === "reinforce"
114
+ ? match.confidence + LEARNING_REINFORCEMENT_INCREMENT * (1 - match.confidence)
115
+ : Math.max(0, match.confidence - LEARNING_CONTRADICTION_DECREMENT);
116
+ const contradictedClause = mode === "contradict" ? ",\n l.contradictedAt = $observedAt" : "";
117
+ const session = getSession();
118
+ try {
119
+ await session.run(`
120
+ MATCH (l:Learning { accountId: $accountId, learningId: $learningId })
121
+ SET l.confidence = $confidence,
122
+ l.observedAt = $observedAt,
123
+ l.observationCount = coalesce(l.observationCount, 0) + 1,
124
+ l.sourceSessionIds =
125
+ CASE WHEN $sessionId IN coalesce(l.sourceSessionIds, [])
126
+ THEN l.sourceSessionIds
127
+ ELSE coalesce(l.sourceSessionIds, []) + $sessionId END${contradictedClause}
128
+ `, {
129
+ accountId,
130
+ learningId: match.learningId,
131
+ confidence,
132
+ observedAt: nowIso,
133
+ sessionId,
134
+ });
135
+ }
136
+ finally {
137
+ await session.close();
138
+ }
139
+ return confidence;
140
+ }
141
+ async function runDecayPass(accountId, now) {
142
+ const session = getSession();
143
+ try {
144
+ // observedAt is an ISO STRING. Comparing it to datetime() in the predicate
145
+ // yields null for EVERY row, so the pass would report 0 decayed behind an
146
+ // otherwise healthy log line. The curve is applied in JS instead, off the
147
+ // same pure function the tests and the injection read use.
148
+ const res = await session.run(`
149
+ MATCH (l:Learning { accountId: $accountId })
150
+ RETURN l.learningId AS learningId, l.confidence AS confidence,
151
+ l.observedAt AS observedAt
152
+ `, { accountId });
153
+ let decayed = 0;
154
+ let floored = 0;
155
+ for (const r of res.records) {
156
+ const learningId = r.get("learningId");
157
+ const before = r.get("confidence");
158
+ const observedAt = String(r.get("observedAt"));
159
+ const after = decayedConfidence(before, observedAt, now);
160
+ if (after === before)
161
+ continue;
162
+ decayed += 1;
163
+ if (after === 0)
164
+ floored += 1;
165
+ await session.run(`MATCH (l:Learning { accountId: $accountId, learningId: $learningId })
166
+ SET l.confidence = $confidence`, { accountId, learningId, confidence: after });
167
+ }
168
+ return { examined: res.records.length, decayed, floored };
169
+ }
170
+ finally {
171
+ await session.close();
172
+ }
173
+ }
174
+ export async function learningsDerive(params) {
175
+ const { accountId, sessionId, claims } = params;
176
+ if (!accountId)
177
+ throw new Error("learnings-derive: accountId is required");
178
+ if (!sessionId)
179
+ throw new Error("learnings-derive: sessionId is required");
180
+ // Validate every claim before writing any of them, so a bad mode halfway
181
+ // down the list cannot leave a half-applied batch behind.
182
+ for (const c of claims) {
183
+ if (!VALID_MODES.has(c.mode)) {
184
+ throw new Error(`learnings-derive: Invalid mode "${c.mode}". Must be one of: reinforce, contradict`);
185
+ }
186
+ }
187
+ const now = new Date();
188
+ const nowIso = now.toISOString();
189
+ let created = 0;
190
+ let amplified = 0;
191
+ let relegated = 0;
192
+ for (const claim of claims) {
193
+ const embedding = await embed(`${claim.title}\n${claim.body}`);
194
+ const match = embedding ? await nearestNeighbour(accountId, embedding) : null;
195
+ if (match && match.similarity >= SAME_SUBJECT_COSINE) {
196
+ const after = await applyToExisting(accountId, sessionId, match, claim.mode, nowIso);
197
+ const action = claim.mode === "reinforce" ? "amplify" : "relegate";
198
+ if (claim.mode === "reinforce")
199
+ amplified += 1;
200
+ else
201
+ relegated += 1;
202
+ process.stderr.write(`[learnings] op=derive accountId=${accountId} action=${action} ` +
203
+ `learningId=${match.learningId} similarity=${match.similarity.toFixed(3)} ` +
204
+ `confBefore=${match.confidence} confAfter=${after}\n`);
205
+ }
206
+ else {
207
+ await createLearning(accountId, sessionId, claim, embedding ?? null, nowIso);
208
+ created += 1;
209
+ process.stderr.write(`[learnings] op=derive accountId=${accountId} action=create ` +
210
+ `title=${JSON.stringify(claim.title)} confidence=${LEARNING_INITIAL_CONFIDENCE}\n`);
211
+ }
212
+ }
213
+ // Sustained amplified=0 against a rising total is the duplicate signature —
214
+ // the match path never firing and every claim minting a new node.
215
+ process.stderr.write(`[learnings] op=derive-summary accountId=${accountId} created=${created} ` +
216
+ `amplified=${amplified} relegated=${relegated}\n`);
217
+ let decay = null;
218
+ if (params.runDecay === true) {
219
+ decay = await runDecayPass(accountId, now);
220
+ process.stderr.write(`[learnings] op=decay accountId=${accountId} examined=${decay.examined} ` +
221
+ `decayed=${decay.decayed} floored=${decay.floored}\n`);
222
+ }
223
+ return { created, amplified, relegated, decay };
224
+ }
225
+ //# sourceMappingURL=learnings-derive.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learnings-derive.js","sourceRoot":"","sources":["../../src/tools/learnings-derive.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * learnings-recall — the only path from an injected learning title to its body.
3
+ *
4
+ * The system prompt carries titles and never bodies (Task 2220). When a turn
5
+ * matches an indexed title, the learnings-recall skill tells the agent to call
6
+ * this. The log line lives here, not in the skill: a SKILL.md is prose loaded
7
+ * into a turn and writes nothing to stderr, so without this tool the
8
+ * "index injected but never used" failure would be unobservable — and that
9
+ * measurement is what decides whether MAX_INDEXED_LEARNINGS should fall.
10
+ */
11
+ export interface RecallRow {
12
+ learningId: string;
13
+ title: string;
14
+ body: string;
15
+ confidence: number;
16
+ observedAt: string;
17
+ observationCount: number;
18
+ /** Set when a later session disputed this learning. Surfaced so the agent
19
+ * can say so rather than acting on it as settled. */
20
+ contradictedAt: string | null;
21
+ }
22
+ export interface LearningsRecallParams {
23
+ accountId: string;
24
+ /** Case-insensitive substring match against title. */
25
+ title?: string;
26
+ learningId?: string;
27
+ }
28
+ export declare function learningsRecall(params: LearningsRecallParams): Promise<{
29
+ rows: RecallRow[];
30
+ }>;
31
+ //# sourceMappingURL=learnings-recall.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learnings-recall.d.ts","sourceRoot":"","sources":["../../src/tools/learnings-recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAIH,MAAM,WAAW,SAAS;IACxB,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,gBAAgB,EAAE,MAAM,CAAC;IACzB;0DACsD;IACtD,cAAc,EAAE,MAAM,GAAG,IAAI,CAAC;CAC/B;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,sDAAsD;IACtD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAID,wBAAsB,eAAe,CACnC,MAAM,EAAE,qBAAqB,GAC5B,OAAO,CAAC;IAAE,IAAI,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC,CA+DhC"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * learnings-recall — the only path from an injected learning title to its body.
3
+ *
4
+ * The system prompt carries titles and never bodies (Task 2220). When a turn
5
+ * matches an indexed title, the learnings-recall skill tells the agent to call
6
+ * this. The log line lives here, not in the skill: a SKILL.md is prose loaded
7
+ * into a turn and writes nothing to stderr, so without this tool the
8
+ * "index injected but never used" failure would be unobservable — and that
9
+ * measurement is what decides whether MAX_INDEXED_LEARNINGS should fall.
10
+ */
11
+ import { getSession } from "../lib/neo4j.js";
12
+ const MAX_ROWS = 5;
13
+ export async function learningsRecall(params) {
14
+ const { accountId, title, learningId } = params;
15
+ if (!accountId)
16
+ throw new Error("learnings-recall: accountId is required");
17
+ if (!title && !learningId) {
18
+ throw new Error("learnings-recall: title or learningId is required");
19
+ }
20
+ const session = getSession();
21
+ try {
22
+ const res = await session.run(`
23
+ MATCH (l:Learning { accountId: $accountId })
24
+ WHERE ($learningId IS NULL OR l.learningId = $learningId)
25
+ AND ($title IS NULL OR toLower(l.title) CONTAINS toLower($title))
26
+ RETURN l.learningId AS learningId, l.title AS title, l.body AS body,
27
+ l.confidence AS confidence, l.observedAt AS observedAt,
28
+ coalesce(l.observationCount, 0) AS observationCount,
29
+ l.contradictedAt AS contradictedAt
30
+ ORDER BY l.confidence DESC
31
+ LIMIT toInteger($limit)
32
+ `, {
33
+ accountId,
34
+ title: title ?? null,
35
+ learningId: learningId ?? null,
36
+ limit: MAX_ROWS,
37
+ });
38
+ const rows = res.records.map((r) => {
39
+ const contradictedAt = r.get("contradictedAt");
40
+ return {
41
+ learningId: r.get("learningId"),
42
+ title: r.get("title"),
43
+ body: r.get("body"),
44
+ confidence: r.get("confidence"),
45
+ observedAt: String(r.get("observedAt")),
46
+ observationCount: Number(r.get("observationCount")),
47
+ contradictedAt: contradictedAt === null || contradictedAt === undefined
48
+ ? null
49
+ : String(contradictedAt),
50
+ };
51
+ });
52
+ for (const row of rows) {
53
+ process.stderr.write(`[learnings] op=recall accountId=${accountId} learningId=${row.learningId} ` +
54
+ `matchedTitle=${JSON.stringify(row.title)}\n`);
55
+ }
56
+ // A miss is still an invocation. Without this line, a week of zero recalls
57
+ // could not be told apart from a week of recalls that all missed.
58
+ if (rows.length === 0) {
59
+ process.stderr.write(`[learnings] op=recall accountId=${accountId} learningId=- matchedTitle=- rows=0\n`);
60
+ }
61
+ return { rows };
62
+ }
63
+ finally {
64
+ await session.close();
65
+ }
66
+ }
67
+ //# sourceMappingURL=learnings-recall.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learnings-recall.js","sourceRoot":"","sources":["../../src/tools/learnings-recall.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAqB7C,MAAM,QAAQ,GAAG,CAAC,CAAC;AAEnB,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,MAA6B;IAE7B,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,UAAU,EAAE,GAAG,MAAM,CAAC;IAChD,IAAI,CAAC,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,yCAAyC,CAAC,CAAC;IAC3E,IAAI,CAAC,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;QAC1B,MAAM,IAAI,KAAK,CAAC,mDAAmD,CAAC,CAAC;IACvE,CAAC;IAED,MAAM,OAAO,GAAG,UAAU,EAAE,CAAC;IAC7B,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,CAC3B;;;;;;;;;;OAUC,EACD;YACE,SAAS;YACT,KAAK,EAAE,KAAK,IAAI,IAAI;YACpB,UAAU,EAAE,UAAU,IAAI,IAAI;YAC9B,KAAK,EAAE,QAAQ;SAChB,CACF,CAAC;QAEF,MAAM,IAAI,GAAgB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE;YAC9C,MAAM,cAAc,GAAG,CAAC,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YAC/C,OAAO;gBACL,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,CAAW;gBACzC,KAAK,EAAE,CAAC,CAAC,GAAG,CAAC,OAAO,CAAW;gBAC/B,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,MAAM,CAAW;gBAC7B,UAAU,EAAE,CAAC,CAAC,GAAG,CAAC,YAAY,CAAW;gBACzC,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC;gBACvC,gBAAgB,EAAE,MAAM,CAAC,CAAC,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;gBACnD,cAAc,EACZ,cAAc,KAAK,IAAI,IAAI,cAAc,KAAK,SAAS;oBACrD,CAAC,CAAC,IAAI;oBACN,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC;aAC7B,CAAC;QACJ,CAAC,CAAC,CAAC;QAEH,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;YACvB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,mCAAmC,SAAS,eAAe,GAAG,CAAC,UAAU,GAAG;gBAC1E,gBAAgB,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAChD,CAAC;QACJ,CAAC;QACD,2EAA2E;QAC3E,kEAAkE;QAClE,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,mCAAmC,SAAS,uCAAuC,CACpF,CAAC;QACJ,CAAC;QAED,OAAO,EAAE,IAAI,EAAE,CAAC;IAClB,CAAC;YAAS,CAAC;QACT,MAAM,OAAO,CAAC,KAAK,EAAE,CAAC;IACxB,CAAC;AACH,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * learnings-scan — page session transcripts to the caller's turn.
3
+ *
4
+ * Mirrors `conversation-archive-list-chunks` in shape: a read-only pager that
5
+ * hands bodies to the agent, which emits per-chunk claims in-turn and passes
6
+ * them to `learnings-derive`. The difference is the source — disk, not Neo4j.
7
+ *
8
+ * Counter contract (Task 2220 observability item 1): `candidateFiles` is the
9
+ * on-disk .jsonl count BEFORE filtering. candidateFiles>0 with sessions=0 is a
10
+ * broken sinceIso comparison; both zero on an install with known-closed
11
+ * sessions is a broken enumerator path. Without the pre-filter count the two
12
+ * are indistinguishable and both read as a quiet night.
13
+ */
14
+ export interface LearningsScanParams {
15
+ accountId: string;
16
+ /** Only sessions whose last write is strictly after this. Omit for all. */
17
+ sinceIso?: string;
18
+ sessionOffset?: number;
19
+ sessionLimit?: number;
20
+ /** Zero-based chunk within each returned session. Default 0. */
21
+ chunkIndex?: number;
22
+ /** Turns per chunk (1..200). Default 40. */
23
+ chunkTurns?: number;
24
+ }
25
+ export interface ScanRow {
26
+ sessionId: string;
27
+ lastWriteIso: string;
28
+ chunkIndex: number;
29
+ totalChunks: number;
30
+ body: string;
31
+ }
32
+ export interface LearningsScanResult {
33
+ candidateFiles: number;
34
+ sessions: number;
35
+ skipped: number;
36
+ sinceIso: string | null;
37
+ rows: ScanRow[];
38
+ }
39
+ export declare function learningsScan(params: LearningsScanParams): Promise<LearningsScanResult>;
40
+ //# sourceMappingURL=learnings-scan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learnings-scan.d.ts","sourceRoot":"","sources":["../../src/tools/learnings-scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAeH,MAAM,WAAW,mBAAmB;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,2EAA2E;IAC3E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,4CAA4C;IAC5C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,mBAAmB;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,IAAI,EAAE,OAAO,EAAE,CAAC;CACjB;AAED,wBAAsB,aAAa,CACjC,MAAM,EAAE,mBAAmB,GAC1B,OAAO,CAAC,mBAAmB,CAAC,CAsD9B"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * learnings-scan — page session transcripts to the caller's turn.
3
+ *
4
+ * Mirrors `conversation-archive-list-chunks` in shape: a read-only pager that
5
+ * hands bodies to the agent, which emits per-chunk claims in-turn and passes
6
+ * them to `learnings-derive`. The difference is the source — disk, not Neo4j.
7
+ *
8
+ * Counter contract (Task 2220 observability item 1): `candidateFiles` is the
9
+ * on-disk .jsonl count BEFORE filtering. candidateFiles>0 with sessions=0 is a
10
+ * broken sinceIso comparison; both zero on an install with known-closed
11
+ * sessions is a broken enumerator path. Without the pre-filter count the two
12
+ * are indistinguishable and both read as a quiet night.
13
+ */
14
+ import { chunkTranscript, claudeProjectsRoot, enumerateSessionJsonls, readSidecarAccountId, sessionLastWriteIso, } from "../lib/session-jsonl.js";
15
+ const DEFAULT_SESSION_LIMIT = 5;
16
+ const MAX_SESSION_LIMIT = 25;
17
+ const DEFAULT_CHUNK_TURNS = 40;
18
+ const MAX_CHUNK_TURNS = 200;
19
+ export async function learningsScan(params) {
20
+ const { accountId } = params;
21
+ if (!accountId)
22
+ throw new Error("learnings-scan: accountId is required");
23
+ const sinceIso = params.sinceIso ?? null;
24
+ const sinceMs = sinceIso === null ? null : Date.parse(sinceIso);
25
+ if (sinceMs !== null && Number.isNaN(sinceMs)) {
26
+ // Fail rather than fall through to "scan everything": a malformed
27
+ // watermark that silently widened the scan would re-process every session
28
+ // and amplify every learning toward 1.0.
29
+ throw new Error(`learnings-scan: sinceIso is not a valid ISO timestamp: ${sinceIso}`);
30
+ }
31
+ const sessionLimit = Math.min(Math.max(1, params.sessionLimit ?? DEFAULT_SESSION_LIMIT), MAX_SESSION_LIMIT);
32
+ const sessionOffset = Math.max(0, params.sessionOffset ?? 0);
33
+ const chunkTurns = Math.min(Math.max(1, params.chunkTurns ?? DEFAULT_CHUNK_TURNS), MAX_CHUNK_TURNS);
34
+ const chunkIndex = Math.max(0, params.chunkIndex ?? 0);
35
+ // Throws when CLAUDE_CONFIG_DIR is absent. Never defaulted.
36
+ const projectsRoot = claudeProjectsRoot();
37
+ const { files, candidateFiles, skipped } = enumerateSessionJsonls(projectsRoot);
38
+ const eligible = files
39
+ .filter((f) => !f.isSubagent && !f.archived)
40
+ .filter((f) => readSidecarAccountId(f.path) === accountId)
41
+ .map((f) => ({ file: f, lastWriteIso: sessionLastWriteIso(f.path) }))
42
+ .filter((e) => e.lastWriteIso !== null)
43
+ .filter((e) => sinceMs === null || Date.parse(e.lastWriteIso) > sinceMs)
44
+ .sort((a, b) => a.lastWriteIso.localeCompare(b.lastWriteIso));
45
+ const page = eligible.slice(sessionOffset, sessionOffset + sessionLimit);
46
+ const rows = page.map(({ file, lastWriteIso }) => {
47
+ const { body, totalChunks } = chunkTranscript(file.path, chunkIndex, chunkTurns);
48
+ return { sessionId: file.sessionId, lastWriteIso, chunkIndex, totalChunks, body };
49
+ });
50
+ process.stderr.write(`[learnings] op=scan accountId=${accountId} sinceIso=${sinceIso ?? "-"} ` +
51
+ `candidateFiles=${candidateFiles} sessions=${eligible.length} skipped=${skipped} ` +
52
+ `returned=${rows.length}\n`);
53
+ return { candidateFiles, sessions: eligible.length, skipped, sinceIso, rows };
54
+ }
55
+ //# sourceMappingURL=learnings-scan.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"learnings-scan.js","sourceRoot":"","sources":["../../src/tools/learnings-scan.ts"],"names":[],"mappings":"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"}
@@ -30,6 +30,7 @@ When loading this reference, confirm which schema files were consulted by noting
30
30
  | Address | `PostalAddress` | `schema:PostalAddress` | — | `streetAddress`, `addressLocality`, `postalCode` |
31
31
  | Task | `Task` | `schema:Action` (semantics) | — | `accountId`, `taskId`, `name`, `status`, `priority` |
32
32
  | UserProfile | `UserProfile` | platform-native | — | `accountId`, `userId` |
33
+ | Learning | `Learning` | platform-native | — | `accountId`, `learningId`, `title`, `body`, `confidence`, `observedAt`, `observationCount`, `sourceSessionIds` |
33
34
  | Lead | `Lead` | `cdm:Lead` | CDM only | `accountId`, `subject`, `customer` (id), `status` |
34
35
  | Case | `Case` | `cdm:Case` | CDM only | `accountId`, `title`, `customer` (id), `status`, `priority` |
35
36
  | OpeningHours | `OpeningHoursSpecification` | `schema:OpeningHoursSpecification` | Schema.org JSON-LD | `accountId`, `dayOfWeek`, `opens`, `closes` |
@@ -66,6 +67,8 @@ When loading this reference, confirm which schema files were consulted by noting
66
67
 
67
68
  **DefinedTerm** covers reusable concepts, learnings, and glossary entries — e.g. project retrospective learnings (`name` is a short handle, `description` is the full learning). Additional properties like `category` are encouraged but not required.
68
69
 
70
+ **Learning** is not a DefinedTerm and the two must not be conflated. A `DefinedTerm` is a glossary entry: it is written once, carries no confidence, never decays, and is retrieved by search. A `Learning` is a claim with a lifecycle — it is created at `0.5`, amplified each time a later session re-observes it, decremented and stamped `contradictedAt` when a session disputes it, and decayed linearly once nobody has re-observed it for fourteen days. Write a `DefinedTerm` when the operator is defining what a thing *is*; write a `Learning` when a session established that something *holds*, and you expect future sessions to confirm or dispute it. A `Learning` hangs off the account's `UserProfile` by `(:UserProfile)-[:HAS_LEARNING]->(:Learning)`, the same parenting `Preference` uses. `confidence` is a float in `[0,1]`; `observedAt` is an ISO string; `observationCount` is the number of distinct sessions that produced the claim; `sourceSessionIds` is the list of those session ids. `contradictedAt` is optional and stamped only when a later claim contradicted the learning. Only the `title` reaches a system prompt; the `body` is fetched on demand through the `learnings-recall` skill.
71
+
69
72
  **Position** carries a single role held by a `UserProfile` at an `Organization` — its title, dates, and per-role properties (e.g. achievement bullets in `description`, `endDate` for a finished role). Each role is its own node so it can carry its own structure; multiple Positions per UserProfile (career history); one Organization may host many Positions across many UserProfiles. Written by the `document-ingest` skill when a CV section classifies as `Position`. The natural pattern is `(:UserProfile)-[:HAS_POSITION]->(:Position)-[:AT]->(:Organization)` — never set the employer name as a property on Position; always create the AT edge to the (MERGEd) Organization.
70
73
 
71
74
  **`authorshipMode` on content labels.** Every `:KnowledgeDocument`, `:ConversationArchive` (every chat transcript parent, including email threads under `source:'email'`), `:Message`, `:SocialPost`, and `:Section` write carries an `authorshipMode` enum drawn from `human-only | human-led-agent-assisted | agent-led-human-reviewed | agent-only | unknown`. The value records who actually wrote the content body, not who triggered the write. `human-only` means every word of the body came from the operator; `human-led-agent-assisted` is operator-authored with light agent polish (the operator chose every claim and structure); `agent-led-human-reviewed` is agent-drafted then operator-edited before send; `agent-only` is pure agent output the operator did not touch. The voice-mirror distillation walks the corpus where `authorshipMode IN ['human-only', 'human-led-agent-assisted']` — both modes carry voice signal. `:ConversationArchive` `:Section` chunks (WhatsApp / Telegram / Signal / Granola / Otter / Circleback exports ingested under `(:ConversationArchive)-[:HAS_SECTION]->(:Section)`) are first-class voice corpus and the highest-leverage signal source because chat captures the unguarded voice in a way email rarely does. Legacy nodes default to `unknown` until reclassified via the voice-mirror backfill flow. The per-label authorshipMode index on `:Section` covers transcript chunks without a secondary label. Writers that cannot determine the mode (e.g. bulk archive imports) write `unknown` rather than guessing.
@@ -0,0 +1,27 @@
1
+ ---
2
+ name: learnings-recall
3
+ description: "Fetch the full text of a learning whose title is already in your system prompt. Activate when the current turn is about a topic named in the learnings index, when you are about to repeat work a past session already resolved, or when the operator says 'what did we learn about X', 'have we hit this before', 'did we solve this already', or 'check the learnings'."
4
+ ---
5
+
6
+ # Learnings recall
7
+
8
+ Your system prompt carries a list of learning **titles**, not their text. A
9
+ title is a handle, not the knowledge. When the turn in front of you is about
10
+ one of those titles, fetch the body before acting.
11
+
12
+ Call `learnings-recall` with the title as written in the index, or with a
13
+ `learningId` if you have one. It returns the full body, the confidence, when it
14
+ was last observed, how many sessions produced it, and whether a later session
15
+ disputed it.
16
+
17
+ Judge what comes back. A learning at low confidence, or one carrying a
18
+ `contradictedAt` stamp, is evidence that something was once believed and later
19
+ disputed — say so rather than acting on it as settled. A learning observed
20
+ across many sessions is stronger than one seen once, and `observationCount`
21
+ tells you which you have.
22
+
23
+ Do not call this speculatively on every turn. The index exists so you can tell,
24
+ cheaply, whether a body is worth fetching. Fetching a body nobody needed is the
25
+ cost the title/body split was built to avoid.
26
+
27
+ If the turn matches no indexed title, this skill has nothing to add. Carry on.
@@ -262,7 +262,7 @@ An `action` invokes one pre-encoded MCP tool with no reasoning. When a due event
262
262
  - `destination` — the E.164 phone (WhatsApp) or chat id (Telegram) to run the turn for and reply to. It **must already be a registered admin or account-manager** on that channel; a destination that is not is rejected when the event is created. For WhatsApp the authoritative admin/manager lists are read from the socket-owning **house** account (never the event's own account), and the destination's registered account must match the account that created the schedule: an admin belongs to the house, a manager to its bound sub-account. The house/owner account may target any registered admin or manager; a sub-account is locked to its own manager(s). A refusal names the true reason — `not-registered` (in no house list) or `cross-account` (registered to a different account, naming both accounts).
263
263
  - `prompt` — the instruction the agent runs.
264
264
 
265
- An event carries **either** an `action` **or** an `agentDispatch`, never both — setting one clears the other. Agent dispatch is stored as `agentChannel`, `agentDestination`, `agentPrompt`. Use `schedule-update` with `agentDispatch` to modify or `agentDispatch: null` to clear. On WhatsApp, a destination bound as an account-manager scopes the spawned session to its sub-account; on Telegram the session runs on the house account.
265
+ An event carries **either** an `action` **or** an `agentDispatch`, never both — setting one clears the other. Agent dispatch is stored as `agentChannel`, `agentDestination`, `agentPrompt`. Use `schedule-update` with `agentDispatch` to modify or `agentDispatch: null` to clear. On WhatsApp, a destination bound as an account-manager scopes the spawned session to its sub-account — unless that same phone is also on the house `adminPhones` list, in which case it scopes to the channel account, because the dispatch rides the house socket and so arrives from the house number. On Telegram the session runs on the house account.
266
266
 
267
267
  A WhatsApp `agentDispatch` destination may therefore be an account manager, not only an admin phone — this is the supported way to send a manager a proactive or scheduled WhatsApp message. The reply is delivered over the house's paired line regardless of which sub-account the session is scoped to; socket ownership never restricts which number the dispatch can reach.
268
268
 
@@ -106,7 +106,7 @@ A native admin channel session runs a per-sender `whatsapp-channel` MCP server (
106
106
 
107
107
  ## Agent-initiated admin send
108
108
 
109
- `whatsapp-send-admin` is the one outbound path that does **not** require a bound inbound conversation, so a webchat session or a scheduled run can push a text summary over WhatsApp. It is hard-gated to the cold-send-ban carve-out: the destination must already be a **registered recipient** — on the house `whatsapp.adminPhones` list (managed via `whatsapp-config add-admin-phone`) **or** a registered account manager in `whatsapp.accountManagers` (managed via `whatsapp-config add-account-manager`). Both lists are read from the socket-owning house account (`resolveAccount`), and the send always rides the house's paired socket. A destination in neither list is refused with `[whatsapp:outbound] op=send-refused reason=not-admin-phone` and no send is attempted — never a cold first contact. An **account-manager** phone (in `accountManagers`, disjoint from `adminPhones`) **is** a valid `send-admin` destination regardless of its binding mode: the operator registered it, so it is not a cold stranger. `passive` governs the manager's **inbound** behaviour only (no auto-reply, intake-only) and never gates this outbound send. A successful send logs its admit basis (`op=send-admin-ok via=admin-phone|account-manager`). Delivery reuses `sendTextMessage`, so the sent id is tracked for echo suppression and the send is recorded as outbound activity. Text only; for files use the bound `reply-document` surface.
109
+ `whatsapp-send-admin` is the one outbound path that does **not** require a bound inbound conversation, so a webchat session or a scheduled run can push a text summary over WhatsApp. It is hard-gated to the cold-send-ban carve-out: the destination must already be a **registered recipient** — on the house `whatsapp.adminPhones` list (managed via `whatsapp-config add-admin-phone`) **or** a registered account manager in `whatsapp.accountManagers` (managed via `whatsapp-config add-account-manager`). Both lists are read from the socket-owning house account (`resolveAccount`), and the send always rides the house's paired socket. A destination in neither list is refused with `[whatsapp:outbound] op=send-refused reason=not-admin-phone` and no send is attempted — never a cold first contact. An **account-manager** phone (in `accountManagers`, which may overlap `adminPhones` — a phone can hold both roles, and the number it messaged decides its inbound scope) **is** a valid `send-admin` destination regardless of its binding mode: the operator registered it, so it is not a cold stranger. `passive` governs the manager's **inbound** behaviour only (no auto-reply, intake-only) and never gates this outbound send. A successful send logs its admit basis (`op=send-admin-ok via=admin-phone|account-manager`). Delivery reuses `sendTextMessage`, so the sent id is tracked for echo suppression and the send is recorded as outbound activity. Text only; for files use the bound `reply-document` surface.
110
110
 
111
111
  ## Self-chat command lane
112
112