loki-mode 11.69.1 → 11.70.0

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 (76) hide show
  1. package/SKILL.md +2 -2
  2. package/VERSION +1 -1
  3. package/agents/managed_registry.py +2 -1
  4. package/autonomy/loki +1 -0
  5. package/dashboard/__init__.py +1 -1
  6. package/docs/CLI-REFERENCE.md +1 -1
  7. package/docs/INSTALLATION.md +1 -1
  8. package/docs/dev/architecture-reference.md +19 -0
  9. package/docs/v10/FAILURE-CLASSES.md +1 -0
  10. package/loki-ts/dist/loki.js +2 -2
  11. package/mcp/__init__.py +1 -1
  12. package/mcp/requirements.txt +3 -2
  13. package/memory/managed_memory/client.py +100 -20
  14. package/memory/managed_memory/fakes.py +42 -11
  15. package/memory/managed_memory/retrieve.py +3 -3
  16. package/memory/managed_memory/shadow_write.py +13 -9
  17. package/package.json +2 -2
  18. package/plugins/loki-mode/.claude-plugin/plugin.json +1 -1
  19. package/providers/managed.py +169 -18
  20. package/web-app/dist/assets/{AdminPage-CSAXKll7.js → AdminPage-BYgBOrp7.js} +1 -1
  21. package/web-app/dist/assets/{Avatar-E3qFUQRZ.js → Avatar--XJgyp7S.js} +1 -1
  22. package/web-app/dist/assets/{Badge-DyxJRKtr.js → Badge-CA9ZmmNt.js} +1 -1
  23. package/web-app/dist/assets/{Button-DlKhD6h-.js → Button-iIDr0YM0.js} +1 -1
  24. package/web-app/dist/assets/{CockpitPage-fBpM30Ly.js → CockpitPage-CCXd-vNb.js} +1 -1
  25. package/web-app/dist/assets/{ComparePage-CSPkieKM.js → ComparePage-DMIapaYr.js} +1 -1
  26. package/web-app/dist/assets/{ErrorBoundary-B-O2qlIq.js → ErrorBoundary-CzuiJS34.js} +1 -1
  27. package/web-app/dist/assets/{EvidenceReceiptPanel-Byl4p0FO.js → EvidenceReceiptPanel-DCM8vWSn.js} +1 -1
  28. package/web-app/dist/assets/{GitHubIssuesPanel-5vP5O61_.js → GitHubIssuesPanel-DcF3DDfw.js} +1 -1
  29. package/web-app/dist/assets/{GitHubPRsPanel-cuO9IztD.js → GitHubPRsPanel-mZO41gW_.js} +1 -1
  30. package/web-app/dist/assets/{HomePage-__Ihb5Q8.js → HomePage-BIjWzEji.js} +2 -2
  31. package/web-app/dist/assets/{LoginPage-FXPPvK-H.js → LoginPage-DICi5kVZ.js} +1 -1
  32. package/web-app/dist/assets/{MagicPage-CkdPmLpz.js → MagicPage-DKsXY1db.js} +1 -1
  33. package/web-app/dist/assets/{MetricsPage-DsGoKoda.js → MetricsPage-BFg1O9ty.js} +1 -1
  34. package/web-app/dist/assets/{NotFoundPage-CXGPTmi-.js → NotFoundPage-DqQUk-56.js} +1 -1
  35. package/web-app/dist/assets/{ProjectPage-BGvS3p6p.js → ProjectPage-DM4IM6wN.js} +3 -3
  36. package/web-app/dist/assets/{ProjectsPage-CRYyScu5.js → ProjectsPage-CzGe32i1.js} +1 -1
  37. package/web-app/dist/assets/{SettingsPage--TNo_LGg.js → SettingsPage-N5VnZNhG.js} +1 -1
  38. package/web-app/dist/assets/{ShowcasePage-DjYmfmcC.js → ShowcasePage-aRH6Ze3C.js} +1 -1
  39. package/web-app/dist/assets/{SystemSettingsPage-Dj1iu06I.js → SystemSettingsPage-CY79fI0n.js} +1 -1
  40. package/web-app/dist/assets/{TeamsPage-6QWcFx9d.js → TeamsPage-0knktxOM.js} +1 -1
  41. package/web-app/dist/assets/{TemplatesPage-BIdn1u1F.js → TemplatesPage-BCa9OiVY.js} +1 -1
  42. package/web-app/dist/assets/{TerminalOutput-BowVws6H.js → TerminalOutput-ibAXZT5_.js} +1 -1
  43. package/web-app/dist/assets/{activity-CKlqCQtE.js → activity-p22XUeoO.js} +1 -1
  44. package/web-app/dist/assets/{bell-0pqy4F4o.js → bell-Bcok_OOB.js} +1 -1
  45. package/web-app/dist/assets/{bot-B-6AYLoj.js → bot-GTdHTaoo.js} +1 -1
  46. package/web-app/dist/assets/{check-DyS_eqto.js → check-D2njzm4d.js} +1 -1
  47. package/web-app/dist/assets/{chevron-left-zA2w8FJS.js → chevron-left-DdYAWi6b.js} +1 -1
  48. package/web-app/dist/assets/{circle-alert-2m-jyEz5.js → circle-alert-Bz1PKYGL.js} +1 -1
  49. package/web-app/dist/assets/{clock-8W-wveKi.js → clock-CKqxy6VD.js} +1 -1
  50. package/web-app/dist/assets/{cloud-bn2QMW0g.js → cloud-CORAtv96.js} +1 -1
  51. package/web-app/dist/assets/{code-xml-CjChmivj.js → code-xml-DPEMWBpp.js} +1 -1
  52. package/web-app/dist/assets/{database-Ba1_-mKG.js → database-Cn2cboI6.js} +1 -1
  53. package/web-app/dist/assets/{dollar-sign-vvVEzM0V.js → dollar-sign-BZq5PiyR.js} +1 -1
  54. package/web-app/dist/assets/{file-code-corner-G7eJpYDZ.js → file-code-corner-oZdv9GWA.js} +1 -1
  55. package/web-app/dist/assets/{file-plus-COu8M3OJ.js → file-plus-Bf-trjX8.js} +1 -1
  56. package/web-app/dist/assets/{globe-B4G4VyRU.js → globe-Cz464A2c.js} +1 -1
  57. package/web-app/dist/assets/{hammer-CdbjK465.js → hammer-BYnhzku7.js} +1 -1
  58. package/web-app/dist/assets/{index-BxQHGwOF.js → index-CeC0H88y.js} +3 -3
  59. package/web-app/dist/assets/{layers-CLk8u3_a.js → layers-tiKAh05w.js} +1 -1
  60. package/web-app/dist/assets/{loader-circle-b_DGgumy.js → loader-circle-aqYJwV71.js} +1 -1
  61. package/web-app/dist/assets/{lock-BM5PgFEy.js → lock--FTx90Il.js} +1 -1
  62. package/web-app/dist/assets/{package-20mcI1Qi.js → package-BxjrLRVZ.js} +1 -1
  63. package/web-app/dist/assets/{plus-7fE9Zq1p.js → plus-DhnjqUjT.js} +1 -1
  64. package/web-app/dist/assets/{refresh-cw-D8KTrV7a.js → refresh-cw-DGtkiauK.js} +1 -1
  65. package/web-app/dist/assets/{rotate-ccw-DDnI9WNy.js → rotate-ccw-hlcCtv5T.js} +1 -1
  66. package/web-app/dist/assets/{scroll-text-DuPMDYBp.js → scroll-text-FWLg-h_Z.js} +1 -1
  67. package/web-app/dist/assets/{server-D8aHVdTH.js → server-DYonzqCF.js} +1 -1
  68. package/web-app/dist/assets/{shield-alert-B222GWNM.js → shield-alert-DlpNXb9G.js} +1 -1
  69. package/web-app/dist/assets/{thumbs-up-B3rtEcc0.js → thumbs-up-C0StPWU8.js} +1 -1
  70. package/web-app/dist/assets/{trash-2-CUXc_X-z.js → trash-2-pM7Ap3BX.js} +1 -1
  71. package/web-app/dist/assets/{trending-down-BBQHbK6E.js → trending-down-FqnOAsjZ.js} +1 -1
  72. package/web-app/dist/assets/{trending-up-CgyGXqpd.js → trending-up-C3o8sYhj.js} +1 -1
  73. package/web-app/dist/assets/{upload-_tm2rwEt.js → upload-CBvBOxsH.js} +1 -1
  74. package/web-app/dist/assets/{usePolling-BbEvz4Nf.js → usePolling-BrHWY5Q-.js} +1 -1
  75. package/web-app/dist/assets/{user-DoHmuFaD.js → user-DLFfE0Si.js} +1 -1
  76. package/web-app/dist/index.html +1 -1
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: loki-mode
3
3
  description: Runs an autonomous software factory that knows what it is supposed to deliver and proves it did, with a signed receipt. Use when the user says Loki Mode or asks to build, fix or verify software autonomously.
