@fastagent-sh/fastagent 0.19.0 → 0.21.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.
- package/README.md +5 -1
- package/dist/atomic-write.d.ts +11 -7
- package/dist/atomic-write.js +16 -11
- package/dist/channels/agentcore-limits.d.ts +9 -0
- package/dist/channels/agentcore-limits.js +9 -0
- package/dist/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +12 -15
- package/dist/channels/agentcore-service.js +15 -24
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +9 -83
- package/dist/channels/agentcore.js +101 -93
- package/dist/channels/control.d.ts +50 -12
- package/dist/channels/control.js +251 -157
- package/dist/channels/discover.d.ts +26 -5
- package/dist/channels/discover.js +13 -26
- package/dist/channels/feishu/context-buffer.d.ts +6 -0
- package/dist/channels/feishu/context-buffer.js +0 -38
- package/dist/channels/feishu/crypto.d.ts +0 -2
- package/dist/channels/feishu/crypto.js +3 -8
- package/dist/channels/feishu/feishu-api.js +7 -12
- package/dist/channels/feishu/feishu.js +86 -128
- package/dist/channels/feishu/parse.d.ts +4 -3
- package/dist/channels/feishu/parse.js +5 -4
- package/dist/channels/feishu/register-webhook.d.ts +5 -5
- package/dist/channels/feishu/register-webhook.js +43 -57
- package/dist/channels/feishu/scaffold/feishu-send.ts +12 -67
- package/dist/channels/feishu/setup-mode.d.ts +30 -0
- package/dist/channels/feishu/setup-mode.js +26 -0
- package/dist/channels/feishu/shared-api.d.ts +10 -0
- package/dist/channels/feishu/shared-api.js +38 -0
- package/dist/channels/http.d.ts +0 -8
- package/dist/channels/http.js +4 -56
- package/dist/channels/kit/attachment-path.d.ts +12 -0
- package/dist/channels/kit/attachment-path.js +43 -0
- package/dist/channels/kit/context-buffer.d.ts +9 -0
- package/dist/channels/kit/context-buffer.js +11 -0
- package/dist/channels/kit/signature.d.ts +12 -0
- package/dist/channels/kit/signature.js +17 -0
- package/dist/channels/kit/stop-command.js +3 -3
- package/dist/channels/kit/tasks.d.ts +1 -1
- package/dist/channels/kit/tasks.js +10 -4
- package/dist/channels/kit/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- package/dist/channels/kit/turn-store.d.ts +19 -3
- package/dist/channels/kit/turn-store.js +29 -2
- package/dist/channels/lark/scaffold/lark-send.ts +12 -67
- package/dist/channels/registration.d.ts +36 -1
- package/dist/channels/registration.js +57 -1
- package/dist/channels/secret.d.ts +1 -0
- package/dist/channels/secret.js +16 -0
- package/dist/channels/slack/config-api.d.ts +35 -3
- package/dist/channels/slack/config-api.js +51 -9
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +5 -1
- package/dist/channels/slack/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +44 -23
- package/dist/channels/slack/onboarding-state.d.ts +7 -3
- package/dist/channels/slack/onboarding-state.js +13 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +35 -15
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -141
- package/dist/channels/slack/shared-api.d.ts +10 -0
- package/dist/channels/slack/shared-api.js +34 -0
- package/dist/channels/slack/slack-api.d.ts +20 -2
- package/dist/channels/slack/slack-api.js +100 -73
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +68 -107
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/parse.d.ts +21 -1
- package/dist/channels/telegram/parse.js +65 -11
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/scaffold/channel.ts +7 -3
- package/dist/channels/telegram/telegram-api.js +5 -6
- package/dist/channels/telegram/telegram.d.ts +2 -2
- package/dist/channels/telegram/telegram.js +97 -204
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +13 -25
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -35
- package/dist/cli/commands/attach.d.ts +2 -4
- package/dist/cli/commands/attach.js +17 -15
- package/dist/cli/commands/chat.js +6 -12
- package/dist/cli/commands/deploy/agentcore.d.ts +2 -0
- package/dist/cli/commands/deploy/agentcore.js +178 -0
- package/dist/cli/commands/deploy/docker.d.ts +2 -0
- package/dist/cli/commands/deploy/docker.js +119 -0
- package/dist/cli/commands/deploy/fly.d.ts +2 -0
- package/dist/cli/commands/deploy/fly.js +131 -0
- package/dist/cli/commands/deploy/railway.d.ts +2 -0
- package/dist/cli/commands/deploy/railway.js +71 -0
- package/dist/cli/commands/deploy/shared.d.ts +114 -0
- package/dist/cli/commands/deploy/shared.js +124 -0
- package/dist/cli/commands/deploy.d.ts +38 -25
- package/dist/cli/commands/deploy.js +101 -631
- package/dist/cli/commands/dev.js +20 -76
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +1 -1
- package/dist/cli/commands/init.js +1 -2
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +41 -80
- package/dist/cli/commands/tool.js +3 -8
- package/dist/cli/kernel.d.ts +0 -2
- package/dist/cli/kernel.js +0 -2
- package/dist/cli/program.js +7 -12
- package/dist/cli/serve.d.ts +42 -34
- package/dist/cli/serve.js +107 -28
- package/dist/cli/shared.d.ts +52 -27
- package/dist/cli/shared.js +69 -6
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/logs.d.ts +2 -2
- package/dist/deploy/agentcore/logs.js +2 -2
- package/dist/deploy/agentcore/plan.d.ts +34 -11
- package/dist/deploy/agentcore/plan.js +52 -259
- package/dist/deploy/agentcore/run.d.ts +9 -24
- package/dist/deploy/agentcore/run.js +35 -43
- package/dist/deploy/channel-ingress.d.ts +73 -0
- package/dist/deploy/channel-ingress.js +101 -0
- package/dist/deploy/docker/plan.d.ts +3 -7
- package/dist/deploy/docker/plan.js +4 -15
- package/dist/deploy/docker/run.d.ts +35 -4
- package/dist/deploy/docker/run.js +40 -8
- package/dist/deploy/fly/plan.d.ts +4 -5
- package/dist/deploy/fly/plan.js +14 -22
- package/dist/deploy/fly/run.d.ts +36 -23
- package/dist/deploy/fly/run.js +129 -83
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.d.ts +6 -7
- package/dist/deploy/preflight.js +27 -23
- package/dist/deploy/railway/plan.d.ts +9 -5
- package/dist/deploy/railway/plan.js +18 -26
- package/dist/deploy/railway/run.d.ts +11 -10
- package/dist/deploy/railway/run.js +44 -43
- package/dist/deploy/runner.js +22 -2
- package/dist/deploy/secrets.d.ts +18 -9
- package/dist/deploy/secrets.js +63 -28
- package/dist/dev-supervisor.js +3 -4
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +111 -106
- package/dist/engines/pi/auth.js +43 -43
- package/dist/engines/pi/config.d.ts +2 -2
- package/dist/engines/pi/create.d.ts +22 -27
- package/dist/engines/pi/create.js +59 -74
- package/dist/engines/pi/definition.d.ts +1 -1
- package/dist/engines/pi/definition.js +11 -12
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/login.js +32 -16
- package/dist/engines/pi/open.d.ts +3 -4
- package/dist/engines/pi/open.js +32 -42
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/search-tools.js +1 -1
- package/dist/engines/pi/service.d.ts +1 -1
- package/dist/engines/pi/service.js +8 -0
- package/dist/engines/pi/session-builder.js +26 -140
- package/dist/engines/pi/session-control.d.ts +27 -22
- package/dist/engines/pi/session-control.js +551 -490
- package/dist/engines/pi/session-inheritance.d.ts +8 -22
- package/dist/engines/pi/session-inheritance.js +95 -76
- package/dist/engines/pi/session-markers.d.ts +48 -0
- package/dist/engines/pi/session-markers.js +59 -0
- package/dist/engines/pi/session-settings.d.ts +5 -5
- package/dist/engines/pi/session-settings.js +8 -5
- package/dist/engines/pi/session-store.d.ts +91 -26
- package/dist/engines/pi/session-store.js +413 -82
- package/dist/engines/pi/tool-context.d.ts +24 -11
- package/dist/engines/pi/tool-context.js +29 -4
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +3 -2
- package/dist/env.js +1 -2
- package/dist/feishu.d.ts +1 -0
- package/dist/feishu.js +1 -0
- package/dist/lark.d.ts +1 -0
- package/dist/lark.js +1 -0
- package/dist/loader.d.ts +51 -7
- package/dist/loader.js +84 -18
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +26 -3
- package/dist/paths.js +43 -5
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +50 -67
- package/dist/scaffold/init.js +7 -2
- package/dist/scaffold/templates/tools/fetch-url.ts +0 -2
- package/dist/schedule/discover.js +3 -15
- package/dist/schedule/wake-alarm.d.ts +14 -19
- package/dist/schedule/wake-alarm.js +89 -48
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +10 -7
- package/dist/service.d.ts +24 -24
- package/dist/service.js +33 -77
- package/dist/session-remote.d.ts +12 -7
- package/dist/session-remote.js +185 -118
- package/dist/session.d.ts +227 -93
- package/dist/session.js +61 -23
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +1 -1
- package/dist/tunnel.d.ts +34 -9
- package/dist/tunnel.js +83 -50
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
/** What a Caller names when a new session should start from an existing one. */
|
|
3
3
|
export interface SessionInheritance {
|
|
4
4
|
/** The session to inherit from. Missing or unreadable → start empty, with a warn: context is not
|
|
@@ -19,26 +19,12 @@ export declare function inheritanceCut(parent: SessionManager, branchHints?: str
|
|
|
19
19
|
* Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to
|
|
20
20
|
* fork has to do instead. Kinds pi models as facts about an entry rather than positions (labels,
|
|
21
21
|
* the session name) are not copied: they describe the parent's record, not the thread's history.
|
|
22
|
-
*/
|
|
23
|
-
export declare function copyBranchInto(parent: SessionManager, child: SessionManager, at: string): void;
|
|
24
|
-
/**
|
|
25
|
-
* Fork `parent` into a record named `id`, up to the branch point the hints locate, in `stagingDir`.
|
|
26
|
-
*
|
|
27
|
-
* Two pi calls rather than one, because neither alone does it: `createBranchedSession` copies
|
|
28
|
-
* exactly the path to an entry but names the result with a generated id, and `forkFrom` takes an id
|
|
29
|
-
* but copies everything. The intermediate is deleted; it exists for one call.
|
|
30
22
|
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* which one a lookup finds a matter of directory order.
|
|
35
|
-
*
|
|
36
|
-
* Returns undefined when inheritance cannot be honored — the caller starts the session empty.
|
|
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.
|
|
37
26
|
*/
|
|
38
|
-
export declare function
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
stagingDir: string;
|
|
43
|
-
branchHints?: string[];
|
|
44
|
-
}): SessionManager | undefined;
|
|
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;
|
|
@@ -1,22 +1,5 @@
|
|
|
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
1
|
import { log } from "../../log.js";
|
|
2
|
+
import { isPlaneMarker } from "./session-markers.js";
|
|
20
3
|
/** Inheritance window: at most this many exchanges of the parent reach the child's model context. */
|
|
21
4
|
const INHERIT_MAX_EXCHANGES = 50;
|
|
22
5
|
/** …and at most roughly this many tokens (~1/4 of a 200K context: generous, not everything). Both
|
|
@@ -37,8 +20,7 @@ function isUserMessage(entry) {
|
|
|
37
20
|
}
|
|
38
21
|
/** Rough token estimate for windowing — text at chars/4, images flat. Precision is not the point:
|
|
39
22
|
* the window is a budget, and being 20% off moves a boundary by an exchange, not correctness. */
|
|
40
|
-
function
|
|
41
|
-
const content = message.content;
|
|
23
|
+
function estimateContentTokens(content) {
|
|
42
24
|
if (typeof content === "string")
|
|
43
25
|
return Math.ceil(content.length / 4);
|
|
44
26
|
if (!Array.isArray(content))
|
|
@@ -54,17 +36,40 @@ function estimateMessageTokens(message) {
|
|
|
54
36
|
}
|
|
55
37
|
return tokens;
|
|
56
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. */
|
|
57
41
|
function estimateEntryTokens(entry) {
|
|
58
|
-
|
|
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;
|
|
59
47
|
}
|
|
60
|
-
/** A compaction entry's summary
|
|
61
|
-
*
|
|
62
|
-
|
|
63
|
-
|
|
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")
|
|
64
63
|
return 0;
|
|
65
|
-
let tokens = Math.ceil((
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
|
68
73
|
return tokens;
|
|
69
74
|
}
|
|
70
75
|
/**
|
|
@@ -116,7 +121,7 @@ function markInheritanceWindow(child) {
|
|
|
116
121
|
const scanned = path.slice(scanFrom);
|
|
117
122
|
// The compaction's own summary + retained tail reach the model regardless of where the window
|
|
118
123
|
// lands, so they charge the budget as a base cost — not estimating them would over-admit.
|
|
119
|
-
const baseTokens = estimateCompactionTokens(path
|
|
124
|
+
const baseTokens = estimateCompactionTokens(path, scanFrom - 1);
|
|
120
125
|
const starts = [];
|
|
121
126
|
scanned.forEach((entry, i) => {
|
|
122
127
|
if (isUserMessage(entry))
|
|
@@ -169,74 +174,88 @@ export function inheritanceCut(parent, branchHints) {
|
|
|
169
174
|
* Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to
|
|
170
175
|
* fork has to do instead. Kinds pi models as facts about an entry rather than positions (labels,
|
|
171
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.
|
|
172
181
|
*/
|
|
173
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
|
+
};
|
|
174
197
|
for (const raw of parent.getBranch(at)) {
|
|
175
198
|
const entry = raw;
|
|
199
|
+
let childId;
|
|
176
200
|
switch (entry.type) {
|
|
177
201
|
case "message":
|
|
178
|
-
if (entry.message)
|
|
179
|
-
child.appendMessage(entry.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);
|
|
180
210
|
break;
|
|
181
211
|
case "compaction":
|
|
182
|
-
|
|
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);
|
|
183
219
|
break;
|
|
184
220
|
case "model_change":
|
|
185
221
|
if (entry.provider && entry.modelId)
|
|
186
|
-
child.appendModelChange(entry.provider, entry.modelId);
|
|
222
|
+
childId = child.appendModelChange(entry.provider, entry.modelId);
|
|
187
223
|
break;
|
|
188
224
|
case "thinking_level_change":
|
|
189
225
|
if (entry.thinkingLevel)
|
|
190
|
-
child.appendThinkingLevelChange(entry.thinkingLevel);
|
|
226
|
+
childId = child.appendThinkingLevelChange(entry.thinkingLevel);
|
|
191
227
|
break;
|
|
192
228
|
case "custom":
|
|
193
|
-
|
|
194
|
-
|
|
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
|
+
}
|
|
195
238
|
break;
|
|
196
239
|
default:
|
|
197
240
|
break; // label / session_info / branch_summary: the parent's facts, not the thread's history
|
|
198
241
|
}
|
|
242
|
+
if (childId === undefined)
|
|
243
|
+
unanchored.push(entry.id);
|
|
244
|
+
else
|
|
245
|
+
record(entry.id, childId);
|
|
199
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);
|
|
200
252
|
markInheritanceWindow(child);
|
|
201
253
|
}
|
|
202
|
-
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
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.
|
|
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.
|
|
215
261
|
*/
|
|
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
|
-
}
|
|
@@ -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
|
-
* `
|
|
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
|
|
@@ -33,9 +33,9 @@ export interface SessionSettings {
|
|
|
33
33
|
model: AnyModel;
|
|
34
34
|
/** Already clamped to what {@link model} supports. */
|
|
35
35
|
thinkingLevel: ThinkingLevel;
|
|
36
|
-
/** What `
|
|
36
|
+
/** What `update({ thinkingLevel })` accepts for this session. */
|
|
37
37
|
availableThinkingLevels: string[];
|
|
38
|
-
/** Recorded
|
|
38
|
+
/** Recorded overrides that could not be applied. */
|
|
39
39
|
dropped?: {
|
|
40
40
|
model?: string;
|
|
41
41
|
thinkingLevel?: {
|
|
@@ -52,7 +52,7 @@ export declare function resolveSessionSettings(entries: OverrideEntryLike[], mod
|
|
|
52
52
|
}): SessionSettings;
|
|
53
53
|
/**
|
|
54
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
|
|
55
|
+
* `getBranch()` is exactly that walk: the journal can hold abandoned branches after a leaf move,
|
|
56
56
|
* and reading it flat would run the session on a setting it moved away from.
|
|
57
57
|
*
|
|
58
58
|
* A chain that is not intact THROWS. `getBranch()` stops where a parent is missing and answers the
|
|
@@ -60,4 +60,4 @@ export declare function resolveSessionSettings(entries: OverrideEntryLike[], mod
|
|
|
60
60
|
* turn silently on assembly defaults. The caller decides what to do with the fault (state() reports
|
|
61
61
|
* the settings as absent, dispatch answers with a code); what it must not do is guess.
|
|
62
62
|
*/
|
|
63
|
-
export declare function activePath(record: SessionManager): OverrideEntryLike[];
|
|
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 `
|
|
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,
|
|
@@ -48,7 +49,7 @@ export function resolveSessionSettings(entries, models, defaults) {
|
|
|
48
49
|
dropped.model = `${recorded.model.provider}/${recorded.model.modelId}`;
|
|
49
50
|
}
|
|
50
51
|
const availableThinkingLevels = getSupportedThinkingLevels(model);
|
|
51
|
-
let thinkingLevel = defaults.thinkingLevel;
|
|
52
|
+
let thinkingLevel = clampThinkingLevel(model, defaults.thinkingLevel);
|
|
52
53
|
if (recorded.thinkingLevel !== undefined) {
|
|
53
54
|
const level = recorded.thinkingLevel;
|
|
54
55
|
if (!THINKING_LEVELS.has(level)) {
|
|
@@ -73,7 +74,7 @@ export function resolveSessionSettings(entries, models, defaults) {
|
|
|
73
74
|
}
|
|
74
75
|
/**
|
|
75
76
|
* 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
|
|
77
|
+
* `getBranch()` is exactly that walk: the journal can hold abandoned branches after a leaf move,
|
|
77
78
|
* and reading it flat would run the session on a setting it moved away from.
|
|
78
79
|
*
|
|
79
80
|
* A chain that is not intact THROWS. `getBranch()` stops where a parent is missing and answers the
|
|
@@ -81,8 +82,10 @@ export function resolveSessionSettings(entries, models, defaults) {
|
|
|
81
82
|
* turn silently on assembly defaults. The caller decides what to do with the fault (state() reports
|
|
82
83
|
* the settings as absent, dispatch answers with a code); what it must not do is guess.
|
|
83
84
|
*/
|
|
84
|
-
export function activePath(record) {
|
|
85
|
-
|
|
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);
|
|
86
89
|
const root = path[0];
|
|
87
90
|
if (root?.parentId != null) {
|
|
88
91
|
throw new Error(`session entry "${root.parentId}" is missing from the journal (parent of "${root.id}")`);
|
|
@@ -1,13 +1,75 @@
|
|
|
1
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";
|
|
2
4
|
import { type SessionInheritance } from "./session-inheritance.ts";
|
|
3
|
-
/**
|
|
4
|
-
*
|
|
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
|
+
*/
|
|
5
17
|
export interface PiSessionRecordStore {
|
|
6
18
|
openOrCreate(sessionId: string, inherit?: SessionInheritance): Promise<SessionManager>;
|
|
7
19
|
/** OPEN-EXISTING sibling: an unknown session answers undefined, never creates one — sessions are
|
|
8
|
-
* the data plane's monopoly.
|
|
9
|
-
*
|
|
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. */
|
|
10
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[];
|
|
11
73
|
}
|
|
12
74
|
/**
|
|
13
75
|
* A Caller's session id, as a name pi will accept.
|
|
@@ -23,42 +85,44 @@ export interface PiSessionRecordStore {
|
|
|
23
85
|
* produce one output. `_` escapes itself for the same reason. A trailing `.` or `-` is legal
|
|
24
86
|
* mid-name but not at the end, so it escapes too.
|
|
25
87
|
*
|
|
26
|
-
* Injective within this encoding — which is only sufficient because
|
|
27
|
-
* directory.
|
|
28
|
-
* called `s42`
|
|
29
|
-
* names ambiguous no matter how
|
|
88
|
+
* Injective within this encoding — which is only sufficient because this store's records live in
|
|
89
|
+
* their own directory. A name this store did not write draws from the same character set and can
|
|
90
|
+
* still decode: a file called `s42` is also this encoding of `42`, so one directory would make some
|
|
91
|
+
* names ambiguous no matter how this side spells them.
|
|
30
92
|
*
|
|
31
93
|
* Readability is deliberate: `-1001234567890` becomes `s-1001234567890`, so an operator can still
|
|
32
94
|
* tell which room a file belongs to.
|
|
33
95
|
*/
|
|
34
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;
|
|
35
105
|
/**
|
|
36
106
|
* Disk-backed store under `dir`: restart the process, conversations continue.
|
|
37
107
|
*
|
|
38
|
-
* Lookup is a directory scan
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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.
|
|
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}).
|
|
49
112
|
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
113
|
+
* Records live in a subdirectory of their own, because a name this store did not write can still
|
|
114
|
+
* decode to a Caller id: a file called `s42` beside them is also this encoding of `42`, so a shared
|
|
115
|
+
* directory would answer `42` with a record it does not own. Only this directory is scanned, which
|
|
116
|
+
* makes this encoding's own injectivity sufficient — a record written before this store existed
|
|
117
|
+
* lies outside it and is never read.
|
|
53
118
|
*
|
|
54
119
|
* SCOPE OF "open-or-create": idempotent against a store that is serialized per session, which is what
|
|
55
120
|
* the serving path provides — the single-writer lease is taken before any store call, so no two
|
|
56
121
|
* turns of one conversation reach this at once. What it does NOT do is arbitrate a FIRST open racing
|
|
57
122
|
* across processes: two instances that scan before either writes will both create, and the
|
|
58
|
-
* conversation forks into two records.
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
* in-process one cannot arbitrate between them, and a file lock here would only look like it could.
|
|
123
|
+
* conversation forks into two records. A horizontally-scaled deployment that wants more owes a lease
|
|
124
|
+
* that spans its instances — an in-process one cannot arbitrate between them, and a file lock here
|
|
125
|
+
* would only look like it could.
|
|
62
126
|
*/
|
|
63
127
|
export declare function piSessionRecordStore(options: {
|
|
64
128
|
dir: string;
|
|
@@ -68,3 +132,4 @@ export declare function piSessionRecordStore(options: {
|
|
|
68
132
|
export declare function piInMemorySessionRecordStore(options?: {
|
|
69
133
|
cwd?: string;
|
|
70
134
|
}): PiSessionRecordStore;
|
|
135
|
+
export {};
|