@fastagent-sh/fastagent 0.17.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -7
- package/dist/agent.d.ts +12 -1
- package/dist/agent.js +1 -1
- package/dist/atomic-write.d.ts +19 -0
- package/dist/atomic-write.js +48 -0
- package/dist/{host/node.d.ts → channel.d.ts} +24 -28
- package/dist/channel.js +1 -0
- package/dist/channels/agentcore-service.d.ts +54 -0
- package/dist/channels/agentcore-service.js +111 -0
- package/dist/channels/agentcore.d.ts +12 -2
- package/dist/channels/agentcore.js +6 -4
- package/dist/channels/busy.d.ts +1 -1
- package/dist/channels/busy.js +1 -1
- package/dist/channels/control.d.ts +16 -6
- package/dist/channels/control.js +106 -4
- package/dist/{engines/pi/channel.d.ts → channels/discover.d.ts} +2 -2
- package/dist/{engines/pi/channel.js → channels/discover.js} +8 -12
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +1 -1
- package/dist/channels/feishu/context-buffer.js +4 -4
- package/dist/channels/feishu/feishu-api.d.ts +4 -2
- package/dist/channels/feishu/feishu-api.js +1 -1
- package/dist/channels/feishu/feishu.d.ts +1 -1
- package/dist/channels/feishu/feishu.js +45 -15
- package/dist/channels/feishu/invoke-turn.d.ts +9 -3
- package/dist/channels/feishu/invoke-turn.js +152 -34
- package/dist/channels/feishu/parse.js +6 -0
- package/dist/channels/feishu/preview.d.ts +1 -1
- package/dist/channels/feishu/preview.js +7 -3
- package/dist/channels/feishu/ws-ingress.d.ts +1 -1
- package/dist/channels/github/github.d.ts +1 -1
- package/dist/channels/http.d.ts +11 -7
- package/dist/channels/http.js +16 -111
- package/dist/channels/{context-buffer.js → kit/context-buffer.js} +2 -2
- package/dist/channels/{invoke-turn-kit.d.ts → kit/invoke-turn-kit.d.ts} +18 -4
- package/dist/channels/{invoke-turn-kit.js → kit/invoke-turn-kit.js} +23 -8
- package/dist/channels/{preview-kit.d.ts → kit/preview-kit.d.ts} +3 -1
- package/dist/channels/{seen.js → kit/seen.js} +1 -1
- package/dist/channels/kit/state.d.ts +6 -0
- package/dist/channels/{state.js → kit/state.js} +4 -24
- package/dist/channels/{stop-command.d.ts → kit/stop-command.d.ts} +1 -1
- package/dist/channels/{stop-command.js → kit/stop-command.js} +2 -2
- package/dist/channels/{tasks.js → kit/tasks.js} +1 -1
- package/dist/channels/{thread-participants.d.ts → kit/thread-participants.d.ts} +7 -0
- package/dist/channels/{thread-participants.js → kit/thread-participants.js} +4 -1
- package/dist/channels/{turn-queue.js → kit/turn-queue.js} +2 -2
- package/dist/channels/{turn-store.js → kit/turn-store.js} +1 -1
- package/dist/channels/lark/lark.d.ts +1 -1
- package/dist/channels/serve.d.ts +73 -0
- package/dist/channels/serve.js +243 -0
- package/dist/channels/slack/bot-auth.js +3 -14
- package/dist/channels/slack/context-buffer.d.ts +2 -2
- package/dist/channels/slack/context-buffer.js +2 -2
- package/dist/channels/slack/invoke-turn.d.ts +1 -1
- package/dist/channels/slack/invoke-turn.js +9 -6
- package/dist/channels/slack/parse.js +2 -2
- package/dist/channels/slack/preview.d.ts +1 -1
- package/dist/channels/slack/preview.js +11 -3
- package/dist/channels/slack/slack-api.js +1 -1
- package/dist/channels/slack/slack.d.ts +1 -1
- package/dist/channels/slack/slack.js +8 -12
- package/dist/channels/slack/welcomed.js +1 -1
- package/dist/channels/telegram/context-buffer.d.ts +2 -2
- package/dist/channels/telegram/context-buffer.js +2 -2
- package/dist/channels/telegram/invoke-turn.d.ts +1 -1
- package/dist/channels/telegram/invoke-turn.js +2 -2
- package/dist/channels/telegram/parse.js +2 -2
- package/dist/channels/telegram/preview.d.ts +1 -1
- package/dist/channels/telegram/preview.js +6 -2
- package/dist/channels/telegram/telegram.d.ts +1 -1
- package/dist/channels/telegram/telegram.js +3 -3
- package/dist/channels/telegram/turn-store.d.ts +2 -2
- package/dist/channels/telegram/turn-store.js +2 -2
- package/dist/cli/commands/add.js +1 -1
- package/dist/cli/commands/deploy.js +13 -5
- package/dist/cli/commands/dev.js +26 -14
- package/dist/cli/commands/fire.js +2 -2
- package/dist/cli/commands/info.js +28 -5
- package/dist/cli/commands/invoke.js +1 -1
- package/dist/cli/commands/schedule.js +1 -1
- package/dist/cli/commands/start.js +64 -95
- package/dist/cli/commands/tool.js +5 -7
- package/dist/cli/program.js +1 -1
- package/dist/cli/serve.d.ts +18 -57
- package/dist/cli/serve.js +57 -243
- package/dist/cli/shared.d.ts +5 -3
- package/dist/cli/shared.js +14 -7
- package/dist/collect.d.ts +30 -4
- package/dist/collect.js +39 -6
- package/dist/core.d.ts +3 -5
- package/dist/core.js +7 -5
- package/dist/deploy/preflight.d.ts +8 -2
- package/dist/deploy/preflight.js +22 -4
- package/dist/deploy/secrets.d.ts +3 -0
- package/dist/deploy/secrets.js +6 -0
- package/dist/dev-supervisor.js +16 -4
- package/dist/engines/pi/agent-session-factory.d.ts +104 -0
- package/dist/engines/pi/agent-session-factory.js +314 -0
- package/dist/engines/pi/config.d.ts +4 -4
- package/dist/engines/pi/config.js +1 -1
- package/dist/engines/pi/create.d.ts +56 -42
- package/dist/engines/pi/create.js +150 -75
- package/dist/engines/pi/definition.d.ts +41 -7
- package/dist/engines/pi/definition.js +115 -11
- package/dist/engines/pi/invoke-session.d.ts +35 -0
- package/dist/engines/pi/invoke-session.js +357 -0
- package/dist/engines/pi/login.js +7 -4
- package/dist/engines/pi/models.d.ts +59 -11
- package/dist/engines/pi/models.js +64 -9
- package/dist/engines/pi/open.d.ts +9 -6
- package/dist/engines/pi/open.js +20 -10
- package/dist/engines/pi/report.d.ts +0 -4
- package/dist/engines/pi/report.js +0 -7
- package/dist/engines/pi/service.d.ts +23 -0
- package/dist/engines/pi/service.js +19 -0
- package/dist/engines/pi/session-builder.js +88 -90
- package/dist/engines/pi/session-control.d.ts +11 -9
- package/dist/engines/pi/session-control.js +114 -77
- package/dist/engines/pi/session-inheritance.d.ts +44 -0
- package/dist/engines/pi/session-inheritance.js +242 -0
- package/dist/engines/pi/session-settings.d.ts +14 -2
- package/dist/engines/pi/session-settings.js +18 -0
- package/dist/engines/pi/session-store.d.ts +70 -0
- package/dist/engines/pi/session-store.js +313 -0
- package/dist/engines/pi/tool-context.d.ts +18 -8
- package/dist/engines/pi/tool-context.js +23 -1
- package/dist/engines/pi/tool.d.ts +5 -5
- package/dist/engines/pi/tool.js +1 -1
- package/dist/engines/pi/{invoke.d.ts → turn-kit.d.ts} +46 -45
- package/dist/engines/pi/turn-kit.js +188 -0
- package/dist/engines/pi/wake-tool.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/log.d.ts +3 -0
- package/dist/log.js +5 -1
- package/dist/node.d.ts +16 -0
- package/dist/node.js +20 -0
- package/dist/paths.d.ts +13 -0
- package/dist/paths.js +16 -0
- package/dist/pi.d.ts +15 -8
- package/dist/pi.js +7 -6
- package/dist/scaffold/templates/fastagent.config.mjs +2 -0
- package/dist/scaffold/templates/persona.md +2 -2
- package/dist/schedule/state.js +4 -9
- package/dist/schedule/wakeups.d.ts +1 -1
- package/dist/schedule/wakeups.js +1 -1
- package/dist/service.d.ts +158 -0
- package/dist/service.js +354 -0
- package/dist/session-remote.d.ts +1 -1
- package/dist/session-remote.js +10 -2
- package/dist/session.d.ts +4 -2
- package/dist/session.js +1 -1
- package/package.json +14 -5
- package/dist/channels/state.d.ts +0 -16
- package/dist/engines/pi/harness.d.ts +0 -112
- package/dist/engines/pi/harness.js +0 -153
- package/dist/engines/pi/invoke.js +0 -618
- package/dist/engines/pi/read-image.d.ts +0 -4
- package/dist/engines/pi/read-image.js +0 -62
- package/dist/engines/pi/sessions.d.ts +0 -44
- package/dist/engines/pi/sessions.js +0 -182
- package/dist/host/node.js +0 -52
- /package/dist/channels/{context-buffer.d.ts → kit/context-buffer.d.ts} +0 -0
- /package/dist/channels/{preview-kit.js → kit/preview-kit.js} +0 -0
- /package/dist/channels/{seen.d.ts → kit/seen.d.ts} +0 -0
- /package/dist/channels/{tasks.d.ts → kit/tasks.d.ts} +0 -0
- /package/dist/channels/{text.d.ts → kit/text.d.ts} +0 -0
- /package/dist/channels/{text.js → kit/text.js} +0 -0
- /package/dist/channels/{turn-queue.d.ts → kit/turn-queue.d.ts} +0 -0
- /package/dist/channels/{turn-store.d.ts → kit/turn-store.d.ts} +0 -0
|
@@ -2,24 +2,22 @@
|
|
|
2
2
|
* The pi implementation of the session control plane: observation (`state`/`entries`/`events`,
|
|
3
3
|
* design Phase 1) and run modulation (`dispatch`: steer/follow_up/abort, Phase 2a) over
|
|
4
4
|
* invoke-driven runs. `createPiSessionControl` returns the neutral `SessionControl` plus the
|
|
5
|
-
* {@link SessionObserver} to plug into the invoke pipeline (`
|
|
5
|
+
* {@link SessionObserver} to plug into the invoke pipeline (`createPiAgent({ observer })`)
|
|
6
6
|
* — the hub derives everything from the rich event stream (plus the {@link RunControls} the
|
|
7
7
|
* run_started event carries), holds no durable state of its own, and never writes: durable truth
|
|
8
|
-
* stays in the session repository (read via {@link
|
|
8
|
+
* stays in the session repository (read via {@link PiSessionRecordStore}), live truth in the events the
|
|
9
9
|
* data plane emits, modulation in the controls the data plane registers.
|
|
10
10
|
*
|
|
11
11
|
* Boundary mutations (Phase 2b: compact/set_model/set_thinking/navigate) take the same lease as runs;
|
|
12
12
|
* without boundary wiring they are rejected before acceptance with `unsupported_capability` — a
|
|
13
13
|
* client gating on `capabilities()` never sends them.
|
|
14
14
|
*/
|
|
15
|
-
import {
|
|
15
|
+
import { prepareCompaction } from "@earendil-works/pi-agent-core";
|
|
16
16
|
import { SESSION_BUSY_CODE } from "../../agent.js";
|
|
17
17
|
import { BOUNDARY_COMMAND_FAILED_CODE, INVALID_COMMAND_CODE, NO_ACTIVE_RUN_CODE, NOTHING_TO_COMPACT_CODE, NO_SUCH_SESSION_CODE, RUN_COMMAND_FAILED_CODE, UNSUPPORTED_CAPABILITY_CODE, } from "../../session.js";
|
|
18
18
|
import { listModels } from "./config.js";
|
|
19
|
-
import {
|
|
20
|
-
import { THINKING_LEVELS, resolveSessionSettings } from "./session-settings.js";
|
|
19
|
+
import { THINKING_LEVELS, activePath, resolveSessionSettings } from "./session-settings.js";
|
|
21
20
|
import { log } from "../../log.js";
|
|
22
|
-
import { activePathEntries } from "./sessions.js";
|
|
23
21
|
// ── Entry normalization (durable plane) ──────────────────────────────────────
|
|
24
22
|
/** Concatenated plain text of a message's content blocks (the L0 rendering payload). A custom
|
|
25
23
|
* AgentMessage role may carry no `content` at all — that reads as empty, not a crash. */
|
|
@@ -34,7 +32,7 @@ function textOf(content) {
|
|
|
34
32
|
.join("");
|
|
35
33
|
}
|
|
36
34
|
/**
|
|
37
|
-
* pi `
|
|
35
|
+
* pi `PiSessionEntry` → neutral {@link SessionEntry}. Message entries map onto the guaranteed
|
|
38
36
|
* kind vocabulary (user/assistant/tool) with a minimal render payload; every other engine record
|
|
39
37
|
* keeps its pi type as an open-set kind with an EMPTY payload — present so `parentId` chains and
|
|
40
38
|
* cursors stay intact, skippable by contract, and no pi message class leaks through the adapter.
|
|
@@ -82,8 +80,12 @@ function toSessionEntry(entry) {
|
|
|
82
80
|
* put the branch head off every conversation path. Withheld from the published plane and refused as
|
|
83
81
|
* a target THROUGH THIS ONE PREDICATE, so a second exclusion cannot make the two disagree.
|
|
84
82
|
*/
|
|
83
|
+
/** Every position a client may move the branch head to. `label` is metadata ABOUT an entry, not a
|
|
84
|
+
* position in the conversation, so it is neither published nor navigable — the client's rule stays
|
|
85
|
+
* "anything published is navigable". pi's own branch() is a pointer move and writes no record, so
|
|
86
|
+
* there is nothing else to exclude. */
|
|
85
87
|
function isNavigable(entry) {
|
|
86
|
-
return entry.type !== "
|
|
88
|
+
return entry.type !== "label";
|
|
87
89
|
}
|
|
88
90
|
// ── Live fan-out (events plane) ──────────────────────────────────────────────
|
|
89
91
|
/** Ceiling for one subscriber's unconsumed backlog. A consumer this far behind (a stalled remote
|
|
@@ -223,19 +225,19 @@ export function createPiSessionControl(options) {
|
|
|
223
225
|
async state(session) {
|
|
224
226
|
const run = active.get(session);
|
|
225
227
|
const opened = await sessions.openIfExists(session);
|
|
226
|
-
const leafEntryId = opened ? (
|
|
228
|
+
const leafEntryId = opened ? (opened.getLeafId() ?? undefined) : undefined;
|
|
227
229
|
// What will RUN, not the raw record: a client steering a session needs the pair that executes.
|
|
228
230
|
// Without a boundary there is no model to resolve against, and the fields are absent.
|
|
229
231
|
// OBSERVATION IS TOTAL: an unreadable entry chain leaves the pair absent too (the same shape a
|
|
230
232
|
// control-less deployment answers with) rather than rejecting a read that has no error-code
|
|
231
233
|
// channel to explain itself. The fault is not swallowed — it surfaces where codes exist: the
|
|
232
|
-
// next invoke fails (
|
|
234
|
+
// next invoke fails (binding a session walks the same chain) and a boundary dispatch answers
|
|
233
235
|
// `boundary_command_failed`. Here it is a server-side warn.
|
|
234
236
|
const b = boundary?.();
|
|
235
237
|
let settings;
|
|
236
238
|
if (opened && b) {
|
|
237
239
|
try {
|
|
238
|
-
settings = resolveSessionSettings((
|
|
240
|
+
settings = resolveSessionSettings(activePath(opened), b.models, b.defaults);
|
|
239
241
|
}
|
|
240
242
|
catch (error) {
|
|
241
243
|
log.warn(`[fastagent] session ${session}: settings unreadable (entry chain): ${String(error)}`);
|
|
@@ -263,8 +265,8 @@ export function createPiSessionControl(options) {
|
|
|
263
265
|
// would race any concurrent append into a leaf the snapshot cannot contain — a live turn
|
|
264
266
|
// reading as a dangling head. This order makes the journal a superset of the leaf's chain,
|
|
265
267
|
// which is what lets the published head be trusted as one of the published entries.
|
|
266
|
-
const leafEntryId =
|
|
267
|
-
const all =
|
|
268
|
+
const leafEntryId = opened.getLeafId() ?? undefined;
|
|
269
|
+
const all = opened.getEntries().filter(isNavigable).map(toSessionEntry);
|
|
268
270
|
let entries = all;
|
|
269
271
|
if (opts?.since !== undefined) {
|
|
270
272
|
const idx = all.findIndex((e) => e.id === opts.since);
|
|
@@ -339,7 +341,7 @@ export function createPiSessionControl(options) {
|
|
|
339
341
|
const run = active.get(session);
|
|
340
342
|
if (!run) {
|
|
341
343
|
// Run/compaction symmetry: an in-flight manual compaction is a model call too, and
|
|
342
|
-
// `abort` is its only door — interrupting
|
|
344
|
+
// `abort` is its only door — interrupting it converges through the detached
|
|
343
345
|
// task's catch into `compaction_finished{aborted}` with the lease released; answering
|
|
344
346
|
// no_active_run against a state() that says "compacting" would be a lie.
|
|
345
347
|
const comp = command.type === "abort" ? compacting.get(session) : undefined;
|
|
@@ -410,8 +412,8 @@ export function createPiSessionControl(options) {
|
|
|
410
412
|
};
|
|
411
413
|
}
|
|
412
414
|
// Payload validation BEFORE the lease — an invalid value must not briefly block a run.
|
|
413
|
-
/** The durable write for set_model/set_thinking/navigate — undefined for compact
|
|
414
|
-
*
|
|
415
|
+
/** The durable write for set_model/set_thinking/navigate — undefined for compact, which
|
|
416
|
+
* writes through pi's own compaction. Answers the event to emit. */
|
|
415
417
|
let apply;
|
|
416
418
|
if (command.type === "set_model") {
|
|
417
419
|
const slash = command.model.indexOf("/");
|
|
@@ -427,10 +429,10 @@ export function createPiSessionControl(options) {
|
|
|
427
429
|
};
|
|
428
430
|
}
|
|
429
431
|
apply = async (s) => {
|
|
430
|
-
|
|
432
|
+
s.appendModelChange(model.provider, model.id);
|
|
431
433
|
// Both halves: a new model can change which level executes. Nothing is re-recorded to
|
|
432
434
|
// make that true — the resolve reports it, so the preference survives a round trip.
|
|
433
|
-
const settings = resolveSessionSettings((
|
|
435
|
+
const settings = resolveSessionSettings(activePath(s), b.models, b.defaults);
|
|
434
436
|
return {
|
|
435
437
|
type: "state_changed",
|
|
436
438
|
timestamp: Date.now(),
|
|
@@ -453,7 +455,7 @@ export function createPiSessionControl(options) {
|
|
|
453
455
|
};
|
|
454
456
|
}
|
|
455
457
|
apply = async (s) => {
|
|
456
|
-
|
|
458
|
+
s.appendThinkingLevelChange(command.level);
|
|
457
459
|
return { type: "state_changed", timestamp: Date.now(), data: { thinkingLevel: command.level } };
|
|
458
460
|
};
|
|
459
461
|
}
|
|
@@ -464,9 +466,9 @@ export function createPiSessionControl(options) {
|
|
|
464
466
|
// selection) would otherwise grow the session by a record no plane publishes. The
|
|
465
467
|
// EVENT is emitted either way — it reports the resulting position, not the fact that
|
|
466
468
|
// a record was written, and a client that dispatched must not have to poll for it.
|
|
467
|
-
if (
|
|
468
|
-
|
|
469
|
-
//
|
|
469
|
+
if (s.getLeafId() !== command.targetId)
|
|
470
|
+
s.branch(command.targetId);
|
|
471
|
+
// branch()'s postcondition IS "targetId is the leaf" (it validates, then sets); a
|
|
470
472
|
// failure throws and travels as boundary_command_failed, so a read-back could only
|
|
471
473
|
// re-report what this line already knows. The SETTINGS ride along because a move can
|
|
472
474
|
// change them — an override recorded on the branch just left stops applying, and a
|
|
@@ -477,11 +479,11 @@ export function createPiSessionControl(options) {
|
|
|
477
479
|
// the settings and let `state()`'s rejection be where the broken chain surfaces.
|
|
478
480
|
let settings;
|
|
479
481
|
try {
|
|
480
|
-
settings = resolveSessionSettings((
|
|
482
|
+
settings = resolveSessionSettings(activePath(s), b.models, b.defaults);
|
|
481
483
|
}
|
|
482
484
|
catch (error) {
|
|
483
485
|
// Absent rather than stale: the session cannot RUN with an unreadable chain either
|
|
484
|
-
// (
|
|
486
|
+
// (binding a session walks the same path), so the next invoke fails visibly — this
|
|
485
487
|
// event does not need to carry a second signal for it.
|
|
486
488
|
log.warn(`[fastagent] session ${session}: leaf moved, settings unreadable: ${String(error)}`);
|
|
487
489
|
}
|
|
@@ -519,7 +521,7 @@ export function createPiSessionControl(options) {
|
|
|
519
521
|
// same disposition as an unknown model spec. Same predicate `entries()` publishes by, so
|
|
520
522
|
// "everything published is navigable" holds by construction rather than by two literals
|
|
521
523
|
// agreeing.
|
|
522
|
-
const entry =
|
|
524
|
+
const entry = existing.getEntry(command.targetId);
|
|
523
525
|
if (!entry || !isNavigable(entry)) {
|
|
524
526
|
return {
|
|
525
527
|
ok: false,
|
|
@@ -539,7 +541,7 @@ export function createPiSessionControl(options) {
|
|
|
539
541
|
// transport promises a SessionResult, so an unreadable chain has to arrive as a code.
|
|
540
542
|
let resolved;
|
|
541
543
|
try {
|
|
542
|
-
resolved = resolveSessionSettings((
|
|
544
|
+
resolved = resolveSessionSettings(activePath(existing), b.models, b.defaults);
|
|
543
545
|
}
|
|
544
546
|
catch (error) {
|
|
545
547
|
return {
|
|
@@ -576,21 +578,19 @@ export function createPiSessionControl(options) {
|
|
|
576
578
|
// ACCEPT-FAST: compaction is a full model call (tens of seconds is normal) — holding
|
|
577
579
|
// the dispatch open until it finishes made acceptance = outcome, the one exception to
|
|
578
580
|
// §5.2, and broke remote clients whose request timeouts are sized for control calls.
|
|
579
|
-
// The dispatch answers once the work is ADMITTED (lease held,
|
|
580
|
-
// outcome travels as compaction_finished{summary|error|aborted}, the bounds contract
|
|
581
|
-
// already rely on.
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
589
|
-
|
|
590
|
-
// would be uncancellable — and `abort` needs a real door (run/compaction symmetry).
|
|
591
|
-
let harness;
|
|
581
|
+
// The dispatch answers once the work is ADMITTED (lease held, session bound); the
|
|
582
|
+
// outcome travels as compaction_finished{summary|error|aborted}, the bounds contract
|
|
583
|
+
// watchers already rely on.
|
|
584
|
+
//
|
|
585
|
+
// The admission step is everything cheap and local: binding the session (the ONE
|
|
586
|
+
// canonical resolution of overrides + auth) plus the compaction PREPARATION, a pure
|
|
587
|
+
// branch read. The boundary between "reject the dispatch" and "the outcome travels as
|
|
588
|
+
// an event" sits where the work becomes asynchronous and expensive: the model call.
|
|
589
|
+
// "Nothing to compact" is therefore a pre-acceptance answer, never a finished{error}
|
|
590
|
+
// dressed as a failure — pi reports it as a throw from compact(), too late to reject.
|
|
591
|
+
let bound;
|
|
592
592
|
try {
|
|
593
|
-
|
|
593
|
+
bound = await b.sessionFactory(session);
|
|
594
594
|
}
|
|
595
595
|
catch (error) {
|
|
596
596
|
release();
|
|
@@ -599,26 +599,28 @@ export function createPiSessionControl(options) {
|
|
|
599
599
|
error: { code: BOUNDARY_COMMAND_FAILED_CODE, message: String(error), retryable: true },
|
|
600
600
|
};
|
|
601
601
|
}
|
|
602
|
-
const teardown =
|
|
602
|
+
const teardown = () => {
|
|
603
603
|
try {
|
|
604
|
-
|
|
604
|
+
bound.dispose();
|
|
605
605
|
}
|
|
606
606
|
catch (error) {
|
|
607
|
-
log.warn(`[fastagent] compaction
|
|
607
|
+
log.warn(`[fastagent] compaction session teardown failed: ${String(error)}`);
|
|
608
608
|
}
|
|
609
609
|
};
|
|
610
|
-
let record;
|
|
611
|
-
let preparation;
|
|
612
610
|
try {
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
const prep = prepareCompaction(
|
|
611
|
+
// The SAME settings pi will use inside compact(): asking with different thresholds
|
|
612
|
+
// would either reject a compaction pi would have run, or admit one it refuses - and
|
|
613
|
+
// its refusal arrives too late to be a pre-acceptance answer.
|
|
614
|
+
const path = bound.sessionManager.getBranch();
|
|
615
|
+
const prep = prepareCompaction(path, bound.settingsManager.getCompactionSettings());
|
|
618
616
|
if (!prep.ok)
|
|
619
617
|
throw prep.error;
|
|
620
|
-
|
|
621
|
-
|
|
618
|
+
// Empty is the same answer as absent: pi ships two prepareCompaction implementations
|
|
619
|
+
// (agent-core answers with a Result, coding-agent with undefined) and they disagree on
|
|
620
|
+
// which one an unsummarizable session gets. What they agree on is the CONTENT — no
|
|
621
|
+
// messages to summarize — so that is what the gate reads.
|
|
622
|
+
if (!prep.value || prep.value.messagesToSummarize.length === 0) {
|
|
623
|
+
teardown();
|
|
622
624
|
release();
|
|
623
625
|
// A no-op, not a failure — its OWN code (the NO_ACTIVE_RUN pattern): a client must
|
|
624
626
|
// machine-distinguish "give up" from "re-dispatch once the session grows", and
|
|
@@ -632,48 +634,83 @@ export function createPiSessionControl(options) {
|
|
|
632
634
|
},
|
|
633
635
|
};
|
|
634
636
|
}
|
|
635
|
-
preparation = prep.value;
|
|
636
637
|
}
|
|
637
638
|
catch (error) {
|
|
638
|
-
|
|
639
|
+
teardown();
|
|
639
640
|
release();
|
|
640
641
|
return {
|
|
641
642
|
ok: false,
|
|
642
643
|
error: { code: BOUNDARY_COMMAND_FAILED_CODE, message: String(error), retryable: true },
|
|
643
644
|
};
|
|
644
645
|
}
|
|
645
|
-
|
|
646
|
-
|
|
646
|
+
// The door is the session's own compaction abort — a real one, unlike a summarization
|
|
647
|
+
// call with no signal: `abort` must reach the model call (run/compaction symmetry).
|
|
648
|
+
//
|
|
649
|
+
// pi builds the controller that makes it abortable AFTER an internal await, so an abort
|
|
650
|
+
// arriving in that window would find nothing to cancel and the compaction would run to
|
|
651
|
+
// completion — the client's cancel silently doing nothing. The intent is latched and
|
|
652
|
+
// re-applied until it takes (`isCompacting` reports when it has).
|
|
653
|
+
//
|
|
654
|
+
// The retry is DEFENSIVE: the window is one await wide, and the test below lands after
|
|
655
|
+
// it, so this loop is not what makes that test pass. It is here because the window is on
|
|
656
|
+
// the code path, not because it has been observed.
|
|
657
|
+
let aborted = false;
|
|
658
|
+
let running = true; // cleared when the compaction settles, however it settles
|
|
659
|
+
const applyAbort = async () => {
|
|
660
|
+
// WAIT for the controller rather than requiring it: an abort that arrives before pi
|
|
661
|
+
// builds one sees isCompacting false, and a loop that only runs WHILE compacting would
|
|
662
|
+
// exit immediately — leaving the intent unapplied in exactly the window it exists for.
|
|
663
|
+
for (let attempt = 0; attempt < 200 && running; attempt++) {
|
|
664
|
+
if (bound.isCompacting) {
|
|
665
|
+
bound.abortCompaction();
|
|
666
|
+
if (!bound.isCompacting)
|
|
667
|
+
return; // it took
|
|
668
|
+
}
|
|
669
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
670
|
+
}
|
|
671
|
+
};
|
|
672
|
+
compacting.set(session, {
|
|
673
|
+
abort: () => {
|
|
674
|
+
aborted = true;
|
|
675
|
+
bound.abortCompaction();
|
|
676
|
+
void applyAbort();
|
|
677
|
+
},
|
|
678
|
+
});
|
|
647
679
|
emitOwn(session, { type: "compaction_started", timestamp: Date.now(), data: {} });
|
|
648
680
|
void (async () => {
|
|
649
681
|
let outcome;
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
682
|
+
// Retries are otherwise invisible between compaction_started and _finished — surface
|
|
683
|
+
// each backoff so a long gap is diagnosable (not confusable with a hang): as a session
|
|
684
|
+
// event for attached observers, as a warn for server logs.
|
|
685
|
+
const unsub = bound.subscribe((event) => {
|
|
686
|
+
if (event.type !== "summarization_retry_scheduled")
|
|
687
|
+
return;
|
|
688
|
+
log.warn(`[fastagent] compaction retry ${event.attempt}/${event.maxAttempts} in ${event.delayMs}ms (session ${session}): ${event.errorMessage}`);
|
|
689
|
+
emitOwn(session, {
|
|
690
|
+
type: "retry_scheduled",
|
|
691
|
+
timestamp: Date.now(),
|
|
692
|
+
data: {
|
|
693
|
+
operation: "compaction",
|
|
694
|
+
attempt: event.attempt,
|
|
695
|
+
maxAttempts: event.maxAttempts,
|
|
696
|
+
delayMs: event.delayMs,
|
|
697
|
+
error: event.errorMessage,
|
|
662
698
|
},
|
|
663
699
|
});
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
outcome = { summary: done.
|
|
700
|
+
});
|
|
701
|
+
try {
|
|
702
|
+
const done = await bound.compact(command.instructions);
|
|
703
|
+
outcome = { summary: done.summary };
|
|
668
704
|
}
|
|
669
705
|
catch (error) {
|
|
670
706
|
// A deliberate stop is not a failure — run/compaction symmetry with
|
|
671
|
-
// run_settled{aborted}: the
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
outcome = door.signal.aborted ? { aborted: true } : { error: String(error) };
|
|
707
|
+
// run_settled{aborted}: the intent is the classification, same discipline as run
|
|
708
|
+
// abort attribution (a racing real failure still reads as aborted).
|
|
709
|
+
outcome = aborted ? { aborted: true } : { error: String(error) };
|
|
675
710
|
}
|
|
676
|
-
|
|
711
|
+
running = false;
|
|
712
|
+
unsub();
|
|
713
|
+
teardown();
|
|
677
714
|
// Release BEFORE emitting finished: a watcher seeing finished may dispatch next —
|
|
678
715
|
// "finished ⇒ the lease is free and status is no longer compacting" must hold.
|
|
679
716
|
compacting.delete(session);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/** What a Caller names when a new session should start from an existing one. */
|
|
3
|
+
export interface SessionInheritance {
|
|
4
|
+
/** The session to inherit from. Missing or unreadable → start empty, with a warn: context is not
|
|
5
|
+
* the ask, and losing it must not cost the turn. */
|
|
6
|
+
parentSession: string;
|
|
7
|
+
/** Opaque markers that MAY locate the branch point on the parent's active path (searched in
|
|
8
|
+
* message content, first hit wins, most recent occurrence). No match → the parent's present. */
|
|
9
|
+
branchHints?: string[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The branch point this inheritance should copy up to, and the parent's path — the decision half,
|
|
13
|
+
* shared by both backends because WHERE a thread branches is policy, not storage.
|
|
14
|
+
*/
|
|
15
|
+
export declare function inheritanceCut(parent: SessionManager, branchHints?: string[]): {
|
|
16
|
+
at: string;
|
|
17
|
+
} | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Copy the parent's path up to `at` into `child`, entry by entry — what a backend with no FILE to
|
|
20
|
+
* fork has to do instead. Kinds pi models as facts about an entry rather than positions (labels,
|
|
21
|
+
* the session name) are not copied: they describe the parent's record, not the thread's history.
|
|
22
|
+
*/
|
|
23
|
+
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
|
+
*
|
|
31
|
+
* STAGED, not published: the caller finishes the record (crash reconciliation) and moves it into
|
|
32
|
+
* place. Publishing here and finishing after would leave a half-prepared record under the id on any
|
|
33
|
+
* later failure — and the fallback path would then create a SECOND record with the same id, making
|
|
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.
|
|
37
|
+
*/
|
|
38
|
+
export declare function forkForInheritance(options: {
|
|
39
|
+
parent: SessionManager;
|
|
40
|
+
id: string;
|
|
41
|
+
cwd: string;
|
|
42
|
+
stagingDir: string;
|
|
43
|
+
branchHints?: string[];
|
|
44
|
+
}): SessionManager | undefined;
|