4
4
  ---
5
5
 
6
- # Loki Mode v11.69.1
6
+ # Loki Mode v11.70.0
7
7
 
8
8
  **Current entry points (Loki 10 engine):** `loki "<task>"`, `loki owner/repo#N` and `loki quick "<task>"` run the Loki 10 engine and end with a signed receipt (`loki verify`). `loki backlog owner/repo --all` runs many issues in parallel. Guide: `docs/v10/GUIDE.md`. The skill instructions below, and `loki start`, describe the previous bash loop. PRD-file starts (`loki start ./prd.md`) and the opencode provider still run on it until FC38-SWEEP ports them; the legacy-engine command and the LOKI_ENGINE switch remain removed. Prefer `loki owner/repo#N` for issues.
9
9
 
@@ -476,4 +476,4 @@ Implemented, planned and deprecated items, with the version each landed in: `ski
476
476
 
477
477
  ---
478
478
 
479
- **v11.69.1 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
479
+ **v11.70.0 | [Autonomi](https://www.autonomi.dev/) flagship product | core skill**
package/VERSION CHANGED
@@ -1 +1 @@
1
- 11.69.1
1
+ 11.70.0
@@ -166,7 +166,7 @@ def _call_create_agent(pool_name: str) -> str:
166
166
  translate to a fallback-capable flow.
167
167
  """
168
168
  # Deferred import to keep the module SDK-free at import time.
169
- from providers.managed import _get_client # local import of shared client
169
+ from providers.managed import _get_client, agent_model # local import of shared client
170
170
 
171
171
  client = _get_client()
172
172
  beta = getattr(client, "beta", None)
@@ -183,6 +183,7 @@ def _call_create_agent(pool_name: str) -> str:
183
183
  try:
184
184
  created = agents_ns.create(
185
185
  name=display_name,
186
+ model=agent_model(),
186
187
  system=persona,
187
188
  metadata={"loki_pool": pool_name},
188
189
  )
package/autonomy/loki CHANGED
@@ -2996,6 +2996,7 @@ cmd_start() {
2996
2996
  echo " --ship Worktree + PR + auto-merge"
2997
2997
  echo " --detach, -d Run in background (implies --worktree)"
2998
2998
  echo " --resume [RUN_ID] Resume an interrupted run at the stage it stopped in"
2999
+ echo " Exit 4 means the run cannot be resumed OR the resumed run ended BLOCKED; read the printed line to tell them apart."
2999
3000
  echo ""
3000
3001
  echo "Preview without spending (any input mode):"
3001
3002
  echo " loki plan <PRD> Analyze a spec WITHOUT executing: complexity,"
@@ -7,6 +7,6 @@ Modules:
7
7
  control: Session control helpers (status, atomic writes, origin check)
8
8
  """
9
9
 
10
- __version__ = "11.69.1"
10
+ __version__ = "11.70.0"
11
11
 
12
12
  __all__ = ["__version__"]
@@ -1,6 +1,6 @@
1
1
  # CLI reference
2
2
 
3
- Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.69.1. Do not edit by hand.
3
+ Generated from `loki-ts/src/cli/registry.ts` by `scripts/generate-stale-zero.sh` for v11.70.0. Do not edit by hand.
4
4
  Hidden and legacy-only commands are omitted; see `docs/v10/CLI-MODERN.md` for the full inventory.
5
5
 
6
6
  | Command | Description |
@@ -2,7 +2,7 @@
2
2
 
3
3
  Loki Mode, by [Autonomi](https://www.autonomi.dev/), turns a spec or an issue into a verified pull request with a receipt you can check. This page covers installing it, a first run, and the options around it.
4
4
 
5
- **Version:** v11.69.1
5
+ **Version:** v11.70.0
6
6
 
7
7
  **Engine note:** `loki start` is the primary command. `loki start`, `loki "<task>"`, `loki owner/repo#N` and `loki quick` all run the Loki 10 engine, whatever the flag order or input form (PRD file, issue ref, task or one-word brief). The previous loop runs only on `--engine legacy`, `LOKI_ENGINE=legacy`, or for one call when a flag Loki 10 cannot honor is present. See the [user guide](v10/GUIDE.md) and the [5-minute quickstart](QUICKSTART.md).
8
8
 
@@ -114,3 +114,22 @@ extract_learnings_from_session (autonomy/run.sh) mirrors learnings and decisions
114
114
  - Lessons: every sealed run appends a receipt fact (`LOKI_AUTO_LEARN_PR`, default on) outside the receipt body; `implement.ts` injects lessons and relevant learnings inside the untrusted-data fence; `loki memory brief --task T` prints the same block.
115
115
 
