@rubytech/create-sitedesk-code 0.1.530 → 0.1.531

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 (97) 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 +7 -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/whatsapp/mcp/dist/index.js +4 -4
  73. package/payload/platform/plugins/whatsapp/mcp/dist/index.js.map +1 -1
  74. package/payload/platform/plugins/whatsapp/references/channels-whatsapp.md +3 -3
  75. package/payload/platform/plugins/whatsapp/skills/connect-whatsapp/SKILL.md +6 -2
  76. package/payload/platform/plugins/whatsapp/skills/manage-whatsapp-config/SKILL.md +5 -1
  77. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.d.ts.map +1 -1
  78. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js +3 -0
  79. package/payload/platform/services/claude-session-manager/dist/canonical-tool-names.generated.js.map +1 -1
  80. package/payload/platform/services/claude-session-manager/dist/learning-index.d.ts +28 -0
  81. package/payload/platform/services/claude-session-manager/dist/learning-index.d.ts.map +1 -0
  82. package/payload/platform/services/claude-session-manager/dist/learning-index.js +70 -0
  83. package/payload/platform/services/claude-session-manager/dist/learning-index.js.map +1 -0
  84. package/payload/platform/services/claude-session-manager/dist/pty-spawner.d.ts.map +1 -1
  85. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js +18 -0
  86. package/payload/platform/services/claude-session-manager/dist/pty-spawner.js.map +1 -1
  87. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts +6 -0
  88. package/payload/platform/services/claude-session-manager/dist/system-prompt.d.ts.map +1 -1
  89. package/payload/platform/services/claude-session-manager/dist/system-prompt.js +17 -0
  90. package/payload/platform/services/claude-session-manager/dist/system-prompt.js.map +1 -1
  91. package/payload/platform/templates/specialists/agents/librarian.md +1 -1
  92. package/payload/server/{chunk-2YMGNYA3.js → chunk-SVB5NZ64.js} +41 -30
  93. package/payload/server/{manager-XPRN5UF5.js → manager-4Q3DTLXO.js} +5 -1
  94. package/payload/server/server.js +65 -17
  95. package/payload/platform/plugins/linkedin-extension/extension/assets/pill.css +0 -52
  96. package/payload/platform/plugins/linkedin-extension/extension/content/profile.js +0 -100
  97. package/payload/platform/plugins/linkedin-extension/extension/content/thread.js +0 -102
@@ -13,7 +13,7 @@ insight surface, and it is called only when the operator asks for it.
13
13
 
14
14
  Run the four passes now over the session so far, then keep going in this same session. Do NOT call `session-reset`, do NOT clear the context, do NOT end the session: the conversation continues afterward and the operator (or a parallel session) keeps working against the same live state. Use the tools you already have.
15
15
 
16
- Pass 1 — Technical learnings. Walk this session for operator corrections and self-detected mistakes. For each one, delegate one graph write to `database-operator` via the Task tool, persisting a Learning or Correction node per the schema. Count what you write.
16
+ Pass 1 — Technical learnings. Walk this session for operator corrections and self-detected mistakes. For each one, delegate one graph write to `database-operator` via the Task tool, persisting a Learning node per the schema. Count what you write.
17
17
 
18
18
  Pass 2 — Operator-relationship updates. Walk this session for tonal signals, rejected phrasings, and working-style preferences. For each one worth carrying forward, Write the observation into `agents/admin/SOUL.md`, the account-scoped identity path the spawn composer reads. That path is the only identity file; never write a bare `SOUL.md` at the account root, which the composer never reads and the schema guard blocks. Count what you write.
19
19
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: platform-architecture
3
3
  description: Use when grounding any documented-surface claim about what SiteDesk 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:26053f4ab631d9ac1c16f933b73f9399ff975b2b3e80b9e14b792ea182e929d3
4
+ content-hash: sha256:10987c045e406cec5ecac35344dbae665a350a08b55ef996d2691be4bff0605f
5
5
  brand: sitedesk-code
6
6
  product-name: SiteDesk
7
7
  ---
@@ -2855,13 +2855,16 @@ Capture a LinkedIn profile or DM thread to your SiteDesk graph with one click. T
2855
2855
  6. Paste two values:
2856
2856
  - **Admin host** — your tunnel URL, e.g. `https://your-tunnel.example.com`.
2857
2857
  - **Ingest token** — ask your SiteDesk agent for the ingest token. It mints one on request and hands it to you in the chat. The same value keeps working: it survives restarts and new chat sessions, and it does not expire. This is not a conversation session key, and not the admin browser's `session_key`; the route accepts neither.
2858
- 7. Save. The pill is now armed.
2858
+ 7. Save. Pin the **SiteDesk LinkedIn Ingest** button in the Chrome toolbar so it is one click away.
2859
2859
 
2860
2860
  ## Use
2861
2861
 
2862
- - **Profile** open any `https://www.linkedin.com/in/<slug>/` page. An **Add to SiteDesk** pill appears in the top section. Click it. Within a few seconds the pill turns green: the profile is in your graph.
2863
- - **DM thread** — open any `https://www.linkedin.com/messaging/thread/<id>/` conversation. The same pill appears. Click it; the full transcript is captured plus the participants and any explicit commitments (meetings booked, actions promised, prices discussed).
2864
- - **Re-click** — the pill is idempotent. Re-clicking the same URL refreshes the document body and regenerates the summary; identities and entities `MERGE` rather than duplicate.
2862
+ Everything happens from the toolbar button. There is no button on the LinkedIn page itself, and that is deliberate: an extension that changes the page before LinkedIn's React finishes loading makes React throw the whole page away and rebuild it. This one only reads, and only when you click it.
2863
+
2864
+ - **Profile** — open any `https://www.linkedin.com/in/<slug>/` page and click the toolbar button. Within a few seconds the badge shows a green tick: the profile is in your graph.
2865
+ - **DM thread** — open any `https://www.linkedin.com/messaging/thread/<id>/` conversation and click the same button. The full transcript is captured plus the participants and any explicit commitments (meetings booked, actions promised, prices discussed).
2866
+ - **Any other page** — the badge shows a grey `?` and nothing is sent. Open a profile or a thread first.
2867
+ - **Re-click** — capture is idempotent. Re-clicking the same URL refreshes the document body and regenerates the summary; identities and entities `MERGE` rather than duplicate.
2865
2868
 
