@fastagent-sh/fastagent 0.18.0 → 0.20.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 (222) hide show
  1. package/README.md +25 -7
  2. package/dist/agent.d.ts +1 -1
  3. package/dist/agent.js +1 -1
  4. package/dist/atomic-write.d.ts +23 -0
  5. package/dist/atomic-write.js +53 -0
  6. package/dist/{host/node.d.ts → channel.d.ts} +24 -28
  7. package/dist/channel.js +1 -0
  8. package/dist/channels/agentcore-limits.d.ts +9 -0
  9. package/dist/channels/agentcore-limits.js +9 -0
  10. package/dist/channels/agentcore-service.d.ts +55 -0
  11. package/dist/channels/agentcore-service.js +107 -0
  12. package/dist/channels/agentcore.d.ts +22 -20
  13. package/dist/channels/agentcore.js +20 -11
  14. package/dist/channels/busy.d.ts +1 -1
  15. package/dist/channels/busy.js +1 -1
  16. package/dist/channels/control.d.ts +55 -7
  17. package/dist/channels/control.js +312 -67
  18. package/dist/channels/discover.d.ts +59 -0
  19. package/dist/{engines/pi/channel.js → channels/discover.js} +19 -34
  20. package/dist/channels/feishu/card.js +1 -1
  21. package/dist/channels/feishu/context-buffer.d.ts +1 -1
  22. package/dist/channels/feishu/context-buffer.js +4 -4
  23. package/dist/channels/feishu/feishu-api.js +6 -9
  24. package/dist/channels/feishu/feishu.d.ts +1 -1
  25. package/dist/channels/feishu/feishu.js +41 -24
  26. package/dist/channels/feishu/invoke-turn.d.ts +1 -1
  27. package/dist/channels/feishu/invoke-turn.js +3 -4
  28. package/dist/channels/feishu/preview.d.ts +1 -1
  29. package/dist/channels/feishu/preview.js +7 -3
  30. package/dist/channels/feishu/ws-ingress.d.ts +1 -1
  31. package/dist/channels/github/github.d.ts +1 -1
  32. package/dist/channels/http.d.ts +11 -7
  33. package/dist/channels/http.js +1 -109
  34. package/dist/channels/kit/attachment-path.d.ts +12 -0
  35. package/dist/channels/kit/attachment-path.js +43 -0
  36. package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +9 -0
  37. package/dist/channels/{context-buffer.js → kit/context-buffer.js} +13 -2
  38. package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +14 -3
  39. package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +17 -6
  40. package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
  41. package/dist/channels/{seen.js → kit/seen.js} +1 -1
  42. package/dist/channels/kit/signature.d.ts +12 -0
  43. package/dist/channels/kit/signature.js +17 -0
  44. package/dist/channels/{state.js → kit/state.js} +4 -7
  45. package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
  46. package/dist/channels/{stop-command.js → kit/stop-command.js} +5 -5
  47. package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +1 -1
  48. package/dist/channels/{tasks.js → kit/tasks.js} +11 -5
  49. package/dist/channels/{thread-participants.js → kit/thread-participants.js} +1 -1
  50. package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
  51. package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +19 -3
  52. package/dist/channels/{turn-store.js → kit/turn-store.js} +30 -3
  53. package/dist/channels/lark/lark.d.ts +1 -1
  54. package/dist/channels/serve.d.ts +73 -0
  55. package/dist/channels/serve.js +243 -0
  56. package/dist/channels/slack/bot-auth.js +3 -14
  57. package/dist/channels/slack/context-buffer.d.ts +2 -2
  58. package/dist/channels/slack/context-buffer.js +2 -2
  59. package/dist/channels/slack/invoke-turn.d.ts +2 -2
  60. package/dist/channels/slack/invoke-turn.js +13 -6
  61. package/dist/channels/slack/onboard.js +5 -5
  62. package/dist/channels/slack/onboarding-state.d.ts +7 -3
  63. package/dist/channels/slack/onboarding-state.js +13 -20
  64. package/dist/channels/slack/parse.js +2 -2
  65. package/dist/channels/slack/preview.d.ts +1 -1
  66. package/dist/channels/slack/preview.js +11 -3
  67. package/dist/channels/slack/register-webhook.js +1 -1
  68. package/dist/channels/slack/scaffold/slack-send.ts +2 -17
  69. package/dist/channels/slack/slack-api.js +23 -55
  70. package/dist/channels/slack/slack.d.ts +1 -1
  71. package/dist/channels/slack/slack.js +20 -20
  72. package/dist/channels/slack/welcomed.js +1 -1
  73. package/dist/channels/telegram/context-buffer.d.ts +2 -2
  74. package/dist/channels/telegram/context-buffer.js +2 -2
  75. package/dist/channels/telegram/invoke-turn.d.ts +1 -1
  76. package/dist/channels/telegram/invoke-turn.js +1 -1
  77. package/dist/channels/telegram/parse.d.ts +21 -1
  78. package/dist/channels/telegram/parse.js +67 -13
  79. package/dist/channels/telegram/preview.d.ts +1 -1
  80. package/dist/channels/telegram/preview.js +6 -2
  81. package/dist/channels/telegram/scaffold/channel.ts +7 -3
  82. package/dist/channels/telegram/telegram-api.js +5 -6
  83. package/dist/channels/telegram/telegram.d.ts +3 -3
  84. package/dist/channels/telegram/telegram.js +60 -72
  85. package/dist/channels/telegram/turn-store.d.ts +2 -2
  86. package/dist/channels/telegram/turn-store.js +2 -2
  87. package/dist/cli/add-slack.js +6 -5
  88. package/dist/cli/commands/add.js +1 -5
  89. package/dist/cli/commands/attach.d.ts +2 -4
  90. package/dist/cli/commands/attach.js +17 -15
  91. package/dist/cli/commands/deploy.d.ts +50 -5
  92. package/dist/cli/commands/deploy.js +183 -134
  93. package/dist/cli/commands/dev.js +25 -40
  94. package/dist/cli/commands/fire.js +1 -1
  95. package/dist/cli/commands/info.js +7 -5
  96. package/dist/cli/commands/init.js +1 -2
  97. package/dist/cli/commands/schedule.js +1 -1
  98. package/dist/cli/commands/start.js +80 -121
  99. package/dist/cli/commands/tool.js +5 -7
  100. package/dist/cli/program.js +1 -1
  101. package/dist/cli/serve.d.ts +22 -70
  102. package/dist/cli/serve.js +66 -257
  103. package/dist/cli/shared.d.ts +40 -10
  104. package/dist/cli/shared.js +53 -6
  105. package/dist/core.d.ts +3 -5
  106. package/dist/core.js +7 -5
  107. package/dist/deploy/agentcore/logs.d.ts +2 -2
  108. package/dist/deploy/agentcore/logs.js +2 -2
  109. package/dist/deploy/agentcore/plan.d.ts +12 -10
  110. package/dist/deploy/agentcore/plan.js +39 -31
  111. package/dist/deploy/agentcore/run.d.ts +5 -4
  112. package/dist/deploy/agentcore/run.js +13 -36
  113. package/dist/deploy/channel-ingress.d.ts +73 -0
  114. package/dist/deploy/channel-ingress.js +101 -0
  115. package/dist/deploy/docker/plan.d.ts +3 -7
  116. package/dist/deploy/docker/plan.js +4 -15
  117. package/dist/deploy/docker/run.d.ts +18 -1
  118. package/dist/deploy/docker/run.js +10 -0
  119. package/dist/deploy/fly/plan.d.ts +4 -5
  120. package/dist/deploy/fly/plan.js +7 -22
  121. package/dist/deploy/fly/run.d.ts +9 -23
  122. package/dist/deploy/fly/run.js +32 -68
  123. package/dist/deploy/preflight.d.ts +6 -7
  124. package/dist/deploy/preflight.js +27 -23
  125. package/dist/deploy/railway/plan.d.ts +4 -5
  126. package/dist/deploy/railway/plan.js +11 -26
  127. package/dist/deploy/railway/run.d.ts +9 -9
  128. package/dist/deploy/railway/run.js +40 -40
  129. package/dist/deploy/runner.js +22 -2
  130. package/dist/deploy/secrets.d.ts +18 -9
  131. package/dist/deploy/secrets.js +64 -12
  132. package/dist/dev-supervisor.js +11 -6
  133. package/dist/engines/pi/agent-session-factory.d.ts +104 -0
  134. package/dist/engines/pi/agent-session-factory.js +295 -0
  135. package/dist/engines/pi/auth.js +43 -43
  136. package/dist/engines/pi/config.d.ts +5 -5
  137. package/dist/engines/pi/config.js +1 -1
  138. package/dist/engines/pi/create.d.ts +67 -42
  139. package/dist/engines/pi/create.js +142 -72
  140. package/dist/engines/pi/definition.d.ts +42 -8
  141. package/dist/engines/pi/definition.js +114 -11
  142. package/dist/engines/pi/invoke-session.d.ts +22 -19
  143. package/dist/engines/pi/invoke-session.js +286 -100
  144. package/dist/engines/pi/login.js +39 -20
  145. package/dist/engines/pi/models.d.ts +14 -0
  146. package/dist/engines/pi/models.js +9 -1
  147. package/dist/engines/pi/open.d.ts +10 -8
  148. package/dist/engines/pi/open.js +21 -11
  149. package/dist/engines/pi/report.d.ts +0 -4
  150. package/dist/engines/pi/report.js +0 -7
  151. package/dist/engines/pi/search-tools.js +1 -1
  152. package/dist/engines/pi/service.d.ts +23 -0
  153. package/dist/engines/pi/service.js +27 -0
  154. package/dist/engines/pi/session-builder.js +87 -118
  155. package/dist/engines/pi/session-control.d.ts +28 -14
  156. package/dist/engines/pi/session-control.js +570 -438
  157. package/dist/engines/pi/session-inheritance.d.ts +30 -0
  158. package/dist/engines/pi/session-inheritance.js +261 -0
  159. package/dist/engines/pi/session-markers.d.ts +48 -0
  160. package/dist/engines/pi/session-markers.js +59 -0
  161. package/dist/engines/pi/session-settings.d.ts +15 -3
  162. package/dist/engines/pi/session-settings.js +22 -1
  163. package/dist/engines/pi/session-store.d.ts +140 -0
  164. package/dist/engines/pi/session-store.js +666 -0
  165. package/dist/engines/pi/tool-context.d.ts +36 -13
  166. package/dist/engines/pi/tool-context.js +51 -4
  167. package/dist/engines/pi/tool.d.ts +5 -5
  168. package/dist/engines/pi/tool.js +3 -3
  169. package/dist/engines/pi/turn-kit.d.ts +32 -6
  170. package/dist/engines/pi/turn-kit.js +28 -1
  171. package/dist/engines/pi/wake-tool.js +1 -1
  172. package/dist/env.js +1 -2
  173. package/dist/index.d.ts +1 -0
  174. package/dist/index.js +1 -0
  175. package/dist/loader.d.ts +49 -7
  176. package/dist/loader.js +79 -18
  177. package/dist/log.d.ts +3 -0
  178. package/dist/log.js +5 -1
  179. package/dist/node.d.ts +16 -0
  180. package/dist/node.js +20 -0
  181. package/dist/paths.d.ts +26 -0
  182. package/dist/paths.js +37 -2
  183. package/dist/pi.d.ts +14 -8
  184. package/dist/pi.js +6 -5
  185. package/dist/scaffold/add-channel.js +2 -2
  186. package/dist/scaffold/init.js +7 -2
  187. package/dist/scaffold/templates/persona.md +2 -2
  188. package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
  189. package/dist/schedule/discover.js +3 -15
  190. package/dist/schedule/state.js +4 -9
  191. package/dist/schedule/wake-alarm.d.ts +13 -7
  192. package/dist/schedule/wake-alarm.js +88 -46
  193. package/dist/schedule/wakeups.d.ts +2 -2
  194. package/dist/schedule/wakeups.js +11 -8
  195. package/dist/service.d.ts +157 -0
  196. package/dist/service.js +376 -0
  197. package/dist/session-remote.d.ts +8 -3
  198. package/dist/session-remote.js +162 -92
  199. package/dist/session.d.ts +229 -93
  200. package/dist/session.js +61 -23
  201. package/dist/telegram.d.ts +1 -1
  202. package/dist/telegram.js +1 -1
  203. package/dist/tunnel.d.ts +18 -3
  204. package/dist/tunnel.js +32 -40
  205. package/package.json +14 -5
  206. package/dist/engines/pi/channel.d.ts +0 -37
  207. package/dist/engines/pi/harness.d.ts +0 -115
  208. package/dist/engines/pi/harness.js +0 -154
  209. package/dist/engines/pi/invoke.d.ts +0 -60
  210. package/dist/engines/pi/invoke.js +0 -459
  211. package/dist/engines/pi/read-image.d.ts +0 -4
  212. package/dist/engines/pi/read-image.js +0 -62
  213. package/dist/engines/pi/sessions.d.ts +0 -60
  214. package/dist/engines/pi/sessions.js +0 -464
  215. package/dist/host/node.js +0 -52
  216. /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
  217. /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
  218. /package/dist/channels/{state.d.ts → kit/state.d.ts} +0 -0
  219. /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
  220. /package/dist/channels/{text.js → kit/text.js} +0 -0
  221. /package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +0 -0
  222. /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