116
116
  - Wall publish (FC-VERIFIED-PR-WITHOUT-ITS-TEST): on a VERIFIED seal only, `publishWall` (`util/check_result.ts`, called from `stages/seal.ts`) makes ONE child commit (trailer `Loki-Wall-Publish: <runId>`) of the SEALED Wall bytes (`runDir/wall`, hash-checked against `wall.files[].sha256`) that verify ran and passed (`publishableWall`). Before writing, the realpath of each Wall file's parent directory must be inside the repo, and an existing file or symlink is removed first. The child commit runs with hooks and signing OFF (the default `git()` call, no `userHooks`), so a pre-commit hook cannot add files or rewrite the Wall bytes. Afterwards the commit must touch exactly the published paths and each committed blob must equal the blob hash of the sealed bytes (computed in process, never read from the worktree); on any mismatch the child is rolled back (`update-ref` HEAD to the product head plus `read-tree`), nothing is published and a NOT PROVEN note is added. The receipt binds the PRODUCT head (head_sha, tree, diff_sha256 are computed before the child commit). A re-seal binds `HEAD^` only when HEAD's message has the exact trailer line for this run, HEAD touches only paths of this run's `wall.files`, and every blob sha256 matches (`wallChild`); a forged trailer is not enough. `util/seal_drift.ts` accepts a moved HEAD after head_sha as benign only when every commit after it is such a Wall publish commit (linear, trailer line for the receipt's run_id, only `wall.files` paths, blob sha256 equal to the sealed one); any commit after head_sha that carries this run's trailer but fails that check is head drift (UNCHECKED). Later commits without the trailer stay under the TEN-GATES-16 sealed-path overlap rule. The PR body gains an "Acceptance tests" section. Commit stage and `backstopCommit` still drop Wall files. `LOKI_COMMIT_WALL=0` restores the old behavior (nothing published).
117
+
118
+ ### Python anthropic SDK (RQ-105: pinned `anthropic==2.2.0`)
119
+
120
+ `mcp/requirements.txt` pins the Python SDK exactly (was `>=0.40,<1.0.0`; the resolver landed on 0.125.0). Requires Python >=3.10 (Ubuntu 24.04 image ships 3.12). The SDK runs on `httpx2`; no repo code passes `httpx` objects to it. v2.0.0 changes (API version 2026-10-21): `temperature`/`top_p`/`top_k` removed from Messages (no call site uses them), Admin API field renames (unused). Python-side call sites: `memory/managed_memory/client.py` (the only importer) and `providers/managed.py` (lazy import, `tests/managed/test_registry_lazy.py` contract unchanged).
121
+
122
+ | Capability (SDK release notes 0.40 to 2.2.0) | Status | Evidence / reason |
123
+ | --- | --- | --- |
124
+ | Client construction `anthropic.Anthropic(api_key, timeout, default_headers)` | WIRED | `memory/managed_memory/client.py:76`, `providers/managed.py:223`; unchanged in 2.x |
125
+ | Managed Agents memory stores (`beta.memory_stores`) | WIRED | `memory/managed_memory/client.py:127`; `create()` has no `scope` in 2.x, scope now sent as `metadata` |
126
+ | Memories nested at `beta.memory_stores.memories` (`memory_store_id`) | WIRED | `memory/managed_memory/client.py:96`; legacy `beta.memories` kept for fakes/preview |
127
+ | Memory paths | WIRED | 2.x paths are absolute and unique per store: `client.py:157` `_abs` plus leading "/" logical paths (`shadow_write.py` verdict/pattern/episode, `retrieve.py` list prefixes); wire test `RealSdkWireTests` asserts the request path |
128
+ | Memories list view and paging | WIRED | `client.py:242` `memories_list` sends `view="full"` (default basic view returns null content), follows `next_page` (20 per page) and drops `memory_prefix` rows; wire test pages 45 rows over 3 requests |
129
+ | Memory create precondition | NOT ADOPTED | 2.x `memories.create` has no precondition; an existing path answers 409 (`client.py:161`, status kept visible) |
130
+ | Memory update with `content_sha256` precondition | WIRED | `client.py:198` `memory_update`; the shadow-write 409 path (`shadow_write.py:197`) re-reads, merges, then updates with the hash of the content currently stored, never the hash of the new content |
131
+ | Memory versions (`memory_stores.memory_versions`) | NOT ADOPTED | `redact` path still reads the legacy shape; separate slice, behind ManagedDisabled fallback |
132
+ | Managed Agents sessions, environments, multiagent coordinator | WIRED | `providers/managed.py` `_RealMultiagentSession.run`: coordinator agent (`multiagent={"type":"coordinator"}`) + cached environment, then `sessions.create(agent=, environment_id=, initial_events=)`; ids cached in `.loki/managed/agent_ids.json`; contract test binds kwargs to the real SDK signatures. Fails safe (ManagedUnavailable) on timeout, terminated status, or any reply set that is not exactly one reply per roster agent; replies are the coordinator's `agent.thread_message_received` events, attributed via `from_session_thread_id` and `sessions.threads.list` to thread.agent.id (agent threads only); the session is ready only when every roster agent has replied; coordinator cache key includes the model, resolved from the model catalog (non-Claude env overrides ignored). WIRED, live reply shape NOT PROVEN (no key) |
133
+ | Vaults, deployments, dreams, webhooks | NOT ADOPTED | no Loki consumer; research preview |
134
+ | Tool runner (`beta.messages.tool_runner`), compaction, structured outputs, adaptive thinking, advisor tool, citations, files API, web search / code execution, prompt cache TTL | NOT ADOPTED | Loki drives models through provider CLIs and the TS Agent SDK; the Python SDK is only the managed-agents bridge |
135
+ | Bedrock / Vertex clients | NOT ADOPTED | no use |
@@ -1549,6 +1549,7 @@ L0 review rule (ENGINE-LAWS.md L0): a fix that adds an `if` or a regex about the
1549
1549
  - Cause (measured): the legacy start branch delegated `engine10 resume` (resumeMain, the reporter) instead of `engine10 --resume` (resumeFlag, the stage resume) and mapped its exit 4 to 0; cmd_verify routed only composite, group and deploy receipts to engine10, so a run receipt fell through to the diff verifier. tests/test-next-resume-agree.sh pinned `STUB-ARGV:engine10 resume` with a stub that exits 0, so it passed while the user path resumed nothing (a guard that passed its own mutation).
1550
1550
  - Siblings swept: `loki resume` (the reporter, keeps its 0/4/5 map), `loki "<task>" --resume` (already resumeFlag), bare `loki verify` and `loki verify e10-*` (already v10 in bin/loki), `--pubkey` verify (already v10), a git ref and a non-receipt .json (stay on the diff verifier, pinned as controls).
1551
1551
  - Mechanism: `start --resume` delegates `engine10 --resume [run-id]` and returns the code unchanged; cmd_verify sends `.loki/runs/e10-*/receipt.json` paths and any existing file whose schema starts `loki.v10.receipt` to `engine10 verify`.
1552
+ - Exit 4 is shared: `start --resume` returns the engine code unchanged, and 4 is both "cannot resume" (for example a run whose base commit is gone) and EXIT.BLOCKED (engine10/output.ts), so a resumed run that ends BLOCKED also exits 4. A script cannot tell the two apart from the code alone; the printed line does. No doc may claim 4 means only "cannot resume". The codes are unchanged.
1552
1553
  - Fixture: tests/test-next-resume-agree.sh drives the real Bun entry on a seeded interrupted run (asserts `engine10: resume from stage implement`, exit 2 not 0) and a stub that exits 4 (asserts 4); tests/test-verify-receipt-route.sh covers the path forms, a tampered receipt (TAMPERED, never CONCERNS) and the diff-verifier controls. Red on origin/main (17 and 4 failures), green after; mutants (delegate back to `resume`, schema route removed) turn them red.