2866
2869
  ## What lands in the graph
2867
2870
 
@@ -2877,14 +2880,20 @@ Soft signals ("interested in chatting", "would love to compare notes") stay in t
2877
2880
 
2878
2881
  The plugin will never create `:Communication`, `:ConversationArchive` rows (i.e. KnowledgeDocument nodes that carry `conversationIdentity`), or `:Message` nodes — those shapes are reserved for other flows (live chat, archive ingest).
2879
2882
 
2880
- ## When the pill turns amber
2883
+ ## When the badge turns amber
2884
+
2885
+ Hover the button for the full sentence.
2881
2886
 
2882
- The pill shows **Paste SiteDesk token** when the ingest token is missing or was refused. Click it to open the options page, ask your SiteDesk agent for the ingest token, paste it, and save. The next click on the LinkedIn pill will succeed. There is no sign-in step: the token is the whole credential, and the only way it stops working is if someone rotates it.
2887
+ - **"rejected the ingest token"** the token is missing or was refused. Open the options page, ask your SiteDesk agent for the ingest token, paste it, and save. There is no sign-in step: the token is the whole credential, and the only way it stops working is if someone rotates it.
2888
+ - **"never granted"** — the admin host was never allowed, so the request never left the browser. Open options, Save, and allow the Chrome prompt.
2883
2889
 
2884
- ## When the pill turns red
2890
+ ## When the badge turns red
2885
2891
 
2886
- - **Missing: ...** — LinkedIn shipped a DOM change and the extractor cannot find a required field. Open a console tab on the LinkedIn page and check the `[linkedin-ext-scrape]` log line for the field names. Drop a ticket pointing at the affected selector; the [`SKILL.md`](../../plugins/linkedin-extension/skills/linkedin-extension/SKILL.md) lists the selector table and the steps for adding a fallback.
2887
- - **Capture failed** — the admin reached, but the request did not complete cleanly. Check the admin logs (`journalctl -u maxy.service | grep linkedin-ingest`). The `[linkedin-ingest-route]` lines name the reason (`schema`, `dispatch-failed`).
2892
+ - **"could not read this page"** — LinkedIn shipped a DOM change and the extractor cannot find a required field. The tooltip names the missing fields, and the service-worker console carries the same names on the `op=scrape` line. Drop a ticket pointing at the affected selector; the [`SKILL.md`](../../plugins/linkedin-extension/skills/linkedin-extension/SKILL.md) lists the selector table and the steps for adding a fallback.
2893
+ - **"did not answer"** — the host was allowed but nothing responded. Check the tunnel and the admin server.
2894
+ - **"unexpected status"** — the admin was reached but the request did not complete cleanly. Check the admin logs with `grep linkedin-ingest ~/.<brand>/logs/server.log`; the `[linkedin-ingest-auth]` and `[linkedin-ingest-route]` lines name the reason (`token-unknown`, `schema`, `dispatch-failed`).
2895
+
2896
+ To read the extension's own side, open `chrome://extensions`, find SiteDesk LinkedIn Ingest, and click **service worker**. The `op=scrape`, `op=permission`, `op=fetch` and `op=state` lines are one capture end to end.
2888
2897
 
2889
2898
  ## Related plugins
2890
2899
 
@@ -3055,6 +3064,45 @@ The pass is one or two messages from the agent, then a short summary of what was
3055
3064
 
3056
3065
  You can tell the pass ran correctly by the final reply naming the five counts (learnings, tonal observations, graph updates, typed edges accepted, prose nodes scanned). The typed-edge pass is scoped by "what changed since the last completed insight pass," so two passes in a row never re-process the same nodes, and a session that never runs one simply defers its extraction to the next time you type `/insight`.
3057
3066
 
3067
+ # Learnings: how they accumulate, fade, and come back
3068
+
3069
+ An `/insight` pass writes learnings. Until now that is all that happened to
3070
+ them: they landed in the graph and nothing ever read them again. A learning is
3071
+ now a thing with a life.
3072
+
3073
+ **A learning carries a confidence.** It starts at 0.5. Every time a later
3074
+ session establishes the same thing again, the confidence moves most of the way
3075
+ toward certain without ever reaching it: `c + 0.15 × (1 − c)`, so 0.5 becomes
3076
+ 0.575, then 0.64, and so on. When a session disputes it, 0.3 comes straight off
3077
+ and the learning is stamped with the date it was contradicted. If nobody
3078
+ mentions it for fourteen days it starts fading, at 0.05 a day, until it reaches
3079
+ zero.
3080
+
3081
+ That means the brain forgets on its own. Something established once and never
3082
+ seen again drops out of sight in about a month; something confirmed every week
3083
+ stays near the top forever. You do not have to prune anything by hand.
3084
+
3085
+ **Your session prompt carries titles, not text.** At the start of a session the
3086
+ agent is given the *titles* of the learnings currently above 0.4 confidence,
3087
+ capped at thirty, and nothing more. A title is a handle. When the conversation
3088
+ turns to something one of those titles names, the agent fetches the full text
3089
+ then, and only then. That is deliberate: injecting thirty full learnings into
3090
+ every session would cost a great deal of prompt for text that mostly goes
3091
+ unread.
3092
+
3093
+ **When a learning has been disputed, the agent says so.** The recall also
3094
+ returns how many separate sessions produced the learning and whether one of
3095
+ them later contradicted it, so the agent can tell you "we established this
3096
+ once, and then later decided otherwise" rather than repeating a stale
3097
+ conclusion as settled fact.
3098
+
3099
+ **What is not built yet.** Nothing runs this on a schedule. The extraction
3100
+ tools, the confidence curve, the fading and the session-start injection are all
3101
+ in place and work when driven by hand, but the overnight pass that walks every
3102
+ session closed since the previous night is a separate piece of work, filed as
3103
+ its own task and waiting on the routine-descriptions work to land first. Until
3104
+ then, `/insight` remains the way learnings get written.
3105
+
3058
3106
  ---
