pi-fabric 0.103.1 → 0.104.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 (52) hide show
  1. package/README.md +7 -6
  2. package/dist/agents.js +3 -3
  3. package/dist/chunks/{chunk-GTRYXHRD.js → chunk-25LXRT3G.js} +4 -4
  4. package/dist/chunks/{chunk-CK3M6NTX.js → chunk-5LKJU6VU.js} +2 -2
  5. package/dist/chunks/{chunk-W63E3WEO.js → chunk-CXTTLBUV.js} +3 -2
  6. package/dist/chunks/chunk-CXTTLBUV.js.map +7 -0
  7. package/dist/chunks/{chunk-D7H5ZV7L.js → chunk-DRZDTBPZ.js} +8 -8
  8. package/dist/chunks/{chunk-OQIP5U4G.js → chunk-HTDX5H7Q.js} +10 -10
  9. package/dist/chunks/{chunk-O6VCKVCU.js → chunk-QABAX5PC.js} +4 -4
  10. package/dist/chunks/{chunk-CWVUV5YO.js → chunk-SV7ONATB.js} +2 -2
  11. package/dist/cli/decisions.js +2 -2
  12. package/dist/cli/mesh.js +3 -3
  13. package/dist/durable.d.ts +38 -0
  14. package/dist/durable.d.ts.map +1 -0
  15. package/dist/durable.js +274 -0
  16. package/dist/durable.js.map +7 -0
  17. package/dist/fabric-runtime-state.js +57 -57
  18. package/dist/index.js +119 -57
  19. package/dist/index.js.map +3 -3
  20. package/dist/jev/observation.js +3 -3
  21. package/dist/jev.js +4 -4
  22. package/dist/providers/jev-provider.js +4 -4
  23. package/dist/residency/host.js +5 -5
  24. package/dist/residency/pi-entry.js +5 -5
  25. package/dist/ui/controller.d.ts.map +1 -1
  26. package/dist/ui/conversation-chrome.d.ts +2 -0
  27. package/dist/ui/conversation-chrome.d.ts.map +1 -1
  28. package/dist/ui/conversation-chrome.js +1 -1
  29. package/dist/ui/conversation-latest.d.ts +10 -0
  30. package/dist/ui/conversation-latest.d.ts.map +1 -0
  31. package/dist/ui/conversation-scrollbar.d.ts +22 -0
  32. package/dist/ui/conversation-scrollbar.d.ts.map +1 -0
  33. package/dist/ui/conversation.d.ts +2 -0
  34. package/dist/ui/conversation.d.ts.map +1 -1
  35. package/dist/ui/conversation.js +228 -23
  36. package/dist/ui/conversation.js.map +4 -4
  37. package/dist/ui/dashboard.js +9 -9
  38. package/dist/ui/retention.d.ts +14 -0
  39. package/dist/ui/retention.d.ts.map +1 -0
  40. package/dist/ui/widget.d.ts +4 -2
  41. package/dist/ui/widget.d.ts.map +1 -1
  42. package/docs/agents.md +2 -0
  43. package/docs/durable-pi.md +186 -0
  44. package/docs/interface.md +3 -1
  45. package/package.json +17 -1
  46. package/dist/chunks/chunk-W63E3WEO.js.map +0 -7
  47. /package/dist/chunks/{chunk-GTRYXHRD.js.map → chunk-25LXRT3G.js.map} +0 -0
  48. /package/dist/chunks/{chunk-CK3M6NTX.js.map → chunk-5LKJU6VU.js.map} +0 -0
  49. /package/dist/chunks/{chunk-D7H5ZV7L.js.map → chunk-DRZDTBPZ.js.map} +0 -0
  50. /package/dist/chunks/{chunk-OQIP5U4G.js.map → chunk-HTDX5H7Q.js.map} +0 -0
  51. /package/dist/chunks/{chunk-O6VCKVCU.js.map → chunk-QABAX5PC.js.map} +0 -0
  52. /package/dist/chunks/{chunk-CWVUV5YO.js.map → chunk-SV7ONATB.js.map} +0 -0
package/README.md CHANGED
@@ -102,8 +102,8 @@ pi install git:github.com/monotykamary/pi-fabric
102
102
  From a local checkout:
103
103
 
104
104
  ```bash
105
- pnpm install
106
- pnpm build
105
+ bun install
106
+ bun run build
107
107
  pi install /absolute/path/to/pi-fabric
108
108
  ```
109
109
 
@@ -160,6 +160,7 @@ See the [interface & commands reference](docs/interface.md) for every view, keyb
160
160
  - [Execution kernels](docs/kernels.md): exclusive TypeScript/Python selection, Monty sandboxing, CPython escape hatch, agent inheritance, and examples.
161
161
  - [Prompt cache](docs/prompt-cache.md): honest cache observations and optional, time-bounded native warming leases.
162
162
  - [Thinking control](docs/thinking.md): scoped host-session reasoning effort with configured bounds that children inherit and never widen.
