@gotgenes/pi-subagents 7.4.0 → 7.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -5,6 +5,34 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [7.5.1](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.5.0...pi-subagents-v7.5.1) (2026-05-26)
9
+
10
+
11
+ ### Documentation
12
+
13
+ * plan decompose buildParentContext ([#215](https://github.com/gotgenes/pi-packages/issues/215)) ([9103609](https://github.com/gotgenes/pi-packages/commit/910360991b50c320927c1457bfef6b7cb5624b7b))
14
+ * **retro:** add planning stage notes for issue [#215](https://github.com/gotgenes/pi-packages/issues/215) ([5c534d5](https://github.com/gotgenes/pi-packages/commit/5c534d5efb640ef1d72d6ccf7bf2e15ac2acf755))
15
+ * **retro:** add TDD stage notes for issue [#215](https://github.com/gotgenes/pi-packages/issues/215) ([79064d0](https://github.com/gotgenes/pi-packages/commit/79064d072c36c2f92013dbfba58ce1de1ab01bce))
16
+
17
+ ## [7.5.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.4.0...pi-subagents-v7.5.0) (2026-05-26)
18
+
19
+
20
+ ### Features
21
+
22
+ * add permission bridge for cross-extension registration ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([1827720](https://github.com/gotgenes/pi-packages/commit/18277203f7ee10e56f90a0d4db587a4aa95376ab))
23
+ * register child sessions with permission system ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([0487828](https://github.com/gotgenes/pi-packages/commit/04878286d7da6660362360482fb916b1b3743ce3))
24
+
25
+
26
+ ### Bug Fixes
27
+
28
+ * resolve pre-existing lint errors in pi-autoformat and pi-permission-system ([68fd516](https://github.com/gotgenes/pi-packages/commit/68fd516e33ddbb9a5e37ef19e949ee9ecdc37252))
29
+
30
+
31
+ ### Documentation
32
+
33
+ * document permission-bridge in architecture ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([d0120ab](https://github.com/gotgenes/pi-packages/commit/d0120abdf049e2aeba14ba75071ed55b24e23dbe))
34
+ * update subagent integration docs for native permission bridge ([#101](https://github.com/gotgenes/pi-packages/issues/101)) ([0bd456b](https://github.com/gotgenes/pi-packages/commit/0bd456befa8ea6918e74f4393d844868795edc77))
35
+
8
36
  ## [7.4.0](https://github.com/gotgenes/pi-packages/compare/pi-subagents-v7.3.2...pi-subagents-v7.4.0) (2026-05-25)
9
37
 
10
38
 
package/README.md CHANGED
@@ -421,27 +421,57 @@ disallowed_tools: write, edit
421
421
 
422
422
  This is useful for creating agents that inherit extension tools but should not have write access.
423
423
 
424
+ ## Permission System Integration
425
+
426
+ When [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system) is installed, this extension integrates automatically:
427
+
428
+ - **Per-agent permission policies** — define `permission:` in agent YAML frontmatter to set allow/ask/deny rules per agent type.
429
+ The permission system resolves the agent name from the `<active_agent>` tag in the child system prompt.
430
+ - **Tool filtering** — the permission system's `before_agent_start` handler removes denied tools from the child session before the agent starts.
431
+ - **`ask`-state forwarding** — when a child session triggers an `ask` permission, the prompt forwards to the parent session's UI.
432
+ The parent approves or denies, and the child resumes.
433
+ - **Deterministic child detection** — every child session registers with the permission system's `SubagentSessionRegistry` before `bindExtensions()` fires, so detection does not rely on env vars or filesystem heuristics.
434
+
435
+ No configuration is required.
436
+ When `@gotgenes/pi-permission-system` is not installed, the registration calls are silent no-ops.
437
+
424
438
  ## Architecture
425
439
 
440
+ See `docs/architecture/architecture.md` for the full architecture document with domain decomposition, Mermaid diagrams, and improvement roadmap.
441
+
426
442
  ```text
427
443
  src/
428
- index.ts # Extension entry: tool/command registration, rendering
429
- types.ts # Type definitions (AgentConfig, AgentRecord, etc.)
430
- default-agents.ts # Embedded default agent configs (general-purpose, Explore, Plan)
431
- agent-types.ts # Unified agent registry (defaults + user), tool name resolution
432
- agent-runner.ts # Session creation, execution, graceful max_turns, steer/resume
433
- agent-manager.ts # Agent lifecycle, concurrency queue, completion notifications
434
- custom-agents.ts # Load user-defined agents from .pi/agents/*.md
435
- memory.ts # Persistent agent memory (resolve, read, build prompt blocks)
436
- skill-loader.ts # Preload skills (Pi-standard + Agent Skills spec layouts)
437
- output-file.ts # Streaming output file transcripts for agent sessions
438
- worktree.ts # Git worktree isolation (create, cleanup, prune)
439
- prompts.ts # Config-driven system prompt builder
440
- context.ts # Parent conversation context for inherit_context
441
- env.ts # Environment detection (git, platform)
442
- ui/
443
- agent-widget.ts # Persistent widget: spinners, activity, status icons, theming
444
- conversation-viewer.ts # Live conversation overlay for viewing agent sessions
444
+ index.ts # Extension entry: tool/command registration, rendering
445
+ runtime.ts # Session-scoped state bag with methods
446
+ types.ts # Shared type definitions
447
+ settings.ts # Persistent settings (concurrency, turn limits)
448
+ config/ # Agent type registry and configuration
449
+ agent-types.ts # Unified agent registry (defaults + custom)
450
+ default-agents.ts # Embedded default agent configs
451
+ custom-agents.ts # Load user-defined agents from .pi/agents/*.md
452
+ invocation-config.ts # Per-call merge of tool params + agent config
453
+ session/ # Pure session assembly
454
+ session-config.ts # Session configuration assembler
455
+ prompts.ts # Config-driven system prompt builder
456
+ context.ts # Parent conversation context for inherit_context
457
+ skill-loader.ts # Preload skills from Pi-standard + Agent Skills spec
458
+ env.ts # Environment detection (git, platform)
459
+ model-resolver.ts # Fuzzy model matching
460
+ lifecycle/ # Agent execution and state tracking
461
+ agent-manager.ts # Spawn, queue, abort, resume, concurrency
462
+ agent-runner.ts # Session creation, turn loop, tool filtering
463
+ agent-record.ts # Status state machine
464
+ parent-snapshot.ts # Immutable spawn-time parent state
465
+ permission-bridge.ts # Optional bridge to pi-permission-system registry
466
+ worktree.ts # Git worktree isolation
467
+ observation/ # Progress tracking and notification
468
+ record-observer.ts # Session-event stats observer
469
+ notification.ts # Completion nudges
470
+ service/ # Cross-extension API boundary
471
+ service.ts # SubagentsService interface + Symbol.for() accessors
472
+ service-adapter.ts # SubagentsService wrapper around AgentManager
473
+ tools/ # LLM-facing tools
474
+ ui/ # Widget, conversation viewer, /agents menu
445
475
  ```
446
476
 
447
477
  ## Deviations from upstream
@@ -458,6 +488,9 @@ Each has a corresponding upstream PR:
458
488
  3. **`<active_agent>` system-prompt tag** (`src/prompts.ts`) — `buildAgentPrompt` prepends `<active_agent name="${config.name}"/>` to every assembled child system prompt (both `replace` and `append` modes).
459
489
  Downstream extensions like [`@gotgenes/pi-permission-system`](https://github.com/gotgenes/pi-permission-system) parse this tag to resolve per-agent `permission:` frontmatter inside the child session.
460
490
  Upstream PR: [tintinweb/pi-subagents#73](https://github.com/tintinweb/pi-subagents/pull/73).
491
+ 4. **Permission-system registration** (`src/lifecycle/permission-bridge.ts`) — `runAgent` registers every child session with `@gotgenes/pi-permission-system`'s `SubagentSessionRegistry` before `bindExtensions()` and unregisters in the `finally` block.
492
+ This enables deterministic child detection and `ask`-state forwarding to the parent UI.
493
+ No upstream equivalent — this feature is specific to the `@gotgenes` fork.
461
494
 
462
495
  The upstream `vitest` suite plus tests added for each patch all pass on every commit.
463
496
 
@@ -259,6 +259,7 @@ src/
259
259
  │ ├── agent-record.ts status state machine
260
260
  │ ├── parent-snapshot.ts immutable spawn-time parent state
261
261
  │ ├── execution-state.ts session/output phase state
262
+ │ ├── permission-bridge.ts optional bridge to pi-permission-system registry
262
263
  │ ├── worktree.ts git worktree isolation
263
264
  │ ├── worktree-state.ts worktree phase state
264
265
  │ └── usage.ts token usage tracking
@@ -333,7 +334,8 @@ They declare this package as an optional peer dependency and use dynamic import
333
334
 
334
335
  - The three tools: `Agent`, `get_subagent_result`, `steer_subagent`.
335
336
  - `AgentManager` — spawn, queue, abort, resume, concurrency control.
336
- - `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3).
337
+ - `agent-runner` — session creation, turn loop, tool filtering, extension binding (Patches 2 and 3), permission-system registration.
338
+ - `permission-bridge` — optional cross-extension bridge to `@gotgenes/pi-permission-system`; registers each child session with `SubagentSessionRegistry` before `bindExtensions()` so the permission system detects in-process children deterministically.
337
339
  - `session-config` — pure configuration assembler (extracted from `agent-runner`).
338
340
  - `SubagentRuntime` — session-scoped state bag with methods.
339
341
  - `ParentSnapshot` — immutable snapshot of parent session state, captured once at spawn time.
@@ -0,0 +1,166 @@
1
+ ---
2
+ issue: 215
3
+ issue_title: "Decompose buildParentContext (cognitive 30) (Phase 13, Step 2)"
4
+ ---
5
+
6
+ # Decompose `buildParentContext`
7
+
8
+ ## Problem Statement
9
+
10
+ `buildParentContext` in `src/session/context.ts` is the only remaining fallow refactoring target in the package.
11
+ The function has a cognitive complexity of 30, driven by a loop with three type-check branches (`message`, `compaction`, default), each with sub-branches for role (`user` vs `assistant`) and content type (`string` vs array).
12
+ The architecture roadmap (Phase 13, Step 2) targets cognitive complexity < 10 and function body < 15 LOC.
13
+
14
+ ## Goals
15
+
16
+ - Extract per-entry-type formatters: `formatMessageEntry(entry)` and `formatCompactionEntry(entry)`.
17
+ - Reduce `buildParentContext` to a loop + filter + join orchestrator (< 15 LOC).
18
+ - Achieve cognitive complexity < 10 for all functions in the file.
19
+ - Add unit tests for the extracted formatters and the orchestrator.
20
+
21
+ ## Non-Goals
22
+
23
+ - Changing the public API surface (`buildParentContext`, `extractText`) — signatures stay the same.
24
+ - Moving `extractText` to another module (noted as a follow-up in prior plans but out of scope).
25
+ - Refactoring callers (`parent-snapshot.ts`) — they are already tested via mocks.
26
+
27
+ ## Background
28
+
29
+ ### Current file: `src/session/context.ts`
30
+
31
+ The file exports two functions:
32
+
33
+ 1. `extractText(content: unknown[]): string` — filters an array of content blocks to `TextContent` items and joins their `.text` values.
34
+ Used by `agent-runner.ts`, `message-formatters.ts`, and `buildParentContext` itself.
35
+ 2. `buildParentContext(ctx: SessionContext): string` — iterates session branch entries, formatting `message` entries (user/assistant) and `compaction` entries into a text representation prefixed with a header.
36
+
37
+ The file also defines three local types (`MessageEntry`, `CompactionEntry`, `BranchEntry`) and one helper (`isTextContent`).
38
+
39
+ ### Callers
40
+
41
+ - `buildParentContext` is called only from `parent-snapshot.ts` (where it is mocked in tests).
42
+ - `extractText` is called from `agent-runner.ts`, `message-formatters.ts`, and internally within `buildParentContext`.
43
+
44
+ ### Existing tests
45
+
46
+ There are no direct unit tests for `context.ts`.
47
+ `parent-snapshot.test.ts` mocks `buildParentContext` entirely, so the formatting logic is currently untested.
48
+
49
+ ## Design Overview
50
+
51
+ ### Extracted formatters
52
+
53
+ Each formatter takes a typed entry and returns `string | undefined` (undefined when the entry should be skipped):
54
+
55
+ ```typescript
56
+ function formatMessageEntry(entry: MessageEntry): string | undefined {
57
+ const msg = entry.message;
58
+ const text =
59
+ typeof msg.content === "string"
60
+ ? msg.content
61
+ : extractText(msg.content);
62
+ if (!text.trim()) return undefined;
63
+ if (msg.role === "user") return `[User]: ${text.trim()}`;
64
+ if (msg.role === "assistant") return `[Assistant]: ${text.trim()}`;
65
+ return undefined; // skip toolResult and other roles
66
+ }
67
+
68
+ function formatCompactionEntry(entry: CompactionEntry): string | undefined {
69
+ return entry.summary ? `[Summary]: ${entry.summary}` : undefined;
70
+ }
71
+ ```
72
+
73
+ ### Simplified orchestrator
74
+
75
+ ```typescript
76
+ export function buildParentContext(ctx: SessionContext): string {
77
+ const entries = ctx.sessionManager.getBranch();
78
+ if (!entries || entries.length === 0) return "";
79
+
80
+ const parts = (entries as BranchEntry[])
81
+ .map(formatBranchEntry)
82
+ .filter((p): p is string => p !== undefined);
83
+
84
+ if (parts.length === 0) return "";
85
+
86
+ return `# Parent Conversation Context
87
+ The following is the conversation history from the parent session that spawned you.
88
+ Use this context to understand what has been discussed and decided so far.
89
+
90
+ ${parts.join("\n\n")}
91
+
92
+ ---
93
+ # Your Task (below)
94
+ `;
95
+ }
96
+ ```
97
+
98
+ A thin dispatcher (`formatBranchEntry`) routes by `type`:
99
+
100
+ ```typescript
101
+ function formatBranchEntry(entry: BranchEntry): string | undefined {
102
+ if (entry.type === "message") return formatMessageEntry(entry as MessageEntry);
103
+ if (entry.type === "compaction") return formatCompactionEntry(entry as CompactionEntry);
104
+ return undefined;
105
+ }
106
+ ```
107
+
108
+ ### Complexity analysis
109
+
110
+ - `formatMessageEntry`: 3 branches (string-vs-array, empty check, role) — estimated cognitive complexity ~4.
111
+ - `formatCompactionEntry`: 1 branch — estimated cognitive complexity ~1.
112
+ - `formatBranchEntry`: 2 branches — estimated cognitive complexity ~2.
113
+ - `buildParentContext`: 2 branches (empty entries, empty parts) — estimated cognitive complexity ~3.
114
+
115
+ All well under the < 10 target.
116
+
117
+ ## Module-Level Changes
118
+
119
+ ### `src/session/context.ts`
120
+
121
+ 1. Add `formatMessageEntry(entry: MessageEntry): string | undefined` — private helper.
122
+ 2. Add `formatCompactionEntry(entry: CompactionEntry): string | undefined` — private helper.
123
+ 3. Add `formatBranchEntry(entry: BranchEntry): string | undefined` — private dispatcher.
124
+ 4. Simplify `buildParentContext` body to use `map(formatBranchEntry).filter(...)`.
125
+ 5. No changes to exports — `buildParentContext` and `extractText` signatures are unchanged.
126
+ 6. No changes to local types (`MessageEntry`, `CompactionEntry`, `BranchEntry`) or `isTextContent`.
127
+
128
+ ### `test/session/context.test.ts` (new)
129
+
130
+ Unit tests for:
131
+
132
+ - `extractText` — string extraction from mixed content arrays.
133
+ - `buildParentContext` — end-to-end formatting with user, assistant, compaction, and skipped entries.
134
+
135
+ The formatters are private, so they are tested indirectly through `buildParentContext`.
136
+
137
+ ## Test Impact Analysis
138
+
139
+ 1. The new `context.test.ts` enables direct testing of formatting logic that was previously untested (mocked away in `parent-snapshot.test.ts`).
140
+ 2. No existing tests become redundant — `parent-snapshot.test.ts` tests snapshot assembly, not formatting.
141
+ 3. No existing tests need modification — the public API is unchanged.
142
+
143
+ ## TDD Order
144
+
145
+ 1. **Red → Green:** Add `test/session/context.test.ts` with tests for `extractText` — empty array, text-only, mixed content types, no text content.
146
+ Commit: `test: add extractText unit tests (#215)`
147
+
148
+ 2. **Red → Green:** Add tests for `buildParentContext` — empty branch, user messages, assistant messages, compaction entries with/without summary, mixed entry types, entries with empty text (skipped), non-message/non-compaction entries (skipped), string vs array content.
149
+ Commit: `test: add buildParentContext unit tests (#215)`
150
+
151
+ 3. **Refactor:** Extract `formatMessageEntry`, `formatCompactionEntry`, and `formatBranchEntry` from `buildParentContext`.
152
+ Simplify `buildParentContext` to map/filter/join.
153
+ All tests from steps 1–2 must still pass.
154
+ Commit: `refactor: decompose buildParentContext into per-entry formatters (#215)`
155
+
156
+ ## Risks and Mitigations
157
+
158
+ | Risk | Mitigation |
159
+ | -------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- |
160
+ | Behavioral regression in formatting | Steps 1–2 lock in current behavior with tests before refactoring |
161
+ | Extracted helpers expose implementation details | Helpers are private (not exported); tested indirectly via public API |
162
+ | `eslint-disable` comment for `no-unnecessary-condition` on `getBranch()` check may need adjustment | Preserve the comment — runtime nullability is documented |
163
+
164
+ ## Open Questions
165
+
166
+ None — the decomposition target and strategy are specified by the architecture roadmap.
@@ -0,0 +1,35 @@
1
+ ---
2
+ issue: 215
3
+ issue_title: "Decompose buildParentContext (cognitive 30) (Phase 13, Step 2)"
4
+ ---
5
+
6
+ # Retro: #215 — Decompose buildParentContext
7
+
8
+ ## Stage: Planning (2026-05-25T12:00:00Z)
9
+
10
+ ### Session summary
11
+
12
+ Produced a 3-step TDD plan to decompose `buildParentContext` in `src/session/context.ts`.
13
+ Steps 1–2 add tests locking current behavior for `extractText` and `buildParentContext`; step 3 extracts three private helpers (`formatMessageEntry`, `formatCompactionEntry`, `formatBranchEntry`) and simplifies the orchestrator to map/filter/join.
14
+
15
+ ### Observations
16
+
17
+ - No existing unit tests cover `context.ts` — `parent-snapshot.test.ts` mocks `buildParentContext` entirely, so the formatting logic is currently untested.
18
+ - The decomposition is straightforward with no design ambiguity; the architecture roadmap specifies the exact extraction targets.
19
+ - All extracted helpers remain private (not exported), keeping the public API surface unchanged.
20
+ - The `eslint-disable` comment on the `getBranch()` nullability check must be preserved through the refactoring step.
21
+
22
+ ## Stage: Implementation — TDD (2026-05-25T22:36:00Z)
23
+
24
+ ### Session summary
25
+
26
+ Completed all 3 TDD steps: 2 test-only commits locking `extractText` (5 tests) and `buildParentContext` (14 tests) behavior, then a refactor commit extracting `formatMessageEntry`, `formatCompactionEntry`, and `formatBranchEntry`.
27
+ Test count increased from 939 to 958 (+19).
28
+ All checks green: full suite, `pnpm run check`, `pnpm run lint`, `pnpm fallow dead-code`.
29
+
30
+ ### Observations
31
+
32
+ - Because `extractText` and `buildParentContext` already existed, both test steps passed immediately (no red phase) — this is correct for behavior-locking tests before a refactor.
33
+ - The `makeCtx` helper in the test file creates a minimal `SessionContext` satisfying only `sessionManager.getBranch()`; the extra required fields (`cwd`, `model`, `modelRegistry`, `getSystemPrompt`) are satisfied with stubs.
34
+ - The `eslint-disable` comment on the `getBranch()` nullability check was preserved unchanged through the refactor.
35
+ - No deviations from the plan.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gotgenes/pi-subagents",
3
- "version": "7.4.0",
3
+ "version": "7.5.1",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/service.ts"
@@ -11,6 +11,7 @@ import {
11
11
  import type { AgentConfigLookup } from "#src/config/agent-types";
12
12
  import type { ParentSessionInfo } from "#src/lifecycle/agent-manager";
13
13
  import type { ParentSnapshot } from "#src/lifecycle/parent-snapshot";
14
+ import { registerChildSession, unregisterChildSession } from "#src/lifecycle/permission-bridge";
14
15
  import { extractAssistantContent } from "#src/session/content-items";
15
16
  import { extractText } from "#src/session/context";
16
17
  import type { EnvInfo } from "#src/session/env";
@@ -347,6 +348,15 @@ export async function runAgent(
347
348
  session.setActiveToolsByName(filtered);
348
349
  }
349
350
 
351
+ // Register with pi-permission-system's SubagentSessionRegistry before
352
+ // bindExtensions() so isSubagentExecutionContext() hits the registry on the
353
+ // first check during child extension initialization. Unregistered in the
354
+ // finally block below to guarantee cleanup on both success and error paths.
355
+ registerChildSession(sessionDir, {
356
+ agentName: type,
357
+ parentSessionId: options.context.parentSession?.parentSessionId,
358
+ });
359
+
350
360
  // Bind extensions so that session_start fires and extensions can initialize
351
361
  // (e.g. loading credentials, setting up state). Placed after tool filtering
352
362
  // so extension-provided skills/prompts from extendResourcesFromExtensions()
@@ -406,6 +416,7 @@ export async function runAgent(
406
416
  unsubTurns();
407
417
  collector.unsubscribe();
408
418
  cleanupAbort();
419
+ unregisterChildSession(sessionDir);
409
420
  }
410
421
 
411
422
  const responseText =
@@ -0,0 +1,63 @@
1
+ /**
2
+ * permission-bridge.ts — Cross-extension bridge to @gotgenes/pi-permission-system.
3
+ *
4
+ * pi-subagents does not import pi-permission-system directly. Instead it
5
+ * accesses the published PermissionsService via a process-global Symbol.for()
6
+ * key, the same mechanism pi-permission-system uses to publish itself.
7
+ *
8
+ * When pi-permission-system is not installed, getPermissionsService() returns
9
+ * undefined and all registration calls are silent no-ops.
10
+ */
11
+
12
+ /**
13
+ * The two PermissionsService methods pi-subagents needs.
14
+ *
15
+ * Follows ISP — does not expose the full PermissionsService surface
16
+ * (checkPermission, getToolPermission, etc.) to avoid coupling.
17
+ */
18
+ interface PermissionsServiceConsumer {
19
+ registerSubagentSession(
20
+ sessionKey: string,
21
+ info: { parentSessionId?: string; agentName: string },
22
+ ): void;
23
+ unregisterSubagentSession(sessionKey: string): void;
24
+ }
25
+
26
+ const PERMISSION_SERVICE_KEY = Symbol.for(
27
+ "@gotgenes/pi-permission-system:service",
28
+ );
29
+
30
+ function getPermissionsService(): PermissionsServiceConsumer | undefined {
31
+ return (globalThis as Record<symbol, unknown>)[
32
+ PERMISSION_SERVICE_KEY
33
+ ] as PermissionsServiceConsumer | undefined;
34
+ }
35
+
36
+ /**
37
+ * Register a child session with pi-permission-system's SubagentSessionRegistry.
38
+ *
39
+ * Must be called after deriving sessionDir but before session.bindExtensions()
40
+ * so isSubagentExecutionContext() hits the registry on the first check during
41
+ * child extension initialization.
42
+ *
43
+ * @param sessionKey - The session directory path (unique per session).
44
+ * @param info - Agent name and optional parent session ID for forwarding.
45
+ */
46
+ export function registerChildSession(
47
+ sessionKey: string,
48
+ info: { parentSessionId?: string; agentName: string },
49
+ ): void {
50
+ getPermissionsService()?.registerSubagentSession(sessionKey, info);
51
+ }
52
+
53
+ /**
54
+ * Unregister a child session from pi-permission-system's SubagentSessionRegistry.
55
+ *
56
+ * Must be called in a finally block so cleanup happens on both success and
57
+ * error paths.
58
+ *
59
+ * @param sessionKey - The session directory path used during registration.
60
+ */
61
+ export function unregisterChildSession(sessionKey: string): void {
62
+ getPermissionsService()?.unregisterSubagentSession(sessionKey);
63
+ }
@@ -30,6 +30,28 @@ export function extractText(content: unknown[]): string {
30
30
  .join("\n");
31
31
  }
32
32
 
33
+ /** Format a message entry (user/assistant); returns undefined for roles to skip. */
34
+ function formatMessageEntry(entry: MessageEntry): string | undefined {
35
+ const msg = entry.message;
36
+ const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
37
+ if (!text.trim()) return undefined;
38
+ if (msg.role === "user") return `[User]: ${text.trim()}`;
39
+ if (msg.role === "assistant") return `[Assistant]: ${text.trim()}`;
40
+ return undefined; // skip toolResult and other roles
41
+ }
42
+
43
+ /** Format a compaction entry; returns undefined when no summary is present. */
44
+ function formatCompactionEntry(entry: CompactionEntry): string | undefined {
45
+ return entry.summary ? `[Summary]: ${entry.summary}` : undefined;
46
+ }
47
+
48
+ /** Dispatch a branch entry to the appropriate formatter. */
49
+ function formatBranchEntry(entry: BranchEntry): string | undefined {
50
+ if (entry.type === "message") return formatMessageEntry(entry as MessageEntry);
51
+ if (entry.type === "compaction") return formatCompactionEntry(entry as CompactionEntry);
52
+ return undefined;
53
+ }
54
+
33
55
  /**
34
56
  * Build a text representation of the parent conversation context.
35
57
  * Used when inherit_context is true to give the subagent visibility
@@ -40,30 +62,9 @@ export function buildParentContext(ctx: SessionContext): string {
40
62
  // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- getBranch() may return undefined at runtime despite its type
41
63
  if (!entries || entries.length === 0) return "";
42
64
 
43
- const parts: string[] = [];
44
-
45
- for (const rawEntry of entries as BranchEntry[]) {
46
- if (rawEntry.type === "message") {
47
- const entry = rawEntry as MessageEntry;
48
- const msg = entry.message;
49
- if (msg.role === "user") {
50
- const text = typeof msg.content === "string"
51
- ? msg.content
52
- : extractText(msg.content);
53
- if (text.trim()) parts.push(`[User]: ${text.trim()}`);
54
- } else if (msg.role === "assistant") {
55
- const text = typeof msg.content === "string" ? msg.content : extractText(msg.content);
56
- if (text.trim()) parts.push(`[Assistant]: ${text.trim()}`);
57
- }
58
- // Skip toolResult messages — too verbose for context
59
- } else if (rawEntry.type === "compaction") {
60
- // Include compaction summaries — they're already condensed
61
- const entry = rawEntry as CompactionEntry;
62
- if (entry.summary) {
63
- parts.push(`[Summary]: ${entry.summary}`);
64
- }
65
- }
66
- }
65
+ const parts = (entries as BranchEntry[])
66
+ .map(formatBranchEntry)
67
+ .filter((p): p is string => p !== undefined);
67
68
 
68
69
  if (parts.length === 0) return "";
69
70
 
@@ -72,9 +72,9 @@ export class AgentsMenuHandler {
72
72
  private readonly registry: AgentTypeRegistry,
73
73
  private readonly agentActivity: AgentActivityReader,
74
74
  private readonly settings: AgentMenuSettings,
75
- private readonly fileOps: AgentFileOps,
76
- private readonly personalAgentsDir: string,
77
- private readonly projectAgentsDir: string,
75
+ fileOps: AgentFileOps,
76
+ personalAgentsDir: string,
77
+ projectAgentsDir: string,
78
78
  ) {
79
79
  this.editor = new AgentConfigEditor(
80
80
  fileOps,