@fastagent-sh/fastagent 0.17.1 → 0.19.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 (170) hide show
  1. package/README.md +25 -7
  2. package/dist/agent.d.ts +12 -1
  3. package/dist/agent.js +1 -1
  4. package/dist/atomic-write.d.ts +19 -0
  5. package/dist/atomic-write.js +48 -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-service.d.ts +54 -0
  9. package/dist/channels/agentcore-service.js +111 -0
  10. package/dist/channels/agentcore.d.ts +12 -2
  11. package/dist/channels/agentcore.js +6 -4
  12. package/dist/channels/busy.d.ts +1 -1
  13. package/dist/channels/busy.js +1 -1
  14. package/dist/channels/control.d.ts +16 -6
  15. package/dist/channels/control.js +106 -4
  16. package/dist/{engines/pi/channel.d.ts → channels/discover.d.ts} +2 -2
  17. package/dist/{engines/pi/channel.js → channels/discover.js} +8 -12
  18. package/dist/channels/feishu/card.js +1 -1
  19. package/dist/channels/feishu/context-buffer.d.ts +1 -1
  20. package/dist/channels/feishu/context-buffer.js +4 -4
  21. package/dist/channels/feishu/feishu-api.d.ts +4 -2
  22. package/dist/channels/feishu/feishu-api.js +1 -1
  23. package/dist/channels/feishu/feishu.d.ts +1 -1
  24. package/dist/channels/feishu/feishu.js +45 -15
  25. package/dist/channels/feishu/invoke-turn.d.ts +9 -3
  26. package/dist/channels/feishu/invoke-turn.js +152 -34
  27. package/dist/channels/feishu/parse.js +6 -0
  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 +16 -111
  34. package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
  35. package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +18 -4
  36. package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +23 -8
  37. package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
  38. package/dist/channels/{seen.js → kit/seen.js} +1 -1
  39. package/dist/channels/kit/state.d.ts +6 -0
  40. package/dist/channels/{state.js → kit/state.js} +4 -24
  41. package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
  42. package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
  43. package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
  44. package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +7 -0
  45. package/dist/channels/{thread-participants.js → kit/thread-participants.js} +4 -1
  46. package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
  47. package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
  48. package/dist/channels/lark/lark.d.ts +1 -1
  49. package/dist/channels/serve.d.ts +73 -0
  50. package/dist/channels/serve.js +243 -0
  51. package/dist/channels/slack/bot-auth.js +3 -14
  52. package/dist/channels/slack/context-buffer.d.ts +2 -2
  53. package/dist/channels/slack/context-buffer.js +2 -2
  54. package/dist/channels/slack/invoke-turn.d.ts +1 -1
  55. package/dist/channels/slack/invoke-turn.js +9 -6
  56. package/dist/channels/slack/parse.js +2 -2
  57. package/dist/channels/slack/preview.d.ts +1 -1
  58. package/dist/channels/slack/preview.js +11 -3
  59. package/dist/channels/slack/slack-api.js +1 -1
  60. package/dist/channels/slack/slack.d.ts +1 -1
  61. package/dist/channels/slack/slack.js +8 -12
  62. package/dist/channels/slack/welcomed.js +1 -1
  63. package/dist/channels/telegram/context-buffer.d.ts +2 -2
  64. package/dist/channels/telegram/context-buffer.js +2 -2
  65. package/dist/channels/telegram/invoke-turn.d.ts +1 -1
  66. package/dist/channels/telegram/invoke-turn.js +2 -2
  67. package/dist/channels/telegram/parse.js +2 -2
  68. package/dist/channels/telegram/preview.d.ts +1 -1
  69. package/dist/channels/telegram/preview.js +6 -2
  70. package/dist/channels/telegram/telegram.d.ts +1 -1
  71. package/dist/channels/telegram/telegram.js +3 -3
  72. package/dist/channels/telegram/turn-store.d.ts +2 -2
  73. package/dist/channels/telegram/turn-store.js +2 -2
  74. package/dist/cli/commands/add.js +1 -1
  75. package/dist/cli/commands/deploy.js +13 -5
  76. package/dist/cli/commands/dev.js +26 -14
  77. package/dist/cli/commands/fire.js +2 -2
  78. package/dist/cli/commands/info.js +28 -5
  79. package/dist/cli/commands/invoke.js +1 -1
  80. package/dist/cli/commands/schedule.js +1 -1
  81. package/dist/cli/commands/start.js +64 -95
  82. package/dist/cli/commands/tool.js +5 -7
  83. package/dist/cli/program.js +1 -1
  84. package/dist/cli/serve.d.ts +18 -57
  85. package/dist/cli/serve.js +57 -243
  86. package/dist/cli/shared.d.ts +5 -3
  87. package/dist/cli/shared.js +14 -7
  88. package/dist/collect.d.ts +30 -4
  89. package/dist/collect.js +39 -6
  90. package/dist/core.d.ts +3 -5
  91. package/dist/core.js +7 -5
  92. package/dist/deploy/preflight.d.ts +8 -2
  93. package/dist/deploy/preflight.js +22 -4
  94. package/dist/deploy/secrets.d.ts +3 -0
  95. package/dist/deploy/secrets.js +6 -0
  96. package/dist/dev-supervisor.js +16 -4
  97. package/dist/engines/pi/agent-session-factory.d.ts +104 -0
  98. package/dist/engines/pi/agent-session-factory.js +314 -0
  99. package/dist/engines/pi/config.d.ts +4 -4
  100. package/dist/engines/pi/config.js +1 -1
  101. package/dist/engines/pi/create.d.ts +56 -42
  102. package/dist/engines/pi/create.js +150 -75
  103. package/dist/engines/pi/definition.d.ts +41 -7
  104. package/dist/engines/pi/definition.js +115 -11
  105. package/dist/engines/pi/invoke-session.d.ts +35 -0
  106. package/dist/engines/pi/invoke-session.js +357 -0
  107. package/dist/engines/pi/login.js +7 -4
  108. package/dist/engines/pi/models.d.ts +59 -11
  109. package/dist/engines/pi/models.js +64 -9
  110. package/dist/engines/pi/open.d.ts +9 -6
  111. package/dist/engines/pi/open.js +20 -10
  112. package/dist/engines/pi/report.d.ts +0 -4
  113. package/dist/engines/pi/report.js +0 -7
  114. package/dist/engines/pi/service.d.ts +23 -0
  115. package/dist/engines/pi/service.js +19 -0
  116. package/dist/engines/pi/session-builder.js +88 -90
  117. package/dist/engines/pi/session-control.d.ts +11 -9
  118. package/dist/engines/pi/session-control.js +114 -77
  119. package/dist/engines/pi/session-inheritance.d.ts +44 -0
  120. package/dist/engines/pi/session-inheritance.js +242 -0
  121. package/dist/engines/pi/session-settings.d.ts +14 -2
  122. package/dist/engines/pi/session-settings.js +18 -0
  123. package/dist/engines/pi/session-store.d.ts +70 -0
  124. package/dist/engines/pi/session-store.js +313 -0
  125. package/dist/engines/pi/tool-context.d.ts +18 -8
  126. package/dist/engines/pi/tool-context.js +23 -1
  127. package/dist/engines/pi/tool.d.ts +5 -5
  128. package/dist/engines/pi/tool.js +1 -1
  129. package/dist/engines/pi/{invoke.d.ts → turn-kit.d.ts} +46 -45
  130. package/dist/engines/pi/turn-kit.js +188 -0
  131. package/dist/engines/pi/wake-tool.js +1 -1
  132. package/dist/index.d.ts +1 -0
  133. package/dist/index.js +1 -0
  134. package/dist/log.d.ts +3 -0
  135. package/dist/log.js +5 -1
  136. package/dist/node.d.ts +16 -0
  137. package/dist/node.js +20 -0
  138. package/dist/paths.d.ts +13 -0
  139. package/dist/paths.js +16 -0
  140. package/dist/pi.d.ts +15 -8
  141. package/dist/pi.js +7 -6
  142. package/dist/scaffold/templates/fastagent.config.mjs +2 -0
  143. package/dist/scaffold/templates/persona.md +2 -2
  144. package/dist/schedule/state.js +4 -9
  145. package/dist/schedule/wakeups.d.ts +1 -1
  146. package/dist/schedule/wakeups.js +1 -1
  147. package/dist/service.d.ts +158 -0
  148. package/dist/service.js +354 -0
  149. package/dist/session-remote.d.ts +1 -1
  150. package/dist/session-remote.js +10 -2
  151. package/dist/session.d.ts +4 -2
  152. package/dist/session.js +1 -1
  153. package/package.json +14 -5
  154. package/dist/channels/state.d.ts +0 -16
  155. package/dist/engines/pi/harness.d.ts +0 -112
  156. package/dist/engines/pi/harness.js +0 -153
  157. package/dist/engines/pi/invoke.js +0 -618
  158. package/dist/engines/pi/read-image.d.ts +0 -4
  159. package/dist/engines/pi/read-image.js +0 -62
  160. package/dist/engines/pi/sessions.d.ts +0 -44
  161. package/dist/engines/pi/sessions.js +0 -182
  162. package/dist/host/node.js +0 -52
  163. /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
  164. /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
  165. /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
  166. /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
  167. /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
  168. /package/dist/channels/{text.js → kit/text.js} +0 -0
  169. /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
  170. /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