1553
1554
 
1554
1555
 
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var ch0=Object.create;var{getPrototypeOf:dh0,defineProperty:pK,getOwnPropertyNames:Mn,getOwnPropertyDescriptor:lh0}=Object,On=Object.prototype.hasOwnProperty;function An(Z){return this[Z]}var ah0,ih0,oh0=(Z,$,J)=>{var X=Z!=null&&typeof Z==="object";if(X){var Y=$?ah0??=new WeakMap:ih0??=new WeakMap,Q=Y.get(Z);if(Q)return Q}J=Z!=null?ch0(dh0(Z)):{};let z=$||!Z||!Z.__esModule?pK(J,"default",{value:Z,enumerable:!0}):J;for(let W of Mn(Z))if(!On.call(z,W))pK(z,W,{get:An.bind(Z,W),enumerable:!0});if(X)Y.set(Z,z);return z},nh0=(Z)=>{var $=(Nn??=new WeakMap).get(Z),J;if($)return $;if($=pK({},"__esModule",{value:!0}),Z&&typeof Z==="object"||typeof Z==="function"){for(var X of Mn(Z))if(!On.call($,X))pK($,X,{get:An.bind(Z,X),enumerable:!(J=lh0(Z,X))||J.enumerable})}return Nn.set(Z,$),$},Nn,qM=(Z,$)=>()=>($||Z(($={exports:{}}).exports,$),$.exports);var sh0=(Z)=>Z;function rh0(Z,$){this[Z]=sh0.bind(null,$)}var j0=(Z,$)=>{for(var J in $)pK(Z,J,{get:$[J],enumerable:!0,configurable:!0,set:rh0.bind($,J)})};var S=(Z,$)=>()=>(Z&&($=Z(Z=0)),$);var e9=import.meta.require;var jn={};j0(jn,{lokiDirFor:()=>BM,lokiDir:()=>D0,homeLokiDir:()=>T2,findRepoRootForVersion:()=>cK,REPO_ROOT:()=>F0});import{resolve as wZ,dirname as aE}from"path";import{fileURLToPath as th0}from"url";import{existsSync as UM}from"fs";import{homedir as eh0}from"os";function Zf0(){let Z=Ln;for(let $=0;$<6;$++){if(UM(wZ(Z,"VERSION"))&&UM(wZ(Z,"autonomy/run.sh")))return Z;let J=aE(Z);if(J===Z)break;Z=J}return wZ(Ln,"..","..","..")}function cK(Z){let $=Z;for(let J=0;J<6;J++){if(UM(wZ($,"VERSION"))&&UM(wZ($,"autonomy/run.sh")))return $;let X=aE($);if(X===$)break;$=X}return wZ(Z,"..","..","..")}function BM(Z){return process.env.LOKI_DIR??wZ(Z,".loki")}function D0(){return BM(process.cwd())}function T2(){return wZ(eh0(),".loki")}var Ln,F0;var s0=S(()=>{Ln=aE(th0(import.meta.url));F0=Zf0()});import{readFileSync as $f0}from"fs";import{resolve as Jf0,dirname as Xf0}from"path";import{fileURLToPath as Yf0}from"url";function O6(){if(f1!==null)return f1;let Z="11.69.1";if(typeof Z==="string"&&Z.length>0)return f1=Z,f1;try{let $=Xf0(Yf0(import.meta.url)),J=cK($);f1=$f0(Jf0(J,"VERSION"),"utf-8").trim()}catch{f1="unknown"}return f1}var f1=null;var _Z=S(()=>{s0()});import{mkdirSync as oE,readFileSync as NM,realpathSync as Qf0,statSync as zf0,writeFileSync as nE}from"fs";import{dirname as MY,join as iE,resolve as Cn}from"path";function Hf0(){return Cn(T2(),"cache","update-check.json")}function xZ(Z){let $=/^(\d+)\.(\d+)\.(\d+)$/.exec(Z.trim());if(!$)return null;return[Number($[1]),Number($[2]),Number($[3])]}function dK(Z,$){let J=xZ(Z),X=xZ($);if(!J||!X)return!1;let[Y,Q,z]=J,[W,K,V]=X;if(Y!==W)return Y>W;if(Q!==K)return Q>K;return z>V}function Fn(){let Z=(process.env.LOKI_NO_UPDATE_CHECK??"").trim().toLowerCase();if(Z==="1"||Z==="true"||Z==="yes")return!0;if(process.env.CI)return!0;if(!process.stdout.isTTY)return!0;return!1}function In(Z){try{let $=NM(Z,"utf-8"),J=JSON.parse($);if(typeof J.checkedAt==="number"&&typeof J.latest==="string"&&xZ(J.latest)!==null)return{checkedAt:J.checkedAt,latest:J.latest}}catch{}return null}function qf0(Z){try{let $=JSON.parse(NM(Z,"utf-8")).failedAt;if(typeof $==="number")return $}catch{}return null}function Uf0(Z,$){let J=In(Z);try{oE(MY(Z),{recursive:!0}),nE(Z,JSON.stringify({...J??{},failedAt:$}),"utf-8")}catch{}}function Bf0(Z,$){try{oE(MY(Z),{recursive:!0}),nE(Z,JSON.stringify($),"utf-8")}catch{}}async function Nf0(){try{let Z=await fetch(Wf0,{signal:AbortSignal.timeout(Vf0),headers:{accept:"application/json"}});if(!Z.ok)return null;let $=await Z.json();if(typeof $.version==="string"&&xZ($.version)!==null)return $.version}catch{}return null}async function Mf0(Z=Date.now(),$=Nf0,J=Hf0()){let X=In(J);if(X&&Z-X.checkedAt<Kf0)return X.latest;let Y=qf0(J);if(Y!==null&&Z-Y>=0&&Z-Y<Gf0)return null;let Q=await $();if(Q===null)return Uf0(J,Z),null;return Bf0(J,{checkedAt:Z,latest:Q}),Q}function Of0(Z,$=process.env){if(xZ(Z)===null)return null;let X=$.PATH;if(!X)return null;let Y=new Set;for(let Q of X.split(":")){if(!Q)continue;let z=iE(Q,"loki"),W;try{zf0(z),W=Qf0(z)}catch{continue}if(Y.has(W))continue;Y.add(W);try{let K=iE(MY(MY(W)),"package.json"),V=JSON.parse(NM(K,"utf8")).version;if(typeof V!=="string")continue;if(dK(V,Z))return{path:z,version:V}}catch{continue}}return null}function Af0(){return Cn(T2(),"cache","last-seen-version.json")}function Lf0(Z,$={}){try{if(Fn())return!1;if(xZ(Z)===null)return!1;let J=$.seenFile??Af0(),X=null;try{let Q=JSON.parse(NM(J,"utf-8")).version;if(typeof Q==="string"&&xZ(Q)!==null)X=Q}catch{}if(X!==null&&!dK(Z,X))return!1;try{oE(MY(J),{recursive:!0}),nE(J,JSON.stringify({version:Z}),"utf-8")}catch{return!1}if(X===null)return!1;return($.write??((Q)=>process.stderr.write(Q)))(`Loki Mode upgraded ${X} -> ${Z}. What is new: https://github.com/asklokesh/loki-mode/releases/tag/v${Z}
2
+ var ch0=Object.create;var{getPrototypeOf:dh0,defineProperty:pK,getOwnPropertyNames:Mn,getOwnPropertyDescriptor:lh0}=Object,On=Object.prototype.hasOwnProperty;function An(Z){return this[Z]}var ah0,ih0,oh0=(Z,$,J)=>{var X=Z!=null&&typeof Z==="object";if(X){var Y=$?ah0??=new WeakMap:ih0??=new WeakMap,Q=Y.get(Z);if(Q)return Q}J=Z!=null?ch0(dh0(Z)):{};let z=$||!Z||!Z.__esModule?pK(J,"default",{value:Z,enumerable:!0}):J;for(let W of Mn(Z))if(!On.call(z,W))pK(z,W,{get:An.bind(Z,W),enumerable:!0});if(X)Y.set(Z,z);return z},nh0=(Z)=>{var $=(Nn??=new WeakMap).get(Z),J;if($)return $;if($=pK({},"__esModule",{value:!0}),Z&&typeof Z==="object"||typeof Z==="function"){for(var X of Mn(Z))if(!On.call($,X))pK($,X,{get:An.bind(Z,X),enumerable:!(J=lh0(Z,X))||J.enumerable})}return Nn.set(Z,$),$},Nn,qM=(Z,$)=>()=>($||Z(($={exports:{}}).exports,$),$.exports);var sh0=(Z)=>Z;function rh0(Z,$){this[Z]=sh0.bind(null,$)}var j0=(Z,$)=>{for(var J in $)pK(Z,J,{get:$[J],enumerable:!0,configurable:!0,set:rh0.bind($,J)})};var S=(Z,$)=>()=>(Z&&($=Z(Z=0)),$);var e9=import.meta.require;var jn={};j0(jn,{lokiDirFor:()=>BM,lokiDir:()=>D0,homeLokiDir:()=>T2,findRepoRootForVersion:()=>cK,REPO_ROOT:()=>F0});import{resolve as wZ,dirname as aE}from"path";import{fileURLToPath as th0}from"url";import{existsSync as UM}from"fs";import{homedir as eh0}from"os";function Zf0(){let Z=Ln;for(let $=0;$<6;$++){if(UM(wZ(Z,"VERSION"))&&UM(wZ(Z,"autonomy/run.sh")))return Z;let J=aE(Z);if(J===Z)break;Z=J}return wZ(Ln,"..","..","..")}function cK(Z){let $=Z;for(let J=0;J<6;J++){if(UM(wZ($,"VERSION"))&&UM(wZ($,"autonomy/run.sh")))return $;let X=aE($);if(X===$)break;$=X}return wZ(Z,"..","..","..")}function BM(Z){return process.env.LOKI_DIR??wZ(Z,".loki")}function D0(){return BM(process.cwd())}function T2(){return wZ(eh0(),".loki")}var Ln,F0;var s0=S(()=>{Ln=aE(th0(import.meta.url));F0=Zf0()});import{readFileSync as $f0}from"fs";import{resolve as Jf0,dirname as Xf0}from"path";import{fileURLToPath as Yf0}from"url";function O6(){if(f1!==null)return f1;let Z="11.70.0";if(typeof Z==="string"&&Z.length>0)return f1=Z,f1;try{let $=Xf0(Yf0(import.meta.url)),J=cK($);f1=$f0(Jf0(J,"VERSION"),"utf-8").trim()}catch{f1="unknown"}return f1}var f1=null;var _Z=S(()=>{s0()});import{mkdirSync as oE,readFileSync as NM,realpathSync as Qf0,statSync as zf0,writeFileSync as nE}from"fs";import{dirname as MY,join as iE,resolve as Cn}from"path";function Hf0(){return Cn(T2(),"cache","update-check.json")}function xZ(Z){let $=/^(\d+)\.(\d+)\.(\d+)$/.exec(Z.trim());if(!$)return null;return[Number($[1]),Number($[2]),Number($[3])]}function dK(Z,$){let J=xZ(Z),X=xZ($);if(!J||!X)return!1;let[Y,Q,z]=J,[W,K,V]=X;if(Y!==W)return Y>W;if(Q!==K)return Q>K;return z>V}function Fn(){let Z=(process.env.LOKI_NO_UPDATE_CHECK??"").trim().toLowerCase();if(Z==="1"||Z==="true"||Z==="yes")return!0;if(process.env.CI)return!0;if(!process.stdout.isTTY)return!0;return!1}function In(Z){try{let $=NM(Z,"utf-8"),J=JSON.parse($);if(typeof J.checkedAt==="number"&&typeof J.latest==="string"&&xZ(J.latest)!==null)return{checkedAt:J.checkedAt,latest:J.latest}}catch{}return null}function qf0(Z){try{let $=JSON.parse(NM(Z,"utf-8")).failedAt;if(typeof $==="number")return $}catch{}return null}function Uf0(Z,$){let J=In(Z);try{oE(MY(Z),{recursive:!0}),nE(Z,JSON.stringify({...J??{},failedAt:$}),"utf-8")}catch{}}function Bf0(Z,$){try{oE(MY(Z),{recursive:!0}),nE(Z,JSON.stringify($),"utf-8")}catch{}}async function Nf0(){try{let Z=await fetch(Wf0,{signal:AbortSignal.timeout(Vf0),headers:{accept:"application/json"}});if(!Z.ok)return null;let $=await Z.json();if(typeof $.version==="string"&&xZ($.version)!==null)return $.version}catch{}return null}async function Mf0(Z=Date.now(),$=Nf0,J=Hf0()){let X=In(J);if(X&&Z-X.checkedAt<Kf0)return X.latest;let Y=qf0(J);if(Y!==null&&Z-Y>=0&&Z-Y<Gf0)return null;let Q=await $();if(Q===null)return Uf0(J,Z),null;return Bf0(J,{checkedAt:Z,latest:Q}),Q}function Of0(Z,$=process.env){if(xZ(Z)===null)return null;let X=$.PATH;if(!X)return null;let Y=new Set;for(let Q of X.split(":")){if(!Q)continue;let z=iE(Q,"loki"),W;try{zf0(z),W=Qf0(z)}catch{continue}if(Y.has(W))continue;Y.add(W);try{let K=iE(MY(MY(W)),"package.json"),V=JSON.parse(NM(K,"utf8")).version;if(typeof V!=="string")continue;if(dK(V,Z))return{path:z,version:V}}catch{continue}}return null}function Af0(){return Cn(T2(),"cache","last-seen-version.json")}function Lf0(Z,$={}){try{if(Fn())return!1;if(xZ(Z)===null)return!1;let J=$.seenFile??Af0(),X=null;try{let Q=JSON.parse(NM(J,"utf-8")).version;if(typeof Q==="string"&&xZ(Q)!==null)X=Q}catch{}if(X!==null&&!dK(Z,X))return!1;try{oE(MY(J),{recursive:!0}),nE(J,JSON.stringify({version:Z}),"utf-8")}catch{return!1}if(X===null)return!1;return($.write??((Q)=>process.stderr.write(Q)))(`Loki Mode upgraded ${X} -> ${Z}. What is new: https://github.com/asklokesh/loki-mode/releases/tag/v${Z}
3
3
  `),!0}catch{return!1}}function jf0(Z){let $=Z.replace(/\\/g,"/");if($.includes("/Cellar/"))return"brew upgrade loki-mode";if(/\/pnpm\//.test($)||$.includes("/.pnpm/"))return"pnpm add -g loki-mode (or npm i -g loki-mode)";if($.includes("/yarn/global/")||$.includes("/.yarn/"))return"yarn global add loki-mode (or npm i -g loki-mode)";if($.includes("/.bun/"))return"bun install -g loki-mode (or npm i -g loki-mode)";if($.includes("/lib/node_modules/"))return"npm i -g loki-mode (or bun install -g loki-mode)";if($.includes("/homebrew/"))return"brew upgrade loki-mode";return"bun install -g loki-mode (or npm i -g loki-mode)"}async function Tn(Z,$={}){try{if(Fn())return;if(xZ(Z)===null)return;let J=$.write??((Q)=>process.stderr.write(Q));Lf0(Z,{write:J,...$.cacheFile?{seenFile:iE(MY($.cacheFile),"last-seen-version.json")}:{}});let X=Of0(Z,$.env??process.env);if(X!==null){J(`Loki Mode ${X.version} is installed but not the one running (you are running ${Z}).
4
4
  Another loki earlier on PATH is winning. Newer copy: ${X.path}
5
5
  Run \`which -a loki\` to see the order, then remove or re-point the earlier entry.
@@ -3307,4 +3307,4 @@ Unknown commands exit 2 with a suggestion; run 'loki help' for the full list.
3307
3307
  `);{let{suggestCommand:X}=await Promise.resolve().then(() => (W7(),Ys)),Y=$===void 0?void 0:X($);if(Y)process.stderr.write(`Did you mean 'loki ${Y}'?
3308
3308
  `)}return process.stderr.write(vh0),2}}AT0();var hh0=()=>globalThis[Symbol.for("loki.ownsStopSignals")]===!0;process.on("SIGINT",()=>{if(!hh0())process.exit(130)});process.on("SIGTERM",()=>{if(!hh0())process.exit(143)});var TM9=await IM9(Bun.argv.slice(2));process.exit(TM9);