163
+ - [Optional durable Pi runner](docs/durable-pi.md): explicit hosted-backend registration, persistent submissions, recovery boundaries, and unchanged default runner behavior.
163
164
  - [Durable decisions](docs/decisions.md): pending approvals and questions in the project mesh, headless approvals, routed child dialogs, `/fabric decisions`, and the `pi-fabric decisions` CLI.
164
165
  - [Saved programs](docs/programs.md): content-addressed programs, nested `programs.run` with the caller's capabilities, `/fabric programs`, and host runs through `/fabric run` or an event.
165
166
  - [Memory & recall](docs/memory-recall.md): compact ranked hits, uniform follow calls, lossless expansion, and guest-local `memory.walk` computation.
@@ -181,10 +182,10 @@ See the [interface & commands reference](docs/interface.md) for every view, keyb
181
182
  ## Development
182
183
 
183
184
  ```bash
184
- pnpm install
185
- pnpm typecheck
186
- pnpm test
187
- pnpm build
185
+ bun install
186
+ bun run check:fast
187
+ bun run test:smoke
188
+ bun run build
188
189
  ```
189
190
 
190
191
  The test suite covers:
package/dist/agents.js CHANGED
@@ -1,10 +1,10 @@
1
- import {
2
- validateAgentResult
3
- } from "./chunks/chunk-3QECYQGA.js";
4
1
  import {
5
2
  AGENTS_ACTION_DESCRIPTORS,
6
3
  normalizeAgentRunRequest
7
4
  } from "./chunks/chunk-V7R3NVOS.js";