@@ -0,0 +1,242 @@
1
+ /**
2
+ * Where a NEW thread starts from, when it names a parent — participant-model.md §5's rule ("a thread
3
+ * starts from what the room knew"), on pi's `SessionManager`.
4
+ *
5
+ * Shape: copy the parent's ACTIVE PATH up to the branch point (everything — text, images, tool
6
+ * results — because they are entries, not prompt text), then bound what the MODEL sees with one
7
+ * mechanical compaction mark (a plain string; zero model calls). Disk keeps the full copy — storage
8
+ * and context are different budgets — and pi honors the mark exactly as it honors a real compaction.
9
+ *
10
+ * Read ONLY on the create path. An existing session ignores it entirely, which is what makes
11
+ * inheritance one-time by construction: no marker to persist, no decision to retry per turn; the
12
+ * session existing IS the record that the decision was taken.
13
+ *
14
+ * Every failure lands on "start empty + warn": a thread must not lose its first turn to an
15
+ * inheritance edge.
16
+ */
17
+ import { unlinkSync } from "node:fs";
18
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
19
+ import { log } from "../../log.js";
20
+ /** Inheritance window: at most this many exchanges of the parent reach the child's model context. */
21
+ const INHERIT_MAX_EXCHANGES = 50;
22
+ /** …and at most roughly this many tokens (~1/4 of a 200K context: generous, not everything). Both
23
+ * limits govern how far the window EXTENDS into older history — the newest exchange is a FLOOR,
24
+ * kept whole even when it alone exceeds the budget: the mark's boundary is entry-granular, and an
25
+ * inheritance that drops the exchange the thread branched off would be no inheritance at all. */
26
+ const INHERIT_MAX_TOKENS = 50_000;
27
+ /** Branch hints are IDS, not payloads: each one costs a scan over the parent's serialized path, and
28
+ * the wire accepts arbitrary arrays — so the engine caps them where the cost lives. */
29
+ const MAX_BRANCH_HINTS = 16;
30
+ const MAX_BRANCH_HINT_CHARS = 128;
31
+ /** A vision image is priced FLAT — what a provider bills for a resized image, roughly — because its
32
+ * base64 length (~1M chars for a photo) measures storage, not context: pricing it by chars would
33
+ * let one photo evict the whole text window. */
34
+ const INHERIT_IMAGE_TOKENS = 1_600;
35
+ function isUserMessage(entry) {
36
+ return entry?.type === "message" && entry.message?.role === "user";
37
+ }
38
+ /** Rough token estimate for windowing — text at chars/4, images flat. Precision is not the point:
39
+ * the window is a budget, and being 20% off moves a boundary by an exchange, not correctness. */
40
+ function estimateMessageTokens(message) {
41
+ const content = message.content;
42
+ if (typeof content === "string")
43
+ return Math.ceil(content.length / 4);
44
+ if (!Array.isArray(content))
45
+ return 0;
46
+ let tokens = 0;
47
+ for (const block of content) {
48
+ if (block.type === "image")
49
+ tokens += INHERIT_IMAGE_TOKENS;
50
+ else if (typeof block.text === "string")
51
+ tokens += Math.ceil(block.text.length / 4);
52
+ else
53
+ tokens += Math.ceil(JSON.stringify(block).length / 4);
54
+ }
55
+ return tokens;
56
+ }
57
+ function estimateEntryTokens(entry) {
58
+ return entry.type === "message" && entry.message ? estimateMessageTokens(entry.message) : 0;
59
+ }
60
+ /** A compaction entry's summary and retained tail DO reach the model — they are the floor under
61
+ * every window that starts above the compaction, so the budget must count them. */
62
+ function estimateCompactionTokens(entry) {
63
+ if (entry?.type !== "compaction")
64
+ return 0;
65
+ let tokens = Math.ceil((entry.summary ?? "").length / 4);
66
+ for (const message of entry.retainedTail ?? [])
67
+ tokens += estimateMessageTokens(message);
68
+ return tokens;
69
+ }
70
+ /**
71
+ * Find the fork target on the parent's active path: the LAST message whose content carries a hint
72
+ * (the most recent turn that talked about that message), extended forward to the end of its exchange
73
+ * — forking mid-exchange would inherit a question without its answer. Hints are tried in caller
74
+ * order; the first that matches anywhere wins. No match → undefined (the caller forks the present).
75
+ */
76
+ function locateBranchPoint(path, hints) {
77
+ const usable = hints
78
+ .filter((hint) => hint.length > 0 && hint.length <= MAX_BRANCH_HINT_CHARS)
79
+ .slice(0, MAX_BRANCH_HINTS);
80
+ if (usable.length < hints.length) {
81
+ 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`);
82
+ }
83
+ if (usable.length === 0)
84
+ return undefined;
85
+ // Serialize each message ONCE — the scan is hints × entries, and stringify must not sit in the
86
+ // inner loop. The whole message, not just content: shape-agnostic, and a hint is a platform id —
87
+ // a false positive would need the id to appear outside content, which is where ids live anyway.
88
+ const serialized = path.map((entry) => (entry.type === "message" ? JSON.stringify(entry.message) : ""));
89
+ for (const hint of usable) {
90
+ for (let i = path.length - 1; i >= 0; i--) {
91
+ if (!serialized[i]?.includes(hint))
92
+ continue;
93
+ let j = i + 1;
94
+ while (j < path.length && !isUserMessage(path[j]))
95
+ j++;
96
+ return path[j - 1]?.id;
97
+ }
98
+ }
99
+ return undefined;
100
+ }
101
+ /**
102
+ * Bound what the child's MODEL CONTEXT starts with: keep the newest exchange unconditionally, extend
103
+ * older while both window limits hold, and mark the boundary with a mechanical compaction entry.
104
+ * Entries above the parent's own last compaction are already outside model context and need no mark;
105
+ * a child whose visible history fits the window gets no mark at all.
106
+ */
107
+ function markInheritanceWindow(child) {
108
+ const path = child.getBranch();
109
+ let scanFrom = 0;
110
+ for (let i = path.length - 1; i >= 0; i--) {
111
+ if (path[i]?.type === "compaction") {
112
+ scanFrom = i + 1;
113
+ break;
114
+ }
115
+ }
116
+ const scanned = path.slice(scanFrom);
117
+ // The compaction's own summary + retained tail reach the model regardless of where the window
118
+ // lands, so they charge the budget as a base cost — not estimating them would over-admit.
119
+ const baseTokens = estimateCompactionTokens(path[scanFrom - 1]);
120
+ const starts = [];
121
+ scanned.forEach((entry, i) => {
122
+ if (isUserMessage(entry))
123
+ starts.push(i);
124
+ });
125
+ if (starts.length <= 1)
126
+ return; // zero or one visible exchange — nothing to cut
127
+ const suffixTokens = new Array(scanned.length + 1).fill(0);
128
+ for (let i = scanned.length - 1; i >= 0; i--) {
129
+ const entry = scanned[i];
130
+ suffixTokens[i] = (suffixTokens[i + 1] ?? 0) + (entry ? estimateEntryTokens(entry) : 0);
131
+ }
132
+ let chosen = starts.length - 1;
133
+ for (let k = starts.length - 2; k >= 0; k--) {
134
+ const exchanges = starts.length - k;
135
+ const startIdx = starts[k];
136
+ if (startIdx === undefined)
137
+ break;
138
+ if (exchanges > INHERIT_MAX_EXCHANGES || baseTokens + (suffixTokens[startIdx] ?? 0) > INHERIT_MAX_TOKENS)
139
+ break;
140
+ chosen = k;
141
+ }
142
+ if (chosen === 0)
143
+ return; // the whole visible history fits the window
144
+ const boundaryIdx = starts[chosen];
145
+ if (boundaryIdx === undefined)
146
+ return;
147
+ const boundary = scanned[boundaryIdx];
148
+ if (boundary === undefined)
149
+ return;
150
+ child.appendCompaction(`Inherited from the parent conversation; ${chosen} earlier exchange(s) are not shown.`, boundary.id, (suffixTokens[0] ?? 0) - (suffixTokens[boundaryIdx] ?? 0));
151
+ }
152
+ /**
153
+ * The branch point this inheritance should copy up to, and the parent's path — the decision half,
154
+ * shared by both backends because WHERE a thread branches is policy, not storage.
155
+ */
156
+ export function inheritanceCut(parent, branchHints) {
157
+ const path = parent.getBranch();
158
+ const leaf = path[path.length - 1];
159
+ if (!leaf)
160
+ return undefined; // an empty parent has nothing to inherit
161
+ const hints = branchHints ?? [];
162
+ const at = locateBranchPoint(path, hints);
163
+ if (at === undefined && hints.length > 0) {
164
+ log.warn("[fastagent] no branch hint matched in the parent session — inheriting from its present");
165
+ }
166
+ return { at: at ?? leaf.id };
167
+ }
168
+ /**
169
+ * Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to
170
+ * fork has to do instead. Kinds pi models as facts about an entry rather than positions (labels,
171
+ * the session name) are not copied: they describe the parent's record, not the thread's history.
172
+ */
173
+ export function copyBranchInto(parent, child, at) {
174
+ for (const raw of parent.getBranch(at)) {
175
+ const entry = raw;
176
+ switch (entry.type) {
177
+ case "message":
178
+ if (entry.message)
179
+ child.appendMessage(entry.message);
180
+ break;
181
+ case "compaction":
182
+ child.appendCompaction(entry.summary ?? "", child.getLeafId() ?? "", entry.tokensBefore ?? 0, entry.details);
183
+ break;
184
+ case "model_change":
185
+ if (entry.provider && entry.modelId)
186
+ child.appendModelChange(entry.provider, entry.modelId);
187
+ break;
188
+ case "thinking_level_change":
189
+ if (entry.thinkingLevel)
190
+ child.appendThinkingLevelChange(entry.thinkingLevel);
191
+ break;
192
+ case "custom":
193
+ if (entry.customType)
194
+ child.appendCustomEntry(entry.customType, entry.data);
195
+ break;
196
+ default:
197
+ break; // label / session_info / branch_summary: the parent's facts, not the thread's history
198
+ }
199
+ }
200
+ markInheritanceWindow(child);
201
+ }
202
+ /**
203
+ * Fork `parent` into a record named `id`, up to the branch point the hints locate, in `stagingDir`.
204
+ *
205
+ * Two pi calls rather than one, because neither alone does it: `createBranchedSession` copies
206
+ * exactly the path to an entry but names the result with a generated id, and `forkFrom` takes an id
207
+ * but copies everything. The intermediate is deleted; it exists for one call.
208
+ *
209
+ * STAGED, not published: the caller finishes the record (crash reconciliation) and moves it into
210
+ * place. Publishing here and finishing after would leave a half-prepared record under the id on any
211
+ * later failure — and the fallback path would then create a SECOND record with the same id, making
212
+ * which one a lookup finds a matter of directory order.
213
+ *
214
+ * Returns undefined when inheritance cannot be honored — the caller starts the session empty.
215
+ */
216
+ export function forkForInheritance(options) {
217
+ const { parent, id, cwd, stagingDir: dir } = options;
218
+ const cut = inheritanceCut(parent, options.branchHints);
219
+ if (!cut)
220
+ return undefined;
221
+ let branched;
222
+ try {
223
+ branched = parent.createBranchedSession(cut.at);
224
+ if (!branched)
225
+ return undefined; // a non-persisting parent has no file to fork from
226
+ const child = SessionManager.forkFrom(branched, cwd, dir, { id });
227
+ markInheritanceWindow(child);
228
+ return child;
229
+ }
230
+ finally {
231
+ if (branched) {
232
+ try {
233
+ unlinkSync(branched);
234
+ }
235
+ catch (error) {
236
+ // The intermediate is inert (it is never listed as this store's record), so a failed
237
+ // cleanup costs a stray file, not correctness — but silence would hide a full disk.
238
+ log.warn(`[fastagent] could not remove the intermediate fork ${branched}: ${String(error)}`);
239
+ }
240
+ }
241
+ }
242
+ }
@@ -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
+ * `set_thinking` 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 {
@@ -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 `navigate`,
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): OverrideEntryLike[];
@@ -71,3 +71,21 @@ export function resolveSessionSettings(entries, models, defaults) {
71
71
  ...(dropped.model || dropped.thinkingLevel ? { dropped } : {}),
72
72
  };
73
73
  }
74
+ /**
75
+ * The entries on the session's ACTIVE path, root→leaf — what every last-wins settings read walks.
76
+ * `getBranch()` is exactly that walk: the journal can hold abandoned branches after a `navigate`,
77
+ * and reading it flat would run the session on a setting it moved away from.
78
+ *
79
+ * A chain that is not intact THROWS. `getBranch()` stops where a parent is missing and answers the
80
+ * SHORT path, which reads exactly like a short session — every override above the gap gone, the next
81
+ * turn silently on assembly defaults. The caller decides what to do with the fault (state() reports
82
+ * the settings as absent, dispatch answers with a code); what it must not do is guess.
83
+ */
84
+ export function activePath(record) {
85
+ const path = record.getBranch();
86
+ const root = path[0];
87
+ if (root?.parentId != null) {
88
+ throw new Error(`session entry "${root.parentId}" is missing from the journal (parent of "${root.id}")`);
89
+ }
90
+ return path;
91
+ }
@@ -0,0 +1,70 @@
1
+ import { SessionManager } from "@earendil-works/pi-coding-agent";
2
+ import { type SessionInheritance } from "./session-inheritance.ts";
3
+ /** What the AgentSession L0 needs from a session backend: open-or-create by opaque id, plus the one
4
+ * creation option — where a NEW session starts from (session-inheritance.ts). */
5
+ export interface PiSessionRecordStore {
6
+ openOrCreate(sessionId: string, inherit?: SessionInheritance): Promise<SessionManager>;
7
+ /** OPEN-EXISTING sibling: an unknown session answers undefined, never creates one — sessions are
8
+ * the data plane's monopoly. This is what the control plane reads and writes boundary records
9
+ * through, so a mutation on an unknown id is rejected rather than minted into a ghost record. */
10
+ openIfExists(sessionId: string): Promise<SessionManager | undefined>;
11
+ }
12
+ /**
13
+ * A Caller's session id, as a name pi will accept.
14
+ *
15
+ * `SessionManager` enforces `^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$` — which no built-in
16
+ * channel satisfies: telegram's `-1001234567890` leads with a dash, feishu and slack keys carry
17
+ * `:` and `/`, and any custom `route()` may mint anything at all.
18
+ *
19
+ * The mapping is INJECTIVE, because two conversations resolving to one record is two rooms sharing a
20
+ * memory. `s` prefix (the pattern demands an alphanumeric head, and prefixing unconditionally is what
21
+ * keeps it injective — a conditional one would map `-a` and `s-a` alike), then each character
22
+ * outside `[A-Za-z0-9.-]` becomes `_XX` / `_uXXXX`, self-describing widths so no two inputs can
23
+ * produce one output. `_` escapes itself for the same reason. A trailing `.` or `-` is legal
24
+ * mid-name but not at the end, so it escapes too.
25
+ *
26
+ * Injective within this encoding — which is only sufficient because new records live in their own
27
+ * directory. The older spelling draws names from the same character set (it stored a room literally
28
+ * called `s42` as `s42`, which is also this encoding of `42`), so one directory would make some
29
+ * names ambiguous no matter how either side spells them.
30
+ *
31
+ * Readability is deliberate: `-1001234567890` becomes `s-1001234567890`, so an operator can still
32
+ * tell which room a file belongs to.
33
+ */
34
+ export declare function piSessionId(sessionId: string): string;
35
+ /**
36
+ * Disk-backed store under `dir`: restart the process, conversations continue.
37
+ *
38
+ * Lookup is a directory scan (`SessionManager.list`) because pi names files `<timestamp>_<id>.jsonl`
39
+ * and the timestamp is not ours to predict — the same trade sessions.ts makes today.
40
+ *
41
+ * A record written before this store existed keeps ITS id (the older path spelled them differently),
42
+ * so the scan accepts either: a conversation that predates this store is continued rather than
43
+ * silently restarted as an empty one. Nothing is rewritten on disk.
44
+ *
45
+ * NEW records live in a subdirectory of their own, because the two engines cannot share a namespace:
46
+ * both spell ids into `[A-Za-z0-9._-]`, so neither can claim a prefix the other cannot produce, and
47
+ * a directory holding both would have names that belong to two conversations at once — in whichever
48
+ * direction it is read. Separate directories make each side's own injectivity sufficient.
49
+ *
50
+ * A PRE-EXISTING record is still continued in place: it is looked up by the older spelling, which is
51
+ * injective on its own terms, and appended to where it lies. Both spellings are the same v3 jsonl,
52
+ * so a conversation started before this store keeps going rather than restarting empty.
53
+ *
54
+ * SCOPE OF "open-or-create": idempotent against a store that is serialized per session, which is what
55
+ * the serving path provides — the single-writer lease is taken before any store call, so no two
56
+ * turns of one conversation reach this at once. What it does NOT do is arbitrate a FIRST open racing
57
+ * across processes: two instances that scan before either writes will both create, and the
58
+ * conversation forks into two records. sessions.ts states the same boundary for the same reason
59
+ * ("the serving path serializes it with the single-writer lease before reaching any store"), and a
60
+ * horizontally-scaled deployment that wants more owes a lease that spans its instances — an
61
+ * in-process one cannot arbitrate between them, and a file lock here would only look like it could.
62
+ */
63
+ export declare function piSessionRecordStore(options: {
64
+ dir: string;
65
+ cwd?: string;
66
+ }): PiSessionRecordStore;
67
+ /** In-process store: continuity lives and dies with the instance. */
68
+ export declare function piInMemorySessionRecordStore(options?: {
69
+ cwd?: string;
70
+ }): PiSessionRecordStore;