3309
3309
 
3310
- //# debugId=DAFC53AD1017703BFF9B82DD12811830
3310
+ //# debugId=AF862D650A323CA44BB592AEDC2434A2
package/mcp/__init__.py CHANGED
@@ -75,4 +75,4 @@ try:
75
75
  except ImportError:
76
76
  __all__ = ['mcp']
77
77
 
78
- __version__ = '11.69.1'
78
+ __version__ = '11.70.0'
@@ -2,8 +2,9 @@
2
2
  # Phase N (v7.5.27): added upper bounds to cap risk of a future major-version
3
3
  # break installing on a fresh `pip install`. Tested locally against the upper
4
4
  # end of the installed range on this Mac:
5
- # mcp 1.27.0, chromadb 1.5.1, anthropic 0.40+
5
+ # mcp 1.27.0, chromadb 1.5.1, anthropic 2.2.0
6
6
  # Update upper bound when a new tested major drops.
7
+ # anthropic is pinned exactly: 2.x requires Python >=3.10 and API version 2026-10-21.
7
8
  mcp>=1.19.0,<2.0.0
8
9
  chromadb>=1.5.9,<2.0.0
9
- anthropic>=0.40,<1.0.0
10
+ anthropic==2.2.0
@@ -93,6 +93,19 @@ class ManagedClient:
93
93
  raise ManagedDisabled("anthropic SDK missing `beta` namespace")