3059
3107
  # Visitor Graph
3060
3108
  Source: https://docs.getmaxy.com/visitor-graph.md
@@ -9,6 +9,13 @@ Invoked by the admin agent directly.
9
9
 
10
10
  This is the platform's release timeline, newest first. Each entry shows the date it shipped and the version it shipped in, so you can tell the operator how current their install is. To compare, read the installed version from `capabilities-here` and match it against the versions below. Keep answers high level and in plain English; this is a summary, not a full commit log.
11
11
 
12
+ ## 2026-07-30 (0.1.531)
13
+
14
+ - Each WhatsApp connection is now recorded against the account it actually belongs to, rather than every connection being filed under the main account. A standing check reports any connection whose account has drifted, and an account created since the last restart is no longer mistaken for the main one.
15
+ - Setting up WhatsApp for a sub-account is now reachable. Asking about a connection without naming an account now means your own account rather than the main one, the status view is no longer limited to the main account, and a sub-account line can run alongside the main one.
16
+ - The assistant now learns from past sessions. It draws lessons out of earlier conversations, lets ones that stop being reconfirmed fade away, and brings only the relevant ones into a new conversation.
17
+ - Capturing a page from the LinkedIn extension now happens from the toolbar button, replacing the floating marker that could be wiped away when the page redrew.
18
+
12
19
  ## 2026-07-30 (0.1.530)
13
20
 
14
21
  - Security fix: the WhatsApp connection library has been moved to a patched release, closing a critical flaw that allowed messages to be faked. Installs on the previous version should update.
@@ -12,13 +12,16 @@ Capture a LinkedIn profile or DM thread to your SiteDesk graph with one click. T
12
12
  6. Paste two values:
13
13
  - **Admin host** — your tunnel URL, e.g. `https://your-tunnel.example.com`.
14
14
  - **Ingest token** — ask your SiteDesk agent for the ingest token. It mints one on request and hands it to you in the chat. The same value keeps working: it survives restarts and new chat sessions, and it does not expire. This is not a conversation session key, and not the admin browser's `session_key`; the route accepts neither.
15
- 7. Save. The pill is now armed.
15
+ 7. Save. Pin the **SiteDesk LinkedIn Ingest** button in the Chrome toolbar so it is one click away.
16
16
 
17
17
  ## Use
18
18
 
19
- - **Profile** open any `https://www.linkedin.com/in/<slug>/` page. An **Add to SiteDesk** pill appears in the top section. Click it. Within a few seconds the pill turns green: the profile is in your graph.
20
- - **DM thread** — open any `https://www.linkedin.com/messaging/thread/<id>/` conversation. The same pill appears. Click it; the full transcript is captured plus the participants and any explicit commitments (meetings booked, actions promised, prices discussed).
21
- - **Re-click** — the pill is idempotent. Re-clicking the same URL refreshes the document body and regenerates the summary; identities and entities `MERGE` rather than duplicate.
19
+ Everything happens from the toolbar button. There is no button on the LinkedIn page itself, and that is deliberate: an extension that changes the page before LinkedIn's React finishes loading makes React throw the whole page away and rebuild it. This one only reads, and only when you click it.
20
+
21
+ - **Profile** — open any `https://www.linkedin.com/in/<slug>/` page and click the toolbar button. Within a few seconds the badge shows a green tick: the profile is in your graph.
22
+ - **DM thread** — open any `https://www.linkedin.com/messaging/thread/<id>/` conversation and click the same button. The full transcript is captured plus the participants and any explicit commitments (meetings booked, actions promised, prices discussed).
23
+ - **Any other page** — the badge shows a grey `?` and nothing is sent. Open a profile or a thread first.
24
+ - **Re-click** — capture is idempotent. Re-clicking the same URL refreshes the document body and regenerates the summary; identities and entities `MERGE` rather than duplicate.
22
25
 
23
26
  ## What lands in the graph
24
27
 
@@ -34,14 +37,20 @@ Soft signals ("interested in chatting", "would love to compare notes") stay in t
34
37
 
35
38
  The plugin will never create `:Communication`, `:ConversationArchive` rows (i.e. KnowledgeDocument nodes that carry `conversationIdentity`), or `:Message` nodes — those shapes are reserved for other flows (live chat, archive ingest).
36
39
 
37
- ## When the pill turns amber
40
+ ## When the badge turns amber
41
+
42
+ Hover the button for the full sentence.
43
+
44
+ - **"rejected the ingest token"** — the token is missing or was refused. Open the options page, ask your SiteDesk agent for the ingest token, paste it, and save. There is no sign-in step: the token is the whole credential, and the only way it stops working is if someone rotates it.
45
+ - **"never granted"** — the admin host was never allowed, so the request never left the browser. Open options, Save, and allow the Chrome prompt.
38
46
 
39
- The pill shows **Paste SiteDesk token** when the ingest token is missing or was refused. Click it to open the options page, ask your SiteDesk agent for the ingest token, paste it, and save. The next click on the LinkedIn pill will succeed. There is no sign-in step: the token is the whole credential, and the only way it stops working is if someone rotates it.
47
+ ## When the badge turns red
40
48
 
41
- ## When the pill turns red
49
+ - **"could not read this page"** — LinkedIn shipped a DOM change and the extractor cannot find a required field. The tooltip names the missing fields, and the service-worker console carries the same names on the `op=scrape` line. Drop a ticket pointing at the affected selector; the [`SKILL.md`](../../plugins/linkedin-extension/skills/linkedin-extension/SKILL.md) lists the selector table and the steps for adding a fallback.
50
+ - **"did not answer"** — the host was allowed but nothing responded. Check the tunnel and the admin server.
51
+ - **"unexpected status"** — the admin was reached but the request did not complete cleanly. Check the admin logs with `grep linkedin-ingest ~/.<brand>/logs/server.log`; the `[linkedin-ingest-auth]` and `[linkedin-ingest-route]` lines name the reason (`token-unknown`, `schema`, `dispatch-failed`).
42
52
 