5
+ import {
6
+ validateAgentResult
7
+ } from "./chunks/chunk-3QECYQGA.js";
8
8
  import {
9
9
  AgentAdmission,
10
10
  assertAgentTask,
@@ -1,3 +1,6 @@
1
+ import {
2
+ processSender
3
+ } from "./chunk-JKQARSWR.js";
1
4
  import {
2
5
  readJsonlPage
3
6
  } from "./chunk-CGPK5WHX.js";
@@ -7,9 +10,6 @@ import {
7
10
  lockOwnerLiveness,
8
11
  writeFileAtomic
9
12
  } from "./chunk-ITVLKXCI.js";
10
- import {
11
- processSender
12
- } from "./chunk-JKQARSWR.js";
13
13
 
14
14
  // src/mesh/store.ts
15
15
  import { randomUUID } from "node:crypto";
@@ -1326,4 +1326,4 @@ export {
1326
1326
  MESH_MAX_SCHEDULE_AHEAD_MS,
1327
1327
  MeshStore
1328
1328
  };
1329
- //# sourceMappingURL=chunk-GTRYXHRD.js.map
1329
+ //# sourceMappingURL=chunk-25LXRT3G.js.map
@@ -5,7 +5,7 @@ import {
5
5
  import {
6
6
  checkObserve,
7
7
  jevObserveSchema
8
- } from "./chunk-O6VCKVCU.js";
8
+ } from "./chunk-QABAX5PC.js";
9
9
  import {
10
10
  checkSchema,
11
11
  checkValue,
@@ -512,4 +512,4 @@ export {
512
512
  JEV_ACTION_DESCRIPTORS,
513
513
  JevProvider
514
514
  };
515
- //# sourceMappingURL=chunk-CK3M6NTX.js.map
515
+ //# sourceMappingURL=chunk-5LKJU6VU.js.map
@@ -19,7 +19,8 @@ function readConversationAppearance(cwd, agentDir, projectTrusted) {
19
19
  hideThinkingBlock: settings.getHideThinkingBlock(),
20
20
  showImages: settings.getShowImages(),
21
21
  imageWidthCells: settings.getImageWidthCells(),
22
- copyOnSelect: settings.getFullscreenCopyOnSelect()
22
+ copyOnSelect: settings.getFullscreenCopyOnSelect(),
23
+ scrollbar: settings.getFullscreenScrollbar()
23
24
  };
24
25
  }
25
26
  var footerPath = (cwd) => {
@@ -62,4 +63,4 @@ export {
62
63
  readConversationAppearance,
63
64
  conversationFooter
64
65
  };
65
- //# sourceMappingURL=chunk-W63E3WEO.js.map
66
+ //# sourceMappingURL=chunk-CXTTLBUV.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/ui/conversation-chrome.ts"],
4
+ "sourcesContent": ["import { homedir } from \"node:os\";\nimport path from \"node:path\";\nimport type { Theme } from \"@earendil-works/pi-coding-agent\";\nimport type { ScrollViewScrollbar } from \"@earendil-works/pi-tui\";\nimport { getConversationHost } from \"./conversation-host.js\";\nimport { truncateToWidth, visibleWidth } from \"@earendil-works/pi-tui\";\nimport { formatTokens, safeText } from \"./format.js\";\nimport type { FabricConversationTarget } from \"./conversation.js\";\n\nexport interface FabricConversationAppearance {\n editorPaddingX: number;\n outputPad: 0 | 1;\n codeBlockIndent?: string;\n hideThinkingBlock?: boolean;\n showImages?: boolean;\n imageWidthCells?: number;\n copyOnSelect?: boolean;\n scrollbar?: ScrollViewScrollbar;\n}\n\n/** Read the host's public Pi settings without changing its editor or footer. */\nexport function readConversationAppearance(cwd: string, agentDir: string, projectTrusted: boolean): FabricConversationAppearance {\n const settings = getConversationHost().SettingsManager.create(cwd, agentDir, { projectTrusted });\n return {\n editorPaddingX: settings.getEditorPaddingX(), outputPad: settings.getOutputPad(),\n codeBlockIndent: settings.getCodeBlockIndent(), hideThinkingBlock: settings.getHideThinkingBlock(),\n showImages: settings.getShowImages(), imageWidthCells: settings.getImageWidthCells(),\n copyOnSelect: settings.getFullscreenCopyOnSelect(),\n scrollbar: settings.getFullscreenScrollbar(),\n };\n}\n\nconst footerPath = (cwd: string): string => {\n const home = homedir();\n if (cwd === home) return \"~\";\n return cwd.startsWith(`${home}${path.sep}`) ? `~${cwd.slice(home.length)}` : cwd;\n};\n\n/** Native-shaped footer backed by this participant, never by Main's session stats. */\nexport function conversationFooter(target: FabricConversationTarget | undefined, theme: Theme, width: number): string[] {\n if (!target || width <= 0) return [];\n const location = target.cwd ? footerPath(target.cwd) : \"cwd unavailable\";\n const branch = target.branch ? ` (${target.branch})` : \"\";\n const pwd = safeText(`${location}${branch} \u2022 ${target.name}`);\n const access = target.readOnlyReason ? theme.fg(\"warning\", \"read-only\") : \"\";\n const separator = theme.fg(\"dim\", \" \u2022 \");\n const locationWidth = width - (access ? visibleWidth(separator) + visibleWidth(access) : 0);\n const pathLine = locationWidth > 0\n ? theme.fg(\"dim\", truncateToWidth(pwd, locationWidth, \"\u2026\")) + (access ? separator + access : \"\")\n : truncateToWidth(access, width, \"\u2026\");\n const stats = [safeText(target.status)];\n if (target.usage) {\n const usage = target.usage;\n // Agent usage is run-scoped; actor values describe the current/last activation.\n stats.push(\"run\", `\u2191${formatTokens(usage.input)}`, `\u2193${formatTokens(usage.output)}`);\n if (usage.cacheRead > 0) stats.push(`R${formatTokens(usage.cacheRead)}`);\n if (usage.cacheWrite > 0) stats.push(`W${formatTokens(usage.cacheWrite)}`);\n if (Number.isFinite(usage.cost)) stats.push(`$${usage.cost.toFixed(3)}`);\n } else {\n stats.push(\"usage unavailable\");\n }\n // Cumulative run tokens are not current context occupancy. Do not invent a percentage.\n stats.push(target.contextWindow ? `?/${formatTokens(target.contextWindow)} ctx` : \"ctx ?\");\n const model = safeText(target.model ?? `${target.runner ?? \"\"} model unavailable`).trim();\n const thinking = target.thinking ? ` \u2022 ${safeText(target.thinking === \"off\" ? \"thinking off\" : target.thinking)}` : \"\";\n const left = truncateToWidth(stats.join(\" \"), width, \"\u2026\");\n const right = truncateToWidth(`${model}${thinking}`, Math.max(0, width - visibleWidth(left) - 2), \"\");\n const gap = \" \".repeat(Math.max(0, width - visibleWidth(left) - visibleWidth(right)));\n return [\n pathLine,\n theme.fg(\"dim\", `${left}${gap}${right}`),\n ];\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -19,6 +19,13 @@ import {
19
19
  isInside,
20
20
  readWritePolicy
21
21
  } from "./chunk-2USMWEE6.js";
22
+ import {
23
+ launchScope,
24
+ normalizeScope,
25
+ processSender,
26
+ senderStamp,
27
+ senderTrusted
28
+ } from "./chunk-JKQARSWR.js";
22
29
  import {
23
30
  peerLabelPrefix,
24
31
  readChildToolAllowlist
@@ -82,13 +89,6 @@ import {
82
89
  isFabricRunnerId,
83
90
  requireAgentRunner
84
91
  } from "./chunk-4RTHQGES.js";
85
- import {
86
- launchScope,
87
- normalizeScope,
88
- processSender,
89
- senderStamp,
90
- senderTrusted
91
- } from "./chunk-JKQARSWR.js";
92
92
 
93
93
  // src/core/model-usage.ts
94
94
  import { readFileSync } from "node:fs";
@@ -7743,4 +7743,4 @@ export {
7743
7743
  agentParticipantRecords,
7744
7744
  actorParticipantRecord
7745
7745
  };
7746
- //# sourceMappingURL=chunk-D7H5ZV7L.js.map
7746
+ //# sourceMappingURL=chunk-DRZDTBPZ.js.map
@@ -8,25 +8,28 @@ import {
8
8
  agentParticipantRecords,
9
9
  loadModelUsage,
10
10
  useBudgetLedger
11
- } from "./chunk-D7H5ZV7L.js";
11
+ } from "./chunk-DRZDTBPZ.js";
12
12
  import {
13
13
  MeshStore
14
- } from "./chunk-GTRYXHRD.js";
14
+ } from "./chunk-25LXRT3G.js";
15
+ import {
16
+ launchScope
17
+ } from "./chunk-JKQARSWR.js";
15
18
  import {
16
19
  RESIDENT_HOST_FORMAT,
17
20
  residentDeliveryPrefix,
18
21
  residentHostId
19
22
  } from "./chunk-NTC2MCB6.js";
23
+ import {
24
+ parseFabricOwnedModelGuidance,
25
+ resolveFabricModelGuidance
26
+ } from "./chunk-UKMQK5VP.js";
20
27
  import {
21
28
  ownerHeartbeatFields,
22
29
  recordOwnerLiveness,
23
30
  startOwnerHeartbeat,
24
31
  writeJsonAtomic
25
32
  } from "./chunk-ITVLKXCI.js";
26
- import {
27
- parseFabricOwnedModelGuidance,
28
- resolveFabricModelGuidance
29
- } from "./chunk-UKMQK5VP.js";
30
33
  import {
31
34
  lifecycleSourceIdentity
32
35
  } from "./chunk-KJZAOENY.js";
@@ -34,9 +37,6 @@ import {
34
37
  normalizeModelAliases,
35
38
  resolveAvailablePiModel
36
39
  } from "./chunk-ZLNJDTCN.js";
37
- import {
38
- launchScope
39
- } from "./chunk-JKQARSWR.js";
40
40
 
41
41
  // src/residency/host.ts
42
42
  import { randomUUID } from "node:crypto";
@@ -815,4 +815,4 @@ export {
815
815
  residentIdleDecision,
816
816
  runResidentHostFromConfigPath
817
817
  };
818
- //# sourceMappingURL=chunk-OQIP5U4G.js.map
818
+ //# sourceMappingURL=chunk-HTDX5H7Q.js.map
@@ -1,9 +1,9 @@
1
- import {
2
- object
3
- } from "./chunk-WHBJDTCR.js";
4
1
  import {
5
2
  prepareFabricActorHostPayload
6
3
  } from "./chunk-CGL7QP4A.js";
4
+ import {
5
+ object
6
+ } from "./chunk-WHBJDTCR.js";
7
7
  import {
8
8
  validationMessage
9
9
  } from "./chunk-SS7FVARK.js";
@@ -283,4 +283,4 @@ export {
283
283
  JevObservationHost,
284
284
  JevSubscription
285
285
  };
286
- //# sourceMappingURL=chunk-O6VCKVCU.js.map
286
+ //# sourceMappingURL=chunk-QABAX5PC.js.map
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  validateMeshTopic
3
- } from "./chunk-GTRYXHRD.js";
3
+ } from "./chunk-25LXRT3G.js";
4
4
  import {
5
5
  writeFileAtomic
6
6
  } from "./chunk-ITVLKXCI.js";
