@oh-my-pi/pi-coding-agent 17.3.4 → 17.3.7
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/CHANGELOG.md +88 -0
- package/dist/{CHANGELOG-trcc215s.md → CHANGELOG-1hmwwt45.md} +88 -0
- package/dist/cli.js +3156 -3163
- package/dist/docs-index.generated.txt +1 -1
- package/dist/types/cli/stats-cli.d.ts +1 -6
- package/dist/types/commands/stats.d.ts +4 -0
- package/dist/types/config/model-discovery.d.ts +2 -0
- package/dist/types/config/model-registry.d.ts +6 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/debug/report-bundle.d.ts +7 -2
- package/dist/types/extensibility/extensions/loader.d.ts +2 -0
- package/dist/types/extensibility/extensions/runner.d.ts +24 -6
- package/dist/types/extensibility/extensions/types.d.ts +60 -0
- package/dist/types/extensibility/legacy-pi-ai-shim.d.ts +4 -0
- package/dist/types/launch/presence.d.ts +11 -0
- package/dist/types/lsp/client.d.ts +22 -0
- package/dist/types/lsp/workspace-diagnostics.d.ts +2 -0
- package/dist/types/mcp/config.d.ts +3 -1
- package/dist/types/modes/components/tool-execution.d.ts +3 -1
- package/dist/types/registry/agent-lifecycle.d.ts +11 -0
- package/dist/types/registry/agent-registry.d.ts +4 -0
- package/dist/types/session/agent-session-types.d.ts +0 -2
- package/dist/types/session/agent-session.d.ts +20 -0
- package/dist/types/session/date-cwd-reminder.d.ts +22 -0
- package/dist/types/session/irc-bridge.d.ts +2 -0
- package/dist/types/session/messages.d.ts +4 -0
- package/dist/types/session/session-loader.d.ts +18 -11
- package/dist/types/session/session-maintenance.d.ts +6 -2
- package/dist/types/session/session-manager.d.ts +19 -3
- package/dist/types/session/session-tools.d.ts +0 -1
- package/dist/types/session/turn-recovery.d.ts +7 -7
- package/dist/types/slash-commands/helpers/stats-dashboard.d.ts +1 -0
- package/dist/types/tools/browser/attach.d.ts +16 -0
- package/dist/types/tools/browser/launch.d.ts +2 -2
- package/dist/types/tools/browser.d.ts +1 -1
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/file-write-fallback.d.ts +124 -0
- package/dist/types/tools/hub/jobs.d.ts +6 -0
- package/dist/types/tools/hub/types.d.ts +6 -0
- package/dist/types/tools/index.d.ts +1 -0
- package/dist/types/tools/path-utils.d.ts +23 -0
- package/dist/types/vibe/runtime.d.ts +3 -3
- package/package.json +13 -13
- package/src/auto-thinking/classifier.ts +37 -23
- package/src/cli/models-cli.ts +1 -1
- package/src/cli/stats-cli.ts +6 -72
- package/src/commands/stats.ts +7 -4
- package/src/commit/analysis/conventional.ts +15 -9
- package/src/commit/analysis/summary.ts +15 -9
- package/src/commit/changelog/generate.ts +15 -9
- package/src/commit/map-reduce/map-phase.ts +3 -19
- package/src/commit/map-reduce/reduce-phase.ts +15 -9
- package/src/config/model-discovery.ts +3 -3
- package/src/config/model-registry.ts +29 -16
- package/src/config/settings-schema.ts +13 -0
- package/src/debug/report-bundle.ts +25 -59
- package/src/discovery/claude-plugins.ts +2 -1
- package/src/discovery/claude.ts +4 -2
- package/src/discovery/helpers.ts +5 -0
- package/src/edit/hashline/filesystem.ts +9 -3
- package/src/edit/modes/patch.ts +31 -5
- package/src/edit/renderer.ts +48 -19
- package/src/extensibility/extensions/loader.ts +20 -5
- package/src/extensibility/extensions/runner.ts +239 -24
- package/src/extensibility/extensions/types.ts +62 -0
- package/src/extensibility/extensions/wrapper.ts +26 -11
- package/src/extensibility/legacy-pi-ai-shim.ts +4 -0
- package/src/extensibility/plugins/marketplace/manager.ts +15 -18
- package/src/irc/bus.ts +1 -1
- package/src/launch/broker.ts +8 -1
- package/src/launch/presence.ts +77 -1
- package/src/lsp/client.ts +35 -3
- package/src/lsp/clients/biome-client.ts +47 -93
- package/src/lsp/servers.ts +17 -11
- package/src/lsp/tool.ts +2 -4
- package/src/lsp/workspace-diagnostics.ts +24 -2
- package/src/lsp/writethrough.ts +20 -10
- package/src/mcp/config.ts +50 -6
- package/src/memories/index.ts +29 -25
- package/src/mnemopi/backend.ts +13 -11
- package/src/modes/components/ask-dialog.ts +25 -5
- package/src/modes/components/tool-execution.ts +10 -6
- package/src/modes/components/welcome.ts +4 -1
- package/src/modes/controllers/extension-ui-controller.ts +52 -32
- package/src/modes/controllers/selector-controller.ts +6 -0
- package/src/modes/controllers/tan-command-controller.ts +1 -0
- package/src/modes/interactive-mode.ts +49 -10
- package/src/modes/theme/defaults/birch.json +2 -2
- package/src/prompts/system/checkpoint-active-notice.md +5 -0
- package/src/prompts/system/date-cwd-reminder.md +3 -0
- package/src/prompts/system/project-prompt.md +0 -1
- package/src/prompts/system/rewind-report.md +1 -3
- package/src/prompts/tools/browser.md +1 -0
- package/src/prompts/tools/rewind.md +1 -13
- package/src/registry/agent-lifecycle.ts +34 -0
- package/src/registry/agent-registry.ts +27 -2
- package/src/registry/persisted-agents.ts +40 -20
- package/src/sdk.ts +32 -2
- package/src/session/agent-session-types.ts +0 -2
- package/src/session/agent-session.ts +178 -46
- package/src/session/date-cwd-reminder.ts +77 -0
- package/src/session/irc-bridge.ts +5 -0
- package/src/session/messages.ts +5 -1
- package/src/session/session-loader.ts +106 -64
- package/src/session/session-maintenance.ts +189 -115
- package/src/session/session-manager.ts +142 -35
- package/src/session/session-persistence.ts +4 -4
- package/src/session/session-storage.ts +18 -5
- package/src/session/session-tools.ts +5 -10
- package/src/session/turn-recovery.ts +102 -20
- package/src/session/unexpected-stop-classifier.ts +33 -21
- package/src/slash-commands/builtin-session.ts +1 -1
- package/src/slash-commands/helpers/stats-dashboard.ts +23 -9
- package/src/system-prompt.ts +0 -5
- package/src/task/executor.ts +4 -15
- package/src/task/persisted-revive.ts +3 -6
- package/src/tools/ask.ts +10 -3
- package/src/tools/browser/attach.ts +80 -25
- package/src/tools/browser/launch.ts +44 -15
- package/src/tools/browser/relay/daemon.ts +3 -7
- package/src/tools/browser/render.ts +1 -1
- package/src/tools/browser/shared-daemon.ts +3 -7
- package/src/tools/browser.ts +5 -5
- package/src/tools/builtin-names.ts +7 -3
- package/src/tools/checkpoint.ts +1 -7
- package/src/tools/file-write-fallback.ts +467 -0
- package/src/tools/hub/index.ts +1 -1
- package/src/tools/hub/jobs.ts +20 -3
- package/src/tools/hub/types.ts +6 -0
- package/src/tools/index.ts +1 -0
- package/src/tools/path-utils.ts +79 -0
- package/src/tools/render-utils.ts +53 -21
- package/src/tools/think.ts +15 -3
- package/src/tts/speech-enhancer.ts +19 -14
- package/src/utils/commit-message-generator.ts +14 -12
- package/src/utils/title-generator.ts +23 -19
- package/src/vibe/runtime.ts +32 -17
|
@@ -59,7 +59,13 @@ import {
|
|
|
59
59
|
type UsageStatistics,
|
|
60
60
|
} from "./session-entries";
|
|
61
61
|
import { findMostRecentSession, listAllSessions, listSessions, type SessionInfo } from "./session-listing";
|
|
62
|
-
import {
|
|
62
|
+
import {
|
|
63
|
+
loadEntriesFromFile,
|
|
64
|
+
loadSessionFile,
|
|
65
|
+
resolveBlobRefsInEntries,
|
|
66
|
+
type SessionLoadResult,
|
|
67
|
+
visitEntriesFromFile,
|
|
68
|
+
} from "./session-loader";
|
|
63
69
|
import { generateId, migrateToCurrentVersion } from "./session-migrations";
|
|
64
70
|
import {
|
|
65
71
|
computeDefaultSessionDir,
|
|
@@ -83,6 +89,7 @@ import {
|
|
|
83
89
|
|
|
84
90
|
const JSONL_SUFFIX_LENGTH = ".jsonl".length;
|
|
85
91
|
const DRAFT_ONLY_SESSION_MARKER = ".draft-only-session";
|
|
92
|
+
const DISCARDED_ENTRY_BRANCH_MARKER = "discarded-entry-branch";
|
|
86
93
|
|
|
87
94
|
function mintSessionId(): string {
|
|
88
95
|
return Bun.randomUUIDv7();
|
|
@@ -97,7 +104,31 @@ function fileSafeTimestamp(iso: string): string {
|
|
|
97
104
|
}
|
|
98
105
|
|
|
99
106
|
function artifactsDirectoryFor(sessionFile: string | undefined): string | null {
|
|
100
|
-
|
|
107
|
+
if (!sessionFile?.endsWith(".jsonl")) return null;
|
|
108
|
+
return sessionFile.slice(0, -JSONL_SUFFIX_LENGTH);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Copy a session's artifact directory to another session, matching interactive `/fork`. */
|
|
112
|
+
export async function copySessionArtifacts(sourceSessionFile: string, destinationSessionFile: string): Promise<void> {
|
|
113
|
+
const sourceArtifactsDir = artifactsDirectoryFor(sourceSessionFile);
|
|
114
|
+
const destinationArtifactsDir = artifactsDirectoryFor(destinationSessionFile);
|
|
115
|
+
if (!sourceArtifactsDir || !destinationArtifactsDir) return;
|
|
116
|
+
if (path.resolve(sourceArtifactsDir) === path.resolve(destinationArtifactsDir)) return;
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const sourceStat = await fs.promises.stat(sourceArtifactsDir);
|
|
120
|
+
if (sourceStat.isDirectory()) {
|
|
121
|
+
await fs.promises.cp(sourceArtifactsDir, destinationArtifactsDir, { recursive: true });
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
if (!isEnoent(error)) {
|
|
125
|
+
logger.warn("Failed to copy artifacts during fork", {
|
|
126
|
+
sourceArtifactsDir,
|
|
127
|
+
destinationArtifactsDir,
|
|
128
|
+
error: error instanceof Error ? error.message : String(error),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
101
132
|
}
|
|
102
133
|
|
|
103
134
|
/**
|
|
@@ -516,6 +547,7 @@ export class SessionManager {
|
|
|
516
547
|
*/
|
|
517
548
|
#breadcrumbFresh = false;
|
|
518
549
|
#sessionNameChangedCallbacks = new Set<() => void>();
|
|
550
|
+
#persistenceErrorCallbacks = new Set<(error: Error) => void>();
|
|
519
551
|
|
|
520
552
|
private constructor(cwd: string, sessionDir: string, persist: boolean, storage: SessionStorage) {
|
|
521
553
|
this.#cwd = cwd;
|
|
@@ -557,6 +589,15 @@ export class SessionManager {
|
|
|
557
589
|
error: error.message,
|
|
558
590
|
stack: error.stack,
|
|
559
591
|
});
|
|
592
|
+
for (const callback of this.#persistenceErrorCallbacks) {
|
|
593
|
+
try {
|
|
594
|
+
callback(error);
|
|
595
|
+
} catch (callbackError) {
|
|
596
|
+
logger.warn("Session persistence error observer failed", {
|
|
597
|
+
error: toError(callbackError).message,
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
560
601
|
}
|
|
561
602
|
|
|
562
603
|
return this.#diskFailure;
|
|
@@ -814,6 +855,7 @@ export class SessionManager {
|
|
|
814
855
|
this.#diskTail = Promise.resolve();
|
|
815
856
|
this.#closeWriterEventually();
|
|
816
857
|
this.#storage.writeTextSync(targetPath, body);
|
|
858
|
+
this.#clearDiskError();
|
|
817
859
|
// Only mark the manager current when writing the active session path.
|
|
818
860
|
// Mid-move writes update the live relocation path; `#sessionFile` is
|
|
819
861
|
// still the pre-repoint source until moveTo repoints it.
|
|
@@ -906,7 +948,13 @@ export class SessionManager {
|
|
|
906
948
|
this.#atomicRewriteDirty = true;
|
|
907
949
|
return;
|
|
908
950
|
}
|
|
909
|
-
if (this.#diskFailure)
|
|
951
|
+
if (this.#diskFailure) {
|
|
952
|
+
// The failed entry and any later entries remain in memory. A full
|
|
953
|
+
// replacement is the writability probe and restores all of them once
|
|
954
|
+
// transient storage pressure clears.
|
|
955
|
+
this.#fileIsCurrent = false;
|
|
956
|
+
this.#rewriteRequired = true;
|
|
957
|
+
}
|
|
910
958
|
|
|
911
959
|
// Lazy gate: a brand-new session is not written until it has an assistant
|
|
912
960
|
// message (or someone forced creation), so sessions that never produce
|
|
@@ -944,9 +992,9 @@ export class SessionManager {
|
|
|
944
992
|
// chain. Prefer appendSync so write failures latch `#diskFailure` before
|
|
945
993
|
// this call returns (not via a discarded rejected Promise after a later
|
|
946
994
|
// microtask). Callers stay non-throwing here — the core turn loop invokes
|
|
947
|
-
// appendMessage/appendCustomEntry without try/catch
|
|
948
|
-
//
|
|
949
|
-
//
|
|
995
|
+
// appendMessage/appendCustomEntry without try/catch. A later entry retries
|
|
996
|
+
// all in-memory state through a full rewrite. File writers apply each line
|
|
997
|
+
// to the OS page cache before return.
|
|
950
998
|
// A mid-close writer leaves `#writer` undefined, so `#appendWriter` simply
|
|
951
999
|
// opens a fresh append handle and the entry still lands.
|
|
952
1000
|
try {
|
|
@@ -955,16 +1003,28 @@ export class SessionManager {
|
|
|
955
1003
|
if (writer.appendSync) {
|
|
956
1004
|
writer.appendSync(line);
|
|
957
1005
|
} else {
|
|
958
|
-
void writer.append(line).catch(err =>
|
|
1006
|
+
void writer.append(line).catch(err => {
|
|
1007
|
+
this.#fileIsCurrent = false;
|
|
1008
|
+
this.#rewriteRequired = true;
|
|
1009
|
+
this.#noteDiskFailure(err);
|
|
1010
|
+
});
|
|
959
1011
|
}
|
|
960
1012
|
} catch (err) {
|
|
1013
|
+
this.#fileIsCurrent = false;
|
|
1014
|
+
this.#rewriteRequired = true;
|
|
961
1015
|
this.#noteDiskFailure(err);
|
|
962
1016
|
}
|
|
963
1017
|
}
|
|
964
1018
|
|
|
965
1019
|
async #persistTitleChangeEntry(entry: TitleChangeEntry, update: SessionTitleUpdate): Promise<void> {
|
|
966
1020
|
if (!this.#persist || !this.#sessionFile) return;
|
|
967
|
-
if (this.#diskFailure)
|
|
1021
|
+
if (this.#diskFailure) {
|
|
1022
|
+
this.#fileIsCurrent = false;
|
|
1023
|
+
this.#rewriteRequired = true;
|
|
1024
|
+
this.#rewriteSynchronously();
|
|
1025
|
+
if (this.#diskFailure) throw this.#diskFailure;
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
968
1028
|
|
|
969
1029
|
if (!this.#shouldHaveSessionFile()) {
|
|
970
1030
|
this.#fileIsCurrent = false;
|
|
@@ -1288,7 +1348,7 @@ export class SessionManager {
|
|
|
1288
1348
|
await this.#setSessionFile(sessionFile);
|
|
1289
1349
|
}
|
|
1290
1350
|
|
|
1291
|
-
async #setSessionFile(sessionFile: string,
|
|
1351
|
+
async #setSessionFile(sessionFile: string, loadedSession?: SessionLoadResult): Promise<void> {
|
|
1292
1352
|
await this.#drainAndCloseWriter();
|
|
1293
1353
|
this.#clearDiskError();
|
|
1294
1354
|
this.#draftOnlySessionCleanupArmed = false;
|
|
@@ -1297,8 +1357,8 @@ export class SessionManager {
|
|
|
1297
1357
|
this.#sessionFile = resolvedSessionFile;
|
|
1298
1358
|
this.#rememberBreadcrumb(this.#cwd, resolvedSessionFile);
|
|
1299
1359
|
|
|
1300
|
-
const
|
|
1301
|
-
const
|
|
1360
|
+
const loaded = loadedSession ?? (await loadSessionFile(resolvedSessionFile, this.#storage));
|
|
1361
|
+
const { entries: fileEntries, titleSlot } = loaded;
|
|
1302
1362
|
if (fileEntries.length === 0) {
|
|
1303
1363
|
// Explicit but empty/missing path (e.g. --session flag): start fresh but
|
|
1304
1364
|
// keep the requested path and materialize the header immediately.
|
|
@@ -1332,7 +1392,7 @@ export class SessionManager {
|
|
|
1332
1392
|
this.#titleUpdatedAt = titleSlot?.updatedAt ?? header.timestamp;
|
|
1333
1393
|
this.#hasTitleSlot = titleSlot !== undefined;
|
|
1334
1394
|
this.#fileIsCurrent = true;
|
|
1335
|
-
this.#rewriteRequired = migrated;
|
|
1395
|
+
this.#rewriteRequired = migrated || loaded.malformedRecords > 0;
|
|
1336
1396
|
this.#forceFileCreation = true;
|
|
1337
1397
|
this.#artifactManager = null;
|
|
1338
1398
|
this.#artifactManagerSessionFile = null;
|
|
@@ -1440,10 +1500,13 @@ export class SessionManager {
|
|
|
1440
1500
|
|
|
1441
1501
|
const oldSessionFile = this.#sessionFile;
|
|
1442
1502
|
const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
|
|
1443
|
-
const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)
|
|
1444
|
-
const newArtifactsDir = artifactsDirectoryFor(newSessionFile)
|
|
1503
|
+
const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile);
|
|
1504
|
+
const newArtifactsDir = artifactsDirectoryFor(newSessionFile);
|
|
1445
1505
|
const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
|
|
1446
|
-
const artifactPathChanged =
|
|
1506
|
+
const artifactPathChanged =
|
|
1507
|
+
oldArtifactsDir !== null &&
|
|
1508
|
+
newArtifactsDir !== null &&
|
|
1509
|
+
path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
|
|
1447
1510
|
sessionFileExisted = this.#storage.existsSync(oldSessionFile);
|
|
1448
1511
|
|
|
1449
1512
|
let sessionMoved = false;
|
|
@@ -1467,7 +1530,7 @@ export class SessionManager {
|
|
|
1467
1530
|
}
|
|
1468
1531
|
}
|
|
1469
1532
|
} catch (err) {
|
|
1470
|
-
if (artifactsMoved) {
|
|
1533
|
+
if (artifactsMoved && oldArtifactsDir && newArtifactsDir) {
|
|
1471
1534
|
try {
|
|
1472
1535
|
await fs.promises.rename(newArtifactsDir, oldArtifactsDir);
|
|
1473
1536
|
} catch (rollbackErr) {
|
|
@@ -1985,6 +2048,14 @@ export class SessionManager {
|
|
|
1985
2048
|
};
|
|
1986
2049
|
}
|
|
1987
2050
|
|
|
2051
|
+
/** Subscribe to persistence failures so hosts can surface lost-durability state. */
|
|
2052
|
+
onPersistenceError(cb: (error: Error) => void): () => void {
|
|
2053
|
+
this.#persistenceErrorCallbacks.add(cb);
|
|
2054
|
+
return () => {
|
|
2055
|
+
this.#persistenceErrorCallbacks.delete(cb);
|
|
2056
|
+
};
|
|
2057
|
+
}
|
|
2058
|
+
|
|
1988
2059
|
/**
|
|
1989
2060
|
* Set the session display name.
|
|
1990
2061
|
* @param source "user" for explicit renames; "auto" for generated titles.
|
|
@@ -2394,6 +2465,37 @@ export class SessionManager {
|
|
|
2394
2465
|
this.#setLeaf(null);
|
|
2395
2466
|
}
|
|
2396
2467
|
|
|
2468
|
+
/**
|
|
2469
|
+
* Durably move the active branch past a discarded entry.
|
|
2470
|
+
*
|
|
2471
|
+
* The loader reconstructs the active branch from the last physical journal
|
|
2472
|
+
* entry, so changing the in-memory leaf alone is lost on reload. Known
|
|
2473
|
+
* metadata children are chained onto the discarded entry's parent before the
|
|
2474
|
+
* entry is removed. If any child may carry content, the subtree is preserved
|
|
2475
|
+
* off-branch instead. Both paths append a metadata-only branch marker and
|
|
2476
|
+
* rewrite the journal, making the selected path durable.
|
|
2477
|
+
*/
|
|
2478
|
+
async discardEntryDurably(entryId: string): Promise<void> {
|
|
2479
|
+
const entry = this.#index.get(entryId);
|
|
2480
|
+
if (!entry) return;
|
|
2481
|
+
const children = this.#index.childrenOf(entryId);
|
|
2482
|
+
const canReparentChildren = children.every(child => child.type === "service_tier_change");
|
|
2483
|
+
let leafId = entry.parentId;
|
|
2484
|
+
if (canReparentChildren) {
|
|
2485
|
+
for (const child of children) {
|
|
2486
|
+
child.parentId = leafId;
|
|
2487
|
+
leafId = child.id;
|
|
2488
|
+
}
|
|
2489
|
+
this.#entries = this.#entries.filter(candidate => candidate.id !== entryId);
|
|
2490
|
+
this.#index.rebuild(this.#entries);
|
|
2491
|
+
}
|
|
2492
|
+
this.branchWithSummary(leafId, "", {
|
|
2493
|
+
kind: DISCARDED_ENTRY_BRANCH_MARKER,
|
|
2494
|
+
discardedEntryId: entryId,
|
|
2495
|
+
});
|
|
2496
|
+
await this.rewriteEntries();
|
|
2497
|
+
}
|
|
2498
|
+
|
|
2397
2499
|
/** Like branch(), but also records a branch_summary of the abandoned path. */
|
|
2398
2500
|
branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string {
|
|
2399
2501
|
if (branchFromId !== null && !this.#index.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
|
|
@@ -2533,16 +2635,16 @@ export class SessionManager {
|
|
|
2533
2635
|
* session file while creating a fresh session file in this sessionDir.
|
|
2534
2636
|
*
|
|
2535
2637
|
* `options.sessionFile` pins the new session's file path (default: an
|
|
2536
|
-
* auto-named `<timestamp>_<id>.jsonl` in `sessionDir`).
|
|
2537
|
-
*
|
|
2538
|
-
*
|
|
2638
|
+
* auto-named `<timestamp>_<id>.jsonl` in `sessionDir`). Artifacts are copied
|
|
2639
|
+
* recursively by default; nested agents that deliberately share their parent's
|
|
2640
|
+
* artifact root may disable this with `copyArtifacts: false`.
|
|
2539
2641
|
*/
|
|
2540
2642
|
static async forkFrom(
|
|
2541
2643
|
sourcePath: string,
|
|
2542
2644
|
cwd: string,
|
|
2543
2645
|
sessionDir?: string,
|
|
2544
2646
|
storage: SessionStorage = new FileSessionStorage(),
|
|
2545
|
-
options?: { suppressBreadcrumb?: boolean; sessionFile?: string },
|
|
2647
|
+
options?: { copyArtifacts?: boolean; suppressBreadcrumb?: boolean; sessionFile?: string },
|
|
2546
2648
|
): Promise<SessionManager> {
|
|
2547
2649
|
const dir = sessionDir ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
2548
2650
|
const manager = new SessionManager(cwd, dir, true, storage);
|
|
@@ -2575,6 +2677,9 @@ export class SessionManager {
|
|
|
2575
2677
|
manager.sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
2576
2678
|
manager.#forceFileCreation = true;
|
|
2577
2679
|
await manager.#rewriteAtomically();
|
|
2680
|
+
if (options?.copyArtifacts !== false) {
|
|
2681
|
+
await copySessionArtifacts(sourcePath, manager.#sessionFile!);
|
|
2682
|
+
}
|
|
2578
2683
|
return manager;
|
|
2579
2684
|
}
|
|
2580
2685
|
|
|
@@ -2589,8 +2694,8 @@ export class SessionManager {
|
|
|
2589
2694
|
storage: SessionStorage = new FileSessionStorage(),
|
|
2590
2695
|
options?: { initialCwd?: string; suppressBreadcrumb?: boolean },
|
|
2591
2696
|
): Promise<SessionManager> {
|
|
2592
|
-
const loaded = await
|
|
2593
|
-
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
2697
|
+
const loaded = await loadSessionFile(filePath, storage);
|
|
2698
|
+
const header = loaded.entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
2594
2699
|
// Resume into the session's recorded cwd only when that directory still
|
|
2595
2700
|
// exists. A deleted project dir would make the constructor's #cwd — and the
|
|
2596
2701
|
// `setProjectDir` chdir interactive mode runs next — point at (and fail on)
|
|
@@ -2638,15 +2743,7 @@ export class SessionManager {
|
|
|
2638
2743
|
advisor?: string;
|
|
2639
2744
|
} | null;
|
|
2640
2745
|
} | null> {
|
|
2641
|
-
let
|
|
2642
|
-
try {
|
|
2643
|
-
loaded = await loadEntriesFromFile(filePath, storage);
|
|
2644
|
-
} catch {
|
|
2645
|
-
return null;
|
|
2646
|
-
}
|
|
2647
|
-
// A missing/empty file has no usable session — nothing to revive from.
|
|
2648
|
-
if (loaded.length === 0) return null;
|
|
2649
|
-
const header = loaded.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
2746
|
+
let header: SessionHeader | undefined;
|
|
2650
2747
|
let init: {
|
|
2651
2748
|
systemPrompt: string;
|
|
2652
2749
|
task: string;
|
|
@@ -2661,8 +2758,11 @@ export class SessionManager {
|
|
|
2661
2758
|
readSummarize?: boolean;
|
|
2662
2759
|
advisor?: string;
|
|
2663
2760
|
} | null = null;
|
|
2664
|
-
|
|
2665
|
-
|
|
2761
|
+
const visit = (entry: FileEntry): void => {
|
|
2762
|
+
if (entry.type === "session") {
|
|
2763
|
+
header ??= entry;
|
|
2764
|
+
return;
|
|
2765
|
+
}
|
|
2666
2766
|
if (entry.type === "session_init") {
|
|
2667
2767
|
init = {
|
|
2668
2768
|
systemPrompt: entry.systemPrompt,
|
|
@@ -2678,10 +2778,17 @@ export class SessionManager {
|
|
|
2678
2778
|
spawns: entry.spawns,
|
|
2679
2779
|
advisor: entry.advisor,
|
|
2680
2780
|
};
|
|
2681
|
-
break;
|
|
2682
2781
|
}
|
|
2782
|
+
};
|
|
2783
|
+
|
|
2784
|
+
try {
|
|
2785
|
+
await visitEntriesFromFile(filePath, visit, storage);
|
|
2786
|
+
} catch {
|
|
2787
|
+
return null;
|
|
2683
2788
|
}
|
|
2684
|
-
|
|
2789
|
+
// A missing, empty, or invalid file has no usable session.
|
|
2790
|
+
if (!header) return null;
|
|
2791
|
+
return { cwd: header.cwd ?? getProjectDir(), init };
|
|
2685
2792
|
}
|
|
2686
2793
|
|
|
2687
2794
|
/** Continue the most recent session, or create a new one if none exists. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isAnthropicServerToolHistoryBlock } from "@oh-my-pi/pi-ai/providers/anthropic-wire";
|
|
2
2
|
import {
|
|
3
3
|
type BlobStore,
|
|
4
4
|
externalizeImageDataSync,
|
|
@@ -100,8 +100,8 @@ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string
|
|
|
100
100
|
// Persist signed blocks verbatim — never truncate, externalize, or descend.
|
|
101
101
|
// Unsigned blocks (e.g. an interrupted stream) have no such binding and stay
|
|
102
102
|
// truncatable for size control.
|
|
103
|
-
// Anthropic validates native web-search history byte-for-byte
|
|
104
|
-
// Keep the complete typed block atomic, including
|
|
103
|
+
// Anthropic validates native web-search and tool-search history byte-for-byte
|
|
104
|
+
// on replay. Keep the complete typed block atomic, including opaque content.
|
|
105
105
|
if (typeof obj === "object" && "type" in obj && obj.type === "anthropicServerTool" && "block" in obj) {
|
|
106
106
|
const block = obj.block;
|
|
107
107
|
if (typeof block === "object" && block !== null && "type" in block && typeof block.type === "string") {
|
|
@@ -112,7 +112,7 @@ function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?: string
|
|
|
112
112
|
...("tool_use_id" in block ? { tool_use_id: block.tool_use_id } : {}),
|
|
113
113
|
...("content" in block ? { content: block.content } : {}),
|
|
114
114
|
};
|
|
115
|
-
if (
|
|
115
|
+
if (isAnthropicServerToolHistoryBlock(validationView)) return obj;
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
if (typeof obj === "object" && "type" in obj) {
|
|
@@ -128,14 +128,27 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
128
128
|
}
|
|
129
129
|
|
|
130
130
|
#writeNow(line: string): void {
|
|
131
|
+
const originalSize = fs.fstatSync(this.#fd).size;
|
|
131
132
|
const buf = Buffer.from(line, "utf-8");
|
|
132
133
|
let offset = 0;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
try {
|
|
135
|
+
while (offset < buf.length) {
|
|
136
|
+
const written = fs.writeSync(this.#fd, buf, offset, buf.length - offset);
|
|
137
|
+
if (written === 0) {
|
|
138
|
+
throw new Error("Short write");
|
|
139
|
+
}
|
|
140
|
+
offset += written;
|
|
141
|
+
}
|
|
142
|
+
} catch (writeError) {
|
|
143
|
+
try {
|
|
144
|
+
fs.ftruncateSync(this.#fd, originalSize);
|
|
145
|
+
} catch (rollbackError) {
|
|
146
|
+
throw new AggregateError(
|
|
147
|
+
[toError(writeError), toError(rollbackError)],
|
|
148
|
+
"Session append failed and its partial bytes could not be rolled back",
|
|
149
|
+
);
|
|
137
150
|
}
|
|
138
|
-
|
|
151
|
+
throw writeError;
|
|
139
152
|
}
|
|
140
153
|
}
|
|
141
154
|
|
|
@@ -26,7 +26,6 @@ import { ToolAbortError, ToolError } from "../tools/tool-errors";
|
|
|
26
26
|
import { isMountableUnderXdev, listXdevTools, type XdevState, xdevDocsFor, xdevEntries } from "../tools/xdev";
|
|
27
27
|
import { type EditMode, resolveEditMode } from "../utils/edit-mode";
|
|
28
28
|
import { type InspectImageMode, isInspectImageToolActive } from "../utils/inspect-image-mode";
|
|
29
|
-
import { formatLocalCalendarDate } from "../utils/local-date";
|
|
30
29
|
import {
|
|
31
30
|
extractPermissionLocations,
|
|
32
31
|
getPermissionIntent,
|
|
@@ -82,7 +81,6 @@ interface SessionToolsOptions {
|
|
|
82
81
|
toolNames: string[],
|
|
83
82
|
tools: Map<string, AgentTool>,
|
|
84
83
|
) => Promise<{ systemPrompt: string[]; xdevCatalogNames?: readonly string[] }>;
|
|
85
|
-
getLocalCalendarDate?: () => string;
|
|
86
84
|
getMcpServerInstructions?: () => Map<string, string> | undefined;
|
|
87
85
|
xdev?: XdevState;
|
|
88
86
|
setActiveToolNames?: (names: Iterable<string>) => void;
|
|
@@ -229,7 +227,6 @@ export class SessionTools {
|
|
|
229
227
|
#toolRegistryMutationTail: Promise<void> = Promise.resolve();
|
|
230
228
|
#promptModelKey: string | undefined;
|
|
231
229
|
#rebuildSystemPrompt: SessionToolsOptions["rebuildSystemPrompt"];
|
|
232
|
-
#getLocalCalendarDate: () => string;
|
|
233
230
|
#getMcpServerInstructions: SessionToolsOptions["getMcpServerInstructions"];
|
|
234
231
|
#setActiveToolNames: SessionToolsOptions["setActiveToolNames"];
|
|
235
232
|
#ensureWriteRegistered: SessionToolsOptions["ensureWriteRegistered"];
|
|
@@ -262,7 +259,6 @@ export class SessionTools {
|
|
|
262
259
|
this.#presentationPinnedToolNames = options.presentationPinnedToolNames;
|
|
263
260
|
this.#ensureWriteRegistered = options.ensureWriteRegistered;
|
|
264
261
|
this.#rebuildSystemPrompt = options.rebuildSystemPrompt;
|
|
265
|
-
this.#getLocalCalendarDate = options.getLocalCalendarDate ?? formatLocalCalendarDate;
|
|
266
262
|
this.#getMcpServerInstructions = options.getMcpServerInstructions;
|
|
267
263
|
this.#xdev = options.xdev;
|
|
268
264
|
if (this.#xdev && this.#xdev.tools !== this.#toolRegistry) {
|
|
@@ -1434,10 +1430,10 @@ export class SessionTools {
|
|
|
1434
1430
|
* For everything else, callers must explicitly call {@link refreshBaseSystemPrompt}
|
|
1435
1431
|
* after side-effecting changes; see the memory hooks and {@link syncAfterModelChange}.
|
|
1436
1432
|
*
|
|
1437
|
-
* The
|
|
1438
|
-
*
|
|
1439
|
-
*
|
|
1440
|
-
*
|
|
1433
|
+
* The calendar date is deliberately NOT part of the signature: the date/cwd
|
|
1434
|
+
* reminder rides on the first user turn at request time (`date-cwd-reminder`),
|
|
1435
|
+
* so a session spanning midnight must NOT rebuild a prompt that no longer
|
|
1436
|
+
* embeds the date — the reminder picks up the new day on its own.
|
|
1441
1437
|
*/
|
|
1442
1438
|
#computeAppliedToolSignature(toolNames: string[], tools: AgentTool[]): string {
|
|
1443
1439
|
// Order-preserving join: any reorder must produce a different signature so
|
|
@@ -1471,8 +1467,7 @@ export class SessionTools {
|
|
|
1471
1467
|
// the provider cache prefix byte-stable. Mounted MCP routes are the narrow
|
|
1472
1468
|
// exception above, bounded to the exact projection rendered in the global
|
|
1473
1469
|
// route guidance so churn wholly behind its fallback does not rebuild.
|
|
1474
|
-
|
|
1475
|
-
return `${nameSegment}\u0003${descriptionSegment}\u0007${instructionsSegment}\u0008${mountedMCPRouteSegment}|${date}`;
|
|
1470
|
+
return `${nameSegment}\u0003${descriptionSegment}\u0007${instructionsSegment}\u0008${mountedMCPRouteSegment}`;
|
|
1476
1471
|
}
|
|
1477
1472
|
|
|
1478
1473
|
/**
|