43
- - **Missing: ...** LinkedIn shipped a DOM change and the extractor cannot find a required field. Open a console tab on the LinkedIn page and check the `[linkedin-ext-scrape]` log line for the field names. Drop a ticket pointing at the affected selector; the [`SKILL.md`](../../plugins/linkedin-extension/skills/linkedin-extension/SKILL.md) lists the selector table and the steps for adding a fallback.
44
- - **Capture failed** — the admin reached, but the request did not complete cleanly. Check the admin logs (`journalctl -u maxy.service | grep linkedin-ingest`). The `[linkedin-ingest-route]` lines name the reason (`schema`, `dispatch-failed`).
53
+ To read the extension's own side, open `chrome://extensions`, find SiteDesk LinkedIn Ingest, and click **service worker**. The `op=scrape`, `op=permission`, `op=fetch` and `op=state` lines are one capture end to end.
45
54
 
46
55
  ## Related plugins
47
56
 
@@ -12,3 +12,42 @@ You can run it whenever you want the brain brought up to date — for example be
12
12
  The pass is one or two messages from the agent, then a short summary of what was written. It runs inside the session you are already in: nothing happens in the background, no second session is spawned, and the session is not closed, reset, or cleared — the conversation continues straight afterward against the same live state. The typed-edge pass itself is delegated to the `database-operator` specialist so the writes land where graph writes are supposed to live.
13
13
 
14
14
  You can tell the pass ran correctly by the final reply naming the five counts (learnings, tonal observations, graph updates, typed edges accepted, prose nodes scanned). The typed-edge pass is scoped by "what changed since the last completed insight pass," so two passes in a row never re-process the same nodes, and a session that never runs one simply defers its extraction to the next time you type `/insight`.
15
+
16
+ # Learnings: how they accumulate, fade, and come back
17
+
18
+ An `/insight` pass writes learnings. Until now that is all that happened to
19
+ them: they landed in the graph and nothing ever read them again. A learning is
20
+ now a thing with a life.
21
+
22
+ **A learning carries a confidence.** It starts at 0.5. Every time a later
23
+ session establishes the same thing again, the confidence moves most of the way
24
+ toward certain without ever reaching it: `c + 0.15 × (1 − c)`, so 0.5 becomes
25
+ 0.575, then 0.64, and so on. When a session disputes it, 0.3 comes straight off
26
+ and the learning is stamped with the date it was contradicted. If nobody
27
+ mentions it for fourteen days it starts fading, at 0.05 a day, until it reaches
28
+ zero.
29
+
30
+ That means the brain forgets on its own. Something established once and never
31
+ seen again drops out of sight in about a month; something confirmed every week
32
+ stays near the top forever. You do not have to prune anything by hand.
33
+
34
+ **Your session prompt carries titles, not text.** At the start of a session the
35
+ agent is given the *titles* of the learnings currently above 0.4 confidence,
36
+ capped at thirty, and nothing more. A title is a handle. When the conversation
37
+ turns to something one of those titles names, the agent fetches the full text
38
+ then, and only then. That is deliberate: injecting thirty full learnings into
39
+ every session would cost a great deal of prompt for text that mostly goes
40
+ unread.
41
+
42
+ **When a learning has been disputed, the agent says so.** The recall also
43
+ returns how many separate sessions produced the learning and whether one of
44
+ them later contradicted it, so the agent can tell you "we established this
45
+ once, and then later decided otherwise" rather than repeating a stale
46
+ conclusion as settled fact.
47
+
48
+ **What is not built yet.** Nothing runs this on a schedule. The extraction
49
+ tools, the confidence curve, the fading and the session-start injection are all
50
+ in place and work when driven by hand, but the overnight pass that walks every
51
+ session closed since the previous night is a separate piece of work, filed as
52
+ its own task and waiting on the routine-descriptions work to land first. Until
53
+ then, `/insight` remains the way learnings get written.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "linkedin-extension",
3
- "description": "One-click LinkedIn ingest. Ships a manifest-v3 Chrome extension that injects an Add-to-SiteDesk pill into `linkedin.com/in/*` profiles and `linkedin.com/messaging/thread/*` DM threads. Each click scrapes the page DOM, POSTs to the admin `linkedin-ingest` route, which dispatches `database-operator` against the generic `document-ingest` skill. The plugin is the **first consumer** of the generic communication-ingest path; every future communication surface (email, Telegram, WhatsApp, group chats, voice memos) plugs into the same backend. Opt-in per brand.",
3
+ "description": "One-click LinkedIn ingest. Ships a manifest-v3 Chrome extension whose toolbar button captures the current `linkedin.com/in/*` profile or `linkedin.com/messaging/thread/*` DM thread. A click injects the extractors into that tab under `activeTab`, reads the page DOM without writing to it, POSTs to the admin `linkedin-ingest` route, which dispatches `database-operator` against the generic `document-ingest` skill. The plugin is the **first consumer** of the generic communication-ingest path; every future communication surface (email, Telegram, WhatsApp, group chats, voice memos) plugs into the same backend. Opt-in per brand.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: linkedin-extension
3
- description: "One-click LinkedIn ingest. Ships a manifest-v3 Chrome extension that injects an Add-to-SiteDesk pill into `linkedin.com/in/*` profiles and `linkedin.com/messaging/thread/*` DM threads. Each click scrapes the page DOM, POSTs to the admin `linkedin-ingest` route, which dispatches `database-operator` against the generic `document-ingest` skill. The plugin is the **first consumer** of the generic communication-ingest path; every future communication surface (email, Telegram, WhatsApp, group chats, voice memos) plugs into the same backend. Opt-in per brand."
3
+ description: "One-click LinkedIn ingest. Ships a manifest-v3 Chrome extension whose toolbar button captures the current `linkedin.com/in/*` profile or `linkedin.com/messaging/thread/*` DM thread. A click injects the extractors into that tab under `activeTab`, reads the page DOM without writing to it, POSTs to the admin `linkedin-ingest` route, which dispatches `database-operator` against the generic `document-ingest` skill. The plugin is the **first consumer** of the generic communication-ingest path; every future communication surface (email, Telegram, WhatsApp, group chats, voice memos) plugs into the same backend. Opt-in per brand."
4
4
  tools: []