@@ -232,4 +232,4 @@ export {
232
232
  meshPostCommand,
233
233
  wrapDurableNotifyScript
234
234
  };
235
- //# sourceMappingURL=chunk-CWVUV5YO.js.map
235
+ //# sourceMappingURL=chunk-SV7ONATB.js.map
@@ -4,10 +4,10 @@ import {
4
4
  } from "../chunks/chunk-FSB2VIRZ.js";
5
5
  import {
6
6
  MeshStore
7
- } from "../chunks/chunk-GTRYXHRD.js";
7
+ } from "../chunks/chunk-25LXRT3G.js";
8
+ import "../chunks/chunk-JKQARSWR.js";
8
9
  import "../chunks/chunk-CGPK5WHX.js";
9
10
  import "../chunks/chunk-ITVLKXCI.js";
10
- import "../chunks/chunk-JKQARSWR.js";
11
11
 
12
12
  // src/cli/decisions.ts
13
13
  import fs from "node:fs";
package/dist/cli/mesh.js CHANGED
@@ -2,13 +2,13 @@ import {
2
2
  MESH_EXTERNAL_DATA_MAX_BYTES,
3
3
  MESH_GRANT_TOKEN_ENV,
4
4
  postWithMeshGrant
5
- } from "../chunks/chunk-CWVUV5YO.js";
5
+ } from "../chunks/chunk-SV7ONATB.js";
6
6
  import {
7
7
  MeshStore
8
- } from "../chunks/chunk-GTRYXHRD.js";
8
+ } from "../chunks/chunk-25LXRT3G.js";
9
+ import "../chunks/chunk-JKQARSWR.js";
9
10
  import "../chunks/chunk-CGPK5WHX.js";
10
11
  import "../chunks/chunk-ITVLKXCI.js";
11
- import "../chunks/chunk-JKQARSWR.js";
12
12
 
13
13
  // src/cli/mesh.ts
14
14
  import fs from "node:fs";