@@ -0,0 +1,30 @@
1
+ import type { SessionManager } from "@earendil-works/pi-coding-agent";
2
+ /** What a Caller names when a new session should start from an existing one. */
3
+ export interface SessionInheritance {
4
+ /** The session to inherit from. Missing or unreadable → start empty, with a warn: context is not
5
+ * the ask, and losing it must not cost the turn. */
6
+ parentSession: string;
7
+ /** Opaque markers that MAY locate the branch point on the parent's active path (searched in
8
+ * message content, first hit wins, most recent occurrence). No match → the parent's present. */
9
+ branchHints?: string[];
10
+ }
11
+ /**
12
+ * The branch point this inheritance should copy up to, and the parent's path — the decision half,
13
+ * shared by both backends because WHERE a thread branches is policy, not storage.
14
+ */
15
+ export declare function inheritanceCut(parent: SessionManager, branchHints?: string[]): {
16
+ at: string;
17
+ } | undefined;
18
+ /**
19
+ * Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to
20
+ * fork has to do instead. Kinds pi models as facts about an entry rather than positions (labels,
21
+ * the session name) are not copied: they describe the parent's record, not the thread's history.
22
+ *
23
+ * The COPY only. Bounding what the child's model sees is {@link markInheritanceWindow}, which only
24
+ * {@link copyBranchForInheritance} applies: a lifecycle fork that marked a window would hide the
25
+ * exact entries its user forked to keep.
26
+ */
27
+ export declare function copyBranchInto(parent: SessionManager, child: SessionManager, at: string): void;
28
+ /** {@link copyBranchInto} plus the inheritance window — what a new THREAD gets and a fork does not.
29
+ * Both backends call THIS one, so neither can drift on where a child's context begins. */
30
+ export declare function copyBranchForInheritance(parent: SessionManager, child: SessionManager, at: string): void;
@@ -0,0 +1,261 @@
1
+ import { log } from "../../log.js";
2
+ import { isPlaneMarker } from "./session-markers.js";
3
+ /** Inheritance window: at most this many exchanges of the parent reach the child's model context. */
4
+ const INHERIT_MAX_EXCHANGES = 50;
5
+ /** …and at most roughly this many tokens (~1/4 of a 200K context: generous, not everything). Both
6
+ * limits govern how far the window EXTENDS into older history — the newest exchange is a FLOOR,
7
+ * kept whole even when it alone exceeds the budget: the mark's boundary is entry-granular, and an
8
+ * inheritance that drops the exchange the thread branched off would be no inheritance at all. */
9
+ const INHERIT_MAX_TOKENS = 50_000;
10
+ /** Branch hints are IDS, not payloads: each one costs a scan over the parent's serialized path, and
11
+ * the wire accepts arbitrary arrays — so the engine caps them where the cost lives. */
12
+ const MAX_BRANCH_HINTS = 16;
13
+ const MAX_BRANCH_HINT_CHARS = 128;
14
+ /** A vision image is priced FLAT — what a provider bills for a resized image, roughly — because its
15
+ * base64 length (~1M chars for a photo) measures storage, not context: pricing it by chars would
16
+ * let one photo evict the whole text window. */
17
+ const INHERIT_IMAGE_TOKENS = 1_600;
18
+ function isUserMessage(entry) {
19
+ return entry?.type === "message" && entry.message?.role === "user";
20
+ }
21
+ /** Rough token estimate for windowing — text at chars/4, images flat. Precision is not the point:
22
+ * the window is a budget, and being 20% off moves a boundary by an exchange, not correctness. */
23
+ function estimateContentTokens(content) {
24
+ if (typeof content === "string")
25
+ return Math.ceil(content.length / 4);
26
+ if (!Array.isArray(content))
27
+ return 0;
28
+ let tokens = 0;
29
+ for (const block of content) {
30
+ if (block.type === "image")
31
+ tokens += INHERIT_IMAGE_TOKENS;
32
+ else if (typeof block.text === "string")
33
+ tokens += Math.ceil(block.text.length / 4);
34
+ else
35
+ tokens += Math.ceil(JSON.stringify(block).length / 4);
36
+ }
37
+ return tokens;
38
+ }
39
+ /** Both entry kinds pi projects into model context from a copied path: a `custom_message` is an
40
+ * extension's injection INTO the conversation, so it charges the budget like any message. */
41
+ function estimateEntryTokens(entry) {
42
+ if (entry.type === "message")
43
+ return estimateContentTokens(entry.message?.content);
44
+ if (entry.type === "custom_message")
45
+ return estimateContentTokens(entry.content);
46
+ return 0;
47
+ }
48
+ /** A compaction entry's summary AND its retained tail — the entries from `firstKeptEntryId` up to
49
+ * the compaction — DO reach the model. They are the floor under every window that starts above the
50
+ * compaction, so the budget must count them; the tail is read off the path, since pi-coding-agent's
51
+ * SessionManager stores it as a POINTER and not as messages on the entry (0.84: `appendCompaction`
52
+ * writes `firstKeptEntryId`, and both of AgentSession's compaction paths go through it).
53
+ *
54
+ * pi-agent-core's harness writes the newer self-contained form instead — `retainedTail` on the entry,
55
+ * no pointer. If pi-coding-agent adopts it the pointer stops resolving, and this function prices the
56
+ * tail at zero while {@link copyBranchInto} drops it. Adopting it the way pi-agent-core did, by
57
+ * REPLACING the pointer, fails to typecheck at `Entry.firstKeptEntryId` rather than running quietly;
58
+ * a `retainedTail` added BESIDE the pointer would not, and is the case to watch on the next bump.
59
+ * Either way this is the place to change — not a reason to read both shapes today. */
60
+ function estimateCompactionTokens(path, compactionIdx) {
61
+ const compaction = path[compactionIdx];
62
+ if (compaction?.type !== "compaction")
63
+ return 0;
64
+ let tokens = Math.ceil((compaction.summary ?? "").length / 4);
65
+ const firstKept = path.findIndex((entry) => entry.id === compaction.firstKeptEntryId);
66
+ if (firstKept < 0)
67
+ return tokens; // an unresolvable pointer keeps nothing — pi's own reading
68
+ for (let i = firstKept; i < compactionIdx; i++) {
69
+ const entry = path[i];
70
+ if (entry)
71
+ tokens += estimateEntryTokens(entry);
72
+ }
73
+ return tokens;
74
+ }
75
+ /**
76
+ * Find the fork target on the parent's active path: the LAST message whose content carries a hint
77
+ * (the most recent turn that talked about that message), extended forward to the end of its exchange
78
+ * — forking mid-exchange would inherit a question without its answer. Hints are tried in caller
79
+ * order; the first that matches anywhere wins. No match → undefined (the caller forks the present).
80
+ */
81
+ function locateBranchPoint(path, hints) {
82
+ const usable = hints
83
+ .filter((hint) => hint.length > 0 && hint.length <= MAX_BRANCH_HINT_CHARS)
84
+ .slice(0, MAX_BRANCH_HINTS);
85
+ if (usable.length < hints.length) {
86
+ log.warn(`[fastagent] ignored ${hints.length - usable.length} branch hint(s) (over ${MAX_BRANCH_HINTS} hints or ${MAX_BRANCH_HINT_CHARS} chars each) — hints are message ids, not payloads`);
87
+ }
88
+ if (usable.length === 0)
89
+ return undefined;
90
+ // Serialize each message ONCE — the scan is hints × entries, and stringify must not sit in the
91
+ // inner loop. The whole message, not just content: shape-agnostic, and a hint is a platform id —
92
+ // a false positive would need the id to appear outside content, which is where ids live anyway.
93
+ const serialized = path.map((entry) => (entry.type === "message" ? JSON.stringify(entry.message) : ""));
94
+ for (const hint of usable) {
95
+ for (let i = path.length - 1; i >= 0; i--) {
96
+ if (!serialized[i]?.includes(hint))
97
+ continue;
98
+ let j = i + 1;
99
+ while (j < path.length && !isUserMessage(path[j]))
100
+ j++;
101
+ return path[j - 1]?.id;
102
+ }
103
+ }
104
+ return undefined;
105
+ }
106
+ /**
107
+ * Bound what the child's MODEL CONTEXT starts with: keep the newest exchange unconditionally, extend
108
+ * older while both window limits hold, and mark the boundary with a mechanical compaction entry.
109
+ * Entries above the parent's own last compaction are already outside model context and need no mark;
110
+ * a child whose visible history fits the window gets no mark at all.
111
+ */
112
+ function markInheritanceWindow(child) {
113
+ const path = child.getBranch();
114
+ let scanFrom = 0;
115
+ for (let i = path.length - 1; i >= 0; i--) {
116
+ if (path[i]?.type === "compaction") {
117
+ scanFrom = i + 1;
118
+ break;
119
+ }
120
+ }
121
+ const scanned = path.slice(scanFrom);
122
+ // The compaction's own summary + retained tail reach the model regardless of where the window
123
+ // lands, so they charge the budget as a base cost — not estimating them would over-admit.
124
+ const baseTokens = estimateCompactionTokens(path, scanFrom - 1);
125
+ const starts = [];
126
+ scanned.forEach((entry, i) => {
127
+ if (isUserMessage(entry))
128
+ starts.push(i);
129
+ });
130
+ if (starts.length <= 1)
131
+ return; // zero or one visible exchange — nothing to cut
132
+ const suffixTokens = new Array(scanned.length + 1).fill(0);
133
+ for (let i = scanned.length - 1; i >= 0; i--) {
134
+ const entry = scanned[i];
135
+ suffixTokens[i] = (suffixTokens[i + 1] ?? 0) + (entry ? estimateEntryTokens(entry) : 0);
136
+ }
137
+ let chosen = starts.length - 1;
138
+ for (let k = starts.length - 2; k >= 0; k--) {
139
+ const exchanges = starts.length - k;
140
+ const startIdx = starts[k];
141
+ if (startIdx === undefined)
142
+ break;
143
+ if (exchanges > INHERIT_MAX_EXCHANGES || baseTokens + (suffixTokens[startIdx] ?? 0) > INHERIT_MAX_TOKENS)
144
+ break;
145
+ chosen = k;
146
+ }
147
+ if (chosen === 0)
148
+ return; // the whole visible history fits the window
149
+ const boundaryIdx = starts[chosen];
150
+ if (boundaryIdx === undefined)
151
+ return;
152
+ const boundary = scanned[boundaryIdx];
153
+ if (boundary === undefined)
154
+ return;
155
+ child.appendCompaction(`Inherited from the parent conversation; ${chosen} earlier exchange(s) are not shown.`, boundary.id, (suffixTokens[0] ?? 0) - (suffixTokens[boundaryIdx] ?? 0));
156
+ }
157
+ /**
158
+ * The branch point this inheritance should copy up to, and the parent's path — the decision half,
159
+ * shared by both backends because WHERE a thread branches is policy, not storage.
160
+ */
161
+ export function inheritanceCut(parent, branchHints) {
162
+ const path = parent.getBranch();
163
+ const leaf = path[path.length - 1];
164
+ if (!leaf)
165
+ return undefined; // an empty parent has nothing to inherit
166
+ const hints = branchHints ?? [];
167
+ const at = locateBranchPoint(path, hints);
168
+ if (at === undefined && hints.length > 0) {
169
+ log.warn("[fastagent] no branch hint matched in the parent session — inheriting from its present");
170
+ }
171
+ return { at: at ?? leaf.id };
172
+ }
173
+ /**
174
+ * Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to
175
+ * fork has to do instead. Kinds pi models as facts about an entry rather than positions (labels,
176
+ * the session name) are not copied: they describe the parent's record, not the thread's history.
177
+ *
178
+ * The COPY only. Bounding what the child's model sees is {@link markInheritanceWindow}, which only
179
+ * {@link copyBranchForInheritance} applies: a lifecycle fork that marked a window would hide the
180
+ * exact entries its user forked to keep.
181
+ */
182
+ export function copyBranchInto(parent, child, at) {
183
+ /** Parent entry id → the child's id for that entry: the copy mints its own, and a compaction
184
+ * points BACK into the path it was appended to. */
185
+ const copied = new Map();
186
+ /** Ids of entries this copy did NOT append. A compaction's `firstKeptEntryId` routinely names one:
187
+ * pi walks the cut point backwards onto the metadata entries adjacent to it, which carry no
188
+ * context. They resolve to the next entry that survived — the retained tail starts there — so a
189
+ * dropped anchor moves the boundary by an invisible entry instead of erasing the whole tail. */
190
+ let unanchored = [];
191
+ const record = (parentId, childId) => {
192
+ for (const id of unanchored)
193
+ copied.set(id, childId);
194
+ unanchored = [];
195
+ copied.set(parentId, childId);
196
+ };
197
+ for (const raw of parent.getBranch(at)) {
198
+ const entry = raw;
199
+ let childId;
200
+ switch (entry.type) {
201
+ case "message":
202
+ if (entry.message) {
203
+ childId = child.appendMessage(entry.message);
204
+ }
205
+ break;
206
+ case "custom_message":
207
+ // Model-visible history, unlike the `custom` entries below it: an extension injected it INTO
208
+ // the conversation, and the assistant messages answering it are being copied.
209
+ childId = child.appendCustomMessageEntry(entry.customType ?? "", entry.content, entry.display ?? false, entry.details);
210
+ break;
211
+ case "compaction":
212
+ // `firstKeptEntryId` is where the RETAINED TAIL starts — the entries pi did not summarize,
213
+ // which still reach the model. Translated through the copy rather than pinned to the child's
214
+ // leaf: pinning kept exactly ONE entry, and when that entry was a toolResult (a compaction
215
+ // lands wherever the turn ended) the child's first request opened with a tool result whose
216
+ // call had been summarized away, which every provider rejects. An id the copy never saw
217
+ // keeps nothing, which is what pi itself does with a pointer it cannot resolve.
218
+ childId = child.appendCompaction(entry.summary ?? "", copied.get(entry.firstKeptEntryId ?? "") ?? "", entry.tokensBefore ?? 0, entry.details);
219
+ break;
220
+ case "model_change":
221
+ if (entry.provider && entry.modelId)
222
+ childId = child.appendModelChange(entry.provider, entry.modelId);
223
+ break;
224
+ case "thinking_level_change":
225
+ if (entry.thinkingLevel)
226
+ childId = child.appendThinkingLevelChange(entry.thinkingLevel);
227
+ break;
228
+ case "custom":
229
+ // The plane's markers describe the parent's RECORD, not the thread's history: a copied
230
+ // provenance would make a fork of a fork claim its grandparent's branch point (the
231
+ // idempotency check reads that value), and a copied leaf anchor would pin the child's head
232
+ // to a position its own history never chose. Every other custom entry is history and travels
233
+ // — the engine's tool-activation delta above all, since the copied assistant messages call
234
+ // the tools it records.
235
+ if (entry.customType && !isPlaneMarker(entry)) {
236
+ childId = child.appendCustomEntry(entry.customType, entry.data);
237
+ }
238
+ break;
239
+ default:
240
+ break; // label / session_info / branch_summary: the parent's facts, not the thread's history
241
+ }
242
+ if (childId === undefined)
243
+ unanchored.push(entry.id);
244
+ else
245
+ record(entry.id, childId);
246
+ }
247
+ }
248
+ /** {@link copyBranchInto} plus the inheritance window — what a new THREAD gets and a fork does not.
249
+ * Both backends call THIS one, so neither can drift on where a child's context begins. */
250
+ export function copyBranchForInheritance(parent, child, at) {
251
+ copyBranchInto(parent, child, at);
252
+ markInheritanceWindow(child);
253
+ }
254
+ /*
255
+ * There is deliberately NO file-level fork here. pi can copy a path into a new file
256
+ * (`createBranchedSession` + `forkFrom`), but that pair writes the intermediate only when the copied
257
+ * path contains an ASSISTANT message — so forking at a user entry hands `forkFrom` a path that does
258
+ * not exist, and the failure reads as retryable for a condition no retry can change. Copying entries
259
+ * is what a backend with no file to fork has to do anyway, so both share these functions and one
260
+ * semantics; a fork is not hot enough to buy a second path back.
261
+ */
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Which journal entries are POSITIONS and which are the control plane's own bookkeeping.
3
+ *
4
+ * pi's journal has one shape for everything, so the plane writes what it needs to remember into the
5
+ * same log the conversation lives in: what fork a record is, and the anchor that makes a leaf move
6
+ * survive a reopen. Both the record store and the history copier have to agree on which is which —
7
+ * one publishes and navigates, the other copies — and a disagreement is invisible until a fork comes
8
+ * back missing something. Hence one module, imported by both, depending on neither.
9
+ */
10
+ import type { SessionManager } from "@earendil-works/pi-coding-agent";
11
+ /** What pins a leaf move to disk. pi's `branch()` writes nothing — the leaf is runtime state, and
12
+ * `open()` puts it back on the file's last entry — so a move that no other write follows is
13
+ * forgotten. Carries no data: its PARENT is the position, which is the whole point of appending it. */
14
+ export declare const LEAF_ANCHOR = "fastagent.leaf";
15
+ /**
16
+ * The CONTROL PLANE's bookkeeping — not a place in a conversation, so never published, never
17
+ * navigable, and never copied by a fork: these describe THIS record, not the history it holds.
18
+ *
19
+ * An EXACT list, not a `fastagent` prefix. The prefix was tried and was wrong for one entry, at a
20
+ * cost worth remembering: `fastagent:tool-activation` is written by the ENGINE and is thread history
21
+ * (which deferred tools this conversation discovered), so matching it here dropped it from every
22
+ * fork and every inherited thread — while the assistant messages that call those tools came along.
23
+ * A new marker joins this list deliberately, which is the point of it being a list.
24
+ */
25
+ export declare function isPlaneMarker(entry: {
26
+ type?: string;
27
+ customType?: string;
28
+ }): boolean;
29
+ /** Every position a client may move the branch head to, and everything `entries()` publishes — ONE
30
+ * predicate, so "anything published is navigable" holds by construction. `label` is metadata ABOUT
31
+ * an entry rather than a place; {@link isPlaneMarker} is ours rather than the conversation's. */
32
+ export declare function isNavigable(entry: {
33
+ type?: string;
34
+ customType?: string;
35
+ }): boolean;
36
+ /**
37
+ * The head a client SEES: the last publishable entry on the active path.
38
+ *
39
+ * Not pi's leaf, which may be a marker — a solo leaf move anchors itself with one — so reporting
40
+ * pi's answer would hand back an id the client did not ask for and cannot find in `entries()`.
41
+ */
42
+ export declare function publishedLeaf(record: SessionManager): string | undefined;
43
+ /** Stamp a fresh fork with where it came from. */
44
+ export declare function stampProvenance(record: SessionManager, provenance: string): void;
45
+ /** What fork this record IS, or undefined for a record that was not forked. The LAST stamp wins — a
46
+ * fork of a fork carries its own — and the whole journal is read rather than the active path, so a
47
+ * later leaf move cannot make a fork stop being one. */
48
+ export declare function forkProvenance(record: SessionManager): string | undefined;
@@ -0,0 +1,59 @@
1
+ /** "This record is a fork of X at Y." Its own entry rather than pi's header `parentSession`, which
2
+ * pi fills from its own fork path and names a FILE, not the branch point idempotency needs. */
3
+ const FORK_PROVENANCE = "fastagent.fork";
4
+ /** What pins a leaf move to disk. pi's `branch()` writes nothing — the leaf is runtime state, and
5
+ * `open()` puts it back on the file's last entry — so a move that no other write follows is
6
+ * forgotten. Carries no data: its PARENT is the position, which is the whole point of appending it. */
7
+ export const LEAF_ANCHOR = "fastagent.leaf";
8
+ /**
9
+ * The CONTROL PLANE's bookkeeping — not a place in a conversation, so never published, never
10
+ * navigable, and never copied by a fork: these describe THIS record, not the history it holds.
11
+ *
12
+ * An EXACT list, not a `fastagent` prefix. The prefix was tried and was wrong for one entry, at a
13
+ * cost worth remembering: `fastagent:tool-activation` is written by the ENGINE and is thread history
14
+ * (which deferred tools this conversation discovered), so matching it here dropped it from every
15
+ * fork and every inherited thread — while the assistant messages that call those tools came along.
16
+ * A new marker joins this list deliberately, which is the point of it being a list.
17
+ */
18
+ export function isPlaneMarker(entry) {
19
+ return entry.type === "custom" && (entry.customType === FORK_PROVENANCE || entry.customType === LEAF_ANCHOR);
20
+ }
21
+ /** Every position a client may move the branch head to, and everything `entries()` publishes — ONE
22
+ * predicate, so "anything published is navigable" holds by construction. `label` is metadata ABOUT
23
+ * an entry rather than a place; {@link isPlaneMarker} is ours rather than the conversation's. */
24
+ export function isNavigable(entry) {
25
+ return entry.type !== "label" && !isPlaneMarker(entry);
26
+ }
27
+ /**
28
+ * The head a client SEES: the last publishable entry on the active path.
29
+ *
30
+ * Not pi's leaf, which may be a marker — a solo leaf move anchors itself with one — so reporting
31
+ * pi's answer would hand back an id the client did not ask for and cannot find in `entries()`.
32
+ */
33
+ export function publishedLeaf(record) {
34
+ const path = record.getBranch();
35
+ for (let i = path.length - 1; i >= 0; i--) {
36
+ const entry = path[i];
37
+ if (entry && isNavigable(entry))
38
+ return entry.id;
39
+ }
40
+ return undefined;
41
+ }
42
+ /** Stamp a fresh fork with where it came from. */
43
+ export function stampProvenance(record, provenance) {
44
+ record.appendCustomEntry(FORK_PROVENANCE, { provenance });
45
+ }
46
+ /** What fork this record IS, or undefined for a record that was not forked. The LAST stamp wins — a
47
+ * fork of a fork carries its own — and the whole journal is read rather than the active path, so a
48
+ * later leaf move cannot make a fork stop being one. */
49
+ export function forkProvenance(record) {
50
+ let found;
51
+ for (const raw of record.getEntries()) {
52
+ if (raw.type !== "custom" || raw.customType !== FORK_PROVENANCE)
53
+ continue;
54
+ const value = raw.data?.provenance;
55
+ if (typeof value === "string")
56
+ found = value;
57
+ }
58
+ return found;
59
+ }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * What a session is SET TO, and what it may be set to. Model and thinking level are ONE setting —
3
3
  * which levels exist is a property of the model — so they resolve together, here, and `state()`, the
