pi-subagents 0.37.1 → 0.38.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/CHANGELOG.md +26 -0
- package/LICENSE +21 -0
- package/README.md +16 -4
- package/package.json +1 -1
- package/src/agents/agents.ts +58 -8
- package/src/agents/skills.ts +14 -12
- package/src/extension/index.ts +3 -0
- package/src/extension/rpc.ts +221 -4
- package/src/extension/schemas.ts +13 -1
- package/src/intercom/intercom-bridge.ts +4 -2
- package/src/runs/background/control-channel.ts +8 -1
- package/src/runs/background/subagent-runner.ts +71 -11
- package/src/runs/foreground/chain-clarify.ts +3 -3
- package/src/runs/foreground/execution.ts +149 -51
- package/src/runs/foreground/foreground-control.ts +12 -0
- package/src/runs/foreground/subagent-executor.ts +1 -0
- package/src/runs/shared/acceptance.ts +15 -7
- package/src/runs/shared/pi-args.ts +7 -0
- package/src/runs/shared/subagent-startup-retry.ts +101 -0
- package/src/shared/types.ts +16 -0
- package/src/shared/utils.ts +43 -0
- package/src/slash/slash-commands.ts +3 -3
- package/src/tui/fleet-status.ts +13 -6
- package/src/tui/render.ts +23 -26
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,32 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.38.0] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- Added `j`/`k` navigation aliases to the non-filterable `/subagents-stop` selector and clarify step list while preserving text input in editor and search modes. Thanks to @magoz for #686.
|
|
9
|
+
- Added the optional versioned `fleetStatus` RPC capability with bounded, current-session child roles, goals, model/effort, split token usage, elapsed timestamps, stable opaque reconciliation keys, and explicit overflow counts. Thanks to @neumie for #682.
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- Enabled the advertised `j`/`k` navigation aliases after activating the persistent FleetView while leaving printable editor input untouched before activation. Thanks to @magoz for #685.
|
|
13
|
+
- Added opt-in `subagents.projectRootResolution: "git-root"` so monorepos and git worktrees can keep the default nearest-root behavior unless they choose to resolve project packages and `agentOverrides` from the git root. Thanks to @klajdo-f for #677.
|
|
14
|
+
- Recognized structurally compatible custom editors in FleetView focus detection, restoring FleetView arrow-key activation and navigation when a custom editor has focus. Thanks to @magoz for #679.
|
|
15
|
+
- Scoped foreground fleet records to their originating parent session and propagated resolved model, thinking effort, and split input/output usage through live foreground controls.
|
|
16
|
+
- Matched fleet RPC filtering to the canonical session-file identity used by live async and foreground state.
|
|
17
|
+
- Kept pi-intercom stable IDs from leaking into child sessions and used the current intercom runtime ID for unnamed supervisor targets.
|
|
18
|
+
- Improved acceptance policy validation errors and tool-schema guidance for invalid evidence kinds. Thanks to @atimofeev for #672.
|
|
19
|
+
- Tolerated temporary steering inbox scan failures so pending steer requests can be retried on the next poll. Thanks to @hughcars for #670.
|
|
20
|
+
- Retried short, zero-activity child startup exits on the same model with bounded backoff, reducing concurrent subagent launch races without replaying model or tool work. Thanks to @felipeteodorocw for #671.
|
|
21
|
+
- Bounded streamed subagent progress snapshots so a long or deeply nested fan-out no longer emits a `tool_execution_update` line above the child-stdout protocol cap and gets the child killed with `protocol_output_limit`. Streamed `onUpdate` snapshots now carry compact tool-call summaries instead of the full message transcript, cap `recentTools`, and truncate `recentOutput` line length; the returned result and detached-exit recovery keep the full transcript. Thanks to @shaharmor for #680/#681.
|
|
22
|
+
|
|
23
|
+
## [0.37.2] - 2026-07-28
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
- Reduced repeated scanning and file reads in live TUI rendering and skill loading.
|
|
27
|
+
|
|
28
|
+
### Fixed
|
|
29
|
+
- Passed `--no-context-files` to child Pi runs when an agent disables inherited project context, avoiding stale prompt-header parsing as Pi's context block format changes. Thanks to @KorenKrita for #667.
|
|
30
|
+
|
|
5
31
|
## [0.37.1] - 2026-07-27
|
|
6
32
|
|
|
7
33
|
### Added
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nico Bailon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -157,6 +157,18 @@ For a persistent override, edit settings. This example pins the reviewer everywh
|
|
|
157
157
|
|
|
158
158
|
Use `~/.pi/agent/settings.json` for a user override or the project config settings file (`.pi/settings.json` in standard Pi) for a project override. `subagents.defaultModel` applies to builtin, package, user, and project agents that do not set `model` in frontmatter. Per-run model overrides and `agentOverrides.<name>.model` still win, and explicit agent frontmatter still wins over the global default. The same `agentOverrides` block can change `tools`, `skills`, inherited context, prompt text, or disable a builtin. Matching user and project agents also receive override fields that their frontmatter leaves unset, so a shared project config agent can keep the persona while local settings choose the model.
|
|
159
159
|
|
|
160
|
+
By default, project settings resolve from the nearest parent directory that contains `.pi` or `.agents`, preserving existing nested-project behavior. In monorepos or git worktrees where an incidental nested `.pi` directory should not shadow the repository-level config, set this in the repository root `.pi/settings.json`:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"subagents": {
|
|
165
|
+
"projectRootResolution": "git-root"
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
`"git-root"` keeps package discovery, project agents, chains, and `agentOverrides` anchored to the git worktree root when that root also has Pi project config. A nested project can still opt back into nearest-root behavior by setting `"projectRootResolution": "nearest"` in its own `.pi/settings.json`.
|
|
171
|
+
|
|
160
172
|
Set `subagents.defaultThinking` to give builtin, package, user, and project agents without a `thinking` value a shared thinking level, independent of the parent session's default. Project settings win over user settings. Explicit frontmatter, `agentOverrides.<name>.thinking`, and per-run thinking overrides still win; `thinking: false` remains an explicit opt-out:
|
|
161
173
|
|
|
162
174
|
```json
|
|
@@ -274,7 +286,7 @@ To keep subagents inside a budget or compliance profile, enforce a model scope.
|
|
|
274
286
|
|
|
275
287
|
Foreground runs stream progress in the conversation while they run. They default to a generous 30-minute wall-clock timeout when neither the call nor the selected agent provides a timeout; explicit `timeoutMs`/`maxRuntimeMs` and agent defaults win.
|
|
276
288
|
|
|
277
|
-
Background runs keep working after control returns to you. Inspect active runs with `subagent({ action: "status" })`, or a specific run with `subagent({ action: "status", id: "..." })`. In the TUI, a persistent FleetView below the editor by default shows `main` plus active children with task, elapsed time, and token totals. Set `fleetViewPlacement` to `"aboveEditor"` to move it above the editor. When the focused editor is empty, use `↑`/`↓` or `j`/`k` to select a child and `Enter` to inspect it;
|
|
289
|
+
Background runs keep working after control returns to you. Inspect active runs with `subagent({ action: "status" })`, or a specific run with `subagent({ action: "status", id: "..." })`. In the TUI, a persistent FleetView below the editor by default shows `main` plus active children with task, elapsed time, and token totals. Set `fleetViewPlacement` to `"aboveEditor"` to move it above the editor. When the focused editor is empty, press `↓` or `←` to activate FleetView, then use `↑`/`↓` or `j`/`k` to select a child and `Enter` to inspect it; printable navigation keys are never intercepted before activation.
|
|
278
290
|
|
|
279
291
|
`/subagents-fleet` opens the live, inspection-only fleet inspector with current-session foreground work, recent async children, structured Markdown/tool transcripts, and completed output/session paths. Use `↑`/`↓` or `j`/`k` to select a child, `Shift+K`/`Shift+J` to scroll one line, `PgUp`/`PgDn` to scroll one page, `x`/`Ctrl+O` to toggle tool details, `r` to refresh, and `Esc` to close. `Ctrl+Alt+F` opens the same inspector even while a foreground turn is active and slash input is queued. Without a TUI, `/subagents-fleet` retains the textual `subagent({ action: "status", view: "fleet" })` fallback. Mutations stay in explicit commands: run `/subagents-stop` and pick from the selector, or use `/subagents-stop <run-id>` / `subagent({ action: "stop", id: "..." })` when you already know the id. To inspect one background child in text, use `subagent({ action: "status", id: "...", view: "transcript" })`; add `index` for a specific child in a parallel or chain run.
|
|
280
292
|
|
|
@@ -310,7 +322,7 @@ pi.events.emit("subagents:rpc:v1:request", {
|
|
|
310
322
|
});
|
|
311
323
|
```
|
|
312
324
|
|
|
313
|
-
The v1 methods are `ping`, `status`, `spawn`, `steer`, `interrupt`, `stop`, and `resume`. `status`, `steer`, `interrupt`, and `resume` reuse the normal package-owned actions. `steer` requires an async run `id` (plus optional child `index`) and a non-empty `message`; its reply preserves the normal acknowledged-delivery result. RPC steering disables the direct tool's pause-and-revive recovery so an extension keeps authority over the exact child it spawned; `ping.capabilities.nonRecoveringSteer` advertises this guarantee. `resume` requires a run target and non-empty `message`; it delegates to the existing revival path, which validates current-session ownership, persisted session/recovery metadata, stopped/live state, capability ceilings, and the exclusive session lease before returning the new async run details. Callers may request a `file-only` output path for the revived result without overriding its model, tools, or budgets. `ping.capabilities.resume` advertises this seam. `spawn` is async-only: omit `async` or set `async: true`, omit `clarify` or set `clarify: false`, and do not pass management `action` values. It goes through the same executor as the `subagent` tool, so agent discovery, validation, session attribution, configured spawn caps, child-safety depth, artifacts, and async status all behave the same. `stop` targets current-session top-level async runs through the stop control channel and records a `stopped` lifecycle instead of reporting a timeout.
|
|
325
|
+
The v1 methods are `ping`, `status`, `spawn`, `steer`, `interrupt`, `stop`, and `resume`. `status`, `steer`, `interrupt`, and `resume` reuse the normal package-owned actions. When `ping.capabilities.fleetStatus` is `{ version: 1 }`, successful `status` replies additionally include `data.fleet`: `{ version: 1, entries, totalActive, omitted }`. Entries are bounded, current-session public display records with an opaque reconciliation `key`, resolved `agent`, optional `role`, `model`, `effort`, caller-facing `goal`, safe `startedAt`, and `{ input, output, total }` tokens. `totalActive` and `omitted` preserve overflow information beyond the bounded entry window. The DTO intentionally never exposes run, async, or tool IDs; clients must ignore unknown fields and fall back to status text when the capability is absent. `steer` requires an async run `id` (plus optional child `index`) and a non-empty `message`; its reply preserves the normal acknowledged-delivery result. RPC steering disables the direct tool's pause-and-revive recovery so an extension keeps authority over the exact child it spawned; `ping.capabilities.nonRecoveringSteer` advertises this guarantee. `resume` requires a run target and non-empty `message`; it delegates to the existing revival path, which validates current-session ownership, persisted session/recovery metadata, stopped/live state, capability ceilings, and the exclusive session lease before returning the new async run details. Callers may request a `file-only` output path for the revived result without overriding its model, tools, or budgets. `ping.capabilities.resume` advertises this seam. `spawn` is async-only: omit `async` or set `async: true`, omit `clarify` or set `clarify: false`, and do not pass management `action` values. It goes through the same executor as the `subagent` tool, so agent discovery, validation, session attribution, configured spawn caps, child-safety depth, artifacts, and async status all behave the same. `stop` targets current-session top-level async runs through the stop control channel and records a `stopped` lifecycle instead of reporting a timeout.
|
|
314
326
|
|
|
315
327
|
`pi.events` is in-process only. It does not reach separate Pi processes or child subagents; use the file lifecycle artifacts or `pi-intercom` for cross-process coordination.
|
|
316
328
|
|
|
@@ -644,7 +656,7 @@ Common clarify keys:
|
|
|
644
656
|
|
|
645
657
|
- `Enter` runs in the foreground, or in the background if background is toggled on
|
|
646
658
|
- `Esc` cancels or backs out
|
|
647
|
-
- `↑↓` moves between steps or tasks
|
|
659
|
+
- `↑↓` or `j`/`k` moves between steps or tasks
|
|
648
660
|
- `e` edits the task/template
|
|
649
661
|
- `m` selects a model
|
|
650
662
|
- `t` selects thinking level
|
|
@@ -1429,7 +1441,7 @@ subagent({ action: "doctor" })
|
|
|
1429
1441
|
|
|
1430
1442
|
`resume` revives a paused, completed, or failed async/foreground child by starting a new child from its stored session file; stopped runs remain non-resumable, and it does not interrupt a live top-level async child. Use `steer` for acknowledged live async guidance. Multi-child async runs and remembered foreground single, parallel, or chain runs can be revived by passing `index` to choose the child. Nested runs can be resumed by nested id when their live route or persisted nested session metadata is available. Revive starts a new child process from the old session context; it does not restart the same OS process, and it requires the chosen child to have a persisted `.jsonl` session file. Direct revival takes an exclusive cross-process lease on the canonical session file until the new child finishes. A concurrent attempt fails before Pi is spawned and identifies the owning revived run; dead-owner leases are reclaimed only when staleness can be proved.
|
|
1431
1443
|
|
|
1432
|
-
`stop` ends a current-session top-level async run. It is deliberately stronger than `interrupt`: it is not a resumable pause, stopped runs should be restarted as new runs, foreground and nested targets are rejected, direct id calls execute immediately, and `/subagents-stop` without an id opens a selector with confirmation when a TUI is available. In non-TUI contexts the slash command prints exact `subagent({ action: "stop", id })` and `/subagents-stop <id>` commands. Scheduled jobs can appear in the selector, but they are labeled as scheduled cancellations and route through `schedule-cancel`, not `stop`.
|
|
1444
|
+
`stop` ends a current-session top-level async run. It is deliberately stronger than `interrupt`: it is not a resumable pause, stopped runs should be restarted as new runs, foreground and nested targets are rejected, direct id calls execute immediately, and `/subagents-stop` without an id opens a selector with confirmation when a TUI is available. Use `↑`/`↓` or `j`/`k` to move through that selector. In non-TUI contexts the slash command prints exact `subagent({ action: "stop", id })` and `/subagents-stop <id>` commands. Scheduled jobs can appear in the selector, but they are labeled as scheduled cancellations and route through `schedule-cancel`, not `stop`.
|
|
1433
1445
|
|
|
1434
1446
|
`steer` waits up to three seconds for a correlated child-Pi input acceptance and returns a request id with `delivered`, `scheduled`, `pending`, `partial`, `recovered`, or `failed` plus per-child states. Delivery means Pi accepted the user message, not model compliance. A pending indexed child returns `scheduled`. Only a top-level single run may interrupt after the acknowledgment deadline and recover after a further 15-second pause/revival bound; chain, parallel, and nested runs never auto-interrupt. Recovery launches a replacement only after the source is confirmed paused, a valid persisted session exists, and deadline, turn, and tool budgets remain. It preserves the original child contract and remaining limits; otherwise the source stays paused with an explicit failure. Late acceptance is recorded but cannot cancel committed recovery. The persisted `steering` ledger retains 20 requests and replaces the old `steerCount`/`lastSteerAt` fields.
|
|
1435
1447
|
|
package/package.json
CHANGED
package/src/agents/agents.ts
CHANGED
|
@@ -152,6 +152,8 @@ export interface AgentConfig {
|
|
|
152
152
|
modelSource?: AgentModelSourceInfo;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
type ProjectRootResolution = "nearest" | "git-root";
|
|
156
|
+
|
|
155
157
|
interface SubagentSettings {
|
|
156
158
|
overrides: Record<string, BuiltinAgentOverrideConfig>;
|
|
157
159
|
defaultModel?: string;
|
|
@@ -422,7 +424,7 @@ function collectSettingsPackageRoots(settingsFile: string, baseDir: string): str
|
|
|
422
424
|
|
|
423
425
|
function collectPackageSubagentPaths(cwd: string, options: { includeUser: boolean; includeProject: boolean } = { includeUser: true, includeProject: true }): PackageSubagentPaths {
|
|
424
426
|
const agentDir = getAgentDir();
|
|
425
|
-
const projectRoot =
|
|
427
|
+
const projectRoot = findConfiguredProjectRoot(cwd) ?? cwd;
|
|
426
428
|
const packageRoots = [
|
|
427
429
|
projectRoot,
|
|
428
430
|
];
|
|
@@ -551,12 +553,26 @@ function cloneOverrideValue(override: BuiltinAgentOverrideConfig): BuiltinAgentO
|
|
|
551
553
|
};
|
|
552
554
|
}
|
|
553
555
|
|
|
554
|
-
|
|
556
|
+
function isProjectRootCandidate(dir: string): boolean {
|
|
557
|
+
return isDirectory(getProjectConfigDir(dir)) || isDirectory(path.join(dir, ".agents"));
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function findProjectRootCandidates(cwd: string): string[] {
|
|
561
|
+
const roots: string[] = [];
|
|
555
562
|
let currentDir = cwd;
|
|
556
563
|
while (true) {
|
|
557
|
-
if (
|
|
558
|
-
|
|
559
|
-
|
|
564
|
+
if (isProjectRootCandidate(currentDir)) roots.push(currentDir);
|
|
565
|
+
|
|
566
|
+
const parentDir = path.dirname(currentDir);
|
|
567
|
+
if (parentDir === currentDir) return roots;
|
|
568
|
+
currentDir = parentDir;
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
function findNearestGitRoot(cwd: string): string | null {
|
|
573
|
+
let currentDir = cwd;
|
|
574
|
+
while (true) {
|
|
575
|
+
if (fs.existsSync(path.join(currentDir, ".git"))) return currentDir;
|
|
560
576
|
|
|
561
577
|
const parentDir = path.dirname(currentDir);
|
|
562
578
|
if (parentDir === currentDir) return null;
|
|
@@ -564,12 +580,46 @@ export function findNearestProjectRoot(cwd: string): string | null {
|
|
|
564
580
|
}
|
|
565
581
|
}
|
|
566
582
|
|
|
583
|
+
function readProjectRootResolution(projectRoot: string): ProjectRootResolution | undefined {
|
|
584
|
+
const settingsPath = path.join(getProjectConfigDir(projectRoot), "settings.json");
|
|
585
|
+
if (!fs.existsSync(settingsPath)) return undefined;
|
|
586
|
+
const settings = readSettingsFileStrict(settingsPath);
|
|
587
|
+
const subagents = settings.subagents;
|
|
588
|
+
if (!subagents || typeof subagents !== "object" || Array.isArray(subagents)) return undefined;
|
|
589
|
+
|
|
590
|
+
const value = (subagents as Record<string, unknown>).projectRootResolution;
|
|
591
|
+
if (value === undefined) return undefined;
|
|
592
|
+
if (value === "nearest" || value === "git-root") return value;
|
|
593
|
+
throw new Error(`Subagent settings in '${settingsPath}' have invalid 'projectRootResolution'; expected 'nearest' or 'git-root'.`);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
export function findNearestProjectRoot(cwd: string): string | null {
|
|
597
|
+
return findProjectRootCandidates(cwd)[0] ?? null;
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function findConfiguredProjectRoot(cwd: string): string | null {
|
|
601
|
+
const candidates = findProjectRootCandidates(cwd);
|
|
602
|
+
const nearestRoot = candidates[0];
|
|
603
|
+
if (!nearestRoot) return null;
|
|
604
|
+
|
|
605
|
+
const nearestMode = readProjectRootResolution(nearestRoot);
|
|
606
|
+
if (nearestMode === "nearest") return nearestRoot;
|
|
607
|
+
|
|
608
|
+
const gitRoot = findNearestGitRoot(cwd);
|
|
609
|
+
const gitProjectRoot = gitRoot ? candidates.find((candidate) => path.resolve(candidate) === path.resolve(gitRoot)) : undefined;
|
|
610
|
+
if (gitProjectRoot && (nearestMode === "git-root" || readProjectRootResolution(gitProjectRoot) === "git-root")) {
|
|
611
|
+
return gitProjectRoot;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
return nearestRoot;
|
|
615
|
+
}
|
|
616
|
+
|
|
567
617
|
function getUserAgentSettingsPath(): string {
|
|
568
618
|
return path.join(getAgentDir(), "settings.json");
|
|
569
619
|
}
|
|
570
620
|
|
|
571
621
|
function getProjectAgentSettingsPath(cwd: string): string | null {
|
|
572
|
-
const projectRoot =
|
|
622
|
+
const projectRoot = findConfiguredProjectRoot(cwd);
|
|
573
623
|
return projectRoot ? path.join(getProjectConfigDir(projectRoot), "settings.json") : null;
|
|
574
624
|
}
|
|
575
625
|
|
|
@@ -1491,7 +1541,7 @@ function isDirectory(p: string): boolean {
|
|
|
1491
1541
|
}
|
|
1492
1542
|
|
|
1493
1543
|
function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
|
|
1494
|
-
const projectRoot =
|
|
1544
|
+
const projectRoot = findConfiguredProjectRoot(cwd);
|
|
1495
1545
|
if (!projectRoot) return { readDirs: [], preferredDir: null };
|
|
1496
1546
|
|
|
1497
1547
|
const legacyDir = path.join(projectRoot, ".agents");
|
|
@@ -1507,7 +1557,7 @@ function resolveNearestProjectAgentDirs(cwd: string): { readDirs: string[]; pref
|
|
|
1507
1557
|
}
|
|
1508
1558
|
|
|
1509
1559
|
function resolveNearestProjectChainDirs(cwd: string): { readDirs: string[]; preferredDir: string | null } {
|
|
1510
|
-
const projectRoot =
|
|
1560
|
+
const projectRoot = findConfiguredProjectRoot(cwd);
|
|
1511
1561
|
if (!projectRoot) return { readDirs: [], preferredDir: null };
|
|
1512
1562
|
|
|
1513
1563
|
const preferredDir = path.join(getProjectConfigDir(projectRoot), "chains");
|
package/src/agents/skills.ts
CHANGED
|
@@ -379,19 +379,21 @@ function chooseHigherPrioritySkill(existing: CachedSkillEntry | undefined, candi
|
|
|
379
379
|
return candidate.order < existing.order ? candidate : existing;
|
|
380
380
|
}
|
|
381
381
|
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
382
|
+
function parseSkillDescription(content: string): string | undefined {
|
|
383
|
+
const normalized = content.replace(/\r\n/g, "\n");
|
|
384
|
+
if (!normalized.startsWith("---")) return undefined;
|
|
385
|
+
|
|
386
|
+
const endIndex = normalized.indexOf("\n---", 3);
|
|
387
|
+
if (endIndex === -1) return undefined;
|
|
387
388
|
|
|
388
|
-
|
|
389
|
-
|
|
389
|
+
const frontmatter = normalized.slice(3, endIndex).trim();
|
|
390
|
+
const match = frontmatter.match(/^description:\s*(.+)$/m);
|
|
391
|
+
return match?.[1]?.trim().replace(/^['\"]|['\"]$/g, "");
|
|
392
|
+
}
|
|
390
393
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
return match[1]?.trim().replace(/^['\"]|['\"]$/g, "");
|
|
394
|
+
function maybeReadSkillDescription(filePath: string): string | undefined {
|
|
395
|
+
try {
|
|
396
|
+
return parseSkillDescription(fs.readFileSync(filePath, "utf-8"));
|
|
395
397
|
} catch {
|
|
396
398
|
// Description parsing is best-effort metadata extraction.
|
|
397
399
|
return undefined;
|
|
@@ -585,7 +587,7 @@ function readSkill(
|
|
|
585
587
|
|
|
586
588
|
const raw = fs.readFileSync(skillPath, "utf-8");
|
|
587
589
|
const content = stripSkillFrontmatter(raw);
|
|
588
|
-
const description =
|
|
590
|
+
const description = parseSkillDescription(raw);
|
|
589
591
|
const skill: ResolvedSkill = {
|
|
590
592
|
name: skillName,
|
|
591
593
|
path: skillPath,
|
package/src/extension/index.ts
CHANGED
|
@@ -389,6 +389,7 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
389
389
|
events: pi.events,
|
|
390
390
|
getContext: () => state.lastUiContext,
|
|
391
391
|
execute: (id, params, signal, onUpdate, ctx) => executor.execute(id, params, signal, onUpdate, ctx),
|
|
392
|
+
state,
|
|
392
393
|
});
|
|
393
394
|
|
|
394
395
|
function effectiveParallelTaskCount(tasks: Array<{ count?: unknown }> | undefined): number {
|
|
@@ -557,6 +558,8 @@ export default function registerSubagentExtension(pi: ExtensionAPI): void {
|
|
|
557
558
|
state.lastUiContext = ctx;
|
|
558
559
|
cleanupSessionArtifacts(ctx);
|
|
559
560
|
clearPendingForegroundControlNotices(state);
|
|
561
|
+
state.foregroundControls.clear();
|
|
562
|
+
state.lastForegroundControlId = null;
|
|
560
563
|
resetJobs(ctx);
|
|
561
564
|
restoreActiveJobs(ctx);
|
|
562
565
|
scheduledRunManager.bindSession(ctx);
|
package/src/extension/rpc.ts
CHANGED
|
@@ -8,7 +8,9 @@ import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.ts";
|
|
|
8
8
|
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
9
9
|
import { resolveCurrentSessionId } from "../shared/session-identity.ts";
|
|
10
10
|
import {
|
|
11
|
+
type AsyncJobStep,
|
|
11
12
|
type Details,
|
|
13
|
+
type SubagentState,
|
|
12
14
|
ASYNC_DIR,
|
|
13
15
|
RESULTS_DIR,
|
|
14
16
|
SUBAGENT_ASYNC_COMPLETE_EVENT,
|
|
@@ -70,6 +72,200 @@ interface EventBus {
|
|
|
70
72
|
emit(event: string, data: unknown): void;
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
export interface SubagentRpcFleetEntry {
|
|
76
|
+
/** Opaque key for client-side reconciliation; never a run or async identifier. */
|
|
77
|
+
key: string;
|
|
78
|
+
/** Resolved child agent/role name. */
|
|
79
|
+
agent: string;
|
|
80
|
+
role?: string;
|
|
81
|
+
model?: string;
|
|
82
|
+
effort?: string;
|
|
83
|
+
startedAt: number;
|
|
84
|
+
tokens: { input: number; output: number; total: number };
|
|
85
|
+
goal?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface SubagentRpcFleetStatus {
|
|
89
|
+
version: 1;
|
|
90
|
+
entries: SubagentRpcFleetEntry[];
|
|
91
|
+
/** Total active children before the bounded entries window. */
|
|
92
|
+
totalActive: number;
|
|
93
|
+
omitted: number;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const MAX_FLEET_ENTRIES = 16;
|
|
97
|
+
const MAX_FLEET_CANDIDATES = 256;
|
|
98
|
+
const MAX_AGENT_LENGTH = 96;
|
|
99
|
+
const MAX_GOAL_LENGTH = 512;
|
|
100
|
+
const MAX_METADATA_LENGTH = 128;
|
|
101
|
+
|
|
102
|
+
function displayText(value: unknown, maxLength: number): string | undefined {
|
|
103
|
+
if (typeof value !== "string") return undefined;
|
|
104
|
+
// Strip complete CSI/OSC/DCS/APC/PM strings and C1 controls before collapsing
|
|
105
|
+
// whitespace; never leave CSI parameters behind after removing ESC.
|
|
106
|
+
const normalized = value.slice(0, 4_096)
|
|
107
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]|\x9b[0-?]*[ -/]*[@-~]|\x1b][\s\S]*?(?:\x07|\x1b\\)|\x1b[PX^_][\s\S]*?\x1b\\|[\u0000-\u001f\u007f-\u009f]/g, " ")
|
|
108
|
+
.replace(/\s+/g, " ").trim();
|
|
109
|
+
return normalized ? normalized.slice(0, maxLength) : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function publicTokens(value: unknown): { input: number; output: number; total: number } {
|
|
113
|
+
const record = isRecord(value) ? value : {};
|
|
114
|
+
const count = (field: "input" | "output" | "total") => {
|
|
115
|
+
const raw = record[field];
|
|
116
|
+
return typeof raw === "number" && Number.isFinite(raw) && raw >= 0
|
|
117
|
+
? Math.min(Number.MAX_SAFE_INTEGER, Math.floor(raw))
|
|
118
|
+
: 0;
|
|
119
|
+
};
|
|
120
|
+
const input = count("input");
|
|
121
|
+
const output = count("output");
|
|
122
|
+
const sum = Math.min(Number.MAX_SAFE_INTEGER, input + output);
|
|
123
|
+
return { input, output, total: Math.max(sum, count("total")) };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function activeState(value: unknown): boolean {
|
|
127
|
+
return value === "running" || value === "queued" || value === "pending";
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface FleetKeyState {
|
|
131
|
+
sessionId: string | null;
|
|
132
|
+
next: number;
|
|
133
|
+
keys: Map<string, string>;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
interface FleetCandidate {
|
|
137
|
+
internalKey: string;
|
|
138
|
+
agent: unknown;
|
|
139
|
+
role?: unknown;
|
|
140
|
+
model?: unknown;
|
|
141
|
+
effort?: unknown;
|
|
142
|
+
startedAt: unknown;
|
|
143
|
+
tokens?: unknown;
|
|
144
|
+
goal?: unknown;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function buildFleetStatus(
|
|
148
|
+
state: SubagentState | undefined,
|
|
149
|
+
keyState: FleetKeyState,
|
|
150
|
+
sessionId: string | null | undefined,
|
|
151
|
+
): SubagentRpcFleetStatus {
|
|
152
|
+
const authoritativeSessionId = sessionId ?? null;
|
|
153
|
+
if (keyState.sessionId !== authoritativeSessionId) {
|
|
154
|
+
keyState.sessionId = authoritativeSessionId;
|
|
155
|
+
keyState.next = 0;
|
|
156
|
+
keyState.keys.clear();
|
|
157
|
+
}
|
|
158
|
+
if (!state || !authoritativeSessionId || state.currentSessionId !== authoritativeSessionId) {
|
|
159
|
+
keyState.keys.clear();
|
|
160
|
+
return { version: 1, entries: [], totalActive: 0, omitted: 0 };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let totalActive = 0;
|
|
164
|
+
const candidates: FleetCandidate[] = [];
|
|
165
|
+
const addCandidate = (candidate: FleetCandidate) => {
|
|
166
|
+
totalActive += 1;
|
|
167
|
+
if (candidates.length < MAX_FLEET_CANDIDATES) candidates.push(candidate);
|
|
168
|
+
};
|
|
169
|
+
for (const control of state.foregroundControls.values()) {
|
|
170
|
+
if (control.sessionId !== authoritativeSessionId) continue;
|
|
171
|
+
if (control.activeChildren?.size) {
|
|
172
|
+
for (const child of control.activeChildren.values()) addCandidate({
|
|
173
|
+
internalKey: `foreground:${control.runId}:${child.index}`,
|
|
174
|
+
agent: child.agent,
|
|
175
|
+
model: child.model,
|
|
176
|
+
effort: child.thinking,
|
|
177
|
+
startedAt: child.startedAt,
|
|
178
|
+
tokens: { input: child.inputTokens ?? 0, output: child.outputTokens ?? 0, total: child.tokens ?? 0 },
|
|
179
|
+
goal: child.description ?? control.description,
|
|
180
|
+
});
|
|
181
|
+
} else {
|
|
182
|
+
addCandidate({
|
|
183
|
+
internalKey: `foreground:${control.runId}:${control.currentIndex ?? 0}`,
|
|
184
|
+
agent: control.currentAgent ?? control.mode,
|
|
185
|
+
model: control.model,
|
|
186
|
+
effort: control.thinking,
|
|
187
|
+
startedAt: control.startedAt,
|
|
188
|
+
tokens: { input: control.inputTokens ?? 0, output: control.outputTokens ?? 0, total: control.tokens ?? 0 },
|
|
189
|
+
goal: control.description,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
for (const job of state.asyncJobs.values()) {
|
|
194
|
+
if (job.sessionId !== authoritativeSessionId || !activeState(job.status)) continue;
|
|
195
|
+
const startedAt = job.startedAt ?? job.updatedAt;
|
|
196
|
+
const steps: AsyncJobStep[] | undefined = job.steps?.length
|
|
197
|
+
? job.steps
|
|
198
|
+
: job.agents?.map((agent, index) => ({
|
|
199
|
+
agent,
|
|
200
|
+
index,
|
|
201
|
+
status: job.status === "queued" ? "pending" : "running",
|
|
202
|
+
}));
|
|
203
|
+
if (!steps?.length) {
|
|
204
|
+
addCandidate({
|
|
205
|
+
internalKey: `async:${job.asyncId}`,
|
|
206
|
+
agent: job.mode ?? "subagent",
|
|
207
|
+
startedAt,
|
|
208
|
+
tokens: job.totalTokens,
|
|
209
|
+
goal: job.description,
|
|
210
|
+
});
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
for (const [offset, step] of steps.entries()) {
|
|
214
|
+
if (!activeState(step.status)) continue;
|
|
215
|
+
const index = step.index ?? offset;
|
|
216
|
+
if (step.status === "pending" && job.mode === "chain" && !job.activeParallelGroup && index !== (job.currentStep ?? 0)) continue;
|
|
217
|
+
addCandidate({
|
|
218
|
+
internalKey: `async:${job.asyncId}:${index}`,
|
|
219
|
+
agent: step.agent,
|
|
220
|
+
role: step.label,
|
|
221
|
+
model: step.model,
|
|
222
|
+
effort: step.thinking,
|
|
223
|
+
startedAt: step.startedAt ?? startedAt,
|
|
224
|
+
tokens: step.tokens ?? (steps.length === 1 ? job.totalTokens : undefined),
|
|
225
|
+
goal: job.description,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
candidates.sort((left, right) => {
|
|
231
|
+
const leftStarted = typeof left.startedAt === "number" ? left.startedAt : Number.MAX_SAFE_INTEGER;
|
|
232
|
+
const rightStarted = typeof right.startedAt === "number" ? right.startedAt : Number.MAX_SAFE_INTEGER;
|
|
233
|
+
return leftStarted - rightStarted || left.internalKey.localeCompare(right.internalKey);
|
|
234
|
+
});
|
|
235
|
+
const activeKeys = new Set(candidates.map((candidate) => candidate.internalKey));
|
|
236
|
+
const entries: SubagentRpcFleetEntry[] = [];
|
|
237
|
+
for (const candidate of candidates) {
|
|
238
|
+
if (entries.length >= MAX_FLEET_ENTRIES) break;
|
|
239
|
+
const agent = displayText(candidate.agent, MAX_AGENT_LENGTH);
|
|
240
|
+
const startedAt = candidate.startedAt;
|
|
241
|
+
if (!agent || typeof startedAt !== "number" || !Number.isSafeInteger(startedAt) || startedAt < 0) continue;
|
|
242
|
+
let key = keyState.keys.get(candidate.internalKey);
|
|
243
|
+
if (!key) {
|
|
244
|
+
key = `fleet-${++keyState.next}`;
|
|
245
|
+
keyState.keys.set(candidate.internalKey, key);
|
|
246
|
+
}
|
|
247
|
+
const role = displayText(candidate.role, MAX_AGENT_LENGTH);
|
|
248
|
+
const model = displayText(candidate.model, MAX_METADATA_LENGTH);
|
|
249
|
+
const effort = displayText(candidate.effort, MAX_METADATA_LENGTH);
|
|
250
|
+
const goal = displayText(candidate.goal, MAX_GOAL_LENGTH);
|
|
251
|
+
entries.push({
|
|
252
|
+
key,
|
|
253
|
+
agent,
|
|
254
|
+
...(role ? { role } : {}),
|
|
255
|
+
...(model ? { model } : {}),
|
|
256
|
+
...(effort ? { effort } : {}),
|
|
257
|
+
startedAt,
|
|
258
|
+
tokens: publicTokens(candidate.tokens),
|
|
259
|
+
...(goal ? { goal } : {}),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
for (const internalKey of keyState.keys.keys()) {
|
|
263
|
+
if (!activeKeys.has(internalKey)) keyState.keys.delete(internalKey);
|
|
264
|
+
}
|
|
265
|
+
const omitted = Math.max(0, totalActive - entries.length);
|
|
266
|
+
return { version: 1, entries, totalActive, omitted };
|
|
267
|
+
}
|
|
268
|
+
|
|
73
269
|
interface RegisterSubagentRpcBridgeOptions {
|
|
74
270
|
events: EventBus;
|
|
75
271
|
getContext: () => ExtensionContext | null;
|
|
@@ -84,6 +280,8 @@ interface RegisterSubagentRpcBridgeOptions {
|
|
|
84
280
|
resultsDir?: string;
|
|
85
281
|
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
86
282
|
now?: () => number;
|
|
283
|
+
/** Native live state, projected into the optional public fleet-status capability. */
|
|
284
|
+
state?: SubagentState;
|
|
87
285
|
}
|
|
88
286
|
|
|
89
287
|
class SubagentRpcError extends Error {
|
|
@@ -141,7 +339,9 @@ function textFromToolResult(result: AgentToolResult<Details>): string {
|
|
|
141
339
|
.join("\n");
|
|
142
340
|
}
|
|
143
341
|
|
|
144
|
-
|
|
342
|
+
type ToolResultWithError = AgentToolResult<Details> & { isError?: boolean };
|
|
343
|
+
|
|
344
|
+
function dataFromToolResult(result: ToolResultWithError): { text: string; details?: Details; isError?: boolean } {
|
|
145
345
|
return {
|
|
146
346
|
text: textFromToolResult(result),
|
|
147
347
|
...(result.details ? { details: result.details } : {}),
|
|
@@ -149,7 +349,7 @@ function dataFromToolResult(result: AgentToolResult<Details>): { text: string; d
|
|
|
149
349
|
};
|
|
150
350
|
}
|
|
151
351
|
|
|
152
|
-
function failIfToolError(result:
|
|
352
|
+
function failIfToolError(result: ToolResultWithError): void {
|
|
153
353
|
if (!result.isError) return;
|
|
154
354
|
throw new SubagentRpcError("execution_failed", textFromToolResult(result) || "Subagent RPC execution failed.");
|
|
155
355
|
}
|
|
@@ -179,6 +379,7 @@ function pingData(ctx: ExtensionContext | null) {
|
|
|
179
379
|
methods: [...SUBAGENT_RPC_METHODS],
|
|
180
380
|
capabilities: {
|
|
181
381
|
status: true,
|
|
382
|
+
fleetStatus: { version: 1 },
|
|
182
383
|
asyncSpawn: true,
|
|
183
384
|
steer: true,
|
|
184
385
|
nonRecoveringSteer: true,
|
|
@@ -324,6 +525,7 @@ function stopAsyncRun(
|
|
|
324
525
|
async function handleRequest(
|
|
325
526
|
request: SubagentRpcRequestEnvelope,
|
|
326
527
|
options: RegisterSubagentRpcBridgeOptions,
|
|
528
|
+
fleetKeys: FleetKeyState,
|
|
327
529
|
): Promise<unknown> {
|
|
328
530
|
const ctx = options.getContext();
|
|
329
531
|
if (request.method === "ping") return pingData(ctx);
|
|
@@ -333,7 +535,21 @@ async function handleRequest(
|
|
|
333
535
|
return executeChecked(options, ctx, request.requestId, request.method, spawnParams(request.params));
|
|
334
536
|
}
|
|
335
537
|
if (request.method === "status") {
|
|
336
|
-
|
|
538
|
+
const status = await executeChecked(
|
|
539
|
+
options,
|
|
540
|
+
ctx,
|
|
541
|
+
request.requestId,
|
|
542
|
+
request.method,
|
|
543
|
+
{ action: "status", ...normalizeTargetParams(request.params, "status") },
|
|
544
|
+
);
|
|
545
|
+
return {
|
|
546
|
+
...status,
|
|
547
|
+
fleet: buildFleetStatus(
|
|
548
|
+
options.state,
|
|
549
|
+
fleetKeys,
|
|
550
|
+
resolveCurrentSessionId(ctx.sessionManager),
|
|
551
|
+
),
|
|
552
|
+
};
|
|
337
553
|
}
|
|
338
554
|
if (request.method === "steer") {
|
|
339
555
|
return executeChecked(options, ctx, request.requestId, request.method, steerParams(request.params));
|
|
@@ -400,11 +616,12 @@ export function registerSubagentRpcBridge(options: RegisterSubagentRpcBridgeOpti
|
|
|
400
616
|
emitReady: (ctx?: ExtensionContext | null) => void;
|
|
401
617
|
dispose: () => void;
|
|
402
618
|
} {
|
|
619
|
+
const fleetKeys: FleetKeyState = { sessionId: null, next: 0, keys: new Map() };
|
|
403
620
|
const unsubscribe = options.events.on(SUBAGENT_RPC_REQUEST_EVENT, async (raw) => {
|
|
404
621
|
let request: SubagentRpcRequestEnvelope | undefined;
|
|
405
622
|
try {
|
|
406
623
|
request = parseRequest(raw);
|
|
407
|
-
const data = await handleRequest(request, options);
|
|
624
|
+
const data = await handleRequest(request, options, fleetKeys);
|
|
408
625
|
options.events.emit(subagentRpcReplyEvent(request.requestId), {
|
|
409
626
|
version: SUBAGENT_RPC_PROTOCOL_VERSION,
|
|
410
627
|
requestId: request.requestId,
|
package/src/extension/schemas.ts
CHANGED
|
@@ -65,6 +65,18 @@ const JsonSchemaObject = Type.Unsafe({
|
|
|
65
65
|
description: "JSON Schema object for strict structured output. Non-object roots are rejected.",
|
|
66
66
|
});
|
|
67
67
|
|
|
68
|
+
const AcceptanceEvidenceKinds = [
|
|
69
|
+
"changed-files",
|
|
70
|
+
"tests-added",
|
|
71
|
+
"commands-run",
|
|
72
|
+
"validation-output",
|
|
73
|
+
"residual-risks",
|
|
74
|
+
"no-staged-files",
|
|
75
|
+
"diff-summary",
|
|
76
|
+
"review-findings",
|
|
77
|
+
"manual-notes",
|
|
78
|
+
];
|
|
79
|
+
|
|
68
80
|
const AcceptanceOverride = Type.Unsafe({
|
|
69
81
|
anyOf: [
|
|
70
82
|
{ type: "string", enum: ["auto", "attested", "checked", "verified"] },
|
|
@@ -77,7 +89,7 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
77
89
|
{ type: "boolean", enum: [false] },
|
|
78
90
|
{ type: "object", additionalProperties: true },
|
|
79
91
|
],
|
|
80
|
-
description:
|
|
92
|
+
description: `Optional acceptance policy. For reviewer/read-only calls, omit acceptance. Example: { level: "checked", evidence: ["commands-run", "changed-files"] }. Supported evidence kinds: ${AcceptanceEvidenceKinds.join(", ")}. Evidence levels end at verified; use acceptance.review.required for review. Omitted means auto-inferred unless agentContract.version=1.`,
|
|
81
93
|
});
|
|
82
94
|
|
|
83
95
|
const AgentContractOverride = Type.Object({
|
|
@@ -16,6 +16,7 @@ function defaultSubagentConfigDir(agentDir = defaultAgentDir()): string {
|
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
const DEFAULT_INTERCOM_TARGET_PREFIX = "subagent-chat";
|
|
19
|
+
export const PI_INTERCOM_SESSION_ID_ENV = "PI_INTERCOM_SESSION_ID";
|
|
19
20
|
export const INTERCOM_BRIDGE_MARKER = "Intercom orchestration channel:";
|
|
20
21
|
const DEFAULT_INTERCOM_BRIDGE_TEMPLATE = `The inherited thread is reference-only. Do not continue that conversation or send questions, status updates, or completion handoffs to the supervisor in normal assistant text.
|
|
21
22
|
|
|
@@ -56,10 +57,11 @@ interface ResolveIntercomBridgeInput {
|
|
|
56
57
|
agentDir?: string;
|
|
57
58
|
}
|
|
58
59
|
|
|
59
|
-
export function resolveIntercomSessionTarget(sessionName: string | undefined, sessionId: string): string {
|
|
60
|
+
export function resolveIntercomSessionTarget(sessionName: string | undefined, sessionId: string, intercomSessionId = process.env[PI_INTERCOM_SESSION_ID_ENV]): string {
|
|
60
61
|
const trimmedName = sessionName?.trim();
|
|
61
62
|
if (trimmedName) return trimmedName;
|
|
62
|
-
const
|
|
63
|
+
const fallbackSessionId = intercomSessionId?.trim() || sessionId;
|
|
64
|
+
const normalizedSessionId = fallbackSessionId.startsWith("session-") ? fallbackSessionId.slice("session-".length) : fallbackSessionId;
|
|
63
65
|
return `${DEFAULT_INTERCOM_TARGET_PREFIX}-${normalizedSessionId.slice(0, 8)}`;
|
|
64
66
|
}
|
|
65
67
|
|