@fastagent-sh/fastagent 0.20.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/channels/agentcore-protocol.d.ts +112 -0
- package/dist/channels/agentcore-protocol.js +22 -0
- package/dist/channels/agentcore-service.d.ts +0 -4
- package/dist/channels/agentcore-service.js +1 -6
- package/dist/channels/agentcore-state.d.ts +5 -11
- package/dist/channels/agentcore-state.js +4 -1
- package/dist/channels/agentcore.d.ts +1 -67
- package/dist/channels/agentcore.js +90 -89
- package/dist/channels/control.d.ts +1 -1
- package/dist/channels/control.js +13 -62
- package/dist/channels/discover.d.ts +0 -1
- package/dist/channels/discover.js +1 -3
- 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 +2 -4
- package/dist/channels/feishu/feishu.js +56 -115
- 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/turn-runner.d.ts +59 -0
- package/dist/channels/kit/turn-runner.js +84 -0
- 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/manifest.js +5 -1
- package/dist/channels/slack/onboard.d.ts +5 -5
- package/dist/channels/slack/onboard.js +41 -20
- package/dist/channels/slack/register-webhook.d.ts +3 -2
- package/dist/channels/slack/register-webhook.js +34 -14
- package/dist/channels/slack/scaffold/channel.ts +3 -10
- package/dist/channels/slack/scaffold/slack-send.ts +18 -126
- 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 +81 -22
- package/dist/channels/slack/slack.d.ts +0 -10
- package/dist/channels/slack/slack.js +60 -99
- package/dist/channels/sse.d.ts +4 -0
- package/dist/channels/sse.js +66 -0
- package/dist/channels/telegram/register-webhook.d.ts +6 -9
- package/dist/channels/telegram/register-webhook.js +44 -42
- package/dist/channels/telegram/telegram.js +47 -142
- package/dist/channels/wait-health.js +7 -4
- package/dist/cli/add-feishu.js +3 -10
- package/dist/cli/add-slack.js +7 -20
- package/dist/cli/commands/add.d.ts +0 -1
- package/dist/cli/commands/add.js +22 -31
- 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 +17 -49
- package/dist/cli/commands/deploy.js +39 -618
- package/dist/cli/commands/dev.js +17 -46
- package/dist/cli/commands/fire.js +6 -12
- package/dist/cli/commands/info.js +2 -1
- package/dist/cli/commands/invoke.js +4 -11
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +17 -46
- 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 +36 -19
- package/dist/cli/serve.js +91 -7
- package/dist/cli/shared.d.ts +13 -18
- package/dist/cli/shared.js +20 -4
- package/dist/deploy/agentcore/forwarder.js +250 -0
- package/dist/deploy/agentcore/plan.d.ts +23 -2
- package/dist/deploy/agentcore/plan.js +29 -244
- package/dist/deploy/agentcore/run.d.ts +4 -20
- package/dist/deploy/agentcore/run.js +22 -7
- package/dist/deploy/channel-ingress.js +1 -1
- package/dist/deploy/docker/run.d.ts +17 -3
- package/dist/deploy/docker/run.js +30 -8
- package/dist/deploy/fly/plan.js +7 -0
- package/dist/deploy/fly/run.d.ts +27 -0
- package/dist/deploy/fly/run.js +102 -20
- package/dist/deploy/hosts.d.ts +5 -0
- package/dist/deploy/hosts.js +4 -0
- package/dist/deploy/preflight.js +3 -3
- package/dist/deploy/railway/plan.d.ts +5 -0
- package/dist/deploy/railway/plan.js +7 -0
- package/dist/deploy/railway/run.d.ts +2 -1
- package/dist/deploy/railway/run.js +5 -4
- package/dist/deploy/secrets.js +0 -17
- package/dist/engines/pi/agent-session-factory.d.ts +37 -17
- package/dist/engines/pi/agent-session-factory.js +109 -85
- package/dist/engines/pi/config.d.ts +1 -1
- package/dist/engines/pi/create.d.ts +22 -39
- package/dist/engines/pi/create.js +58 -73
- package/dist/engines/pi/definition.js +8 -8
- package/dist/engines/pi/invoke-session.js +13 -30
- package/dist/engines/pi/open.d.ts +2 -2
- package/dist/engines/pi/open.js +31 -41
- package/dist/engines/pi/retry-event.d.ts +6 -0
- package/dist/engines/pi/retry-event.js +15 -0
- package/dist/engines/pi/session-builder.js +26 -113
- package/dist/engines/pi/session-control.d.ts +10 -18
- package/dist/engines/pi/session-control.js +47 -81
- package/dist/engines/pi/session-settings.d.ts +1 -1
- package/dist/engines/pi/session-settings.js +1 -1
- package/dist/engines/pi/session-store.d.ts +12 -17
- package/dist/engines/pi/session-store.js +18 -40
- package/dist/engines/pi/tool-context.d.ts +3 -3
- package/dist/engines/pi/tool.d.ts +6 -9
- package/dist/engines/pi/tool.js +1 -0
- 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 +2 -0
- package/dist/loader.js +5 -0
- package/dist/log.d.ts +9 -17
- package/dist/log.js +25 -30
- package/dist/paths.d.ts +7 -3
- package/dist/paths.js +17 -4
- package/dist/scaffold/add-channel.d.ts +6 -1
- package/dist/scaffold/add-channel.js +48 -65
- package/dist/schedule/wake-alarm.d.ts +1 -12
- package/dist/schedule/wake-alarm.js +2 -3
- package/dist/service.d.ts +23 -22
- package/dist/service.js +7 -73
- package/dist/session-remote.d.ts +5 -5
- package/dist/session-remote.js +25 -28
- package/dist/session.d.ts +1 -1
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +1 -0
- package/dist/tunnel.d.ts +16 -6
- package/dist/tunnel.js +53 -12
- package/package.json +5 -4
- package/dist/channels/slack/bot-auth.d.ts +0 -15
- package/dist/channels/slack/bot-auth.js +0 -135
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Manual compaction retries are session-scoped; retries inside an invoke carry its runId. */
|
|
2
|
+
export function toRetryScheduledEvent(event, runId) {
|
|
3
|
+
return {
|
|
4
|
+
type: "retry_scheduled",
|
|
5
|
+
timestamp: Date.now(),
|
|
6
|
+
...(runId === undefined ? {} : { runId }),
|
|
7
|
+
data: {
|
|
8
|
+
operation: event.type === "auto_retry_start" ? "assistant" : "compaction",
|
|
9
|
+
attempt: event.attempt,
|
|
10
|
+
maxAttempts: event.maxAttempts,
|
|
11
|
+
delayMs: event.delayMs,
|
|
12
|
+
error: event.errorMessage,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shared definition-aware session builder: open a directory's assembled agent as a resident pi
|
|
3
|
-
* `AgentSessionRuntime
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* and deliberately not chat sessions (design §10/§15).
|
|
3
|
+
* `AgentSessionRuntime`, running the SAME agent that `dev`/`start` serve. The TUI (chat.ts) is its
|
|
4
|
+
* one consumer: the session control plane was built on the invoke pipeline instead of this resident
|
|
5
|
+
* runtime, so control-plane observation covers invoke-driven runs and deliberately not chat sessions
|
|
6
|
+
* (docs/design/session-control.md §10, §15).
|
|
8
7
|
*
|
|
9
8
|
* FIDELITY: pi's vanilla discovery (AGENTS.md walk to repo root, machine-global skills/extensions)
|
|
10
9
|
* is suppressed; fastagent's assembly is INJECTED into pi's session:
|
|
@@ -28,16 +27,15 @@
|
|
|
28
27
|
* workspace.
|
|
29
28
|
*/
|
|
30
29
|
import { existsSync, readFileSync } from "node:fs";
|
|
31
|
-
import {
|
|
30
|
+
import { resolve } from "node:path";
|
|
32
31
|
import { NodeExecutionEnv } from "@earendil-works/pi-agent-core/node";
|
|
33
|
-
import { SessionManager,
|
|
34
|
-
import { definitionResourceLoaderOptions, reportExtensionErrors } from "./agent-session-factory.js";
|
|
32
|
+
import { SessionManager, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, } from "@earendil-works/pi-coding-agent";
|
|
33
|
+
import { bindPiSession, definitionResourceLoaderOptions, reportExtensionErrors } from "./agent-session-factory.js";
|
|
35
34
|
import { resolveModel } from "./config.js";
|
|
36
35
|
import { assembleSystemPrompt, piBasePrompt } from "./create.js";
|
|
37
36
|
import { canonicalPath, loadAgentDefinition, loadExtensionPaths } from "./definition.js";
|
|
38
|
-
import { createPiModelRuntime
|
|
39
|
-
import {
|
|
40
|
-
import { agentSessionManager, sessionToolActivation, turnContext, } from "./tool-context.js";
|
|
37
|
+
import { createPiModelRuntime } from "./models.js";
|
|
38
|
+
import { reportModuleLoadFailures } from "../../loader.js";
|
|
41
39
|
import { reportFindingsIfChanged, reportToolCollisions } from "./report.js";
|
|
42
40
|
import { resolveAgentAssembly } from "./open.js";
|
|
43
41
|
/**
|
|
@@ -51,14 +49,9 @@ sessionManager) {
|
|
|
51
49
|
async function resolveAssembly(cwd) {
|
|
52
50
|
// The shared front half — the SAME placement/config/model-spec/tool/auth resolution the serving
|
|
53
51
|
// opener uses (open.ts); those inputs cannot drift between the two consumption shapes.
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
// (what you iterate is what you serve): the initial active set excludes deferred tools (applied
|
|
58
|
-
// on the session in createRuntime — pi's session starts all-active), and the activation bridge
|
|
59
|
-
// above rides the same turn context, so the SAME search_tools works against pi's AgentSession
|
|
60
|
-
// instead of the served one.
|
|
61
|
-
const { config, modelSpec, agentDir, authPath, stateRoot, tools, deferredToolNames, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
|
|
52
|
+
// Serving re-reads the definition per invoke; chat keeps a startup snapshot. Both pass identity
|
|
53
|
+
// and project context to Pi, which appends skills/cwd. `tools` already includes search_tools.
|
|
54
|
+
const { config, modelSpec, agentDir, authPath, stateRoot, tools, toolCollisions, toolFailures } = await resolveAgentAssembly(cwd, options);
|
|
62
55
|
reportToolCollisions(toolCollisions);
|
|
63
56
|
reportModuleLoadFailures(toolFailures);
|
|
64
57
|
// ONE hub owns model resolution AND per-request auth; see models.ts.
|
|
@@ -71,33 +64,6 @@ sessionManager) {
|
|
|
71
64
|
// Assembly-time, like serving's: this whole function is memoized, so the scan and its warnings
|
|
72
65
|
// happen once per runtime rather than per session rebuild (/new, /resume, fork).
|
|
73
66
|
const extensionPaths = await loadExtensionPaths(agentDir, { cwd, env });
|
|
74
|
-
// fastagent mounts pi's complete coding set itself; `noTools: "builtin"` below keeps the runtime
|
|
75
|
-
// from adding duplicate copies.
|
|
76
|
-
// Adapt fastagent's AgentTool to pi's ToolDefinition (`parameters` is plain JSON-Schema; pi accepts
|
|
77
|
-
// it). Each execute runs inside the turn context with the CURRENT session's activation bridge — the
|
|
78
|
-
// assembly is memoized across /new//resume/fork rebuilds while the session changes, so the bridge
|
|
79
|
-
// resolves through sessionRef at call time, exactly like the serving path resolves its session.
|
|
80
|
-
const customToolDefs = tools.map((t) => ({
|
|
81
|
-
name: t.name,
|
|
82
|
-
label: t.name,
|
|
83
|
-
description: t.description ?? "",
|
|
84
|
-
parameters: t.parameters,
|
|
85
|
-
// Propagate the execution mode — an activating tool (the builtin loader) declares "sequential"
|
|
86
|
-
// so pi serializes its batch; without this, pi's outer active-set diff double-stamps parallels.
|
|
87
|
-
executionMode: t.executionMode,
|
|
88
|
-
execute: (id, params, signal) => {
|
|
89
|
-
const bound = sessionRef.current;
|
|
90
|
-
// Unreachable by construction (createRuntime sets sessionRef before any turn can run a tool).
|
|
91
|
-
// Throw rather than silently run outside the turn context — that would disguise a broken
|
|
92
|
-
// session-lifecycle invariant as a normal out-of-turn call (fail visibly).
|
|
93
|
-
if (!bound)
|
|
94
|
-
throw new Error("tool executed before its session was built (lifecycle invariant broken)");
|
|
95
|
-
return turnContext.run({ cwd, sessionManager: bound.sessionManager, tools: bound.activation },
|
|
96
|
-
// Lower-level MountedTools may consume the fifth-argument env. Directory coding tools are
|
|
97
|
-
// cwd-bound and ignore it; authored tools read FastAgent's turnContext instead.
|
|
98
|
-
() => t.execute(id, params, signal, undefined, { env }));
|
|
99
|
-
},
|
|
100
|
-
}));
|
|
101
67
|
// base + instructions ONLY — pi appends the skill section and env (cwd) itself (including
|
|
102
68
|
// them here would duplicate them).
|
|
103
69
|
const systemPrompt = assembleSystemPrompt({
|
|
@@ -107,14 +73,11 @@ sessionManager) {
|
|
|
107
73
|
return {
|
|
108
74
|
modelRuntime,
|
|
109
75
|
modelSpec,
|
|
110
|
-
authPath,
|
|
111
76
|
// Serving honors config.thinkingLevel (config → L2); the resident session must too (fidelity).
|
|
112
77
|
thinkingLevel: config.thinkingLevel,
|
|
113
78
|
definition,
|
|
114
79
|
extensionPaths,
|
|
115
|
-
|
|
116
|
-
customToolDefs,
|
|
117
|
-
deferredToolNames,
|
|
80
|
+
tools,
|
|
118
81
|
systemPrompt,
|
|
119
82
|
};
|
|
120
83
|
}
|
|
@@ -124,15 +87,6 @@ sessionManager) {
|
|
|
124
87
|
// load edits. And keep it workspace-scoped — `.env` is process-global, so a switch to another cwd
|
|
125
88
|
// would leak env or require mutating global env at runtime.
|
|
126
89
|
const rootCwd = canonicalPath(dir);
|
|
127
|
-
// The CURRENT pi session + its activation bridge, BOUND TOGETHER — rebuilt on /new//resume/fork
|
|
128
|
-
// while the memoized assembly (and its tool execute closures) stays. The bridge shares the
|
|
129
|
-
// session's lifetime because a tool call has to see what the previous one activated. Note on
|
|
130
|
-
// parallel batches: pi wraps SDK customTools in its own before/after active-set diff, so an
|
|
131
|
-
// activating tool must carry `executionMode: "sequential"` (the builtin loader does) — pi then runs
|
|
132
|
-
// the whole batch serially and the outer diff sees correct snapshots.
|
|
133
|
-
// NO activation record here: pi's chat session has nowhere to put one (see sessionToolActivation's
|
|
134
|
-
// `onActivated`), which is the documented divergence from serving — a resumed chat re-discovers.
|
|
135
|
-
const sessionRef = {};
|
|
136
90
|
let assembly;
|
|
137
91
|
const assemblyFor = (cwd) => {
|
|
138
92
|
// Canonical paths: pi's process.cwd() fallback is a realpath, so a symlinked workspace would
|
|
@@ -144,12 +98,8 @@ sessionManager) {
|
|
|
144
98
|
assembly ??= resolveAssembly(rootCwd);
|
|
145
99
|
return assembly;
|
|
146
100
|
};
|
|
147
|
-
// The credential hint belongs to the RUNTIME, not to each session it builds: model resolution
|
|
148
|
-
// moved into createRuntime (extensions must load first), and repeating this on every /new,
|
|
149
|
-
// /resume and fork would nag about a setting that did not change.
|
|
150
|
-
let credentialHintShown = false;
|
|
151
101
|
const createRuntime = async ({ cwd, sessionManager, sessionStartEvent }) => {
|
|
152
|
-
const { modelRuntime, modelSpec,
|
|
102
|
+
const { modelRuntime, modelSpec, thinkingLevel, definition, extensionPaths, tools, systemPrompt } = await assemblyFor(cwd);
|
|
153
103
|
// Per session, NOT memoized with the assembly: pi replaces the session on /new, /resume and
|
|
154
104
|
// fork, and its extension contract is that the replacement gets freshly loaded extensions
|
|
155
105
|
// rather than the previous session's objects. The expensive halves (model hub, auth) are shared
|
|
@@ -172,62 +122,25 @@ sessionManager) {
|
|
|
172
122
|
// AFTER the services, because an extension may be what defines the model. `registerProvider()`
|
|
173
123
|
// is pi's documented way for one to add providers, and extensions do not execute until the
|
|
174
124
|
// services are built — resolving first fails a definition whose configured model comes from its
|
|
175
|
-
// own extension with a bare "unknown model"
|
|
176
|
-
// that does not exist yet.
|
|
177
|
-
//
|
|
178
|
-
// MIGRATION HINT (deliberate breaking change): chat historically used pi's own `~/.pi` auth; it
|
|
179
|
-
// now reads the agent's credential file like every other command. Probe the RESOLVED model's
|
|
180
|
-
// provider through the normal resolution path (stored credential OR env var — an env-authed
|
|
181
|
-
// user is fine and must not be warned): only when that provider has no usable auth AND pi's old
|
|
182
|
-
// file exists does the bare provider error get its cause named.
|
|
183
|
-
if (!credentialHintShown &&
|
|
184
|
-
(await probeAuthSource(modelRuntime, modelSpec)) === undefined &&
|
|
185
|
-
existsSync(join(getAgentDir(), "auth.json"))) {
|
|
186
|
-
credentialHintShown = true;
|
|
187
|
-
log.warn(`[fastagent] no credentials for ${modelSpec} in ${authPath} — this runtime no longer reads ` +
|
|
188
|
-
`pi's ~/.pi auth; run \`fastagent login\` (or /login in the TUI) to store credentials for this agent`);
|
|
189
|
-
}
|
|
125
|
+
// own extension with a bare "unknown model".
|
|
190
126
|
const model = resolveModel(modelRuntime, modelSpec);
|
|
191
|
-
|
|
127
|
+
// The same bind serving performs, minus the activation record: pi's chat session has nowhere to
|
|
128
|
+
// put one, which is the documented divergence — a resumed chat re-discovers via search_tools.
|
|
129
|
+
// NOT bound to the host here: InteractiveMode.bindCurrentSessionExtensions() calls
|
|
130
|
+
// session.bindExtensions() with the TUI's uiContext, abort handler and command actions — binding
|
|
131
|
+
// here too would emit session_start twice per chat, so an extension opening a resource on start
|
|
132
|
+
// would open two.
|
|
133
|
+
const result = await bindPiSession({
|
|
192
134
|
services,
|
|
193
135
|
sessionManager,
|
|
194
136
|
sessionStartEvent,
|
|
195
137
|
model,
|
|
196
138
|
thinkingLevel,
|
|
197
|
-
|
|
198
|
-
//
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
// was really there for (the machine's `defaultTools` setting cannot add pi's own copies on top
|
|
202
|
-
// of ours) without freezing anything.
|
|
203
|
-
noTools: "builtin",
|
|
204
|
-
customTools: customToolDefs,
|
|
139
|
+
tools,
|
|
140
|
+
// A tool must see one spelling of the workspace, including when opened through a symlink.
|
|
141
|
+
cwd: rootCwd,
|
|
142
|
+
recordActivations: false,
|
|
205
143
|
});
|
|
206
|
-
sessionRef.current = {
|
|
207
|
-
session: result.session,
|
|
208
|
-
sessionManager: agentSessionManager(result.session, result.session.sessionManager.getSessionId()),
|
|
209
|
-
activation: sessionToolActivation(result.session),
|
|
210
|
-
};
|
|
211
|
-
// NOT bound here: the HOST does it. InteractiveMode.bindCurrentSessionExtensions() calls
|
|
212
|
-
// session.bindExtensions() with the TUI's uiContext, abort handler and command actions — binding
|
|
213
|
-
// here too would emit session_start twice per chat, so an extension opening a resource on start
|
|
214
|
-
// would open two.
|
|
215
|
-
// Deferral emulation: pi starts THIS agent's tools active — every coding tool it mounted, plus
|
|
216
|
-
// every custom and extension tool. So narrow by SUBTRACTING the deferred names from whatever is
|
|
217
|
-
// active, rather than stating a set: an exact-set-equality gate would silently stop narrowing the
|
|
218
|
-
// day pi activates one more.
|
|
219
|
-
//
|
|
220
|
-
// Applied on EVERY build including /resume: pi's chat session does not record activations (its
|
|
221
|
-
// SessionContext has no activeToolNames), so "restore prior activations" is not implementable
|
|
222
|
-
// here — deferral stays consistently ON and a resumed conversation re-discovers via search_tools
|
|
223
|
-
// (a documented divergence from serving, where activations persist in the session). The deferred
|
|
224
|
-
// SET comes from the shared assembly (one definition of "deferred"), never recomputed here.
|
|
225
|
-
if (deferredToolNames.length > 0) {
|
|
226
|
-
const active = result.session.getActiveToolNames();
|
|
227
|
-
if (deferredToolNames.some((n) => active.includes(n))) {
|
|
228
|
-
result.session.setActiveToolsByName(active.filter((n) => !deferredToolNames.includes(n)));
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
144
|
return { ...result, services, diagnostics: services.diagnostics };
|
|
232
145
|
};
|
|
233
146
|
const runtime = await createAgentSessionRuntime(createRuntime, {
|
|
@@ -12,22 +12,17 @@
|
|
|
12
12
|
* Writes take the same lease as runs. Without boundary wiring they reject before acceptance with
|
|
13
13
|
* `unsupported_capability` — a client gating on `capabilities()` never sends them.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
16
16
|
import { type Models } from "@earendil-works/pi-ai";
|
|
17
17
|
import { type AgentCommand, type SessionControl, type SessionEvent } from "../../session.ts";
|
|
18
18
|
import type { SessionObserver, Lease } from "./turn-kit.ts";
|
|
19
19
|
import type { AnyModel } from "./models.ts";
|
|
20
20
|
import type { PiAgentSessionFactory } from "./invoke-session.ts";
|
|
21
21
|
import type { PiSessionRecordStore } from "./session-store.ts";
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
24
|
-
* has its buffer FROZEN at the cap (memory bounded — the actual goal: ≈10k small events ≈ a few
|
|
25
|
-
* MB worst case per stuck connection) and its subscription marked closed. The close is observed
|
|
26
|
-
* via pulls — which a stalled connection by definition does not make — so a consumer that RESUMES
|
|
27
|
-
* pulling first drains the frozen backlog, then gets done (no buffered event dropped), while a
|
|
28
|
-
* permanently stalled one holds the frozen buffer until its TCP connection dies. Recovery either
|
|
29
|
-
* way is the standard reconnect+backfill, semantically lossless. */
|
|
22
|
+
/** Per-subscriber backlog limits: event count and UTF-8 JSON bytes. Overflow freezes the accepted
|
|
23
|
+
* prefix; the reader drains it before seeing done, then reconnects and backfills via entries(). */
|
|
30
24
|
export declare const SUBSCRIBER_BUFFER_CAP = 10000;
|
|
25
|
+
export declare const SUBSCRIBER_BUFFER_BYTES: number;
|
|
31
26
|
/** What the plane's writes (`update` / `compact` / `fork` / `delete`) need — the SAME instances the
|
|
32
27
|
* agent assembly uses: the lease (a write must not race a run), the model registry (validation +
|
|
33
28
|
* allowedModels), and the session factory (compaction is a model call). Writes go through the
|
|
@@ -49,15 +44,12 @@ export interface PiBoundaryWiring {
|
|
|
49
44
|
export interface CreatePiSessionControlOptions {
|
|
50
45
|
/** Read-only access to the durable session records (the same root the agent writes). */
|
|
51
46
|
sessions: PiSessionRecordStore;
|
|
52
|
-
/** Boundary-mutation wiring
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
* exists before the assembly that can read a definition) — and async because the definition is
|
|
59
|
-
* live: this must re-read it, not close over a boot snapshot, or `commands()` would advertise a
|
|
60
|
-
* list the next turn no longer runs.
|
|
47
|
+
/** Boundary-mutation wiring — the assembly's own parts. Absent → boundary commands are gated off
|
|
48
|
+
* in `capabilities()` and rejected `unsupported_capability`. */
|
|
49
|
+
boundary?: PiBoundaryWiring;
|
|
50
|
+
/** The definition's names, as an async thunk because the definition is live: this must re-read it,
|
|
51
|
+
* not close over a boot snapshot, or `commands()` would advertise a list the next turn no longer
|
|
52
|
+
* runs.
|
|
61
53
|
*
|
|
62
54
|
* OPTIONAL because absence is a TRUE answer for the assembly that omits it: a hub over an L1
|
|
63
55
|
* agent (`createPiAgent({ model, instructions, tools })`) has no definition and therefore no
|
|
@@ -1,25 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
* The pi implementation of the session control plane. `createPiSessionControl` returns the neutral
|
|
3
|
-
* `SessionControl` plus the {@link SessionObserver} to plug into the invoke pipeline
|
|
4
|
-
* (`createPiAgent({ observer })`).
|
|
5
|
-
*
|
|
6
|
-
* It holds no durable state of its own: live truth comes from the event stream (plus the
|
|
7
|
-
* {@link RunControls} a `run_started` carries), durable truth from {@link PiSessionRecordStore} —
|
|
8
|
-
* which is also what performs every write, so how a record takes a property is not knowledge this
|
|
9
|
-
* file has. What it owns is the vocabulary: capability gating, the lease, error codes, and the
|
|
10
|
-
* events its own writes emit.
|
|
11
|
-
*
|
|
12
|
-
* Writes take the same lease as runs. Without boundary wiring they reject before acceptance with
|
|
13
|
-
* `unsupported_capability` — a client gating on `capabilities()` never sends them.
|
|
14
|
-
*/
|
|
15
|
-
import { prepareCompaction } from "@earendil-works/pi-agent-core";
|
|
1
|
+
import { findCutPoint, getLatestCompactionEntry, sessionEntryToContextMessages, } from "@earendil-works/pi-coding-agent";
|
|
16
2
|
import { getSupportedThinkingLevels } from "@earendil-works/pi-ai";
|
|
17
3
|
import { SESSION_BUSY_CODE } from "../../agent.js";
|
|
18
4
|
import { BOUNDARY_COMMAND_FAILED_CODE, INVALID_COMMAND_CODE, isAddressableSession, NO_ACTIVE_RUN_CODE, NOTHING_TO_COMPACT_CODE, NO_SUCH_SESSION_CODE, PARTIAL_UPDATE_CODE, UPDATE_FIELDS, RUN_COMMAND_FAILED_CODE, UNSUPPORTED_CAPABILITY_CODE, } from "../../session.js";
|
|
19
5
|
import { listModels } from "./config.js";
|
|
20
6
|
import { forkProvenance, isNavigable, publishedLeaf } from "./session-markers.js";
|
|
7
|
+
import { toRetryScheduledEvent } from "./retry-event.js";
|
|
21
8
|
import { THINKING_LEVELS, activePath, resolveSessionSettings } from "./session-settings.js";
|
|
22
9
|
import { log } from "../../log.js";
|
|
10
|
+
/** Admission uses coding-agent's cut point and context rules, including a split-turn prefix.
|
|
11
|
+
* Its prepareCompaction is private; agent-core's namesake uses a different journal format. */
|
|
12
|
+
function hasCompactableHistory(path, keepRecentTokens) {
|
|
13
|
+
if (path.at(-1)?.type === "compaction")
|
|
14
|
+
return false;
|
|
15
|
+
const previous = getLatestCompactionEntry(path);
|
|
16
|
+
let start = 0;
|
|
17
|
+
if (previous) {
|
|
18
|
+
const kept = path.findIndex((entry) => entry.id === previous.firstKeptEntryId);
|
|
19
|
+
start = kept >= 0 ? kept : path.indexOf(previous) + 1;
|
|
20
|
+
}
|
|
21
|
+
const { firstKeptEntryIndex } = findCutPoint(path, start, path.length, keepRecentTokens);
|
|
22
|
+
return path
|
|
23
|
+
.slice(start, firstKeptEntryIndex)
|
|
24
|
+
.some((entry) => entry.type !== "compaction" && sessionEntryToContextMessages(entry).length > 0);
|
|
25
|
+
}
|
|
23
26
|
// ── Entry normalization (durable plane) ──────────────────────────────────────
|
|
24
27
|
/** Concatenated plain text of a message's content blocks (the L0 rendering payload). A custom
|
|
25
28
|
* AgentMessage role may carry no `content` at all — that reads as empty, not a crash. */
|
|
@@ -76,16 +79,11 @@ function toSessionEntry(entry, parentId) {
|
|
|
76
79
|
return { ...base, kind: entry.type, data: {} };
|
|
77
80
|
}
|
|
78
81
|
// ── Live fan-out (events plane) ──────────────────────────────────────────────
|
|
79
|
-
/**
|
|
80
|
-
*
|
|
81
|
-
* has its buffer FROZEN at the cap (memory bounded — the actual goal: ≈10k small events ≈ a few
|
|
82
|
-
* MB worst case per stuck connection) and its subscription marked closed. The close is observed
|
|
83
|
-
* via pulls — which a stalled connection by definition does not make — so a consumer that RESUMES
|
|
84
|
-
* pulling first drains the frozen backlog, then gets done (no buffered event dropped), while a
|
|
85
|
-
* permanently stalled one holds the frozen buffer until its TCP connection dies. Recovery either
|
|
86
|
-
* way is the standard reconnect+backfill, semantically lossless. */
|
|
82
|
+
/** Per-subscriber backlog limits: event count and UTF-8 JSON bytes. Overflow freezes the accepted
|
|
83
|
+
* prefix; the reader drains it before seeing done, then reconnects and backfills via entries(). */
|
|
87
84
|
export const SUBSCRIBER_BUFFER_CAP = 10_000;
|
|
88
|
-
|
|
85
|
+
export const SUBSCRIBER_BUFFER_BYTES = 8 * 1024 * 1024;
|
|
86
|
+
/** One subscriber's bounded push→pull queue. `close()` settles a
|
|
89
87
|
* pending pull — an async generator suspended on a quiet stream cannot be ended by `return()`
|
|
90
88
|
* alone (it queues behind the never-settling await), so teardown needs this explicit door. */
|
|
91
89
|
class Subscriber {
|
|
@@ -96,7 +94,9 @@ class Subscriber {
|
|
|
96
94
|
constructor(session) {
|
|
97
95
|
this.session = session;
|
|
98
96
|
}
|
|
97
|
+
// Serialized snapshots cannot grow when a producer later mutates its event or tool result.
|
|
99
98
|
buffer = [];
|
|
99
|
+
bufferedBytes = 0;
|
|
100
100
|
// A QUEUE of waiters, not a single slot: concurrent next() calls are contract-legal (any wrapper
|
|
101
101
|
// may poll twice), and a single `wake` field would let the second await overwrite the first's
|
|
102
102
|
// resolver — hanging the first next() forever. Every wake flushes all waiters; each re-checks the
|
|
@@ -112,12 +112,15 @@ class Subscriber {
|
|
|
112
112
|
push(event) {
|
|
113
113
|
if (this.closed)
|
|
114
114
|
return;
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
const json = JSON.stringify(event);
|
|
116
|
+
const bytes = Buffer.byteLength(json);
|
|
117
|
+
if (this.buffer.length >= SUBSCRIBER_BUFFER_CAP || this.bufferedBytes + bytes > SUBSCRIBER_BUFFER_BYTES) {
|
|
118
|
+
log.warn(`[fastagent] session-control subscriber for session "${this.session}" exceeded its buffer limit (${SUBSCRIBER_BUFFER_CAP} events / ${SUBSCRIBER_BUFFER_BYTES} bytes; ${this.bufferedBytes} bytes queued, ${bytes} incoming); its stream ends after draining the backlog, then the client resyncs via entries()`);
|
|
117
119
|
this.close();
|
|
118
120
|
return;
|
|
119
121
|
}
|
|
120
|
-
this.buffer.push(
|
|
122
|
+
this.buffer.push({ json, bytes });
|
|
123
|
+
this.bufferedBytes += bytes;
|
|
121
124
|
this.flush();
|
|
122
125
|
}
|
|
123
126
|
close() {
|
|
@@ -126,8 +129,11 @@ class Subscriber {
|
|
|
126
129
|
}
|
|
127
130
|
async next() {
|
|
128
131
|
while (true) {
|
|
129
|
-
|
|
130
|
-
|
|
132
|
+
const item = this.buffer.shift();
|
|
133
|
+
if (item) {
|
|
134
|
+
this.bufferedBytes -= item.bytes;
|
|
135
|
+
return { done: false, value: JSON.parse(item.json) };
|
|
136
|
+
}
|
|
131
137
|
if (this.closed)
|
|
132
138
|
return { done: true, value: undefined };
|
|
133
139
|
await new Promise((resolve) => {
|
|
@@ -195,7 +201,7 @@ export function createPiSessionControl(options) {
|
|
|
195
201
|
return (await options.commands?.()) ?? [];
|
|
196
202
|
},
|
|
197
203
|
capabilities: () => {
|
|
198
|
-
const b = boundary
|
|
204
|
+
const b = boundary;
|
|
199
205
|
return {
|
|
200
206
|
steering: true,
|
|
201
207
|
followUp: true,
|
|
@@ -227,7 +233,7 @@ export function createPiSessionControl(options) {
|
|
|
227
233
|
// channel to explain itself. The fault is not swallowed — it surfaces where codes exist: the
|
|
228
234
|
// next invoke fails (binding a session walks the same chain) and a boundary dispatch answers
|
|
229
235
|
// `boundary_command_failed`. Here it is a server-side warn.
|
|
230
|
-
const b = boundary
|
|
236
|
+
const b = boundary;
|
|
231
237
|
let settings;
|
|
232
238
|
if (opened && b) {
|
|
233
239
|
try {
|
|
@@ -455,7 +461,7 @@ export function createPiSessionControl(options) {
|
|
|
455
461
|
const fields = named.filter((f) => patch[f] !== undefined);
|
|
456
462
|
if (fields.length === 0)
|
|
457
463
|
return { ok: true }; // an empty patch asks for nothing, and gets it
|
|
458
|
-
const b = boundary
|
|
464
|
+
const b = boundary;
|
|
459
465
|
if (!b)
|
|
460
466
|
return unsupported(`update(${fields.join(", ")})`);
|
|
461
467
|
// PAYLOAD validation first — before the session is even opened, and long before the lease: an
|
|
@@ -598,7 +604,7 @@ export function createPiSessionControl(options) {
|
|
|
598
604
|
* finished{error} dressed as a failure — pi reports it as a throw from compact(), too late.
|
|
599
605
|
*/
|
|
600
606
|
const compactOf = async (session, instructions) => {
|
|
601
|
-
const b = boundary
|
|
607
|
+
const b = boundary;
|
|
602
608
|
if (!b)
|
|
603
609
|
return unsupported("compact()");
|
|
604
610
|
const existing = await sessions.openIfExists(session);
|
|
@@ -628,14 +634,7 @@ export function createPiSessionControl(options) {
|
|
|
628
634
|
// either reject a compaction pi would have run, or admit one it refuses — and its refusal
|
|
629
635
|
// arrives too late to be a pre-acceptance answer.
|
|
630
636
|
const path = bound.sessionManager.getBranch();
|
|
631
|
-
|
|
632
|
-
if (!prep.ok)
|
|
633
|
-
throw prep.error;
|
|
634
|
-
// Empty is the same answer as absent: pi ships two prepareCompaction implementations
|
|
635
|
-
// (agent-core answers with a Result, coding-agent with undefined) and they disagree on which
|
|
636
|
-
// one an unsummarizable session gets. What they agree on is the CONTENT — no messages to
|
|
637
|
-
// summarize — so that is what the gate reads.
|
|
638
|
-
if (!prep.value || prep.value.messagesToSummarize.length === 0) {
|
|
637
|
+
if (!hasCompactableHistory(path, bound.settingsManager.getCompactionSettings().keepRecentTokens)) {
|
|
639
638
|
teardown();
|
|
640
639
|
release();
|
|
641
640
|
// A no-op, not a failure — its OWN code (the NO_ACTIVE_RUN pattern): a client must
|
|
@@ -656,37 +655,13 @@ export function createPiSessionControl(options) {
|
|
|
656
655
|
release();
|
|
657
656
|
return failed(error);
|
|
658
657
|
}
|
|
659
|
-
//
|
|
660
|
-
//
|
|
661
|
-
//
|
|
662
|
-
// pi builds the controller that makes it abortable AFTER an internal await, so an abort arriving
|
|
663
|
-
// in that window would find nothing to cancel and the compaction would run to completion — the
|
|
664
|
-
// client's cancel silently doing nothing. The intent is latched and re-applied until it takes
|
|
665
|
-
// (`isCompacting` reports when it has).
|
|
666
|
-
//
|
|
667
|
-
// The retry is DEFENSIVE: the window is one await wide, and the test below lands after it, so
|
|
668
|
-
// this loop is not what makes that test pass. It is here because the window is on the code path,
|
|
669
|
-
// not because it has been observed.
|
|
658
|
+
// Pi creates the abort controller after an await and emits compaction_start immediately after.
|
|
659
|
+
// Retain early cancellation until that event; there is no time limit on session startup.
|
|
670
660
|
let aborted = false;
|
|
671
|
-
let running = true; // cleared when the compaction settles, however it settles
|
|
672
|
-
const applyAbort = async () => {
|
|
673
|
-
// WAIT for the controller rather than requiring it: an abort that arrives before pi builds one
|
|
674
|
-
// sees isCompacting false, and a loop that only runs WHILE compacting would exit immediately —
|
|
675
|
-
// leaving the intent unapplied in exactly the window it exists for.
|
|
676
|
-
for (let attempt = 0; attempt < 200 && running; attempt++) {
|
|
677
|
-
if (bound.isCompacting) {
|
|
678
|
-
bound.abortCompaction();
|
|
679
|
-
if (!bound.isCompacting)
|
|
680
|
-
return; // it took
|
|
681
|
-
}
|
|
682
|
-
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
683
|
-
}
|
|
684
|
-
};
|
|
685
661
|
compacting.set(session, {
|
|
686
662
|
abort: () => {
|
|
687
663
|
aborted = true;
|
|
688
664
|
bound.abortCompaction();
|
|
689
|
-
void applyAbort();
|
|
690
665
|
},
|
|
691
666
|
});
|
|
692
667
|
emitOwn(session, { type: "compaction_started", timestamp: Date.now(), data: {} });
|
|
@@ -696,20 +671,12 @@ export function createPiSessionControl(options) {
|
|
|
696
671
|
// backoff so a long gap is diagnosable (not confusable with a hang): as a session event for
|
|
697
672
|
// attached observers, as a warn for server logs.
|
|
698
673
|
const unsub = bound.subscribe((event) => {
|
|
674
|
+
if (event.type === "compaction_start" && event.reason === "manual" && aborted)
|
|
675
|
+
bound.abortCompaction();
|
|
699
676
|
if (event.type !== "summarization_retry_scheduled")
|
|
700
677
|
return;
|
|
701
678
|
log.warn(`[fastagent] compaction retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms (session ${session}): ${event.errorMessage}`);
|
|
702
|
-
emitOwn(session,
|
|
703
|
-
type: "retry_scheduled",
|
|
704
|
-
timestamp: Date.now(),
|
|
705
|
-
data: {
|
|
706
|
-
operation: "compaction",
|
|
707
|
-
attempt: event.attempt,
|
|
708
|
-
maxAttempts: event.maxAttempts,
|
|
709
|
-
delayMs: event.delayMs,
|
|
710
|
-
error: event.errorMessage,
|
|
711
|
-
},
|
|
712
|
-
});
|
|
679
|
+
emitOwn(session, toRetryScheduledEvent(event));
|
|
713
680
|
});
|
|
714
681
|
try {
|
|
715
682
|
const done = await bound.compact(instructions);
|
|
@@ -721,7 +688,6 @@ export function createPiSessionControl(options) {
|
|
|
721
688
|
// failure still reads as aborted).
|
|
722
689
|
outcome = aborted ? { aborted: true } : { error: String(error) };
|
|
723
690
|
}
|
|
724
|
-
running = false;
|
|
725
691
|
unsub();
|
|
726
692
|
teardown();
|
|
727
693
|
// Release BEFORE emitting finished: a watcher seeing finished may act next — "finished ⇒ the
|
|
@@ -744,7 +710,7 @@ export function createPiSessionControl(options) {
|
|
|
744
710
|
/** WHICH fork this is: source + branch point. Two forks of one session at different entries are
|
|
745
711
|
* different requests, so a retry of one must not be answered by the other. */
|
|
746
712
|
const provenance = `${from}@${at}`;
|
|
747
|
-
const b = boundary
|
|
713
|
+
const b = boundary;
|
|
748
714
|
if (!b)
|
|
749
715
|
return unsupported("fork()");
|
|
750
716
|
// An id no client could then open: the empty string, `.` and `..` are not URL path segments
|
|
@@ -807,7 +773,7 @@ export function createPiSessionControl(options) {
|
|
|
807
773
|
return { ok: true };
|
|
808
774
|
};
|
|
809
775
|
const deleteOf = async (session) => {
|
|
810
|
-
const b = boundary
|
|
776
|
+
const b = boundary;
|
|
811
777
|
if (!b)
|
|
812
778
|
return unsupported("delete()");
|
|
813
779
|
const existing = await sessions.openIfExists(session);
|
|
@@ -35,7 +35,7 @@ export interface SessionSettings {
|
|
|
35
35
|
thinkingLevel: ThinkingLevel;
|
|
36
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?: {
|
|
@@ -49,7 +49,7 @@ export function resolveSessionSettings(entries, models, defaults) {
|
|
|
49
49
|
dropped.model = `${recorded.model.provider}/${recorded.model.modelId}`;
|
|
50
50
|
}
|
|
51
51
|
const availableThinkingLevels = getSupportedThinkingLevels(model);
|
|
52
|
-
let thinkingLevel = defaults.thinkingLevel;
|
|
52
|
+
let thinkingLevel = clampThinkingLevel(model, defaults.thinkingLevel);
|
|
53
53
|
if (recorded.thinkingLevel !== undefined) {
|
|
54
54
|
const level = recorded.thinkingLevel;
|
|
55
55
|
if (!THINKING_LEVELS.has(level)) {
|
|
@@ -85,10 +85,10 @@ interface AppliedProperties {
|
|
|
85
85
|
* produce one output. `_` escapes itself for the same reason. A trailing `.` or `-` is legal
|
|
86
86
|
* mid-name but not at the end, so it escapes too.
|
|
87
87
|
*
|
|
88
|
-
* Injective within this encoding — which is only sufficient because
|
|
89
|
-
* directory.
|
|
90
|
-
* called `s42`
|
|
91
|
-
* 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.
|
|
92
92
|
*
|
|
93
93
|
* Readability is deliberate: `-1001234567890` becomes `s-1001234567890`, so an operator can still
|
|
94
94
|
* tell which room a file belongs to.
|
|
@@ -110,24 +110,19 @@ export declare function callerSessionId(recordId: string): string | undefined;
|
|
|
110
110
|
* each header and would make a renamed agent directory look like an empty store (see
|
|
111
111
|
* {@link recordFiles}).
|
|
112
112
|
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
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.
|
|
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.
|
|
122
118
|
*
|
|
123
119
|
* SCOPE OF "open-or-create": idempotent against a store that is serialized per session, which is what
|
|
124
120
|
* the serving path provides — the single-writer lease is taken before any store call, so no two
|
|
125
121
|
* turns of one conversation reach this at once. What it does NOT do is arbitrate a FIRST open racing
|
|
126
122
|
* across processes: two instances that scan before either writes will both create, and the
|
|
127
|
-
* conversation forks into two records.
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* 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.
|
|
131
126
|
*/
|
|
132
127
|
export declare function piSessionRecordStore(options: {
|
|
133
128
|
dir: string;
|