@@ -0,0 +1,38 @@
1
+ import type { HarnessOptions, ModelRef, Registry, Storage } from "@earendil-works/pi-durable";
2
+ import type { FabricHostedRunner } from "./agents/runner-registry.js";
3
+ /** The host must hold exclusive ownership until release, including across processes. */
4
+ export interface PiDurableStorageLease {
5
+ storage: Storage;
6
+ release(): void | Promise<void>;
7
+ }
8
+ export type PiDurableStorageOptions = {
9
+ kind: "jsonl";
10
+ directory: string;
11
+ } | {
12
+ kind: "factory";
13
+ identity: string;
14
+ acquire(runId: string): Promise<PiDurableStorageLease>;
15
+ };
16
+ export interface PiDurableRunnerOptions {
17
+ id?: string;
18
+ label?: string;
19
+ residentModule?: string;
20
+ models: HarnessOptions["models"];
21
+ registry: Registry;
22
+ env: NonNullable<HarnessOptions["env"]>;
23
+ allowedModels: readonly ModelRef[];
24
+ storage: PiDurableStorageOptions;
25
+ }
26
+ export interface PiDurableRunner extends FabricHostedRunner {
27
+ /** Seal this instance, join invocations and release storage; never abort work. Create a new runner to reopen. */
28
+ close(): Promise<void>;
29
+ }
30
+ export interface PiDurableLocator {
31
+ version: 1;
32
+ backend: "pi-durable";
33
+ storage: string;
34
+ runId: string;
35
+ }
36
+ /** Opt-in hosted adapter. No registration or runtime package loading occurs at factory creation. */
37
+ export declare function createPiDurableRunner(options: PiDurableRunnerOptions): PiDurableRunner;
38
+ //# sourceMappingURL=durable.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"durable.d.ts","sourceRoot":"","sources":["../src/durable.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAW,cAAc,EAAE,QAAQ,EAAE,QAAQ,EAAE,OAAO,EAAmF,MAAM,4BAA4B,CAAC;AACxL,OAAO,KAAK,EAAE,kBAAkB,EAAsE,MAAM,6BAA6B,CAAC;AAG1I,wFAAwF;AACxF,MAAM,WAAW,qBAAqB;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,OAAO,IAAI,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC;AACD,MAAM,MAAM,uBAAuB,GAC/B;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,GACpC;IAAE,IAAI,EAAE,SAAS,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAAA;CAAE,CAAC;AAClG,MAAM,WAAW,sBAAsB;IACrC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC,QAAQ,CAAC,CAAC;IACjC,QAAQ,EAAE,QAAQ,CAAC;IACnB,GAAG,EAAE,WAAW,CAAC,cAAc,CAAC,KAAK,CAAC,CAAC,CAAC;IACxC,aAAa,EAAE,SAAS,QAAQ,EAAE,CAAC;IACnC,OAAO,EAAE,uBAAuB,CAAC;CAClC;AACD,MAAM,WAAW,eAAgB,SAAQ,kBAAkB;IACzD,iHAAiH;IACjH,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AACD,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,CAAC,CAAC;IACX,OAAO,EAAE,YAAY,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAmCD,oGAAoG;AACpG,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CAmNtF"}
@@ -0,0 +1,274 @@
1
+ // src/durable.ts
2
+ import { createHash } from "node:crypto";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import { createRequire } from "node:module";
6
+ var runtimePromise;
7
+ async function runtime() {
8
+ return runtimePromise ??= Promise.resolve().then(() => {
9
+ const require2 = createRequire(import.meta.url);
10
+ for (const name of ["@earendil-works/pi-durable", "@earendil-works/chord"]) {
11
+ const metadata = require2(`${name}/package.json`);
12
+ if (metadata.version !== "1.0.0") throw new Error(`Unsupported ${name} version ${metadata.version ?? "unknown"}; expected 1.0.0`);
13
+ }
14
+ return Promise.all([import("@earendil-works/pi-durable"), import("@earendil-works/chord/context")]);
15
+ }).then(([durable, context]) => ({ durable, context })).catch((cause) => {
16
+ runtimePromise = void 0;
17
+ throw new Error(`Pi durable runner requires optional peers @earendil-works/pi-durable@1.0.0 and @earendil-works/chord@1.0.0: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
18
+ });
19
+ }
20
+ var hash = (value) => createHash("sha256").update(value).digest("hex");
21
+ var ownershipSymbol = Symbol.for("pi-fabric.durable.owners.v1");
22
+ var globals = globalThis;
23
+ var owners = globals[ownershipSymbol] ??= /* @__PURE__ */ new Set();
24
+ function createPiDurableRunner(options) {
25
+ if (!options.models || !options.registry || typeof options.env !== "function") throw new Error("Pi durable requires host models, registry and environment");
26
+ const storage = options.storage;
27
+ const directory = storage.kind === "jsonl" ? path.resolve(storage.directory) : void 0;
28
+ if (storage.kind === "factory" && (!storage.identity || typeof storage.acquire !== "function")) throw new Error("Storage factory requires identity and exclusive acquire()");
29
+ const identity = hash(storage.kind === "jsonl" ? `jsonl:${directory}` : `factory:${storage.identity}`);
30
+ const allowed = new Map(options.allowedModels.map((model) => [`${model.provider}/${model.modelId}`, { ...model }]));
31
+ if (!allowed.size) throw new Error("Pi durable requires an explicit model allowlist");
32
+ const runs = /* @__PURE__ */ new Map();
33
+ const transcriptSeen = /* @__PURE__ */ new WeakMap();
34
+ let sealed = false;
35
+ let tail = Promise.resolve();
36
+ let closing;
37
+ const serial = (fn) => {
38
+ if (sealed) return Promise.reject(new Error("Pi durable runner is closed"));
39
+ const result = tail.then(fn);
40
+ tail = result.catch(() => void 0);
41
+ return result;
42
+ };
43
+ function locator(value) {
44
+ const loc = value;
45
+ if (!loc || loc.version !== 1 || loc.backend !== "pi-durable" || loc.storage !== identity || typeof loc.runId !== "string" || !loc.runId || loc.runId.length > 512) throw new Error("Invalid Pi durable locator or storage identity");
46
+ return loc;
47
+ }
48
+ function validate(context) {
49
+ if (context.id !== context.idempotencyKey) throw new Error("Pi durable requires Fabric run ID as idempotencyKey");
50
+ if (context.residency === "durable" && !options.residentModule) throw new Error("Durable residency requires an explicit residentModule");
51
+ for (const key of ["kernel", "sessionFile", "actorId", "actorName", "writePolicy", "scope", "schema"]) {
52
+ if (context[key] !== void 0) throw new Error(`Pi durable does not support ${key}`);
53
+ }
54
+ if (context.recursive || context.images?.length) throw new Error("Pi durable does not support recursiveFabric or image input");
55
+ const model = context.model ? allowed.get(context.model) : void 0;
56
+ if (!model || !options.models.getModel(model.provider, model.modelId)) throw new Error(`Unknown or disallowed Pi durable model: ${context.model ?? "(missing explicit provider/model)"}`);
57
+ const catalogModel = options.models.getModel(model.provider, model.modelId);
58
+ if (context.thinking && (!catalogModel.reasoning && context.thinking !== "off" || catalogModel.thinkingLevelMap?.[context.thinking] === null)) throw new Error(`Unsupported thinking level: ${context.thinking}`);
59
+ const snapshot = options.registry.snapshot();
60
+ const available = new Map(snapshot.tools().map(({ tool }) => [tool.name, tool]));
61
+ const tools = context.tools.map((name) => {
62
+ let tool = available.get(name);
63
+ if (!tool) throw new Error(`Unknown Pi durable tool: ${name}`);
64
+ for (const extension of snapshot.installed()) for (const wrap of extension.wraps ?? []) {
65
+ if ("tool" in wrap && wrap.tool === name) tool = wrap.wrap(tool);
66
+ }
67
+ if (tool.name !== name) throw new Error(`Registry wrapper renamed tool ${name}`);
68
+ const implementation = tool;
69
+ return { ...implementation, replay: name === "fabric_exec" ? "unsafe" : implementation.replay ?? "unsafe", execute: async (...args) => {
70
+ const result = await implementation.execute(...args);
71
+ if (result.control?.addTools || result.control?.handoff) throw new Error("Pi durable adapter does not support tool-set changes or handoff controls");
72
+ return result;
73
+ } };
74
+ });
75
+ const fingerprint = hash(JSON.stringify({ task: context.task, cwd: context.cwd, model, tools: [...context.tools], thinking: context.thinking ?? "off", systemPrompt: context.systemPrompt ?? "" }));
76
+ return { model, tools, fingerprint };
77
+ }
78
+ async function acquire(runId, rt) {
79
+ if (storage.kind === "factory") return storage.acquire(runId);
80
+ const base = directory;
81
+ await fs.mkdir(base, { recursive: true });
82
+ const canonical = await fs.realpath(base);
83
+ const stem = hash(runId);
84
+ const lock = path.join(canonical, `${stem}.writer`);
85
+ try {
86
+ await fs.mkdir(lock);
87
+ } catch (cause) {
88
+ throw new Error(`Pi durable single-writer lock unavailable: ${lock}. Never remove a stale lock until its owner is verified dead.`, { cause });
89
+ }
90
+ try {
91
+ const { openNodeJsonlStorage } = await import("@earendil-works/pi-durable/storage/jsonl/node");
92
+ const opened = await openNodeJsonlStorage(path.join(canonical, stem), rt.context.BACKGROUND_CONTEXT, { fsync: true });
93
+ return { storage: opened, release: () => fs.rmdir(lock) };
94
+ } catch (error) {
95
+ await fs.rmdir(lock);
96
+ throw error;
97
+ }
98
+ }
99
+ async function open(runId, selected) {
100
+ const existing = runs.get(runId);
101
+ if (existing) {
102
+ if (existing.fingerprint && existing.fingerprint !== selected.fingerprint) throw new Error("Fabric run ID already bound to different work");
103
+ return existing;
104
+ }
105
+ const ownerKey = `${identity}:${runId}`;
106
+ if (owners.has(ownerKey)) throw new Error("Pi durable storage already has an active writer");
107
+ owners.add(ownerKey);
108
+ let lease;
109
+ try {
110
+ const rt = await runtime();
111
+ lease = await acquire(runId, rt);
112
+ const registry = rt.durable.createRegistry();
113
+ registry.install(rt.durable.defineExtension({ name: "fabric.durable.tools", tools: selected.tools }));
114
+ const harness = await rt.durable.Harness.open(lease.storage, { models: options.models, registry, env: options.env, settings: { compaction: { enabled: false }, retry: { enabled: false }, stream: { maxRetries: 0 } } }, rt.context.BACKGROUND_CONTEXT);
115
+ const run = { harness, lease, ownerKey, reporters: /* @__PURE__ */ new Set() };
116
+ runs.set(runId, run);
117
+ return run;
118
+ } catch (error) {
119
+ if (lease) {
120
+ try {
121
+ await lease.storage.close((await runtime()).context.BACKGROUND_CONTEXT);
122
+ await lease.release();
123
+ } catch (cleanupError) {
124
+ throw new AggregateError([error, cleanupError], "Pi durable open failed; cleanup failed, lease remains owned");
125
+ }
126
+ }
127
+ owners.delete(ownerKey);
128
+ throw error;
129
+ }
130
+ }
131
+ async function bind(value, context, reporter, admit) {
132
+ const loc = locator(value);
133
+ if (loc.runId !== context.id) throw new Error("Pi durable locator does not match Fabric run ID");
134
+ const selected = validate(context);
135
+ const rt = await runtime();
136
+ const { getSupportedThinkingLevels } = await import("@earendil-works/pi-ai/models");
137
+ if (!getSupportedThinkingLevels(options.models.getModel(selected.model.provider, selected.model.modelId)).includes(context.thinking ?? "off")) throw new Error(`Unsupported thinking level: ${context.thinking ?? "off"}`);
138
+ const ctx = rt.context.BACKGROUND_CONTEXT;
139
+ const run = await open(loc.runId, selected);
140
+ const Manifest = rt.durable.defineDoc({ kind: "fabric.durable.request", version: 1, scope: "conversation", history: "latest", fork: "initial", initial: () => ({ fingerprint: selected.fingerprint }) });
141
+ let conversation = await run.harness.conversation(rt.durable.ROOT_CONVERSATION_ID, ctx);
142
+ if (!conversation && admit) conversation = await run.harness.root(ctx, { agent: { model: selected.model, tools: selected.tools, thinkingLevel: context.thinking ?? "off", cwd: context.cwd, instructions: context.systemPrompt ?? "" }, init: async (tx, id) => {
143
+ await tx.doc(Manifest, id);
144
+ } });
145
+ if (!conversation) throw new Error("Pi durable attach found no existing conversation; outcome indeterminate (not resubmitted)");
146
+ const manifest = await run.harness.snapshot(Manifest, conversation.id, ctx);
147
+ if (manifest?.fingerprint !== selected.fingerprint) throw new Error("Fabric run ID already bound to different work or missing committed manifest");
148
+ run.fingerprint = selected.fingerprint;
149
+ run.conversation = conversation;
150
+ const found = await conversation.commit((tx) => tx.submissionByRequest(conversation.id, context.id), ctx);
151
+ let submission = found ? await run.harness.submission(found.id, ctx) : void 0;
152
+ if (!submission && admit) submission = await conversation.submit({ type: "input", content: context.task, requestId: context.id }, ctx);
153
+ if (!submission) throw new Error("Pi durable attach found no existing submission; outcome indeterminate (not resubmitted)");
154
+ run.submission = submission;
155
+ run.reporters.add(reporter);
156
+ if (!run.watch) {
157
+ run.watch = await conversation.watch(ctx);
158
+ run.watch.start(async (view) => {
159
+ for (const target of run.reporters) report(view, target);
160
+ });
161
+ }
162
+ report(run.watch.value, reporter);
163
+ if (!run.observing) {
164
+ run.observing = submission.wait(ctx).then(async (result) => {
165
+ if (sealed) return;
166
+ const view = await conversation.context(ctx);
167
+ const answer = result.type === "input" && result.status === "done" ? view.entries.find((entry) => entry.id === result.answer) : void 0;
168
+ const output = answer?.model?.flatMap((message) => message.role === "assistant" ? message.content.filter((block) => block.type === "text").map((block) => block.text) : []).join("") ?? "";
169
+ const usage = await run.harness.usage(ctx);
170
+ for (const target of run.reporters) {
171
+ target.usage(totalUsage(usage));
172
+ target.finish({ status: result.status === "done" ? "completed" : result.reason === "aborted" ? "stopped" : "failed", output: result.status === "done" ? output : `Pi durable unanswered: ${result.reason}` });
173
+ }
174
+ delete run.observing;
175
+ }).catch((error) => {
176
+ if (!sealed) for (const target of run.reporters) target.fail({ error: `Pi durable observation failed: ${String(error)}` });
177
+ });
178
+ }
179
+ }
180
+ function totalUsage(usage) {
181
+ const total = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0 };
182
+ for (const bucket of [usage.models, usage.tools]) for (const item of Object.values(bucket)) {
183
+ total.input += item.input;
184
+ total.output += item.output;
185
+ total.cacheRead += item.cacheRead;
186
+ total.cacheWrite += item.cacheWrite;
187
+ total.cost += item.cost.total;
188
+ }
189
+ return total;
190
+ }
191
+ function report(view, reporter) {
192
+ let seen = transcriptSeen.get(reporter);
193
+ if (!seen) {
194
+ seen = /* @__PURE__ */ new Set();
195
+ transcriptSeen.set(reporter, seen);
196
+ }
197
+ for (const entry of view.entries) {
198
+ if (seen.has(entry.id)) continue;
199
+ seen.add(entry.id);
200
+ for (const message of entry.model ?? []) {
201
+ if (message.role === "assistant" || message.role === "user") {
202
+ const content = typeof message.content === "string" ? message.content : message.content.filter((block) => block.type === "text").map((block) => ({ type: "text", text: block.text }));
203
+ reporter.transcript({ type: "message_end", message: { role: message.role, content } });
204
+ if (message.role === "assistant") {
205
+ for (const block of message.content) if (block.type === "toolCall") reporter.transcript({ type: "tool_execution_start", toolCallId: block.id, toolName: block.name, args: block.arguments });
206
+ }
207
+ } else if (message.role === "toolResult") reporter.transcript({ type: "tool_execution_end", toolCallId: message.toolCallId, toolName: message.toolName, result: message.content, isError: message.isError });
208
+ }
209
+ }
210
+ const assistants = view.entries.flatMap((entry) => entry.model?.filter((message) => message.role === "assistant") ?? []);
211
+ const live = view.docs["pi.live"];
212
+ const last = live?.generation?.message ?? assistants.at(-1);
213
+ const calls = assistants.flatMap((message) => message.role === "assistant" ? message.content.filter((block) => block.type === "toolCall") : []);
214
+ reporter.progress({ turns: assistants.length, toolCalls: calls.length, currentTool: live?.tools?.find((tool) => tool.status === "running")?.name ?? null, text: last?.role === "assistant" ? last.content.filter((block) => block.type === "text").map((block) => block.text).join("") : "" });
215
+ const usage = view.docs["pi.usage"];
216
+ if (usage) reporter.usage(totalUsage(usage));
217
+ }
218
+ return {
219
+ id: options.id ?? "pi-durable",
220
+ label: options.label ?? "Pi durable (opt-in)",
221
+ kind: "hosted",
222
+ ...options.residentModule ? { residentModule: options.residentModule } : {},
223
+ capabilities: Object.freeze({ recursiveFabric: false, steer: false, followUp: false, persistentSessions: false, kernels: false, handoff: false, modelDiscovery: false, imageInput: false, compaction: false, questions: false, sleep: false, writePolicy: false }),
224
+ prepare(context) {
225
+ if (sealed) throw new Error("Pi durable runner is closed");
226
+ validate(context);
227
+ return { version: 1, backend: "pi-durable", storage: identity, runId: context.id };
228
+ },
229
+ normalizeModel(model) {
230
+ if (!allowed.has(model)) throw new Error(`Unknown or disallowed Pi durable model: ${model}`);
231
+ return model;
232
+ },
233
+ mapTools(tools) {
234
+ const names = new Set(options.registry.snapshot().tools().map((item) => item.tool.name));
235
+ for (const tool of tools) if (!names.has(tool)) throw new Error(`Unknown Pi durable tool: ${tool}`);
236
+ return [...tools];
237
+ },
238
+ start: (loc, context, reporter) => serial(() => bind(loc, context, reporter, true)),
239
+ attach: (loc, context, reporter) => serial(() => bind(loc, context, reporter, false)),
240
+ liveness: (value) => serial(async () => {
241
+ const run = runs.get(locator(value).runId);
242
+ if (!run?.submission) return "unknown";
243
+ const result = await run.submission.status((await runtime()).context.BACKGROUND_CONTEXT);
244
+ return result.status === "done" ? "settled" : result.status === "unanswered" ? result.reason === "aborted" ? "cancelled" : "interrupted" : "running";
245
+ }),
246
+ stop: (value) => serial(async () => {
247
+ const run = runs.get(locator(value).runId);
248
+ if (!run?.conversation || !run.submission) return { confirmed: false };
249
+ await run.conversation.abort((await runtime()).context.BACKGROUND_CONTEXT, { background: true });
250
+ return { confirmed: true };
251
+ }),
252
+ close() {
253
+ if (closing) return closing;
254
+ sealed = true;
255
+ closing = tail.then(async () => {
256
+ const results = await Promise.allSettled([...runs].map(async ([id, run]) => {
257
+ await run.watch?.stop();
258
+ await run.harness.close((await runtime()).context.BACKGROUND_CONTEXT);
259
+ await run.observing;
260
+ await run.lease.release();
261
+ owners.delete(run.ownerKey);
262
+ runs.delete(id);
263
+ }));
264
+ const failed = results.filter((result) => result.status === "rejected");
265
+ if (failed.length) throw new AggregateError(failed.map((result) => result.reason), "Pi durable close failed; failed leases remain owned");
266
+ });
267
+ return closing;
268
+ }
269
+ };
270
+ }
271
+ export {
272
+ createPiDurableRunner
273
+ };
274
+ //# sourceMappingURL=durable.js.map