5
5
  always: false
6
6
  embed: false
@@ -26,10 +26,10 @@ Re-clicking the same URL refreshes the KD body and regenerates the summary. Pers
26
26
  ```
27
27
  Chrome extension Hono admin Specialist
28
28
  ───────────────── ────────── ──────────
29
- content/profile.js ──POST──> /api/admin/linkedin-ingest ──> database-operator
30
- content/thread.js requireAdminSession document-ingest skill
31
- schema validate memory-ingest tools
32
- spawn database-operator graph writes
29
+ toolbar click /api/admin/linkedin-ingest ──> database-operator
30
+ inject extractors.js requireIngestToken document-ingest skill
31
+ read the page ──POST──> schema validate memory-ingest tools
32
+ badge + tooltip spawn database-operator graph writes
33
33
  ```
34
34
 
35
35
  The route's job is auth + schema + dispatch. Extraction logic lives in `document-ingest`; ontology decisions live in the schema reference. The extension is the only LinkedIn-bespoke part.
@@ -37,11 +37,8 @@ The route's job is auth + schema + dispatch. Extraction logic lives in `document
37
37
  ## Files
38
38
 
39
39
  - `extension/manifest.json` — manifest v3
40
- - `extension/content/profile.js` — content script for `linkedin.com/in/*`
41
- - `extension/content/thread.js` — content script for `linkedin.com/messaging/thread/*`
42
- - `extension/background/sw.js` — service worker handling POSTs
43
- - `extension/options/options.{html,js}` — admin host + bearer token entry
44
- - `extension/assets/pill.css` — pill styling
40
+ - `extension/background/sw.js` — service worker. Owns the capture: injects on toolbar click, reads, POSTs, reports via badge + tooltip
41
+ - `extension/options/options.{html,js}` — admin host + ingest token entry
45
42
  - `extension/README.md` — developer install guide
46
43
  - `extension/__tests__/{profile,thread}.test.ts` — jsdom extractor tests
47
44
  - `extension/__tests__/fixtures/{profile,thread}.html` — frozen DOM fixtures captured 2026-05-22
@@ -9,35 +9,38 @@ Unpacked-extension dev build. No Chrome Web Store publication.
9
9
  3. **Load unpacked** → select this `extension/` directory.
10
10
  4. Click the new puzzle icon → open **SiteDesk LinkedIn Ingest** options.
11
11
  5. Paste your admin host (e.g. `https://your-tunnel.example.com`) and your **ingest token**. Ask your SiteDesk agent for the ingest token; it mints one on request and gives it to you in the chat. The token is stable, so this is a one-time paste unless someone rotates it. Save.
12
- 6. Chrome asks you to allow the extension to talk to that host. **Allow it.** The pill cannot post without it, so declining saves nothing.
12
+ 6. Chrome asks you to allow the extension to talk to that host. **Allow it.** Captures cannot post without it, so declining saves nothing.
13
13
 
14
14
  ## Use
15
15
 
16
16
  - Open any `https://www.linkedin.com/in/<slug>/` profile or `https://www.linkedin.com/messaging/thread/<id>/` thread.
17
- - Click the **Add to SiteDesk** pill at the top of the page.
18
- - The pill turns green and writes one `:KnowledgeDocument` plus the entities the body assertively states.
17
+ - Click the **SiteDesk LinkedIn Ingest** button in the Chrome toolbar. Pin it first if it is hidden behind the puzzle icon.
18
+ - The badge shows a green tick and SiteDesk writes one `:KnowledgeDocument` plus the entities the body assertively states.
19
19
 
20
- The pill tells you which thing is wrong:
20
+ There is no button on the page itself. The extension reads the page only when you click, and never changes it: an extension that alters the HTML before LinkedIn's React hydrates raises React error #418, which makes React throw the page away and rebuild it. That is what the old in-page pill did to every profile you opened.
21
21
 
22
- | Pill | Meaning | Fix |
22
+ The badge says what happened; hover the button for the full sentence:
23
+
24
+ | Badge | Meaning | Fix |
23
25
  |---|---|---|
24
- | **Paste SiteDesk token** (amber) | the ingest token is missing or was refused | ask the agent for the ingest token, paste it in options |
25
- | **Grant access** (amber) | the admin host was never allowed, so the post never left the browser | open options, Save, allow the prompt |
26
- | **SiteDesk unreachable** (red) | the host was allowed but did not answer | check the tunnel and the admin server |
27
- | **Capture failed** (red) | the admin rejected the payload, or the extension's own service worker did not answer | read the service-worker console: `reason=extract` is a rejected payload, `reason=worker-unreachable` is the worker |
26
+ | `✓` green | captured | nothing to do |
27
+ | `…` grey | capture in progress | wait |
28
+ | `!` amber, "rejected the ingest token" | the token is missing or was refused | ask your SiteDesk agent for the ingest token, paste it in options |
29
+ | `!` amber, "never granted" | the admin host was never allowed, so the post never left the browser | open options, Save, allow the prompt |
30
+ | `!` red, "did not answer" | the host was allowed but did not answer | check the tunnel and the admin server |
31
+ | `!` red, "unexpected status" | the admin rejected the payload | read the service-worker console for the `op=fetch` line |
32
+ | `!` red, "could not read this page" | the extractors found no required field | LinkedIn's markup moved; the tooltip names the missing fields |
33
+ | `?` grey | the tab is not a profile or DM thread | open one first |
28
34
 
29
35
  ## File layout
30
36
 
31
37
  | File | Role |
32
38
  |---|---|
33
- | `manifest.json` | MV3 manifest. Declares profile + thread content scripts. |
34
- | `content/extractors.js` | Pure DOM extractor functions. Loaded first into each content-script context. |
35
- | `content/profile.js` | Profile pill + click handler. |
36
- | `content/thread.js` | Thread pill + click handler. |
37
- | `background/sw.js` | Service worker. Checks the admin-host grant, then POSTs envelopes to the admin route. |
39
+ | `manifest.json` | MV3 manifest. No content scripts and no standing host permission: `activeTab` plus `scripting` grant access to the tab you click. |
40
+ | `content/extractors.js` | Pure DOM extractor functions, injected on click. |
41
+ | `background/sw.js` | Service worker. Owns the whole capture: injects, reads, POSTs, and reports through the badge and tooltip. |
38
42
  | `options/options.html` + `options/options.js` | Admin host + ingest-token entry. |