4
- * `set_thinking` gate and the fresh-harness resolve all read this rather than deriving their own.
4
+ * `update({ thinkingLevel })` gate and the per-invoke binding all read this rather than deriving their own.
5
5
  *
6
6
  * Read-only by design. The durable record may hold a level the current model cannot do; that record
7
7
  * is the user's PREFERENCE, so resolving per read restores it when the session returns to a capable
@@ -10,7 +10,8 @@
10
10
  */
11
11
  import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
12
12
  import { type Models } from "@earendil-works/pi-ai";
13
- import type { AnyModel } from "./harness.ts";
13
+ import type { SessionManager } from "@earendil-works/pi-coding-agent";
14
+ import type { AnyModel } from "./models.ts";
14
15
  export declare const THINKING_LEVELS: ReadonlySet<ThinkingLevel>;
15
16
  /** The shape both override consumers walk — a session entry, structurally. */
16
17
  export interface OverrideEntryLike {
@@ -32,7 +33,7 @@ export interface SessionSettings {
32
33
  model: AnyModel;
33
34
  /** Already clamped to what {@link model} supports. */
34
35
  thinkingLevel: ThinkingLevel;
35
- /** What `set_thinking` accepts for this session. */
36
+ /** What `update({ thinkingLevel })` accepts for this session. */
36
37
  availableThinkingLevels: string[];
37
38
  /** Recorded but not honored — only the execution path reports it (as a warn). */
38
39
  dropped?: {
@@ -49,3 +50,14 @@ export declare function resolveSessionSettings(entries: OverrideEntryLike[], mod
49
50
  model: AnyModel;
50
51
  thinkingLevel: ThinkingLevel;
51
52
  }): SessionSettings;
53
+ /**
54
+ * The entries on the session's ACTIVE path, root→leaf — what every last-wins settings read walks.
55
+ * `getBranch()` is exactly that walk: the journal can hold abandoned branches after a leaf move,
56
+ * and reading it flat would run the session on a setting it moved away from.
57
+ *
58
+ * A chain that is not intact THROWS. `getBranch()` stops where a parent is missing and answers the
59
+ * SHORT path, which reads exactly like a short session — every override above the gap gone, the next
60
+ * turn silently on assembly defaults. The caller decides what to do with the fault (state() reports
61
+ * the settings as absent, dispatch answers with a code); what it must not do is guess.
62
+ */
63
+ export declare function activePath(record: SessionManager, from?: string): OverrideEntryLike[];
@@ -1,7 +1,8 @@
1
1
  import { clampThinkingLevel, getSupportedThinkingLevels } from "@earendil-works/pi-ai";
2
2
  /** Which strings are levels at all — the vocabulary. What a MODEL supports is
3
3
  * `getSupportedThinkingLevels`. The `satisfies` anchor keeps this exhaustive against pi's union: a
4
- * level pi adds becomes a type error here rather than a value `set_thinking` silently rejects. */
4
+ * level pi adds becomes a type error here rather than a value `update({ thinkingLevel })` silently
5
+ * rejects. */
5
6
  const ALL_THINKING_LEVELS = {
6
7
  off: true,
7
8
  minimal: true,
@@ -71,3 +72,23 @@ export function resolveSessionSettings(entries, models, defaults) {
71
72
  ...(dropped.model || dropped.thinkingLevel ? { dropped } : {}),
72
73
  };
73
74
  }
75
+ /**
76
+ * The entries on the session's ACTIVE path, root→leaf — what every last-wins settings read walks.
77
+ * `getBranch()` is exactly that walk: the journal can hold abandoned branches after a leaf move,
78
+ * and reading it flat would run the session on a setting it moved away from.
79
+ *
80
+ * A chain that is not intact THROWS. `getBranch()` stops where a parent is missing and answers the
81
+ * SHORT path, which reads exactly like a short session — every override above the gap gone, the next
82
+ * turn silently on assembly defaults. The caller decides what to do with the fault (state() reports
83
+ * the settings as absent, dispatch answers with a code); what it must not do is guess.
84
+ */
85
+ export function activePath(record, from) {
86
+ // `from` asks a different question: the path a leaf move is ABOUT to make active, which a caller
87
+ // validating a patch needs before the move exists. Absent, it is the session's current path.
88
+ const path = record.getBranch(from);
89
+ const root = path[0];
90
+ if (root?.parentId != null) {
91
+ throw new Error(`session entry "${root.parentId}" is missing from the journal (parent of "${root.id}")`);
92
+ }
93
+ return path;
94
+ }
@@ -0,0 +1,140 @@
1
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import type { SessionSummary, SessionUpdateField } from "../../session.ts";
3
+ import { type OverrideEntryLike } from "./session-settings.ts";
4
+ import { type SessionInheritance } from "./session-inheritance.ts";
5
+ /**
6
+ * The session RECORDS, as operations rather than as pi handles.
7
+ *
8
+ * The distinction is the point. `openOrCreate`/`openIfExists` hand out a live `SessionManager` for
9
+ * the two things that genuinely need one — driving a turn, and reading a session's contents — and
10
+ * everything else is a whole-record operation implemented HERE. That line was drawn after the
11
+ * lifecycle work: when the control plane wrote properties by calling pi's append methods itself, it
12
+ * had to know pi's rules to do it (every append advances the single leaf pointer, so order decides
13
+ * where a fork's head lands; `appendSessionInfo` rewrites the name it is given; a record buffers in
14
+ * memory until its first assistant message). Every caller learning those separately is how the same
15
+ * fact ended up half-known in two modules — so they are known once, here.
16
+ */
17
+ export interface PiSessionRecordStore {
18
+ openOrCreate(sessionId: string, inherit?: SessionInheritance): Promise<SessionManager>;
19
+ /** OPEN-EXISTING sibling: an unknown session answers undefined, never creates one — sessions are
20
+ * the data plane's monopoly. The READ path (state/entries) and the turn binding use this; the
21
+ * write path does not, because a caller holding a handle is a caller learning pi's rules. */
22
+ openIfExists(sessionId: string): Promise<SessionManager | undefined>;
23
+ /**
24
+ * Write session properties, in the order pi's leaf pointer requires, and report what LANDED.
25
+ *
26
+ * Each property is its own journal entry — pi has no "append these together" — so a failure
27
+ * partway is a real state, and this answers it rather than pretending a rollback happened. The
28
+ * caller supplies an already-VALIDATED patch (a model resolved to provider+id, a level this model
29
+ * accepts): what belongs here is how a record is written, not what a value means.
30
+ *
31
+ * `undefined` = no such record (it vanished between the caller's check and this call).
32
+ */
33
+ applyProperties(sessionId: string, writes: PropertyWrites): Promise<AppliedProperties | undefined>;
34
+ /** Every record this store holds, in CALLER ids. Rejects when the store cannot be enumerated —
35
+ * `[]` means "no sessions", so it must not double as "could not look". Answers the CONTRACT's row
36
+ * type: the hub forwards it, and a second identical shape here would only be a thing to keep in
37
+ * sync. */
38
+ list(): Promise<SessionSummary[]>;
39
+ /** Copy `from`'s history up to entry `at` into a new record named `into`, stamped with `provenance`
40
+ * so a repeat of the SAME fork can be recognised as one ({@link forkProvenance}) instead of
41
+ * becoming a second record or an overwrite. Throws on any failure: a half-copied fork is never
42
+ * left in place, and the caller turns the throw into a coded result. */
43
+ fork(from: string, at: string, into: string, provenance: string): Promise<void>;
44
+ /** Destroy a record. `false` = there was none (the caller answers `no_such_session`). */
45
+ delete(sessionId: string): Promise<boolean>;
46
+ }
47
+ /** A validated property patch, in the shape a RECORD takes it: the model already resolved to the
48
+ * provider + id pi's append wants, so this layer never asks what a model spec means. */
49
+ export interface PropertyWrites {
50
+ name?: string;
51
+ model?: {
52
+ provider: string;
53
+ id: string;
54
+ };
55
+ thinkingLevel?: string;
56
+ leafEntryId?: string;
57
+ }
58
+ /** What a property write actually did, and what the record holds after it. */
59
+ interface AppliedProperties {
60
+ /** In write order. A field asked for but absent here did not land. */
61
+ landed: SessionUpdateField[];
62
+ /** Why the rest stopped, if anything did. The caller turns this into a coded result; the record
63
+ * already reflects `landed`. */
64
+ failure?: unknown;
65
+ /** The record AFTER the writes — what the caller reports, rather than echoing the request. pi
66
+ * rewrites a name (newlines collapse, ends trim), so the request is not what was stored. */
67
+ name?: string;
68
+ leafEntryId?: string;
69
+ /** The active path after the writes, for a caller that resolves settings against a model registry
70
+ * this layer has no business knowing. Absent when the chain cannot be walked — the caller decides
71
+ * what an unreadable chain means (design §7); it is never silently a short path. */
72
+ path?: OverrideEntryLike[];
73
+ }
74
+ /**
75
+ * A Caller's session id, as a name pi will accept.
76
+ *
77
+ * `SessionManager` enforces `^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$` — which no built-in
78
+ * channel satisfies: telegram's `-1001234567890` leads with a dash, feishu and slack keys carry
79
+ * `:` and `/`, and any custom `route()` may mint anything at all.
80
+ *
81
+ * The mapping is INJECTIVE, because two conversations resolving to one record is two rooms sharing a
82
+ * memory. `s` prefix (the pattern demands an alphanumeric head, and prefixing unconditionally is what
83
+ * keeps it injective — a conditional one would map `-a` and `s-a` alike), then each character
84
+ * outside `[A-Za-z0-9.-]` becomes `_XX` / `_uXXXX`, self-describing widths so no two inputs can
85
+ * produce one output. `_` escapes itself for the same reason. A trailing `.` or `-` is legal
86
+ * mid-name but not at the end, so it escapes too.
87
+ *
88
+ * Injective within this encoding — which is only sufficient because new records live in their own
89
+ * directory. The older spelling draws names from the same character set (it stored a room literally
90
+ * called `s42` as `s42`, which is also this encoding of `42`), so one directory would make some
91
+ * names ambiguous no matter how either side spells them.
92
+ *
93
+ * Readability is deliberate: `-1001234567890` becomes `s-1001234567890`, so an operator can still
94
+ * tell which room a file belongs to.
95
+ */
96
+ export declare function piSessionId(sessionId: string): string;
97
+ /**
98
+ * {@link piSessionId} backwards — what `list()` needs, because a session id belongs to the CALLER and
99
+ * a record name is storage detail. The encoding is self-describing (fixed widths, `_` escapes
100
+ * itself), so this is a decode rather than a guess; a name this store did not write (no `s` head, a
101
+ * truncated escape) answers undefined and is left out of the listing rather than reported under a
102
+ * name nobody can dial.
103
+ */
104
+ export declare function callerSessionId(recordId: string): string | undefined;
105
+ /**
106
+ * Disk-backed store under `dir`: restart the process, conversations continue.
107
+ *
108
+ * Lookup is a directory scan of THIS store's own directory, reading ids out of the filenames pi
109
+ * writes (`<timestamp>_<id>.jsonl`) — not `SessionManager.list`, which filters by the cwd recorded in
110
+ * each header and would make a renamed agent directory look like an empty store (see
111
+ * {@link recordFiles}).
112
+ *
113
+ * NEW records live in a subdirectory of their own, because the two engines cannot share a namespace:
114
+ * both spell ids into `[A-Za-z0-9._-]`, so neither can claim a prefix the other cannot produce, and
115
+ * a directory holding both would have names that belong to two conversations at once — in whichever
116
+ * direction it is read. Separate directories make each side's own injectivity sufficient.
117
+ *
118
+ * A PRE-EXISTING record is continued in place: looked up by the older spelling, which is injective
119
+ * on its own terms, and appended to where it lies. Both spellings are the same v3 jsonl, so a
120
+ * conversation started before this store keeps going rather than restarting empty. Nothing on disk
121
+ * is rewritten.
122
+ *
123
+ * SCOPE OF "open-or-create": idempotent against a store that is serialized per session, which is what
124
+ * the serving path provides — the single-writer lease is taken before any store call, so no two
125
+ * turns of one conversation reach this at once. What it does NOT do is arbitrate a FIRST open racing
126
+ * across processes: two instances that scan before either writes will both create, and the
127
+ * conversation forks into two records. sessions.ts states the same boundary for the same reason
128
+ * ("the serving path serializes it with the single-writer lease before reaching any store"), and a
129
+ * horizontally-scaled deployment that wants more owes a lease that spans its instances — an
130
+ * in-process one cannot arbitrate between them, and a file lock here would only look like it could.
131
+ */
132
+ export declare function piSessionRecordStore(options: {
133
+ dir: string;
134
+ cwd?: string;
135
+ }): PiSessionRecordStore;
136
+ /** In-process store: continuity lives and dies with the instance. */
137
+ export declare function piInMemorySessionRecordStore(options?: {
138
+ cwd?: string;
139
+ }): PiSessionRecordStore;
140
+ export {};