@junghanacs/entwurf 0.36.1 → 0.37.1
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/AGENTS.md +2 -2
- package/CHANGELOG.md +301 -0
- package/CONTRIBUTING.md +1 -1
- package/README.md +5 -0
- package/VERIFY.md +26 -17
- package/docs/setup-clean-host.md +1 -0
- package/mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js +16 -1
- package/package.json +4 -3
- package/pi/pi-durable/bootstrap.mjs +113 -11
- package/pi/pi-durable/carrier/harness-setup.js +4 -1
- package/pi/pi-durable/carrier/prompt.js +31 -2
- package/pi/pi-durable/carrier/runtime.js +11 -2
- package/pi/pi-durable/carrier/sessions.js +75 -5
- package/pi/pi-durable/carrier/tui.js +23 -0
- package/pi/pi-durable/overlay/upstream-pin.json +10 -10
- package/pi-extensions/meta-bridge-pi-durable.ts +20 -0
- package/run.sh +44 -17
- package/scripts/check-gate-qualification.ts +3 -3
- package/scripts/check-release-gate-outcomes.ts +200 -387
- package/scripts/fixtures/herdr-supply.json +11 -10
- package/scripts/lib/codex-fresh-live-protocol.ts +6 -1
- package/scripts/lib/qualification-ledger.ts +2 -2
- package/scripts/mutants/herdr-supply.json +5 -5
- package/scripts/mutants/pi-durable.json +117 -45
- package/scripts/mutants/release-gate.json +18 -52
- package/scripts/smoke-mux-lifecycle-live.ts +1 -1
- package/scripts/ci-qualify-decide.sh +0 -256
- package/scripts/fixtures/qualify-replay.json +0 -149
|
@@ -80,19 +80,21 @@ import {
|
|
|
80
80
|
failAfterCleanup,
|
|
81
81
|
PI_DURABLE_EXTENSION_NAME,
|
|
82
82
|
PI_DURABLE_TOOL_NAMES,
|
|
83
|
+
piDurableGardenIds,
|
|
83
84
|
withGardenIdentity,
|
|
84
85
|
} from "../../mcp/entwurf-bridge/dist/pi-extensions/meta-bridge-pi-durable.js";
|
|
85
86
|
|
|
86
87
|
const RUNTIME = "./carrier/runtime.js";
|
|
87
88
|
const TUI = "./carrier/tui.js";
|
|
89
|
+
const SESSIONS = "./carrier/sessions.js";
|
|
88
90
|
|
|
89
91
|
/** The entwurf-bridge of THIS package closure: the one the contact spawns as its MCP hand. */
|
|
90
92
|
export const PACKAGED_BRIDGE_ENTRY = fileURLToPath(
|
|
91
93
|
new URL("../../mcp/entwurf-bridge/dist/mcp/entwurf-bridge/src/index.js", import.meta.url),
|
|
92
94
|
);
|
|
93
95
|
|
|
94
|
-
/** Flags that take a value. Anything else but `--continue`/`-c` is an unknown argument — a
|
|
95
|
-
* entry included: the launch argv cannot point the contact at another bridge. */
|
|
96
|
+
/** Flags that take a value. Anything else but `--continue`/`-c` and `--resume` is an unknown argument — a
|
|
97
|
+
* bridge entry included: the launch argv cannot point the contact at another bridge. */
|
|
96
98
|
const VALUE_FLAGS = [
|
|
97
99
|
"--provider",
|
|
98
100
|
"--model",
|
|
@@ -293,12 +295,13 @@ async function loadNativeModules(files) {
|
|
|
293
295
|
* @param {{
|
|
294
296
|
* bridgeEntry?: string,
|
|
295
297
|
* continueSession?: boolean,
|
|
298
|
+
* session?: string,
|
|
296
299
|
* cwd?: string,
|
|
297
300
|
* model?: { provider: string, model: string },
|
|
298
301
|
* firstInput?: string,
|
|
299
302
|
* extensions?: readonly object[],
|
|
300
303
|
* contact?: Record<string, unknown>,
|
|
301
|
-
* open?: (options: { cwd: string, continueSession: boolean, extensions: readonly object[], model?: { provider: string, model: string } }) => Promise<any>,
|
|
304
|
+
* open?: (options: { cwd: string, continueSession: boolean, session?: string, extensions: readonly object[], model?: { provider: string, model: string } }) => Promise<any>,
|
|
302
305
|
* }} options
|
|
303
306
|
* @returns {Promise<{ durable: any, contact: any, attachment: { gardenId: string } & Record<string, any>, firstInput?: any, close(): Promise<void> }>}
|
|
304
307
|
* (stated, because the compiled closure carries no declarations: a cleanup path that rethrows
|
|
@@ -307,6 +310,7 @@ async function loadNativeModules(files) {
|
|
|
307
310
|
export async function openDurableCitizen({
|
|
308
311
|
bridgeEntry = PACKAGED_BRIDGE_ENTRY,
|
|
309
312
|
continueSession = false,
|
|
313
|
+
session,
|
|
310
314
|
cwd = process.cwd(),
|
|
311
315
|
model,
|
|
312
316
|
firstInput,
|
|
@@ -320,6 +324,12 @@ export async function openDurableCitizen({
|
|
|
320
324
|
"an explicit model and a first input apply only to a new session; a continued session is reopened as it was saved",
|
|
321
325
|
);
|
|
322
326
|
}
|
|
327
|
+
if (session !== undefined && (continueSession || model !== undefined || firstInput !== undefined)) {
|
|
328
|
+
throw refuse(
|
|
329
|
+
"fresh-only-argument-with-resume",
|
|
330
|
+
"a chosen session is reopened as it was saved: no --continue, explicit model or first input",
|
|
331
|
+
);
|
|
332
|
+
}
|
|
323
333
|
if (model !== undefined && !(nonEmpty(model.provider) && nonEmpty(model.model))) {
|
|
324
334
|
throw refuse("explicit-model-incomplete", "an explicit model names both its provider and its model id");
|
|
325
335
|
}
|
|
@@ -333,6 +343,7 @@ export async function openDurableCitizen({
|
|
|
333
343
|
durable = await openDurable({
|
|
334
344
|
cwd,
|
|
335
345
|
continueSession,
|
|
346
|
+
...(session === undefined ? {} : { session }),
|
|
336
347
|
extensions: [contact.extension, ...extensions],
|
|
337
348
|
...(model === undefined ? {} : { model }),
|
|
338
349
|
});
|
|
@@ -389,6 +400,7 @@ export async function openDurableCitizen({
|
|
|
389
400
|
|
|
390
401
|
function parseArgs(argv) {
|
|
391
402
|
let continueSession = false;
|
|
403
|
+
let resume = false;
|
|
392
404
|
const values = new Map();
|
|
393
405
|
for (let i = 0; i < argv.length; i++) {
|
|
394
406
|
const arg = argv[i];
|
|
@@ -396,6 +408,10 @@ function parseArgs(argv) {
|
|
|
396
408
|
continueSession = true;
|
|
397
409
|
continue;
|
|
398
410
|
}
|
|
411
|
+
if (arg === "--resume") {
|
|
412
|
+
resume = true;
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
399
415
|
if (!VALUE_FLAGS.includes(arg)) throw new Error(`Unknown argument: ${arg}`);
|
|
400
416
|
if (values.has(arg)) throw refuse("argument-repeated", `${arg} is given more than once`);
|
|
401
417
|
const value = argv[++i];
|
|
@@ -404,7 +420,19 @@ function parseArgs(argv) {
|
|
|
404
420
|
}
|
|
405
421
|
values.set(arg, value);
|
|
406
422
|
}
|
|
423
|
+
if (resume && continueSession) {
|
|
424
|
+
throw refuse(
|
|
425
|
+
"resume-continue-exclusive",
|
|
426
|
+
"--resume chooses a session and --continue opens the newest one; give one of them",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
407
429
|
const fresh = FRESH_ONLY_FLAGS.filter((flag) => values.has(flag));
|
|
430
|
+
if (resume && fresh.length > 0) {
|
|
431
|
+
throw refuse(
|
|
432
|
+
"fresh-only-argument-with-resume",
|
|
433
|
+
`${fresh.join(", ")} apply only to a new session; --resume reopens the chosen session as it was saved`,
|
|
434
|
+
);
|
|
435
|
+
}
|
|
408
436
|
if (continueSession && fresh.length > 0) {
|
|
409
437
|
throw refuse(
|
|
410
438
|
"fresh-only-argument-with-continue",
|
|
@@ -426,6 +454,7 @@ function parseArgs(argv) {
|
|
|
426
454
|
? { nativeModules: nativeModuleDirFiles(dirArg) }
|
|
427
455
|
: {};
|
|
428
456
|
if (continueSession) return { continueSession, ...nativeModule };
|
|
457
|
+
if (resume) return { resume, ...nativeModule };
|
|
429
458
|
const provider = values.get("--provider");
|
|
430
459
|
if (provider === undefined) {
|
|
431
460
|
throw refuse(
|
|
@@ -457,29 +486,102 @@ function parseArgs(argv) {
|
|
|
457
486
|
};
|
|
458
487
|
}
|
|
459
488
|
|
|
489
|
+
/** A local `YYYY-MM-DD HH:MM` for a list line. */
|
|
490
|
+
function localTime(ms) {
|
|
491
|
+
const date = new Date(ms);
|
|
492
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
493
|
+
return `${date.getFullYear()}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
|
|
494
|
+
}
|
|
495
|
+
|
|
460
496
|
/**
|
|
461
|
-
*
|
|
462
|
-
*
|
|
463
|
-
*
|
|
497
|
+
* `--resume` (#143): this directory's durable sessions, most recently written first, each by what its files and
|
|
498
|
+
* the record store already say — creation time, garden id, last write, whether another process holds it. No
|
|
499
|
+
* session database is opened to list them, and nothing is named that the store does not hold. Resolves the
|
|
500
|
+
* chosen session's id, or undefined when the choice was cancelled; never a fallback to another session.
|
|
501
|
+
*/
|
|
502
|
+
async function chooseSession(cwd, sessionsDir, pick) {
|
|
503
|
+
const { listSessions } = await import(SESSIONS);
|
|
504
|
+
const sessions = await listSessions(cwd);
|
|
505
|
+
if (sessions.length === 0) throw refuse("resume-no-session", `no durable session exists for ${cwd}`);
|
|
506
|
+
const { gardens, unreadable } = piDurableGardenIds(
|
|
507
|
+
sessions.map((session) => session.id),
|
|
508
|
+
sessionsDir,
|
|
509
|
+
);
|
|
510
|
+
const items = sessions.map((session) => ({
|
|
511
|
+
value: session.id,
|
|
512
|
+
label: `${localTime(session.createdAt)} ${gardens.get(session.id) ?? "no record"}`,
|
|
513
|
+
description: [
|
|
514
|
+
session.lastWriteAt === undefined ? "never written" : `last write ${localTime(session.lastWriteAt)}`,
|
|
515
|
+
session.id,
|
|
516
|
+
...(session.openElsewhere ? ["open elsewhere"] : []),
|
|
517
|
+
].join(" · "),
|
|
518
|
+
}));
|
|
519
|
+
const title =
|
|
520
|
+
unreadable === 0 ? "Resume durable session:" : `Resume durable session (${unreadable} unreadable record(s)):`;
|
|
521
|
+
const id = await pick(title, items);
|
|
522
|
+
if (id === undefined) return undefined;
|
|
523
|
+
const chosen = sessions.find((session) => session.id === id);
|
|
524
|
+
if (chosen === undefined) throw refuse("session-not-found", `${id} is not one of the listed sessions`);
|
|
525
|
+
if (chosen.openElsewhere) {
|
|
526
|
+
throw refuse("session-open-elsewhere", `${id} is open in another process; close it there first`);
|
|
527
|
+
}
|
|
528
|
+
return chosen.id;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/**
|
|
532
|
+
* Parse, initialize the operator's module, load the TUI, open, run, close. `loadTui`, `open`,
|
|
533
|
+
* `contact` and `pick` (the `--resume` list's choice) are test seams; `bridgeEntry` is the
|
|
534
|
+
* checkout-only gates' private bundle, as in `openDurableCitizen` — never the launch argv.
|
|
464
535
|
*
|
|
465
536
|
* @param {readonly string[]} argv
|
|
466
537
|
* @param {{
|
|
467
|
-
* loadTui?: () => Promise<{ runDurableTui: (...args: any[]) => Promise<void> }>,
|
|
538
|
+
* loadTui?: () => Promise<{ runDurableTui: (...args: any[]) => Promise<void>, pickDurableSession?: (title: string, items: object[]) => Promise<string | undefined> }>,
|
|
468
539
|
* open?: (options: any) => Promise<any>,
|
|
469
540
|
* contact?: Record<string, unknown>,
|
|
470
541
|
* bridgeEntry?: string,
|
|
542
|
+
* pick?: (title: string, items: { value: string, label: string, description: string }[]) => Promise<string | undefined>,
|
|
471
543
|
* }} [seams]
|
|
472
544
|
*/
|
|
473
|
-
export async function main(argv, { loadTui = () => import(TUI), open, contact, bridgeEntry } = {}) {
|
|
545
|
+
export async function main(argv, { loadTui = () => import(TUI), open, contact, bridgeEntry, pick } = {}) {
|
|
474
546
|
// Every argument refusal is decided here, before any module, the TUI or the app loads.
|
|
475
|
-
const { nativeModules = [], ...args } = parseArgs(argv);
|
|
547
|
+
const { nativeModules = [], resume = false, ...args } = parseArgs(argv);
|
|
476
548
|
// The caller's directory, fixed before any operator code runs and handed to the app explicitly.
|
|
477
549
|
const cwd = process.cwd();
|
|
478
550
|
const extensions = await loadNativeModules(nativeModules);
|
|
479
551
|
// Loaded before anything opens, as upstream main.ts imports it statically: a TUI that cannot
|
|
480
552
|
// load opens no session, mints no citizen and leaves nothing to clean up.
|
|
481
|
-
const
|
|
482
|
-
const
|
|
553
|
+
const tui = await loadTui();
|
|
554
|
+
const { runDurableTui } = tui;
|
|
555
|
+
let session;
|
|
556
|
+
if (resume) {
|
|
557
|
+
session = await chooseSession(
|
|
558
|
+
cwd,
|
|
559
|
+
contact?.sessionsDir,
|
|
560
|
+
pick ??
|
|
561
|
+
((title, items) => {
|
|
562
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
563
|
+
throw refuse(
|
|
564
|
+
"resume-needs-terminal",
|
|
565
|
+
"--resume lists the sessions to choose from in an interactive terminal",
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return tui.pickDurableSession(title, items);
|
|
569
|
+
}),
|
|
570
|
+
);
|
|
571
|
+
if (session === undefined) {
|
|
572
|
+
process.stderr.write("[pi-durable] resume-cancelled: nothing was opened\n");
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
const citizen = await openDurableCitizen({
|
|
577
|
+
...args,
|
|
578
|
+
...(session === undefined ? {} : { session }),
|
|
579
|
+
cwd,
|
|
580
|
+
extensions,
|
|
581
|
+
bridgeEntry,
|
|
582
|
+
open,
|
|
583
|
+
contact,
|
|
584
|
+
});
|
|
483
585
|
try {
|
|
484
586
|
await runDurableTui(
|
|
485
587
|
withGardenIdentity(citizen.durable.view, citizen.attachment.gardenId),
|
|
@@ -28,13 +28,16 @@ export function createHarnessSettings(settingsManager )
|
|
|
28
28
|
const provider = settingsManager.getProviderRetrySettings();
|
|
29
29
|
const idle = settingsManager.getHttpIdleTimeoutMs();
|
|
30
30
|
return {
|
|
31
|
+
transport: settingsManager.getTransport(),
|
|
31
32
|
timeoutMs: provider.timeoutMs ?? (idle === 0 ? 2147483647 : idle),
|
|
32
33
|
maxRetryDelayMs: provider.maxRetryDelayMs,
|
|
33
34
|
...(provider.maxRetries === undefined ? {} : { maxRetries: provider.maxRetries }),
|
|
34
35
|
};
|
|
35
36
|
},
|
|
37
|
+
// The reference app waits for a compaction instead of summarizing beside the running turn (#143): no background
|
|
38
|
+
// compaction. Threshold, manual and overflow compaction stay the SDK's, on pi's own compaction settings.
|
|
36
39
|
get compaction() {
|
|
37
|
-
return settingsManager.getCompactionSettings();
|
|
40
|
+
return { ...settingsManager.getCompactionSettings(), backgroundTokens: 0 };
|
|
38
41
|
},
|
|
39
42
|
get retry() {
|
|
40
43
|
return settingsManager.getRetrySettings();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
1
2
|
import { defineExtension, section } from "@earendil-works/pi-durable";
|
|
2
|
-
import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
3
|
+
import { CONFIG_DIR_NAME, getAgentDir } from "entwurf-pi-dist:config.js";
|
|
3
4
|
import { loadProjectContextFiles } from "entwurf-pi-dist:core/resource-loader.js";
|
|
4
5
|
|
|
5
6
|
import { loadSkills, } from "entwurf-pi-dist:core/skills.js";
|
|
@@ -19,6 +20,29 @@ const CONTRIBUTIONS = {
|
|
|
19
20
|
/** pi's section order; `buildSystemPromptSections()` omits the ones without content. */
|
|
20
21
|
const KEYS = ["preamble", "tools", "rules", "docs", "project_context", "skills", "cwd"] ;
|
|
21
22
|
|
|
23
|
+
/** An entry pi reads as a pattern, not a path: an override or a glob. */
|
|
24
|
+
const PATTERN = /^[!+-]|[*?]/u;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The `skills` setting with each scope's relative entries read against that scope's own directory, as pi resolves
|
|
28
|
+
* them: the project's against the session directory's `.pi`, the global ones against the agent directory, the
|
|
29
|
+
* project's first. Read against the conversation's directory, a relative entry names another place. Absolute, `~`
|
|
30
|
+
* and pattern entries go on unchanged.
|
|
31
|
+
*/
|
|
32
|
+
function scopedSkillPaths(settings , agentDir , projectCwd ) {
|
|
33
|
+
const against = (entries , base ) =>
|
|
34
|
+
(entries ?? []).map((entry) => {
|
|
35
|
+
const path = entry.trim();
|
|
36
|
+
return path === "" || isAbsolute(path) || path === "~" || path.startsWith("~/") || PATTERN.test(path)
|
|
37
|
+
? entry
|
|
38
|
+
: resolve(base, path);
|
|
39
|
+
});
|
|
40
|
+
return [
|
|
41
|
+
...against(settings.getProjectSettings().skills, join(projectCwd, CONFIG_DIR_NAME)),
|
|
42
|
+
...against(settings.getGlobalSettings().skills, agentDir),
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
|
|
22
46
|
/**
|
|
23
47
|
* pi's system prompt as one extension: the sections of `buildSystemPromptSections()` for the request's tools and the
|
|
24
48
|
* conversation's directory. Context files and skills load once per directory, like pi at startup.
|
|
@@ -31,7 +55,12 @@ export function createPiPrompt(settings , fallbackCwd ) {
|
|
|
31
55
|
const agentDir = getAgentDir();
|
|
32
56
|
found = {
|
|
33
57
|
contextFiles: loadProjectContextFiles({ cwd, agentDir }),
|
|
34
|
-
skills: loadSkills({
|
|
58
|
+
skills: loadSkills({
|
|
59
|
+
cwd,
|
|
60
|
+
agentDir,
|
|
61
|
+
skillPaths: scopedSkillPaths(settings, agentDir, fallbackCwd),
|
|
62
|
+
includeDefaults: true,
|
|
63
|
+
}).skills,
|
|
35
64
|
};
|
|
36
65
|
resources.set(cwd, found);
|
|
37
66
|
}
|
|
@@ -90,6 +90,8 @@ const context = BACKGROUND_CONTEXT;
|
|
|
90
90
|
|
|
91
91
|
|
|
92
92
|
|
|
93
|
+
|
|
94
|
+
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
|
|
@@ -143,10 +145,17 @@ function titleOf(entry ) {
|
|
|
143
145
|
}
|
|
144
146
|
|
|
145
147
|
export async function openDurable(options = {}) {
|
|
146
|
-
if (options.model !== undefined && options.continueSession) {
|
|
148
|
+
if (options.model !== undefined && (options.continueSession || options.session !== undefined)) {
|
|
147
149
|
throw new Error("An explicit model applies only to a new session, not to a continued one");
|
|
148
150
|
}
|
|
149
|
-
|
|
151
|
+
if (options.continueSession && options.session !== undefined) {
|
|
152
|
+
throw new Error("A chosen session and the newest one are two different sessions; name one");
|
|
153
|
+
}
|
|
154
|
+
const location = await selectSession(
|
|
155
|
+
options.cwd ?? process.cwd(),
|
|
156
|
+
options.continueSession ?? false,
|
|
157
|
+
options.session,
|
|
158
|
+
);
|
|
150
159
|
const envs = new ExecutionEnvs(location.cwd);
|
|
151
160
|
let harness ;
|
|
152
161
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { mkdir, readdir, realpath, stat } from "node:fs/promises";
|
|
3
4
|
import { basename, join, resolve } from "node:path";
|
|
4
5
|
import lockfile from "proper-lockfile";
|
|
5
6
|
import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
@@ -14,8 +15,31 @@ import { getAgentDir } from "entwurf-pi-dist:config.js";
|
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
|
|
17
|
-
/**
|
|
18
|
-
|
|
18
|
+
/** One session of a directory as its files show it (#143): nothing in it is opened. */
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
const SESSION_NAME = /^\d{13}-[0-9a-f-]{36}$/u;
|
|
31
|
+
|
|
32
|
+
/** A path's stat, or undefined when nothing is there; any other failure is thrown, never read as absence. */
|
|
33
|
+
async function statIfPresent(path ) {
|
|
34
|
+
try {
|
|
35
|
+
return await stat(path);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
if ((error ).code === "ENOENT") return undefined;
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function sessionRoot(cwdInput ) {
|
|
19
43
|
const cwd = await realpath(resolve(cwdInput));
|
|
20
44
|
const root = join(
|
|
21
45
|
getAgentDir(),
|
|
@@ -23,14 +47,60 @@ export async function selectSession(cwdInput , continueSession )
|
|
|
23
47
|
"durable-sessions",
|
|
24
48
|
createHash("sha256").update(cwd).digest("hex").slice(0, 24),
|
|
25
49
|
);
|
|
50
|
+
return { cwd, root };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The sessions of `cwd`, most recently written first. Reads names, file times and locks; creates nothing. */
|
|
54
|
+
export async function listSessions(cwdInput ) {
|
|
55
|
+
const { root } = await sessionRoot(cwdInput);
|
|
56
|
+
let entries ;
|
|
57
|
+
try {
|
|
58
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if ((error ).code === "ENOENT") return [];
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
const sessions = [];
|
|
64
|
+
for (const entry of entries) {
|
|
65
|
+
if (!entry.isDirectory() || !SESSION_NAME.test(entry.name)) continue;
|
|
66
|
+
const directory = join(root, entry.name);
|
|
67
|
+
let lastWriteAt ;
|
|
68
|
+
for (const file of ["session.sqlite", "session.sqlite-wal"]) {
|
|
69
|
+
const mtime = (await statIfPresent(join(directory, file)))?.mtimeMs;
|
|
70
|
+
if (mtime !== undefined && (lastWriteAt === undefined || mtime > lastWriteAt)) lastWriteAt = mtime;
|
|
71
|
+
}
|
|
72
|
+
sessions.push({
|
|
73
|
+
id: entry.name,
|
|
74
|
+
directory,
|
|
75
|
+
createdAt: Number(entry.name.slice(0, 13)),
|
|
76
|
+
...(lastWriteAt === undefined ? {} : { lastWriteAt }),
|
|
77
|
+
openElsewhere: await lockfile.check(directory, { realpath: false }),
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return sessions.sort((a, b) => (b.lastWriteAt ?? b.createdAt) - (a.lastWriteAt ?? a.createdAt));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** A new session for `cwd`, its newest one with `continueSession`, or the existing one named by `sessionId`. */
|
|
84
|
+
export async function selectSession(
|
|
85
|
+
cwdInput ,
|
|
86
|
+
continueSession ,
|
|
87
|
+
sessionId ,
|
|
88
|
+
) {
|
|
89
|
+
const { cwd, root } = await sessionRoot(cwdInput);
|
|
26
90
|
await mkdir(root, { recursive: true });
|
|
27
91
|
|
|
28
92
|
let directory ;
|
|
29
93
|
let created = false;
|
|
30
|
-
if (
|
|
94
|
+
if (sessionId !== undefined) {
|
|
95
|
+
const chosen = SESSION_NAME.test(sessionId) ? join(root, sessionId) : undefined;
|
|
96
|
+
if (chosen === undefined || !(await statIfPresent(chosen))?.isDirectory()) {
|
|
97
|
+
throw new Error(`No durable session ${sessionId} exists for ${cwd}`);
|
|
98
|
+
}
|
|
99
|
+
directory = chosen;
|
|
100
|
+
} else if (continueSession) {
|
|
31
101
|
const entries = await readdir(root, { withFileTypes: true });
|
|
32
102
|
const newest = entries
|
|
33
|
-
.filter((entry) => entry.isDirectory() &&
|
|
103
|
+
.filter((entry) => entry.isDirectory() && SESSION_NAME.test(entry.name))
|
|
34
104
|
.map((entry) => entry.name)
|
|
35
105
|
.sort()
|
|
36
106
|
.at(-1);
|
|
@@ -113,6 +113,29 @@ class ListSelector extends Container {
|
|
|
113
113
|
}
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Choose one of the sessions `--resume` lists (#143) before any of them opens: the /agents list on a screen of its
|
|
118
|
+
* own, stopped before the session's TUI starts. Resolves the chosen value, or undefined when cancelled.
|
|
119
|
+
*/
|
|
120
|
+
export function pickDurableSession(title , items ) {
|
|
121
|
+
initTheme();
|
|
122
|
+
const ui = new TuiAltScreen(new ProcessTerminal(), false, getAgentDir());
|
|
123
|
+
setKeybindings(KeybindingsManager.create());
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
let done = false;
|
|
126
|
+
const finish = (value ) => {
|
|
127
|
+
if (done) return;
|
|
128
|
+
done = true;
|
|
129
|
+
ui.stop();
|
|
130
|
+
resolve(value);
|
|
131
|
+
};
|
|
132
|
+
const selector = new ListSelector(title, items, finish, () => finish(undefined));
|
|
133
|
+
ui.addChild(selector);
|
|
134
|
+
ui.setFocus(selector);
|
|
135
|
+
ui.start();
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
116
139
|
/** The summary that replaced earlier context: collapsed to one line until expanded. */
|
|
117
140
|
class CompactionComponent extends Box {
|
|
118
141
|
#summary ;
|
|
@@ -70,32 +70,32 @@
|
|
|
70
70
|
{
|
|
71
71
|
"name": "runtime.js",
|
|
72
72
|
"source": "pi/pi-durable/src/experimental/durable/runtime.ts",
|
|
73
|
-
"blob": "
|
|
74
|
-
"sha256": "
|
|
73
|
+
"blob": "75d7bf2b4fdc36aa775f73f9d7f3de8659f19187",
|
|
74
|
+
"sha256": "725757156580ad2a2d170c790440763371bb8035052a0c6caae6af5159d4718f"
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
"name": "tui.js",
|
|
78
78
|
"source": "pi/pi-durable/src/experimental/durable/tui.ts",
|
|
79
|
-
"blob": "
|
|
80
|
-
"sha256": "
|
|
79
|
+
"blob": "eb5e2ec4e98919cd3ff4f81f166eb52afe6555af",
|
|
80
|
+
"sha256": "a1b0d9d9358c5b54ac0974de84068b7792669171917ebd7b7351374ed625c9c5"
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"name": "harness-setup.js",
|
|
84
84
|
"source": "pi/pi-durable/src/experimental/durable/harness-setup.ts",
|
|
85
|
-
"blob": "
|
|
86
|
-
"sha256": "
|
|
85
|
+
"blob": "36a622ec6d5dccda828ffaceaf74ce8ec241e39d",
|
|
86
|
+
"sha256": "efb06ae25258fbbab5b28f8a9bbb50956c7ebfd77a5ed23b065706f662e70ba3"
|
|
87
87
|
},
|
|
88
88
|
{
|
|
89
89
|
"name": "prompt.js",
|
|
90
90
|
"source": "pi/pi-durable/src/experimental/durable/prompt.ts",
|
|
91
|
-
"blob": "
|
|
92
|
-
"sha256": "
|
|
91
|
+
"blob": "0e99ff64214c44986c1c2259490e401898659fd5",
|
|
92
|
+
"sha256": "15870ea4c9cb465395d70189be4f1480c48544d724bd9fc3b0e6650f17226c0f"
|
|
93
93
|
},
|
|
94
94
|
{
|
|
95
95
|
"name": "sessions.js",
|
|
96
96
|
"source": "pi/pi-durable/src/experimental/durable/sessions.ts",
|
|
97
|
-
"blob": "
|
|
98
|
-
"sha256": "
|
|
97
|
+
"blob": "eed4b3ca51c6f5a3bab8106adaf0062f3f7d5b56",
|
|
98
|
+
"sha256": "668373feadabcf13bd4dd7431e5429b7d7376c47c367e4b15353c57cb8711892"
|
|
99
99
|
},
|
|
100
100
|
{
|
|
101
101
|
"name": "subagent.js",
|
|
@@ -113,6 +113,7 @@ import {
|
|
|
113
113
|
defaultMetaReceiversDir,
|
|
114
114
|
defaultMetaSendersDir,
|
|
115
115
|
defaultMetaSessionsDir,
|
|
116
|
+
listAllMetaIdentitiesDir,
|
|
116
117
|
readAddressableMetaIdentity,
|
|
117
118
|
readMetaReceiverMarker,
|
|
118
119
|
readMetaSenderMarker,
|
|
@@ -125,6 +126,25 @@ import {
|
|
|
125
126
|
/** The backend id this contact mints under. */
|
|
126
127
|
export const PI_DURABLE_BACKEND = "pi-durable" as const;
|
|
127
128
|
|
|
129
|
+
/**
|
|
130
|
+
* The garden ids the record store holds for these durable sessions (#143 `--resume`), read only: a session no record
|
|
131
|
+
* names is absent, and an unreadable record is counted, never guessed.
|
|
132
|
+
*/
|
|
133
|
+
export function piDurableGardenIds(
|
|
134
|
+
nativeSessionIds: readonly string[],
|
|
135
|
+
sessionsDir: string = defaultMetaSessionsDir(),
|
|
136
|
+
): { gardens: Map<string, string>; unreadable: number } {
|
|
137
|
+
const wanted = new Set(nativeSessionIds);
|
|
138
|
+
const { identities, errors } = listAllMetaIdentitiesDir(sessionsDir);
|
|
139
|
+
const gardens = new Map<string, string>();
|
|
140
|
+
for (const identity of identities) {
|
|
141
|
+
if (identity.backend === PI_DURABLE_BACKEND && wanted.has(identity.nativeSessionId)) {
|
|
142
|
+
gardens.set(identity.nativeSessionId, identity.gardenId);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { gardens, unreadable: errors.length };
|
|
146
|
+
}
|
|
147
|
+
|
|
128
148
|
/** The durable Extension's name, owned exactly. */
|
|
129
149
|
export const PI_DURABLE_EXTENSION_NAME = "entwurf";
|
|
130
150
|
|