39
- | `assets/pill.css` | Pill styling. |
40
- | `__tests__/` | jsdom unit tests for the extractors, run via vitest from `platform/ui`. |
43
+ | `__tests__/` | jsdom fixtures for the extractors, run via vitest from `platform/ui`. |
41
44
 
42
45
  ## How the wire works
43
46
 
@@ -45,7 +48,7 @@ The extension does no extraction or graph-write logic — those live in `platfor
45
48
 
46
49
  ## Updating selectors
47
50
 
48
- LinkedIn ships DOM changes from time to time. When the pill misses a field:
51
+ LinkedIn ships DOM changes from time to time. When a capture reports missing fields:
49
52
 
50
53
  1. Save the current page as `__tests__/fixtures/<kind>.html` (overwrite).
51
54
  2. Update the primary selector in `content/extractors.js`; keep the prior one as fallback if older clients may still serve it.
@@ -1,5 +1,12 @@
1
- // Service worker. Receives envelopes from content scripts, POSTs them
2
- // to the admin host with the ingest token stored in extension options.
1
+ // Service worker. Owns the whole capture: a toolbar click injects the
2
+ // extractors into the active tab, reads the page, and POSTs the envelope to
3
+ // the admin host with the ingest token stored in extension options.
4
+ //
5
+ // Task 2221 — capture moved here from an in-page pill. The pill was prepended
6
+ // into LinkedIn's `main` before React hydrated, which raised React #418 and
7
+ // had the tree regenerated, destroying the pill and degrading the page. The
8
+ // extension now injects only on an explicit click, under `activeTab`, so it
9
+ // holds no standing access to linkedin.com and never mutates the page.
3
10
  //
4
11
  // Task 2185 — the manifest's static host_permissions cover linkedin.com only,
5
12
  // so the admin host has to be granted at runtime from the options page before
@@ -82,17 +89,93 @@ async function postEnvelope(envelope) {
82
89
  return { ok: true }
83
90
  }
84
91
 
85
- chrome.runtime.onMessage.addListener(function (msg, _sender, sendResponse) {
86
- if (!msg || msg.type !== 'linkedin-ingest' || !msg.envelope) {
87
- sendResponse({ ok: false, reason: 'extract', detail: 'bad-message' })
88
- return false
92
+ // Injected into the page by chrome.scripting with `func`, so it is serialised
93
+ // and cannot close over anything in this file. Everything it needs is inline.
94
+ //
95
+ // It reads the DOM and returns; it never writes. That is the whole point of
96
+ // Task 2221: the previous in-page pill was prepended into `main` before React
97
+ // hydrated, which raised React #418 ("Hydration failed ... this tree will be
98
+ // regenerated on the client") and the regeneration took the pill with it. An
99
+ // extension that only reads cannot mismatch hydration.
100
+ function captureInPage() {
101
+ const ex = globalThis.MaxyLinkedinExtractors
102
+ if (!ex) return { ok: false, reason: 'extract', detail: 'extractors-missing' }
103
+ const path = location.pathname
104
+ let kind = null
105
+ if (/^\/in\/[^/]+/.test(path)) kind = 'profile'
106
+ else if (/^\/messaging\/thread\/[^/]+/.test(path)) kind = 'thread'
107
+ if (!kind) return { ok: false, reason: 'wrong-page', detail: path }
108
+ const result = kind === 'profile' ? ex.extractProfile(document) : ex.extractThread(document)
109
+ if (!result.ok) return { ok: false, reason: 'extract', detail: result.missing.join(',') }
110
+ return {
111
+ ok: true,
112
+ kind: kind,
113
+ pageUrl: location.href.split('?')[0].split('#')[0],
114
+ payload: result.payload,
89
115
  }
90
- postEnvelope(msg.envelope).then(sendResponse).catch(function (err) {
91
- sendResponse({ ok: false, reason: 'extract', detail: err && err.message ? err.message : String(err) })
92
- })
93
- return true // async sendResponse
94
- })
116
+ }
117
+
118
+ // Badge carries the glyph, title carries the sentence. A toolbar button cannot
119
+ // both fire onClicked and open a popup — declaring `default_popup` suppresses
120
+ // onClicked — so the tooltip is where the reason lives. A badge alone cannot
121
+ // separate Task 2185's three reasons from Task 2208's token states.
122
+ const STATES = {
123
+ busy: { text: '…', color: '#46596d', title: 'Capturing…' },
124
+ ok: { text: '✓', color: '#2e7d32', title: 'Captured to SiteDesk' },
125
+ auth: { text: '!', color: '#b8860b', title: 'SiteDesk rejected the ingest token. Open options and paste a fresh one.' },
126
+ permission: { text: '!', color: '#b8860b', title: 'The SiteDesk admin host was never granted. Open options and save to grant it.' },
127
+ unreachable: { text: '!', color: '#b00020', title: 'The SiteDesk admin host did not answer. Check the tunnel and the admin server.' },
128
+ server: { text: '!', color: '#b00020', title: 'SiteDesk answered with an unexpected status.' },
129
+ extract: { text: '!', color: '#b00020', title: 'Could not read this page.' },
130
+ 'wrong-page': { text: '?', color: '#46596d', title: 'Open a LinkedIn profile or DM thread first.' },
131
+ }
132
+
133
+ async function setState(tabId, state, detail) {
134
+ const s = STATES[state] || STATES.extract
135
+ const title = detail ? s.title + ' (' + detail + ')' : s.title
136
+ await chrome.action.setBadgeText({ tabId: tabId, text: s.text })
137
+ await chrome.action.setBadgeBackgroundColor({ tabId: tabId, color: s.color })
138
+ await chrome.action.setTitle({ tabId: tabId, title: title })
139
+ console.log(TAG + ' op=state tabId=' + tabId + ' state=' + state + (detail ? ' detail=' + detail : ''))
140
+ }
141
+
142
+ async function runCapture(tab) {
143
+ const dispatchId = crypto.randomUUID()
144
+ await setState(tab.id, 'busy')
95
145
 
96
- chrome.action.onClicked.addListener(function () {
97
- chrome.runtime.openOptionsPage()
146
+ let injected
147
+ try {
148
+ await chrome.scripting.executeScript({ target: { tabId: tab.id }, files: ['content/extractors.js'] })
149
+ injected = await chrome.scripting.executeScript({ target: { tabId: tab.id }, func: captureInPage })
150
+ } catch (err) {
151
+ // activeTab was not granted, or this is a page no extension may touch.
152
+ return setState(tab.id, 'wrong-page', err && err.message ? err.message : String(err))
153
+ }
154
+
155
+ const scraped = injected && injected[0] ? injected[0].result : null
156
+ if (!scraped || !scraped.ok) {
157
+ const reason = scraped ? scraped.reason : 'extract'
158
+ console.error(TAG + ' op=scrape dispatchId=' + dispatchId + ' ok=false reason=' + reason)
159
+ return setState(tab.id, reason, scraped ? scraped.detail : 'no-result')
160
+ }
161
+ console.log(TAG + ' op=scrape dispatchId=' + dispatchId + ' kind=' + scraped.kind + ' ok=true')
162
+
163
+ const envelope = {
164
+ source: 'linkedin',
165
+ kind: scraped.kind,
166
+ capturedAt: new Date().toISOString(),
167
+ pageUrl: scraped.pageUrl,
168
+ dispatchId: dispatchId,
169
+ }
170
+ envelope[scraped.kind] = scraped.payload
171
+
172
+ const posted = await postEnvelope(envelope)
173
+ if (posted.ok) return setState(tab.id, 'ok')
174
+ return setState(tab.id, posted.reason, posted.detail)
175
+ }
176
+
177
+ chrome.action.onClicked.addListener(function (tab) {
178
+ runCapture(tab).catch(function (err) {
179
+ setState(tab.id, 'extract', err && err.message ? err.message : String(err))
180
+ })
98
181
  })
@@ -2,32 +2,17 @@
2
2
  "manifest_version": 3,
3
3
  "name": "SiteDesk LinkedIn Ingest",
4
4
  "version": "0.1.0",
5
- "description": "Add an Add-to-SiteDesk pill to LinkedIn profiles and DM threads. One click captures the page and posts to your SiteDesk admin.",
6
- "permissions": ["storage", "activeTab"],
7
- "host_permissions": ["https://www.linkedin.com/*"],
5
+ "description": "Click the toolbar button on a LinkedIn profile or DM thread to capture it into your SiteDesk admin.",
6
+ "permissions": ["storage", "activeTab", "scripting"],
8
7
  "optional_host_permissions": ["http://*/*", "https://*/*"],
9
8
  "background": {
10
9
  "service_worker": "background/sw.js"
11
10
  },
12
- "content_scripts": [
13
- {
14
- "matches": ["https://www.linkedin.com/in/*"],
15
- "js": ["content/extractors.js", "content/profile.js"],
16
- "css": ["assets/pill.css"],
17
- "run_at": "document_idle"
18
- },
19
- {
20
- "matches": ["https://www.linkedin.com/messaging/thread/*"],
21
- "js": ["content/extractors.js", "content/thread.js"],
22
- "css": ["assets/pill.css"],
23
- "run_at": "document_idle"
24
- }
25
- ],
26
11
  "options_ui": {
27
12
  "page": "options/options.html",
28
13
  "open_in_tab": true
29
14
  },
30
15
  "action": {
31
- "default_title": "SiteDesk LinkedIn Ingest open options"
16
+ "default_title": "Capture this LinkedIn page to SiteDesk"
32
17
  }
33
18
  }
@@ -17,8 +17,8 @@
17
17
  </head>
18
18
  <body>
19
19
  <h1>SiteDesk LinkedIn Ingest</h1>
20
- <p>Point the extension at your SiteDesk admin host and paste your ingest token. Ask your SiteDesk agent for the token and it will give you one. The pill on LinkedIn will then post one-click captures to your admin.</p>
21
- <p>On Save, Chrome asks you to allow this extension to talk to that host. The capture pill cannot post without it, so declining leaves the extension unconfigured.</p>
20
+ <p>Point the extension at your SiteDesk admin host and paste your ingest token. Ask your SiteDesk agent for the token and it will give you one. The toolbar button will then post one-click captures to your admin.</p>
21
+ <p>On Save, Chrome asks you to allow this extension to talk to that host. Captures cannot post without it, so declining leaves the extension unconfigured.</p>
22
22
 
23
23
  <label for="admin-host">Admin host</label>
24
24
  <input id="admin-host" type="url" placeholder="https://your-tunnel.example.com" />
@@ -96,7 +96,7 @@ async function save() {
96
96
  console.log(TAG + ' op=permission origin=' + origin + ' granted=' + granted)
97
97
  if (!granted) {
98
98
  return setStatus(
99
- 'Access to ' + origin + ' was declined, so nothing was saved. The pill cannot post without it.',
99
+ 'Access to ' + origin + ' was declined, so nothing was saved. Captures cannot post without it.',
100
100
  true,
101
101
  )
102
102
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memory",
3
- "description": "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match — a Person, which carries no `name`, is matched on its composed `givenName` + `familyName` display name instead — optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools: `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks). Ships three thinking-tool skills: `challenge` (adversarial retrieval — strongest counter-evidence from the graph for a given claim), `connect` (bridge-finding between two topics via shared graph neighbors), and `emerge` (clusters uncategorised KnowledgeDocument and Section nodes into operator-approved Concept proposals).",
3
+ "description": "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match — a Person, which carries no `name`, is matched on its composed `givenName` + `familyName` display name instead — optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools: `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks). Ships three thinking-tool skills: `challenge` (adversarial retrieval — strongest counter-evidence from the graph for a given claim), `connect` (bridge-finding between two topics via shared graph neighbors), and `emerge` (clusters uncategorised KnowledgeDocument and Section nodes into operator-approved Concept proposals). The learnings surface is three tools plus the `learnings-recall` skill: `learnings-scan` pages session transcripts off disk (requiring CLAUDE_CONFIG_DIR, refusing without it rather than guessing a projects root), `learnings-derive` reconciles the turn's claims into `:Learning` writes on a confidence curve (create at 0.5, reinforce as c + 0.15(1-c), contradict by 0.3 with contradictedAt stamped) and optionally runs the decay pass, and `learnings-recall` is the only path from a title injected into a system prompt to its full body.",
4
4
  "version": "0.1.0",