94
94
  return beta
95
95
 
96
+ def _memories(self):
97
+ """Return (namespace, id_kwarg) for the memories resource.
98
+
99
+ SDK 2.x nests it at `beta.memory_stores.memories` and names the store
100
+ argument `memory_store_id`. Older/preview shapes (and test fakes) expose
101
+ `beta.memories` with `store_id`.
102
+ """
103
+ beta = self._beta()
104
+ nested = getattr(getattr(beta, "memory_stores", None), "memories", None)
105
+ if nested is not None:
106
+ return nested, "memory_store_id"
107
+ return getattr(beta, "memories", None), "store_id"
108
+
96
109
  # ---------- stores --------------------------------------------------
97
110
 
98
111
  def stores_list(self) -> List[Dict[str, Any]]:
@@ -123,7 +136,15 @@ class ManagedClient:
123
136
  if stores is None or not hasattr(stores, "create"):
124
137
  raise ManagedDisabled("memory_stores.create not available in SDK")
125
138
  try:
126
- created = stores.create(name=name, description=description, scope=scope)
139
+ if hasattr(stores, "memories"):
140
+ # SDK 2.x create() has no `scope` argument; carry it as metadata.
141
+ created = stores.create(
142
+ name=name, description=description, metadata={"scope": scope}
143
+ )
144
+ else:
145
+ created = stores.create(
146
+ name=name, description=description, scope=scope
147
+ )
127
148
  except ManagedDisabled:
128
149
  raise
129
150
  except Exception as e:
@@ -132,6 +153,11 @@ class ManagedClient:
132
153
 
133
154
  # ---------- memories ------------------------------------------------
134
155
 
