@oh-my-pi/pi-coding-agent 17.2.2 → 17.2.4
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 +55 -0
- package/dist/{CHANGELOG-xfpkakrn.md → CHANGELOG-bpmhv26t.md} +55 -0
- package/dist/cli.js +3294 -3294
- package/dist/types/advisor/advise-tool.d.ts +6 -0
- package/dist/types/advisor/runtime.d.ts +4 -4
- package/dist/types/capability/mcp.d.ts +9 -0
- package/dist/types/cli/models-cli.d.ts +1 -1
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +6 -0
- package/dist/types/discovery/omp-extension-roots.d.ts +21 -2
- package/dist/types/extensibility/extensions/loader.d.ts +6 -2
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +2 -2
- package/dist/types/launch/paths.d.ts +2 -1
- package/dist/types/mcp/request-id.d.ts +19 -0
- package/dist/types/mcp/types.d.ts +14 -0
- package/dist/types/memory-backend/index.d.ts +1 -0
- package/dist/types/memory-backend/messages.d.ts +15 -0
- package/dist/types/modes/components/ask-dialog.d.ts +0 -1
- package/dist/types/modes/components/model-browser.d.ts +7 -5
- package/dist/types/modes/components/model-picker.d.ts +9 -3
- package/dist/types/modes/components/read-tool-group.d.ts +9 -0
- package/dist/types/modes/types.d.ts +1 -1
- package/dist/types/registry/agent-lifecycle.d.ts +14 -5
- package/dist/types/registry/agent-registry.d.ts +4 -3
- package/dist/types/sdk.d.ts +1 -1
- package/dist/types/secrets/index.d.ts +6 -8
- package/dist/types/session/agent-session.d.ts +6 -1
- package/dist/types/session/model-controls.d.ts +0 -1
- package/dist/types/session/session-advisors.d.ts +7 -0
- package/dist/types/session/session-maintenance.d.ts +1 -0
- package/dist/types/session/session-manager.d.ts +11 -5
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tools/browser/launch.d.ts +30 -0
- package/dist/types/tools/browser/registry.d.ts +6 -1
- package/dist/types/tools/browser/shared-daemon.d.ts +26 -0
- package/dist/types/tools/default-renderer.d.ts +3 -0
- package/package.json +12 -12
- package/scripts/legacy-pi-virtual-module.ts +12 -2
- package/src/advisor/advise-tool.ts +18 -0
- package/src/advisor/runtime.ts +6 -6
- package/src/capability/mcp.ts +7 -0
- package/src/cli/models-cli.ts +13 -16
- package/src/collab/host.ts +1 -1
- package/src/config/keybindings.ts +14 -9
- package/src/config/mcp-schema.json +5 -0
- package/src/config/model-registry.ts +7 -1
- package/src/config/settings-schema.ts +1 -1
- package/src/discovery/builtin.ts +10 -0
- package/src/discovery/helpers.ts +10 -0
- package/src/discovery/mcp-json.ts +11 -1
- package/src/discovery/omp-extension-roots.ts +79 -20
- package/src/discovery/omp-plugins.ts +10 -1
- package/src/extensibility/extensions/loader.ts +66 -28
- package/src/extensibility/plugins/marketplace/registry.ts +4 -8
- package/src/launch/broker.ts +59 -41
- package/src/launch/paths.ts +2 -5
- package/src/main.ts +24 -22
- package/src/mcp/config.ts +1 -0
- package/src/mcp/request-id.ts +24 -0
- package/src/mcp/transports/http.ts +4 -2
- package/src/mcp/transports/sse.ts +4 -2
- package/src/mcp/transports/stdio.ts +4 -2
- package/src/mcp/types.ts +15 -0
- package/src/memory-backend/index.ts +1 -0
- package/src/memory-backend/messages.ts +19 -0
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/ask-dialog.ts +65 -23
- package/src/modes/components/custom-editor.ts +1 -1
- package/src/modes/components/model-browser.ts +25 -17
- package/src/modes/components/model-picker.ts +10 -6
- package/src/modes/components/read-tool-group.ts +26 -0
- package/src/modes/components/tool-execution.ts +77 -20
- package/src/modes/controllers/command-controller.ts +2 -2
- package/src/modes/controllers/event-controller.ts +182 -10
- package/src/modes/controllers/selector-controller.ts +27 -6
- package/src/modes/types.ts +5 -1
- package/src/prompts/tools/bash.md +1 -1
- package/src/registry/agent-lifecycle.ts +36 -11
- package/src/registry/agent-registry.ts +12 -5
- package/src/sdk.ts +68 -29
- package/src/secrets/index.ts +15 -17
- package/src/session/agent-session.ts +11 -2
- package/src/session/agent-storage.ts +6 -4
- package/src/session/history-storage.ts +4 -2
- package/src/session/messages.ts +31 -7
- package/src/session/model-controls.ts +0 -1
- package/src/session/session-advisors.ts +19 -1
- package/src/session/session-maintenance.ts +5 -2
- package/src/session/session-manager.ts +128 -76
- package/src/session/session-tools.ts +25 -23
- package/src/slash-commands/builtin-registry.ts +2 -2
- package/src/system-prompt.ts +6 -1
- package/src/task/executor.ts +8 -1
- package/src/tiny/text.ts +12 -0
- package/src/tools/bash.ts +11 -5
- package/src/tools/browser/launch.ts +54 -9
- package/src/tools/browser/registry.ts +123 -30
- package/src/tools/browser/shared-daemon.ts +190 -0
- package/src/tools/browser.ts +1 -1
- package/src/tools/default-renderer.ts +19 -4
- package/src/tools/index.ts +10 -6
- package/src/web/search/providers/anthropic.ts +3 -1
- package/src/web/search/providers/codex.ts +31 -6
|
@@ -405,11 +405,17 @@ export class SessionPersistenceIndeterminateError extends AggregateError {
|
|
|
405
405
|
* tree by `(id, parentId)`, and the mutable leaf pointer selects which path is
|
|
406
406
|
* active for future appends and for LLM context construction.
|
|
407
407
|
*
|
|
408
|
-
* Durability is software-crash safe but not power-loss safe: appends are
|
|
409
|
-
* to the OS synchronously in-body
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
*
|
|
408
|
+
* Durability is software-crash safe but not power-loss safe: appends are normally
|
|
409
|
+
* handed to the OS synchronously in-body but never `fsync`'d. While an in-place
|
|
410
|
+
* atomic rewrite publishes, a synchronous `flushSync` still persists fenced
|
|
411
|
+
* appends by superseding the pending publish against the same (stable) path.
|
|
412
|
+
* A {@link moveTo} relocation is the one exception: the session file is being
|
|
413
|
+
* renamed to a new path, so appends landing in the rename window are held in
|
|
414
|
+
* memory and persisted only by the move's trailing rewrite. A `flushSync` in
|
|
415
|
+
* that narrow window cannot durably place them — writing the vacated source
|
|
416
|
+
* would recreate the orphan the move eliminates, and a queued backend publish
|
|
417
|
+
* (fire-and-forget on `IndexedSessionStorage`) could land after the rename all
|
|
418
|
+
* the same. This gap is inherent to closing the writer for a Windows-safe rename.
|
|
413
419
|
*/
|
|
414
420
|
export class SessionManager {
|
|
415
421
|
#cwd: string;
|
|
@@ -477,6 +483,16 @@ export class SessionManager {
|
|
|
477
483
|
#atomicRewriteFenceEpoch: number | null = null;
|
|
478
484
|
/** Set by synchronous appends that land while an atomic replacement is active. */
|
|
479
485
|
#atomicRewriteDirty = false;
|
|
486
|
+
/**
|
|
487
|
+
* True while {@link moveTo} is renaming the session file but has not yet
|
|
488
|
+
* repointed `#sessionFile`. Both the synchronous append hot path
|
|
489
|
+
* ({@link #appendToSessionFile}) and synchronous rewrites
|
|
490
|
+
* ({@link #rewriteSynchronously}, reached via `flushSync` on Ctrl+C) would
|
|
491
|
+
* otherwise touch the pre-rename path — opening a fresh writer or writing the
|
|
492
|
+
* full body — recreating the very orphan the move eliminates. Both defer
|
|
493
|
+
* while this is set; the move's trailing atomic rewrite persists them.
|
|
494
|
+
*/
|
|
495
|
+
#sessionFileRelocating = false;
|
|
480
496
|
/** Atomic entry batch currently staged for a full-file commit. */
|
|
481
497
|
#atomicEntryBatch: AtomicEntryBatch | undefined;
|
|
482
498
|
|
|
@@ -754,6 +770,12 @@ export class SessionManager {
|
|
|
754
770
|
*/
|
|
755
771
|
#rewriteSynchronously(): void {
|
|
756
772
|
if (!this.#persist || !this.#sessionFile || !this.#shouldHaveSessionFile()) return;
|
|
773
|
+
// A move is renaming the file out from under `#sessionFile`. Writing the
|
|
774
|
+
// full body now would target either the vacated source (recreating the
|
|
775
|
+
// orphan the move eliminates) or a path a queued backend publish may clobber
|
|
776
|
+
// post-rename, so defer to moveTo's trailing atomic rewrite. This narrows
|
|
777
|
+
// flushSync durability for entries in the rename window (see class doc).
|
|
778
|
+
if (this.#sessionFileRelocating) return;
|
|
757
779
|
|
|
758
780
|
try {
|
|
759
781
|
const body = this.#fileBody();
|
|
@@ -851,13 +873,18 @@ export class SessionManager {
|
|
|
851
873
|
}
|
|
852
874
|
|
|
853
875
|
// Atomic replacement window: the old path may be moved aside underneath
|
|
854
|
-
// any newly-opened append handle (Windows EPERM fallback)
|
|
855
|
-
//
|
|
876
|
+
// any newly-opened append handle (Windows EPERM fallback), or a `moveTo`
|
|
877
|
+
// may be renaming the session file to a new directory. Do not open a
|
|
878
|
+
// writer here; the active rewrite loops and serializes a fresh full body,
|
|
879
|
+
// and the move's trailing atomic rewrite folds in these deferred entries.
|
|
856
880
|
// A superseding synchronous rewrite bumps `#diskEpoch`, at which point
|
|
857
881
|
// the pending atomic publish is guaranteed to abandon via its
|
|
858
882
|
// `commitGuard`, so appends can (and must) take the hot path so they
|
|
859
883
|
// don't strand in memory while `close()` returns without a rewrite.
|
|
860
|
-
if (
|
|
884
|
+
if (
|
|
885
|
+
this.#sessionFileRelocating ||
|
|
886
|
+
(this.#atomicRewriteFenceEpoch !== null && this.#atomicRewriteFenceEpoch === this.#diskEpoch)
|
|
887
|
+
) {
|
|
861
888
|
this.#fileIsCurrent = false;
|
|
862
889
|
this.#rewriteRequired = true;
|
|
863
890
|
this.#atomicRewriteDirty = true;
|
|
@@ -893,6 +920,16 @@ export class SessionManager {
|
|
|
893
920
|
return;
|
|
894
921
|
}
|
|
895
922
|
|
|
923
|
+
// Title changes use their own asynchronous append path rather than
|
|
924
|
+
// #appendToSessionFile. Defer them under the same move fence so they cannot
|
|
925
|
+
// recreate the vacated source path before #sessionFile is repointed.
|
|
926
|
+
if (this.#sessionFileRelocating) {
|
|
927
|
+
this.#fileIsCurrent = false;
|
|
928
|
+
this.#rewriteRequired = true;
|
|
929
|
+
this.#atomicRewriteDirty = true;
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
|
|
896
933
|
if (
|
|
897
934
|
!this.#fileIsCurrent ||
|
|
898
935
|
this.#rewriteRequired ||
|
|
@@ -1325,90 +1362,105 @@ export class SessionManager {
|
|
|
1325
1362
|
: computeDefaultSessionDir(resolvedCwd, this.#storage));
|
|
1326
1363
|
|
|
1327
1364
|
let sessionFileExisted = false;
|
|
1328
|
-
|
|
1365
|
+
// Fence synchronous appends and rewrites away from the session file for the
|
|
1366
|
+
// duration of the relocation (see `#sessionFileRelocating`). This flag —
|
|
1367
|
+
// not a `#diskEpoch` bump — is used deliberately: bumping the epoch here
|
|
1368
|
+
// would cancel any disk task already queued at the current epoch (e.g. a
|
|
1369
|
+
// header-only `ensureOnDisk()` materializing rewrite) before the drain
|
|
1370
|
+
// below runs it, silently dropping an explicitly materialized session.
|
|
1329
1371
|
if (this.#persist && this.#sessionFile) {
|
|
1330
|
-
this.#
|
|
1331
|
-
|
|
1332
|
-
this.#clearDiskError();
|
|
1333
|
-
|
|
1334
|
-
const oldSessionFile = this.#sessionFile;
|
|
1335
|
-
const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
|
|
1336
|
-
const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)!;
|
|
1337
|
-
const newArtifactsDir = artifactsDirectoryFor(newSessionFile)!;
|
|
1338
|
-
const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
|
|
1339
|
-
const artifactPathChanged = path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
|
|
1340
|
-
sessionFileExisted = this.#storage.existsSync(oldSessionFile);
|
|
1372
|
+
this.#sessionFileRelocating = true;
|
|
1373
|
+
}
|
|
1341
1374
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1375
|
+
try {
|
|
1376
|
+
if (this.#persist && this.#sessionFile) {
|
|
1377
|
+
this.#storage.ensureDirSync(nextSessionDir);
|
|
1378
|
+
await this.#drainAndCloseWriter();
|
|
1379
|
+
this.#clearDiskError();
|
|
1380
|
+
|
|
1381
|
+
const oldSessionFile = this.#sessionFile;
|
|
1382
|
+
const newSessionFile = path.join(nextSessionDir, path.basename(oldSessionFile));
|
|
1383
|
+
const oldArtifactsDir = artifactsDirectoryFor(oldSessionFile)!;
|
|
1384
|
+
const newArtifactsDir = artifactsDirectoryFor(newSessionFile)!;
|
|
1385
|
+
const sessionPathChanged = path.resolve(oldSessionFile) !== path.resolve(newSessionFile);
|
|
1386
|
+
const artifactPathChanged = path.resolve(oldArtifactsDir) !== path.resolve(newArtifactsDir);
|
|
1387
|
+
sessionFileExisted = this.#storage.existsSync(oldSessionFile);
|
|
1388
|
+
|
|
1389
|
+
let sessionMoved = false;
|
|
1390
|
+
let artifactsMoved = false;
|
|
1344
1391
|
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1392
|
+
try {
|
|
1393
|
+
if (sessionFileExisted && sessionPathChanged) {
|
|
1394
|
+
await fs.promises.rename(oldSessionFile, newSessionFile);
|
|
1395
|
+
sessionMoved = true;
|
|
1396
|
+
}
|
|
1350
1397
|
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1398
|
+
if (artifactPathChanged) {
|
|
1399
|
+
try {
|
|
1400
|
+
const artifactStat = await fs.promises.stat(oldArtifactsDir);
|
|
1401
|
+
if (artifactStat.isDirectory()) {
|
|
1402
|
+
await fs.promises.rename(oldArtifactsDir, newArtifactsDir);
|
|
1403
|
+
artifactsMoved = true;
|
|
1404
|
+
}
|
|
1405
|
+
} catch (err) {
|
|
1406
|
+
if (!isEnoent(err)) throw err;
|
|
1357
1407
|
}
|
|
1358
|
-
} catch (err) {
|
|
1359
|
-
if (!isEnoent(err)) throw err;
|
|
1360
1408
|
}
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
if (artifactsMoved) {
|
|
1411
|
+
try {
|
|
1412
|
+
await fs.promises.rename(newArtifactsDir, oldArtifactsDir);
|
|
1413
|
+
} catch (rollbackErr) {
|
|
1414
|
+
throw new Error(
|
|
1415
|
+
`Failed to move artifacts and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1370
1418
|
}
|
|
1371
|
-
}
|
|
1372
1419
|
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1420
|
+
if (sessionMoved) {
|
|
1421
|
+
try {
|
|
1422
|
+
await fs.promises.rename(newSessionFile, oldSessionFile);
|
|
1423
|
+
} catch (rollbackErr) {
|
|
1424
|
+
throw new Error(
|
|
1425
|
+
`Failed to move session file and rollback: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`,
|
|
1426
|
+
);
|
|
1427
|
+
}
|
|
1380
1428
|
}
|
|
1429
|
+
|
|
1430
|
+
throw err;
|
|
1381
1431
|
}
|
|
1382
1432
|
|
|
1383
|
-
|
|
1433
|
+
this.#sessionFile = newSessionFile;
|
|
1434
|
+
this.#artifactManager = null;
|
|
1435
|
+
this.#artifactManagerSessionFile = null;
|
|
1436
|
+
// Path is repointed; a sync rewrite may now safely target the new file.
|
|
1437
|
+
this.#sessionFileRelocating = false;
|
|
1384
1438
|
}
|
|
1385
1439
|
|
|
1386
|
-
this.#
|
|
1387
|
-
this.#
|
|
1388
|
-
this.#
|
|
1389
|
-
|
|
1440
|
+
this.#cwd = resolvedCwd;
|
|
1441
|
+
this.#sessionDir = nextSessionDir;
|
|
1442
|
+
this.#header.cwd = resolvedCwd;
|
|
1443
|
+
// Re-filter additional roots: the new cwd may have been an additional root,
|
|
1444
|
+
// or it may now contain/subsume one. Re-normalize to keep the invariant
|
|
1445
|
+
// that cwd is never also listed as an additional directory.
|
|
1446
|
+
if (this.#additionalDirectories.length > 0) {
|
|
1447
|
+
this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
|
|
1448
|
+
this.#header.additionalDirectories =
|
|
1449
|
+
this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
|
|
1450
|
+
}
|
|
1390
1451
|
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
|
|
1399
|
-
this.#header.additionalDirectories =
|
|
1400
|
-
this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
|
|
1401
|
-
}
|
|
1452
|
+
// Rewrite at the new location when the file already existed (update cwd) or
|
|
1453
|
+
// there is in-memory output worth materializing; otherwise stay lazy.
|
|
1454
|
+
const hasAssistant = this.#historyContainsAssistantMessage();
|
|
1455
|
+
if (this.#persist && this.#sessionFile && (sessionFileExisted || hasAssistant)) {
|
|
1456
|
+
this.#forceFileCreation = true;
|
|
1457
|
+
await this.#rewriteAtomically();
|
|
1458
|
+
}
|
|
1402
1459
|
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
if (this.#persist && this.#sessionFile && (sessionFileExisted || hasAssistant)) {
|
|
1407
|
-
this.#forceFileCreation = true;
|
|
1408
|
-
await this.#rewriteAtomically();
|
|
1460
|
+
if (this.#sessionFile) this.#rememberBreadcrumb(resolvedCwd, this.#sessionFile);
|
|
1461
|
+
} finally {
|
|
1462
|
+
this.#sessionFileRelocating = false;
|
|
1409
1463
|
}
|
|
1410
|
-
|
|
1411
|
-
if (this.#sessionFile) this.#rememberBreadcrumb(resolvedCwd, this.#sessionFile);
|
|
1412
1464
|
}
|
|
1413
1465
|
|
|
1414
1466
|
/**
|
|
@@ -574,24 +574,28 @@ export class SessionTools {
|
|
|
574
574
|
/** Applies an enabled tool set and reconciles its `xd://` partition. */
|
|
575
575
|
async applyActiveToolsByName(toolNames: string[]): Promise<void> {
|
|
576
576
|
toolNames = normalizeToolNames(toolNames);
|
|
577
|
+
let builtInWriteAvailable = this.#builtInToolNames.has("write");
|
|
578
|
+
if (toolNames.includes("write") && !builtInWriteAvailable) {
|
|
579
|
+
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
580
|
+
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
581
|
+
}
|
|
577
582
|
const selectedTools = toolNames.flatMap(name => {
|
|
578
583
|
const tool = this.#toolRegistry.get(name);
|
|
579
584
|
return tool ? [{ name, tool }] : [];
|
|
580
585
|
});
|
|
581
586
|
const xdevReadAvailable = this.#builtInToolNames.has("read") && selectedTools.some(({ name }) => name === "read");
|
|
587
|
+
const xdevWriteAvailable = builtInWriteAvailable && selectedTools.some(({ name }) => name === "write");
|
|
582
588
|
const isPresentationPinned = (name: string): boolean =>
|
|
583
589
|
this.#presentationPinnedToolNames?.has(name) === true || this.#runtimeSelectedToolNames?.has(name) === true;
|
|
584
590
|
const mountCandidates = selectedTools.filter(
|
|
585
591
|
({ name, tool }) =>
|
|
586
|
-
this.#xdev !== undefined &&
|
|
592
|
+
this.#xdev !== undefined &&
|
|
593
|
+
xdevReadAvailable &&
|
|
594
|
+
xdevWriteAvailable &&
|
|
595
|
+
!isPresentationPinned(name) &&
|
|
596
|
+
isMountableUnderXdev(tool),
|
|
587
597
|
);
|
|
588
|
-
|
|
589
|
-
let builtInWriteAvailable = this.#builtInToolNames.has("write");
|
|
590
|
-
if (mountCandidates.length > 0 && !builtInWriteAvailable) {
|
|
591
|
-
builtInWriteAvailable = (await this.#ensureWriteRegistered?.()) === true;
|
|
592
|
-
if (builtInWriteAvailable) this.#builtInToolNames.add("write");
|
|
593
|
-
}
|
|
594
|
-
const mountNames = builtInWriteAvailable ? new Set(mountCandidates.map(({ name }) => name)) : new Set<string>();
|
|
598
|
+
const mountNames = new Set(mountCandidates.map(({ name }) => name));
|
|
595
599
|
const tools: AgentTool[] = [];
|
|
596
600
|
const validToolNames: string[] = [];
|
|
597
601
|
for (const { name, tool } of selectedTools) {
|
|
@@ -831,23 +835,21 @@ export class SessionTools {
|
|
|
831
835
|
|
|
832
836
|
/** Rediscovers reloadable skills and refreshes prompt metadata. */
|
|
833
837
|
async refreshSkills(): Promise<void> {
|
|
834
|
-
if (!this.#skillsReloadable) {
|
|
835
|
-
return;
|
|
836
|
-
}
|
|
837
|
-
|
|
838
838
|
resetCapabilities();
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
839
|
+
if (this.#skillsReloadable) {
|
|
840
|
+
const skillsSettings = this.#host.settings.getGroup("skills");
|
|
841
|
+
const discovered = await loadSkills({
|
|
842
|
+
...skillsSettings,
|
|
843
|
+
cwd: this.#host.sessionManager.getCwd(),
|
|
844
|
+
disabledExtensions: this.#host.settings.get("disabledExtensions") ?? [],
|
|
845
|
+
});
|
|
846
|
+
this.#skills = discovered.skills;
|
|
847
|
+
this.#skillWarnings = discovered.warnings;
|
|
848
|
+
this.#skillsSettings = skillsSettings;
|
|
848
849
|
|
|
849
|
-
|
|
850
|
-
|
|
850
|
+
if (this.#host.agentKind() === "main") {
|
|
851
|
+
setActiveSkills(this.#skills);
|
|
852
|
+
}
|
|
851
853
|
}
|
|
852
854
|
await this.refreshBaseSystemPrompt();
|
|
853
855
|
this.#host.notifyCommandMetadataChanged();
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
MarketplaceManager,
|
|
33
33
|
} from "../extensibility/plugins/marketplace";
|
|
34
34
|
import { readMCPConfigFile } from "../mcp/config-writer";
|
|
35
|
-
import { resolveMemoryBackend } from "../memory-backend";
|
|
35
|
+
import { memoryStatsUnavailableMessage, resolveMemoryBackend } from "../memory-backend";
|
|
36
36
|
import { runPauseScreen } from "../modes/components/pause-screen";
|
|
37
37
|
import { collectMcpServerNames, MCPCommandController } from "../modes/controllers/mcp-command-controller";
|
|
38
38
|
import { describeLoopLimitRuntime } from "../modes/loop-limit";
|
|
@@ -1947,7 +1947,7 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1947
1947
|
case "diagnose": {
|
|
1948
1948
|
const hook = verb === "stats" ? backend.stats : backend.diagnose;
|
|
1949
1949
|
const payload = await hook?.(runtime.settings.getAgentDir(), runtime.cwd, runtime.session);
|
|
1950
|
-
await runtime.output(payload ??
|
|
1950
|
+
await runtime.output(payload ?? memoryStatsUnavailableMessage(backend.id, verb));
|
|
1951
1951
|
return commandConsumed();
|
|
1952
1952
|
}
|
|
1953
1953
|
case "mm":
|
package/src/system-prompt.ts
CHANGED
|
@@ -332,6 +332,8 @@ export async function resolvePromptInput(input: string | undefined, description:
|
|
|
332
332
|
export interface LoadContextFilesOptions {
|
|
333
333
|
/** Working directory to start walking up from. Default: getProjectDir() */
|
|
334
334
|
cwd?: string;
|
|
335
|
+
/** Disabled extension IDs to honor instead of the process-global settings. */
|
|
336
|
+
disabledExtensions?: string[];
|
|
335
337
|
}
|
|
336
338
|
|
|
337
339
|
function dedupeExactContextFiles(
|
|
@@ -356,7 +358,10 @@ export async function loadProjectContextFiles(
|
|
|
356
358
|
): Promise<Array<{ path: string; content: string; depth?: number }>> {
|
|
357
359
|
const resolvedCwd = options.cwd ?? getProjectDir();
|
|
358
360
|
|
|
359
|
-
const result = await loadCapability(contextFileCapability.id, {
|
|
361
|
+
const result = await loadCapability(contextFileCapability.id, {
|
|
362
|
+
cwd: resolvedCwd,
|
|
363
|
+
disabledExtensions: options.disabledExtensions,
|
|
364
|
+
});
|
|
360
365
|
|
|
361
366
|
// Materialize ContextFile items, expanding any `@path/to/file` includes
|
|
362
367
|
// in their content. The expansion uses the file's own directory as the
|
package/src/task/executor.ts
CHANGED
|
@@ -2364,7 +2364,14 @@ export async function finalizeSubagentLifecycle(args: {
|
|
|
2364
2364
|
const resumableAbort =
|
|
2365
2365
|
args.abortKind === "budget" && args.keepAlive && !args.isolated && args.reviveSession !== null;
|
|
2366
2366
|
if (args.aborted && !resumableAbort) {
|
|
2367
|
-
if (ref && ownsRef)
|
|
2367
|
+
if (ref && ownsRef) {
|
|
2368
|
+
// Terminal hard kill: mark `aborted` and detach the session before
|
|
2369
|
+
// disposing so the ref satisfies the AgentRef invariant (session null
|
|
2370
|
+
// when aborted) — ensureLive/hub focus must treat it as terminal, never
|
|
2371
|
+
// route into the disposed session.
|
|
2372
|
+
registry.setStatus(args.id, "aborted", ref);
|
|
2373
|
+
registry.detachSession(args.id, ref);
|
|
2374
|
+
}
|
|
2368
2375
|
await disposeSession();
|
|
2369
2376
|
return;
|
|
2370
2377
|
}
|
package/src/tiny/text.ts
CHANGED
|
@@ -142,6 +142,17 @@ export function isLowSignalTitleInput(message: string): boolean {
|
|
|
142
142
|
*/
|
|
143
143
|
export const NO_TITLE_SENTINEL = "none";
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Upper bounds on an accepted title. Titling is a 3-7 word task, so any output
|
|
147
|
+
* past these limits is a model that ignored the task and answered the user's
|
|
148
|
+
* message instead — its whole reply must not become the session title (issue
|
|
149
|
+
* #7303). Rejecting (return null) lets the caller defer titling to the next
|
|
150
|
+
* user turn; truncating would keep half an assistant blob, which is still an
|
|
151
|
+
* assistant blob.
|
|
152
|
+
*/
|
|
153
|
+
const MAX_TITLE_CHARS = 80;
|
|
154
|
+
const MAX_TITLE_WORDS = 12;
|
|
155
|
+
|
|
145
156
|
export function normalizeGeneratedTitle(value: string | null | undefined, sourceText?: string): string | null {
|
|
146
157
|
const firstLine = value?.trim().split(/\r?\n/, 1)[0]?.trim();
|
|
147
158
|
if (!firstLine) return null;
|
|
@@ -154,6 +165,7 @@ export function normalizeGeneratedTitle(value: string | null | undefined, source
|
|
|
154
165
|
.replace(/[.!?]$/, "")
|
|
155
166
|
.trim();
|
|
156
167
|
if (!title || title.toLowerCase() === NO_TITLE_SENTINEL) return null;
|
|
168
|
+
if (title.length > MAX_TITLE_CHARS || (title.match(TITLE_WORD)?.length ?? 0) > MAX_TITLE_WORDS) return null;
|
|
157
169
|
return sourceText === undefined ? title : reconcileTitleCasing(title, sourceText);
|
|
158
170
|
}
|
|
159
171
|
|
package/src/tools/bash.ts
CHANGED
|
@@ -843,13 +843,18 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
843
843
|
return { kind: "steer" };
|
|
844
844
|
}
|
|
845
845
|
|
|
846
|
+
// Cancellable threshold: a bare Bun.sleep(thresholdMs) leaves a live, ref'd
|
|
847
|
+
// timer for the full threshold after the command finishes (or abort/steer)
|
|
848
|
+
// wins the race first — delaying SDK/headless shutdown and accumulating
|
|
849
|
+
// timers under fast command rates. Settle a withResolvers promise from
|
|
850
|
+
// setTimeout so the finally can clear it regardless of which waiter wins.
|
|
851
|
+
const { promise: thresholdPromise, resolve: resolveThreshold } = Promise.withResolvers<{
|
|
852
|
+
kind: "running";
|
|
853
|
+
}>();
|
|
854
|
+
const thresholdTimer = setTimeout(() => resolveThreshold({ kind: "running" }), thresholdMs);
|
|
846
855
|
const waiters: Array<
|
|
847
856
|
Promise<ManagedBashJobCompletion | { kind: "running" } | { kind: "steer" } | { kind: "aborted" }>
|
|
848
|
-
> = [job.completion,
|
|
849
|
-
|
|
850
|
-
if (!signal && !steeringSignal) {
|
|
851
|
-
return await Promise.race(waiters);
|
|
852
|
-
}
|
|
857
|
+
> = [job.completion, thresholdPromise];
|
|
853
858
|
|
|
854
859
|
const { promise: abortedPromise, resolve: resolveAborted } = Promise.withResolvers<{ kind: "aborted" }>();
|
|
855
860
|
const onAbort = () => resolveAborted({ kind: "aborted" });
|
|
@@ -866,6 +871,7 @@ export class BashTool implements AgentTool<typeof bashSchemaBase | typeof bashSc
|
|
|
866
871
|
try {
|
|
867
872
|
return await Promise.race(waiters);
|
|
868
873
|
} finally {
|
|
874
|
+
clearTimeout(thresholdTimer);
|
|
869
875
|
signal?.removeEventListener("abort", onAbort);
|
|
870
876
|
steeringSignal?.removeEventListener("abort", onSteer);
|
|
871
877
|
}
|
|
@@ -296,19 +296,17 @@ export interface LaunchHeadlessResult {
|
|
|
296
296
|
userDataDir?: string;
|
|
297
297
|
}
|
|
298
298
|
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
};
|
|
306
|
-
const puppeteer = await loadPuppeteer();
|
|
299
|
+
/**
|
|
300
|
+
* Base Chromium argv shared by process-local puppeteer launches and the
|
|
301
|
+
* broker-owned shared browser: sandbox/stealth flags, window size, and
|
|
302
|
+
* PUPPETEER_PROXY* env-derived proxy flags.
|
|
303
|
+
*/
|
|
304
|
+
export function buildHeadlessLaunchArgs(viewport: { width: number; height: number }): string[] {
|
|
307
305
|
const launchArgs = [
|
|
308
306
|
"--no-sandbox",
|
|
309
307
|
"--disable-setuid-sandbox",
|
|
310
308
|
"--disable-blink-features=AutomationControlled",
|
|
311
|
-
`--window-size=${
|
|
309
|
+
`--window-size=${viewport.width},${viewport.height}`,
|
|
312
310
|
];
|
|
313
311
|
const proxy = process.env.PUPPETEER_PROXY;
|
|
314
312
|
if (proxy) {
|
|
@@ -324,6 +322,18 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
|
|
|
324
322
|
if (ignoreCert === "true" || ignoreCert === "1" || ignoreCert === "yes" || ignoreCert === "on") {
|
|
325
323
|
launchArgs.push("--ignore-certificate-errors");
|
|
326
324
|
}
|
|
325
|
+
return launchArgs;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promise<LaunchHeadlessResult> {
|
|
329
|
+
const vp = opts.viewport ?? DEFAULT_VIEWPORT;
|
|
330
|
+
const initialViewport = {
|
|
331
|
+
width: vp.width,
|
|
332
|
+
height: vp.height,
|
|
333
|
+
deviceScaleFactor: vp.deviceScaleFactor ?? DEFAULT_VIEWPORT.deviceScaleFactor,
|
|
334
|
+
};
|
|
335
|
+
const puppeteer = await loadPuppeteer();
|
|
336
|
+
const launchArgs = buildHeadlessLaunchArgs(initialViewport);
|
|
327
337
|
for (const arg of opts.args ?? []) {
|
|
328
338
|
if (!launchArgs.includes(arg)) launchArgs.push(arg);
|
|
329
339
|
}
|
|
@@ -357,6 +367,41 @@ export async function launchHeadlessBrowser(opts: LaunchHeadlessOptions): Promis
|
|
|
357
367
|
}
|
|
358
368
|
}
|
|
359
369
|
|
|
370
|
+
/** Fully resolved executable and argv for a broker-spawned shared Chromium. */
|
|
371
|
+
export interface SharedBrowserLaunchSpec {
|
|
372
|
+
executablePath: string;
|
|
373
|
+
args: string[];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Resolve the executable and complete argv for a shared Chromium the daemon
|
|
378
|
+
* broker spawns directly (no puppeteer inside the broker). Mirrors
|
|
379
|
+
* `launchHeadlessBrowser` flag assembly — puppeteer's default args minus the
|
|
380
|
+
* stealth-suppressed set — plus `--remote-debugging-port=0` so every client
|
|
381
|
+
* attaches over CDP. Returns null when no Chromium executable resolves;
|
|
382
|
+
* callers fall back to a process-local launch.
|
|
383
|
+
*/
|
|
384
|
+
export async function resolveSharedBrowserLaunchSpec(opts: {
|
|
385
|
+
headless: boolean;
|
|
386
|
+
userDataDir: string;
|
|
387
|
+
viewport?: { width: number; height: number };
|
|
388
|
+
}): Promise<SharedBrowserLaunchSpec | null> {
|
|
389
|
+
const executablePath = await ensureChromiumExecutable();
|
|
390
|
+
if (!executablePath) return null;
|
|
391
|
+
const puppeteer = await loadPuppeteer();
|
|
392
|
+
const vp = opts.viewport ?? DEFAULT_VIEWPORT;
|
|
393
|
+
const ignored = new Set(stealthIgnoreDefaultArgs(executablePath));
|
|
394
|
+
const defaults = await puppeteer.defaultArgs({
|
|
395
|
+
headless: opts.headless,
|
|
396
|
+
args: buildHeadlessLaunchArgs(vp),
|
|
397
|
+
userDataDir: opts.userDataDir,
|
|
398
|
+
});
|
|
399
|
+
return {
|
|
400
|
+
executablePath,
|
|
401
|
+
args: [...defaults.filter(arg => !ignored.has(arg)), "--remote-debugging-port=0"],
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
|
|
360
405
|
/**
|
|
361
406
|
* Remove an OMP-owned headless Chromium profile directory, tolerating the brief
|
|
362
407
|
* window on Windows in which Chromium (or an orphaned browser subprocess) still
|