5
5
  "author": {
6
6
  "name": "Rubytech LLC"
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: memory
3
- description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match — a Person, which carries no `name`, is matched on its composed `givenName` + `familyName` display name instead — optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools: `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks). Ships three thinking-tool skills: `challenge` (adversarial retrieval — strongest counter-evidence from the graph for a given claim), `connect` (bridge-finding between two topics via shared graph neighbors), and `emerge` (clusters uncategorised KnowledgeDocument and Section nodes into operator-approved Concept proposals)."
3
+ description: "Graph memory plugin. Provides memory-search (with optional `fields` projection for known-shape lookups), memory-write, memory-update, memory-edge (create/delete typed directed edges between pre-existing nodes), memory-update-by-name (repair surface that resolves an elementId from a (label, name, accountId) tuple when search cannot return one), memory-lookup-by-name (deterministic read surface that returns nodes by case-insensitive exact `name` match — a Person, which carries no `name`, is matched on its composed `givenName` + `familyName` display name instead — optionally constrained by labels, bypassing the memory-search ranking stack so a present entity is never missed because a diluted query ranked it below the cut), and the :Report surface (memory-report-write / memory-report-read-latest / memory-report-list) for reading from, writing to, and updating the Neo4j knowledge graph. Includes conversational memory — organic preference learning, evidence-backed recall, and transparent 'what do you know about me?' responses. Document ingestion goes through memory-ingest; the dispatched specialist produces typed-section JSON in-turn from the loaded ontology. Operator-solicited reclassify of an already-stored thread-shaped KD (today: email-thread KDs) is exposed as `kd-classify <attachmentId>` — runs a body-growth gate (proceed iff body grew ≥25% since the last classify, or first-ever classify), stages the body to a temp file under `$ACCOUNT_DIR/tmp/`, and returns a dispatch envelope for the `librarian`; on success `memory-ingest` replaces the prior :Section children and stamps `lastClassifiedAt` + `lastClassifiedBodyLength` on the parent KD. Two modes: `document` (default) for unstructured PDF/web content → :KnowledgeDocument (keyed on `attachmentId`) + :Section, and `chat` for conversation transcripts → :ConversationArchive (keyed on `conversationIdentity`) + :Section chunks; two parent labels, two writer paths chosen by which identity property is set. Conversation-archive Phase 2 splits across two read-only tools: `conversation-archive-list-chunks` pages chunk bodies for the dispatched specialist to read in-turn; `conversation-archive-derive-insights` converts the specialist's per-chunk claims into operator-facing proposals with per-kind cypher. `conversation-archive-enrich-rejection` records (or undoes) durable per-row rejections so already-triaged claims do not re-surface on re-runs. Ships five skills: `conversational-memory`, `document-ingest`, `conversation-archive` (source-agnostic transcript ingest for WhatsApp, Telegram, Signal, LinkedIn DMs, Zoom, meeting minutes, iMessage, Slack), `conversation-archive-mcp` (operator-initiated MCP-driven ingest for Granola, Otter, Circleback meetings — sibling of `conversation-archive` sharing the same writer surface and identity formula), and `conversation-archive-enrich` (per-row operator-gated insight derivation over a named archive's chunks). Ships three thinking-tool skills: `challenge` (adversarial retrieval — strongest counter-evidence from the graph for a given claim), `connect` (bridge-finding between two topics via shared graph neighbors), and `emerge` (clusters uncategorised KnowledgeDocument and Section nodes into operator-approved Concept proposals). The learnings surface is three tools plus the `learnings-recall` skill: `learnings-scan` pages session transcripts off disk (requiring CLAUDE_CONFIG_DIR, refusing without it rather than guessing a projects root), `learnings-derive` reconciles the turn's claims into `:Learning` writes on a confidence curve (create at 0.5, reinforce as c + 0.15(1-c), contradict by 0.3 with contradictedAt stamped) and optionally runs the decay pass, and `learnings-recall` is the only path from a title injected into a system prompt to its full body."
4
4
  account-owned-dirs: [{"dir": "archive", "description": "Archived obsidian imports and their embedded attachments."}]
5
5
  tools:
6
6
  - name: memory-search
@@ -167,6 +167,18 @@ tools:
167
167
  publicAllowlist: false
168
168
  adminAllowlist: true
169
169
  riskClass: read
170
+ - name: learnings-scan
171
+ publicAllowlist: false
172
+ adminAllowlist: true
173
+ riskClass: read
174
+ - name: learnings-derive
175
+ publicAllowlist: false
176
+ adminAllowlist: true
177
+ riskClass: write_local
178
+ - name: learnings-recall
179
+ publicAllowlist: false
180
+ adminAllowlist: true
181
+ riskClass: read
170
182
  hidden:
171
183
  - name: session-compact
172
184
  riskClass: write_local
@@ -185,6 +197,7 @@ skills:
185
197
  - skills/challenge/SKILL.md
186
198
  - skills/connect/SKILL.md
187
199
  - skills/emerge/SKILL.md
200
+ - skills/learnings-recall/SKILL.md
188
201
  always: true
189
202
  embed: false
190
203
  mcp:
@@ -200,6 +213,7 @@ mcp:
200
213
  SESSION_ID: ${SESSION_ID}
201
214
  AGENT_SLUG: ${AGENT_SLUG}
202
215
  AGENT_SLUG_SCOPE: ${AGENT_SLUG_SCOPE}
216
+ CLAUDE_CONFIG_DIR: ${CLAUDE_CONFIG_DIR}
203
217
  mcp-manifest: auto
204
218
  ---
205
219