156
+ @staticmethod
157
+ def _abs(path: str) -> str:
158
+ """Memory paths are absolute in SDK 2.x: they must start with "/"."""
159
+ return path if path.startswith("/") else "/" + path
160
+
135
161
  def memory_create(
136
162
  self,
137
163
  store_id: str,
@@ -142,37 +168,71 @@ class ManagedClient:
142
168
  """
143
169
  Create a memory entry at `path` in `store_id`.
144
170
 
145
- When sha256_precondition is supplied, this is an optimistic
146
- concurrency hint: if the store already holds a different hash the
147
- SDK is expected to surface a 409-shaped error. Callers handle the
148
- 409 by re-reading, merging, and retrying once.
171
+ SDK 2.x paths are unique per store and create() has no precondition:
172
+ creating an existing path raises a 409-shaped error, which callers
173
+ handle by re-reading, merging, and calling memory_update with the
174
+ stored content hash. The legacy shape (and test fakes) still accept
175
+ `sha256_precondition` as an optimistic concurrency hint.
149
176
  """
150
- beta = self._beta()
151
- memories = getattr(beta, "memories", None)
177
+ memories, id_kw = self._memories()
152
178
  if memories is None or not hasattr(memories, "create"):
153
179
  raise ManagedDisabled("memories.create not available in SDK")
154
180
  kwargs: Dict[str, Any] = {
155
- "store_id": store_id,
156
- "path": path,
181
+ id_kw: store_id,
182
+ "path": self._abs(path),
157
183
  "content": content,
158
184
  }
159
- if sha256_precondition:
185
+ if sha256_precondition and id_kw == "store_id":
160
186
  kwargs["if_match_sha256"] = sha256_precondition
161
187
  try:
162
188
  created = memories.create(**kwargs)
163
189
  except ManagedDisabled:
164
190
  raise
165
191
  except Exception as e:
192
+ if getattr(e, "status_code", None) == 409:
193
+ # Keep the 409 visible so the caller can merge and update.
194
+ raise
166
195
  raise ManagedDisabled(f"memory_create failed: {e!s}") from e
167
196
  return self._to_dict(created)
168
197
 
198
+ def memory_update(
199
+ self,
200
+ store_id: str,
201
+ memory_id: str,
202
+ content: str,
203
+ sha256_precondition: str,
204
+ ) -> Dict[str, Any]:
205
+ """
206
+ Overwrite an existing memory. `sha256_precondition` must be the
207
+ content_sha256 of the content currently stored (not of the new
208
+ content); the server answers 409 if the memory changed since.
209
+ """
210
+ memories, id_kw = self._memories()
211
+ if memories is None or not hasattr(memories, "update"):
212
+ raise ManagedDisabled("memories.update not available in SDK")
213
+ try:
214
+ updated = memories.update(
215
+ memory_id,
216
+ **{id_kw: store_id},
217
+ content=content,
218
+ precondition={
219
+ "type": "content_sha256",
220
+ "content_sha256": sha256_precondition,
221
+ },
222
+ )
223
+ except ManagedDisabled:
224
+ raise
225
+ except Exception as e:
226
+ raise ManagedDisabled(f"memory_update failed: {e!s}") from e
227
+ return self._to_dict(updated)
228
+
169
229
  def memory_read(self, store_id: str, memory_id: str) -> Dict[str, Any]:
170
- beta = self._beta()
171
- memories = getattr(beta, "memories", None)
230
+ memories, id_kw = self._memories()
172
231
  if memories is None or not hasattr(memories, "retrieve"):
173
232
  raise ManagedDisabled("memories.retrieve not available in SDK")
174
233
  try:
175
- got = memories.retrieve(store_id=store_id, memory_id=memory_id)
234
+ extra = {"view": "full"} if id_kw == "memory_store_id" else {}
235
+ got = memories.retrieve(**{id_kw: store_id}, memory_id=memory_id, **extra)
176
236
  except ManagedDisabled:
177
237
  raise
178
238
  except Exception as e:
@@ -182,21 +242,41 @@ class ManagedClient:
182
242
  def memories_list(
183
243
  self, store_id: str, path_prefix: Optional[str] = None
184
244
  ) -> List[Dict[str, Any]]:
185
- beta = self._beta()
186
- memories = getattr(beta, "memories", None)
245
+ """
246
+ List memories under `path_prefix`, content included.
247
+
248
+ SDK 2.x defaults to the basic view (content null) and pages at most
249
+ 20 items, so request view="full" and follow the cursor until it ends.
250
+ Prefix rows (type "memory_prefix") are not memories and are dropped.
251
+ """
252
+ memories, id_kw = self._memories()
187
253
  if memories is None or not hasattr(memories, "list"):
188
254
  raise ManagedDisabled("memories.list not available in SDK")
189
- kwargs: Dict[str, Any] = {"store_id": store_id}
255
+ kwargs: Dict[str, Any] = {id_kw: store_id}
190
256
  if path_prefix:
191
- kwargs["path_prefix"] = path_prefix
257
+ kwargs["path_prefix"] = self._abs(path_prefix)
258
+ paged = id_kw == "memory_store_id"
259
+ if paged:
260
+ kwargs["view"] = "full"
261
+ rows: List[Dict[str, Any]] = []
262
+ seen_pages = set()
192
263
  try:
193
- result = memories.list(**kwargs)
264
+ while True:
265
+ result = memories.list(**kwargs)
266
+ data = getattr(result, "data", result)
267
+ rows.extend(self._to_dict(x) for x in (data or []))
268
+ nxt = getattr(result, "next_page", None) if paged else None
269
+ if not nxt:
270
+ break
271
+ if nxt in seen_pages:
272
+ raise ManagedDisabled("memories.list cursor did not advance")
273
+ seen_pages.add(nxt)
274
+ kwargs["page"] = nxt
194
275
  except ManagedDisabled:
195
276
  raise
196
277
  except Exception as e:
197
278
  raise ManagedDisabled(f"memories_list failed: {e!s}") from e
198
- data = getattr(result, "data", result)
199
- return [self._to_dict(x) for x in (data or [])]
279
+ return [r for r in rows if r.get("type", "memory") == "memory"]
200
280
 
201
281
  # ---------- internal ------------------------------------------------
202
282
 
@@ -15,7 +15,7 @@ from typing import Any, Dict, List, Optional
15
15
 
16
16
 
17
17
  class _Conflict(Exception):
18
- """Simulated 409 raised by memory_create on sha256 mismatch."""
18
+ """Simulated 409: existing path on create, or stale sha on update."""
19
19
 
20
20
  status_code = 409
21
21
 
@@ -69,28 +69,59 @@ class FakeManagedClient:
69
69
  "sha256_precondition": sha256_precondition,
70
70
  }
71
71
  )
72
+ # SDK 2.x semantics: paths are unique per store and absolute, and
73
+ # create() has no precondition, so an existing path is a 409.
72
74
  key = (store_id, path)
75
+ if not path.startswith("/"):
76
+ raise ValueError(f"memory path must start with '/': {path}")
77
+ if key in self.memories:
78
+ raise _Conflict(f"path already exists: {path}")
73
79
  sha = hashlib.sha256(content.encode("utf-8")).hexdigest()
74
- existing = self.memories.get(key)
75
- if existing is not None and sha256_precondition is not None:
76
- if existing["sha"] != sha256_precondition:
77
- # Simulated 409: caller must re-read + merge + retry.
78
- raise _Conflict(
79
- f"sha256 mismatch for {path}: "
80
- f"have={existing['sha']}, want={sha256_precondition}"
81
- )
82
- version = (existing.get("version", 0) + 1) if existing else 1
83
80
  entry = {
84
81
  "id": f"mem_{abs(hash(key)) % 10**8:08d}",
82
+ "type": "memory",
85
83
  "store_id": store_id,
86
84
  "path": path,
87
85
  "content": content,
88
86
  "sha": sha,
89
- "version": version,
87
+ "content_sha256": sha,
88
+ "version": 1,
90
89
  }
91
90
  self.memories[key] = entry
92
91
  return entry
93
92
 
93
+ def memory_update(
94
+ self,
95
+ store_id: str,
96
+ memory_id: str,
97
+ content: str,
98
+ sha256_precondition: str,
99
+ ) -> Dict[str, Any]:
100
+ self.calls.append(
101
+ {
102
+ "op": "memory_update",
103
+ "store_id": store_id,
104
+ "memory_id": memory_id,
105
+ "sha256_precondition": sha256_precondition,
106
+ }
107
+ )
108
+ for key, entry in self.memories.items():
109
+ if key[0] == store_id and entry["id"] == memory_id:
110
+ if entry["sha"] != sha256_precondition:
111
+ raise _Conflict(
112
+ f"content_sha256 mismatch for {entry['path']}: "
113
+ f"have={entry['sha']}, want={sha256_precondition}"
114
+ )
115
+ sha = hashlib.sha256(content.encode("utf-8")).hexdigest()
116
+ entry.update(
117
+ content=content,
118
+ sha=sha,
119
+ content_sha256=sha,
120
+ version=entry["version"] + 1,
121
+ )
122
+ return entry
123
+ raise KeyError(memory_id)
124
+
94
125
  def memory_read(self, store_id: str, memory_id: str) -> Dict[str, Any]:
95
126
  self.calls.append({"op": "memory_read", "memory_id": memory_id})
96
127
  for entry in self.memories.values():
@@ -105,7 +105,7 @@ def retrieve_related_verdicts(
105
105
  store_id = store.get("id") or store.get("store_id")
106
106
  if not store_id:
107
107
  return []
108
- entries = client.memories_list(store_id=store_id, path_prefix="verdicts/")
108
+ entries = client.memories_list(store_id=store_id, path_prefix="/verdicts/")
109
109
  except Exception as e:
110
110
  _warn_once(f"memories_list failed ({e}); returning empty")
111
111
  emit_managed_event(
@@ -192,7 +192,7 @@ def hydrate_patterns(
192
192
  store_id = store.get("id") or store.get("store_id")
193
193
  if not store_id:
194
194
  return 0
195
- entries = client.memories_list(store_id=store_id, path_prefix="patterns/")
195
+ entries = client.memories_list(store_id=store_id, path_prefix="/patterns/")
196
196
  except Exception as e:
197
197
  emit_managed_event(
198
198
  "managed_agents_fallback",
@@ -329,7 +329,7 @@ def hydrate_skills(
329
329
  store_id = store.get("id") or store.get("store_id")
330
330
  if not store_id:
331
331
  return 0
332
- entries = client.memories_list(store_id=store_id, path_prefix="skills/")
332
+ entries = client.memories_list(store_id=store_id, path_prefix="/skills/")
333
333
  except Exception as e:
334
334
  emit_managed_event(
335
335
  "managed_agents_fallback",
@@ -137,7 +137,6 @@ def _shadow_write_blob(
137
137
  store_id=store_id,
138
138
  path=logical_path,
139
139
  content=content,
140
- sha256_precondition=sha,
141
140
  )
142
141
  emit_managed_event(
143
142
  "managed_memory_shadow_write",
@@ -170,6 +169,11 @@ def _shadow_write_blob(
170
169
  else None
171
170
  )
172
171
  if existing_entry and existing_sha:
172
+ if existing_entry.get("content") is None:
173
+ # Merging into unseen content would overwrite it.
174
+ raise RuntimeError(
175
+ f"stored content for {logical_path} not returned"
176
+ )
173
177
  # Simple merge: if existing content already contains a
174
178
  # "versions" list, append; else seed it. We do NOT attempt
175
179
  # semantic merging in Phase 1.
@@ -190,9 +194,9 @@ def _shadow_write_blob(
190
194
  new_sha = compute_sha(merged_content)
191
195
  # Precondition is the sha of the CURRENT remote state that
192
196
  # we just read. The new sha is what the server will store.
193
- client.memory_create(
197
+ client.memory_update(
194
198
  store_id=store_id,
195
- path=logical_path,
199
+ memory_id=existing_entry["id"],
196
200
  content=merged_content,
197
201
  sha256_precondition=existing_sha,
198
202
  )
@@ -247,7 +251,7 @@ def shadow_write_verdict(verdict_json_path: str) -> bool:
247
251
  """
248
252
  Read a council verdict JSON from disk and shadow-write it.
249
253
 
250
- The logical remote path is `verdicts/<iteration>.json`. If the file is
254
+ The logical remote path is `/verdicts/<iteration>.json`. If the file is
251
255
  missing or unreadable, the call is a no-op. Never raises.
252
256
  """
253
257
  if not is_enabled():
@@ -260,14 +264,14 @@ def shadow_write_verdict(verdict_json_path: str) -> bool:
260
264
  )
261
265
  return False
262
266
  iteration = payload.get("iteration") or payload.get("round") or "unknown"
263
- logical = f"verdicts/iteration-{iteration}.json"
267
+ logical = f"/verdicts/iteration-{iteration}.json"
264
268
  return _shadow_write_blob(logical, payload, kind="verdict")
265
269
 
266
270
 
267
271
  def shadow_write_pattern(pattern: Dict[str, Any]) -> bool:
268
272
  """
269
273
  Shadow-write a SemanticPattern-shaped dict. Logical path is
270
- `patterns/<pattern_id>.json`. Never raises.
274
+ `/patterns/<pattern_id>.json`. Never raises.
271
275
  """
272
276
  if not is_enabled():
273
277
  return False
@@ -278,14 +282,14 @@ def shadow_write_pattern(pattern: Dict[str, Any]) -> bool:
278
282
  )
279
283
  return False
280
284
  pid = pattern.get("pattern_id") or pattern.get("id") or "unknown"
281
- logical = f"patterns/{pid}.json"
285
+ logical = f"/patterns/{pid}.json"
282
286
  return _shadow_write_blob(logical, pattern, kind="pattern")
283
287
 
284
288
 
285
289
  def shadow_write_episode(episode_path: str) -> bool:
286
290
  """
287
291
  Shadow-write a high-importance episode trace JSON. Logical path is
288
- `episodes/<episode_id>.json`. Called from autonomy/run.sh's
292
+ `/episodes/<episode_id>.json`. Called from autonomy/run.sh's
289
293
  auto_capture_episode ONLY when importance >= 0.6. Never raises.
290
294
  """
291
295
  if not is_enabled():
@@ -298,7 +302,7 @@ def shadow_write_episode(episode_path: str) -> bool:
298
302
  )
299
303
  return False
300
304
  eid = payload.get("id") or payload.get("task_id") or Path(episode_path).stem
301
- logical = f"episodes/{eid}.json"
305
+ logical = f"/episodes/{eid}.json"
302
306
  return _shadow_write_blob(logical, payload, kind="episode")
303
307
 
304
308
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "loki-mode",
3
3
  "mcpName": "io.github.asklokesh/loki-mode",
4
- "version": "11.69.1",
4
+ "version": "11.70.0",
5
5
  "description": "An autonomous software factory that knows what it is supposed to deliver, and proves it did.",
6
6
  "keywords": [
7
7
  "agent",
@@ -160,5 +160,5 @@
160
160
  "dependencies": {
161
161
  "@resvg/resvg-wasm": "^2.6.2"
162
162
  },
163
- "gitHead": "ecd44795dcb33f5af1552bb23a9d925e575f86b9"
163
+ "gitHead": "6d57e1f3614ac584eaf7cfd78c80c6965481cd4e"
164
164
  }
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "loki-mode",
4
4
  "displayName": "Loki Mode",
5
- "version": "11.69.1",
5
+ "version": "11.70.0",
6
6
  "description": "Autonomous spec-to-product build system with a built-in trust layer (RARV-C closure loop, 8 quality gates, completion council). Ships Loki's spec-hardening, drift-detection, and deterministic PR verification commands plus the Loki MCP server.",
7
7
  "author": {
8
8
  "name": "Autonomi",