@gotgenes/pi-permission-system 8.3.1 → 9.0.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 +34 -0
- package/package.json +1 -1
- package/src/forwarded-permissions/polling.ts +1 -2
- package/src/handlers/lifecycle.ts +9 -0
- package/src/index.ts +30 -11
- package/src/permission-events.ts +3 -2
- package/src/permission-forwarding.ts +4 -4
- package/src/service.ts +17 -4
- package/src/subagent-context.ts +33 -5
- package/src/subagent-lifecycle-events.ts +6 -6
- package/src/subagent-registry.ts +29 -21
- package/test/composition-root.test.ts +398 -0
- package/test/handlers/lifecycle.test.ts +15 -2
- package/test/helpers/make-fake-pi.ts +95 -0
- package/test/permission-events.test.ts +32 -2
- package/test/permission-forwarding.test.ts +12 -15
- package/test/permission-system.test.ts +16 -34
- package/test/service.test.ts +25 -6
- package/test/subagent-context.test.ts +79 -17
- package/test/subagent-lifecycle-events.test.ts +37 -18
- package/test/subagent-registry.test.ts +51 -44
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ 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.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [9.0.0](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v8.3.2...pi-permission-system-v9.0.0) (2026-06-01)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### ⚠ BREAKING CHANGES
|
|
12
|
+
|
|
13
|
+
* unpublishPermissionsService() now requires the service to remove as its sole argument. The package's public export is service.ts, so this changes the published API surface.
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* scope service teardown to the publishing instance ([#302](https://github.com/gotgenes/pi-packages/issues/302)) ([72180e9](https://github.com/gotgenes/pi-packages/commit/72180e906f7370c842cd5e31a11726c2971fc988))
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* keep the parent's service published across child shutdown ([#302](https://github.com/gotgenes/pi-packages/issues/302)) ([300214c](https://github.com/gotgenes/pi-packages/commit/300214ca21d985bfba7231f261c022c394d8bf5a))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
### Documentation
|
|
26
|
+
|
|
27
|
+
* document session_start service publication and ready timing ([#302](https://github.com/gotgenes/pi-packages/issues/302)) ([a894fb8](https://github.com/gotgenes/pi-packages/commit/a894fb8d5c2bbc7cd9d33769859d172c5a7dbb73))
|
|
28
|
+
|
|
29
|
+
## [8.3.2](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v8.3.1...pi-permission-system-v8.3.2) (2026-06-01)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
### Bug Fixes
|
|
33
|
+
|
|
34
|
+
* **pi-permission-system:** key subagent registry by session id and drop vestigial agentName ([d299c54](https://github.com/gotgenes/pi-packages/commit/d299c5421f41ab0829fb83fcf4e030d1c7af6d56))
|
|
35
|
+
* **pi-permission-system:** resolve subagent detection and forwarding target by session id ([0f7e079](https://github.com/gotgenes/pi-packages/commit/0f7e0795b911797e645f4d44c42bb314bf0cb103))
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
### Documentation
|
|
39
|
+
|
|
40
|
+
* **retro:** add retro notes for issue [#296](https://github.com/gotgenes/pi-packages/issues/296) ([75743ab](https://github.com/gotgenes/pi-packages/commit/75743abe92604de142ff6e77c9c0fbc44266e12a))
|
|
41
|
+
|
|
8
42
|
## [8.3.1](https://github.com/gotgenes/pi-packages/compare/pi-permission-system-v8.3.0...pi-permission-system-v8.3.1) (2026-06-01)
|
|
9
43
|
|
|
10
44
|
|
package/package.json
CHANGED
|
@@ -105,7 +105,6 @@ export async function waitForForwardedPermissionApproval(
|
|
|
105
105
|
deps: PermissionForwardingDeps,
|
|
106
106
|
): Promise<PermissionPromptDecision> {
|
|
107
107
|
const requesterSessionId = getSessionId(ctx);
|
|
108
|
-
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
109
108
|
const targetSessionId = resolvePermissionForwardingTargetSessionId({
|
|
110
109
|
hasUI: ctx.hasUI,
|
|
111
110
|
isSubagent: isSubagentExecutionContext(
|
|
@@ -115,7 +114,7 @@ export async function waitForForwardedPermissionApproval(
|
|
|
115
114
|
),
|
|
116
115
|
currentSessionId: requesterSessionId,
|
|
117
116
|
env: process.env,
|
|
118
|
-
|
|
117
|
+
sessionId: requesterSessionId,
|
|
119
118
|
registry: deps.registry,
|
|
120
119
|
});
|
|
121
120
|
|
|
@@ -18,11 +18,14 @@ interface ResourcesDiscoverPayload {
|
|
|
18
18
|
*
|
|
19
19
|
* Constructor deps:
|
|
20
20
|
* - `session` — encapsulates all mutable session state
|
|
21
|
+
* - `activateService` — publishes the process-global service for this session
|
|
22
|
+
* (skipped for in-process subagent children) and emits the ready event
|
|
21
23
|
* - `cleanupRpc` — unsubscribes RPC handlers on shutdown
|
|
22
24
|
*/
|
|
23
25
|
export class SessionLifecycleHandler {
|
|
24
26
|
constructor(
|
|
25
27
|
private readonly session: PermissionSession,
|
|
28
|
+
private readonly activateService: (ctx: ExtensionContext) => void,
|
|
26
29
|
private readonly cleanupRpc: () => void,
|
|
27
30
|
) {}
|
|
28
31
|
|
|
@@ -47,6 +50,12 @@ export class SessionLifecycleHandler {
|
|
|
47
50
|
cwd: ctx.cwd,
|
|
48
51
|
});
|
|
49
52
|
}
|
|
53
|
+
|
|
54
|
+
// Publish the process-global service now that a ctx (and therefore the
|
|
55
|
+
// session id) is available, so an in-process subagent child can be
|
|
56
|
+
// identified and excluded. Emitting ready here keeps the
|
|
57
|
+
// service-resolvable-when-ready ordering contract.
|
|
58
|
+
this.activateService(ctx);
|
|
50
59
|
return Promise.resolve();
|
|
51
60
|
}
|
|
52
61
|
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type {
|
|
2
|
+
ExtensionAPI,
|
|
3
|
+
ExtensionContext,
|
|
4
|
+
} from "@earendil-works/pi-coding-agent";
|
|
2
5
|
import { registerBuiltinToolInputFormatters } from "./builtin-tool-input-formatters";
|
|
3
6
|
import { registerPermissionSystemCommand } from "./config-modal";
|
|
4
7
|
import { getGlobalConfigPath } from "./config-paths";
|
|
@@ -27,7 +30,10 @@ import {
|
|
|
27
30
|
unpublishPermissionsService,
|
|
28
31
|
} from "./service";
|
|
29
32
|
import { createSessionLogger } from "./session-logger";
|
|
30
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
isRegisteredSubagentChild,
|
|
35
|
+
isSubagentExecutionContext,
|
|
36
|
+
} from "./subagent-context";
|
|
31
37
|
import { subscribeSubagentLifecycle } from "./subagent-lifecycle-events";
|
|
32
38
|
import { getSubagentSessionRegistry } from "./subagent-registry";
|
|
33
39
|
import { ToolInputFormatterRegistry } from "./tool-input-formatter-registry";
|
|
@@ -129,7 +135,18 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
129
135
|
return formatterRegistry.register(toolName, formatter);
|
|
130
136
|
},
|
|
131
137
|
};
|
|
132
|
-
|
|
138
|
+
|
|
139
|
+
// Publish the service to the process-global slot only when this instance is
|
|
140
|
+
// not an in-process subagent child, then emit ready. Deferred to
|
|
141
|
+
// session_start (vs. factory init) because identifying a child requires the
|
|
142
|
+
// session id from ctx, which the factory body does not have. A registered
|
|
143
|
+
// child therefore never clobbers the parent's published service. See #302.
|
|
144
|
+
const activateServiceForSession = (ctx: ExtensionContext): void => {
|
|
145
|
+
if (!isRegisteredSubagentChild(ctx, subagentRegistry)) {
|
|
146
|
+
publishPermissionsService(permissionsService);
|
|
147
|
+
}
|
|
148
|
+
emitReadyEvent(pi.events);
|
|
149
|
+
};
|
|
133
150
|
|
|
134
151
|
// Subscribe to @gotgenes/pi-subagents' child lifecycle events so child
|
|
135
152
|
// sessions register/unregister without the core calling us (ADR 0002).
|
|
@@ -138,19 +155,21 @@ export default function piPermissionSystemExtension(pi: ExtensionAPI): void {
|
|
|
138
155
|
subagentRegistry,
|
|
139
156
|
);
|
|
140
157
|
|
|
141
|
-
emitReadyEvent(pi.events);
|
|
142
|
-
|
|
143
158
|
const toolRegistry = {
|
|
144
159
|
getAll: () => pi.getAllTools(),
|
|
145
160
|
setActive: (names: string[]) => pi.setActiveTools(names),
|
|
146
161
|
};
|
|
147
162
|
|
|
148
|
-
const lifecycle = new SessionLifecycleHandler(
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
163
|
+
const lifecycle = new SessionLifecycleHandler(
|
|
164
|
+
session,
|
|
165
|
+
activateServiceForSession,
|
|
166
|
+
() => {
|
|
167
|
+
rpcHandles.unsubCheck();
|
|
168
|
+
rpcHandles.unsubPrompt();
|
|
169
|
+
unsubSubagentLifecycle();
|
|
170
|
+
unpublishPermissionsService(permissionsService);
|
|
171
|
+
},
|
|
172
|
+
);
|
|
154
173
|
const agentPrep = new AgentPrepHandler(session, toolRegistry);
|
|
155
174
|
const gates = new PermissionGateHandler(
|
|
156
175
|
session,
|
package/src/permission-events.ts
CHANGED
|
@@ -24,7 +24,7 @@ export const PERMISSIONS_PROTOCOL_VERSION = 1;
|
|
|
24
24
|
|
|
25
25
|
// ── Channel name constants ─────────────────────────────────────────────────
|
|
26
26
|
|
|
27
|
-
/** Emitted
|
|
27
|
+
/** Emitted at `session_start`, after the service is published. */
|
|
28
28
|
export const PERMISSIONS_READY_CHANNEL = "permissions:ready";
|
|
29
29
|
|
|
30
30
|
/** Emitted after every permission gate resolution. */
|
|
@@ -160,7 +160,8 @@ export interface PermissionsPromptReplyData {
|
|
|
160
160
|
|
|
161
161
|
/**
|
|
162
162
|
* Emit the `permissions:ready` broadcast.
|
|
163
|
-
* Call
|
|
163
|
+
* Call at `session_start`, after the service is published, so a consumer
|
|
164
|
+
* reacting to ready can immediately resolve `getPermissionsService()`.
|
|
164
165
|
*/
|
|
165
166
|
export function emitReadyEvent(events: PermissionEventBus): void {
|
|
166
167
|
const payload: PermissionsReadyEvent = {
|
|
@@ -119,8 +119,8 @@ export function resolvePermissionForwardingTargetSessionId(options: {
|
|
|
119
119
|
isSubagent: boolean;
|
|
120
120
|
currentSessionId?: string | null;
|
|
121
121
|
env?: NodeJS.ProcessEnv;
|
|
122
|
-
/**
|
|
123
|
-
|
|
122
|
+
/** Child session id for registry lookup. */
|
|
123
|
+
sessionId?: string;
|
|
124
124
|
/** In-process subagent session registry (checked before env vars). */
|
|
125
125
|
registry?: SubagentSessionRegistry;
|
|
126
126
|
}): string | null {
|
|
@@ -133,8 +133,8 @@ export function resolvePermissionForwardingTargetSessionId(options: {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
// 1. Registry — in-process subagents register parentSessionId explicitly.
|
|
136
|
-
if (options.registry && options.
|
|
137
|
-
const entry = options.registry.get(options.
|
|
136
|
+
if (options.registry && options.sessionId) {
|
|
137
|
+
const entry = options.registry.get(options.sessionId);
|
|
138
138
|
const resolved = normalizePermissionForwardingSessionId(
|
|
139
139
|
entry?.parentSessionId,
|
|
140
140
|
);
|
package/src/service.ts
CHANGED
|
@@ -81,7 +81,10 @@ export interface PermissionsService {
|
|
|
81
81
|
* Store a `PermissionsService` on `globalThis` so other extensions can
|
|
82
82
|
* retrieve it via `getPermissionsService()`.
|
|
83
83
|
*
|
|
84
|
-
*
|
|
84
|
+
* Called at `session_start` by the top-level (parent) instance only — an
|
|
85
|
+
* in-process subagent child skips publishing so it cannot clobber the parent's
|
|
86
|
+
* service. Overwrites any previously published service, which keeps `/reload`
|
|
87
|
+
* working: a reloaded parent re-publishes its fresh service.
|
|
85
88
|
*/
|
|
86
89
|
export function publishPermissionsService(service: PermissionsService): void {
|
|
87
90
|
(globalThis as Record<symbol, unknown>)[SERVICE_KEY] = service;
|
|
@@ -98,12 +101,22 @@ export function getPermissionsService(): PermissionsService | undefined {
|
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/**
|
|
101
|
-
* Remove
|
|
104
|
+
* Remove `service` from `globalThis`, but only when the current slot still
|
|
105
|
+
* holds it (identity compare-and-delete).
|
|
102
106
|
*
|
|
103
107
|
* Called during `session_shutdown` to avoid stale references after the
|
|
104
|
-
* extension is torn down.
|
|
108
|
+
* extension is torn down. Scoping the delete to the publishing instance keeps
|
|
109
|
+
* two cases correct:
|
|
110
|
+
*
|
|
111
|
+
* - An in-process subagent child never published the parent's service, so its
|
|
112
|
+
* shutdown is a no-op and the parent's slot survives.
|
|
113
|
+
* - A superseded `/reload` generation no longer owns the slot, so its late
|
|
114
|
+
* shutdown cannot wipe the new generation's freshly published service.
|
|
105
115
|
*/
|
|
106
|
-
export function unpublishPermissionsService(): void {
|
|
116
|
+
export function unpublishPermissionsService(service: PermissionsService): void {
|
|
117
|
+
if (getPermissionsService() !== service) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
107
120
|
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Symbol-keyed global property; Map.delete() is not applicable
|
|
108
121
|
delete (globalThis as Record<symbol, unknown>)[SERVICE_KEY];
|
|
109
122
|
}
|
package/src/subagent-context.ts
CHANGED
|
@@ -28,19 +28,47 @@ function isPathWithinDirectoryForSubagent(
|
|
|
28
28
|
return pathValue.startsWith(prefix);
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Return `true` when `ctx` belongs to an in-process subagent child registered
|
|
33
|
+
* in `registry` by its session id.
|
|
34
|
+
*
|
|
35
|
+
* This is the only signal that identifies an **in-process** child (one sharing
|
|
36
|
+
* the parent's `globalThis`); env-hint and filesystem heuristics identify
|
|
37
|
+
* **process-based** subagents instead. The composition root uses this to decide
|
|
38
|
+
* whether the instance owns the process-global service slot — a registered
|
|
39
|
+
* child must not publish over its parent.
|
|
40
|
+
*/
|
|
41
|
+
export function isRegisteredSubagentChild(
|
|
42
|
+
ctx: ExtensionContext,
|
|
43
|
+
registry: SubagentSessionRegistry,
|
|
44
|
+
): boolean {
|
|
45
|
+
try {
|
|
46
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
47
|
+
if (!sessionId) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
return registry.has(sessionId);
|
|
51
|
+
} catch {
|
|
52
|
+
// getSessionId() unavailable — treat as not-a-registered-child.
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
31
57
|
export function isSubagentExecutionContext(
|
|
32
58
|
ctx: ExtensionContext,
|
|
33
59
|
subagentSessionsDir: string,
|
|
34
60
|
registry?: SubagentSessionRegistry,
|
|
35
61
|
): boolean {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
if (registry &&
|
|
62
|
+
// 1. Explicit registry — in-process subagent extensions register by child
|
|
63
|
+
// session id before bindExtensions(); checked first so it takes priority
|
|
64
|
+
// over heuristics. Each concurrent sibling has a unique session id, so
|
|
65
|
+
// one sibling's disposed event cannot affect another's registration.
|
|
66
|
+
if (registry && isRegisteredSubagentChild(ctx, registry)) {
|
|
41
67
|
return true;
|
|
42
68
|
}
|
|
43
69
|
|
|
70
|
+
const sessionDir = ctx.sessionManager.getSessionDir();
|
|
71
|
+
|
|
44
72
|
// 2. Env vars — process-based subagent extensions (nicobailon/pi-subagents,
|
|
45
73
|
// HazAT/pi-interactive-subagents, pi-agent-router, etc.).
|
|
46
74
|
for (const key of SUBAGENT_ENV_HINT_KEYS) {
|
|
@@ -32,14 +32,15 @@ interface LifecycleEventBus {
|
|
|
32
32
|
|
|
33
33
|
/** Fields read from the `session-created` payload (ISP). */
|
|
34
34
|
interface ChildSessionCreatedEvent {
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/** Child session id — the registry key. Must match the publisher. */
|
|
36
|
+
sessionId: string;
|
|
37
37
|
parentSessionId?: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/** Fields read from the `disposed` payload (ISP). */
|
|
41
41
|
interface ChildDisposedEvent {
|
|
42
|
-
|
|
42
|
+
/** Child session id — the registry key. Must match the publisher. */
|
|
43
|
+
sessionId: string;
|
|
43
44
|
}
|
|
44
45
|
|
|
45
46
|
/**
|
|
@@ -54,15 +55,14 @@ export function subscribeSubagentLifecycle(
|
|
|
54
55
|
): () => void {
|
|
55
56
|
const unsubCreated = events.on(SUBAGENT_CHILD_SESSION_CREATED, (data) => {
|
|
56
57
|
const event = data as ChildSessionCreatedEvent;
|
|
57
|
-
registry.register(event.
|
|
58
|
-
agentName: event.agentName,
|
|
58
|
+
registry.register(event.sessionId, {
|
|
59
59
|
parentSessionId: event.parentSessionId,
|
|
60
60
|
});
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
const unsubDisposed = events.on(SUBAGENT_CHILD_DISPOSED, (data) => {
|
|
64
64
|
const event = data as ChildDisposedEvent;
|
|
65
|
-
registry.unregister(event.
|
|
65
|
+
registry.unregister(event.sessionId);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
return () => {
|
package/src/subagent-registry.ts
CHANGED
|
@@ -7,15 +7,22 @@
|
|
|
7
7
|
* can detect them without relying on environment variables or filesystem
|
|
8
8
|
* heuristics.
|
|
9
9
|
*
|
|
10
|
-
* The registry is keyed by session
|
|
11
|
-
*
|
|
12
|
-
* `
|
|
10
|
+
* The registry is keyed by the child's **session id**, which is unique per
|
|
11
|
+
* child and available to both producer (via `sessionManager.getSessionId()`
|
|
12
|
+
* after `newSession()` in `create-subagent-session.ts`) and consumer (via
|
|
13
|
+
* `ctx.sessionManager.getSessionId()`). Two concurrent siblings of the same
|
|
14
|
+
* parent therefore occupy distinct keys, so one sibling's `disposed` event
|
|
15
|
+
* cannot evict the entry the others depend on.
|
|
13
16
|
*
|
|
14
17
|
* The single registry instance is stored on `globalThis` (via `Symbol.for()`)
|
|
15
18
|
* so that the parent's permission-system instance (which registers children
|
|
16
19
|
* on the parent's event bus) and each child's separate jiti instance (which
|
|
17
20
|
* reads the registry to detect itself and resolve its forwarding target) share
|
|
18
21
|
* one store across per-session event buses. See `getSubagentSessionRegistry()`.
|
|
22
|
+
*
|
|
23
|
+
* When a future code path needs the child's agent name, read it from
|
|
24
|
+
* `tcc.agentName` (resolved from the `<active_agent>` system-prompt tag) —
|
|
25
|
+
* not from this registry.
|
|
19
26
|
*/
|
|
20
27
|
|
|
21
28
|
/** Process-global key for the shared registry slot. */
|
|
@@ -53,19 +60,20 @@ export function getSubagentSessionRegistry(): SubagentSessionRegistry {
|
|
|
53
60
|
export interface SubagentSessionInfo {
|
|
54
61
|
/** Parent session ID for permission forwarding. Omit when unknown. */
|
|
55
62
|
parentSessionId?: string;
|
|
56
|
-
/** Agent name for per-agent policy resolution. */
|
|
57
|
-
agentName: string;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
65
|
/**
|
|
61
66
|
* Registry of active in-process subagent sessions.
|
|
62
67
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
68
|
+
* A process-global singleton — obtain it via `getSubagentSessionRegistry()`,
|
|
69
|
+
* never `new` (see that accessor for why). Written exclusively by
|
|
70
|
+
* `subscribeSubagentLifecycle` via the `subagents:child:session-created` /
|
|
71
|
+
* `subagents:child:disposed` event subscription (ADR 0002 — the core
|
|
72
|
+
* publishes, consumers observe).
|
|
66
73
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
74
|
+
* Keyed by child session id. Each concurrent child of the same parent receives
|
|
75
|
+
* a unique session id from `sessionManager.newSession()`, so siblings occupy
|
|
76
|
+
* distinct keys and one sibling's `disposed` cannot evict another's entry.
|
|
69
77
|
*/
|
|
70
78
|
export class SubagentSessionRegistry {
|
|
71
79
|
private readonly sessions = new Map<string, SubagentSessionInfo>();
|
|
@@ -73,25 +81,25 @@ export class SubagentSessionRegistry {
|
|
|
73
81
|
/**
|
|
74
82
|
* Register an in-process subagent session.
|
|
75
83
|
*
|
|
76
|
-
* If a previous entry exists for `
|
|
84
|
+
* If a previous entry exists for `sessionId`, it is overwritten
|
|
77
85
|
* (last-write-wins; single-writer expected per key).
|
|
78
86
|
*/
|
|
79
|
-
register(
|
|
80
|
-
this.sessions.set(
|
|
87
|
+
register(sessionId: string, info: SubagentSessionInfo): void {
|
|
88
|
+
this.sessions.set(sessionId, info);
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
/** Remove a previously registered session. No-op if the key is absent. */
|
|
84
|
-
unregister(
|
|
85
|
-
this.sessions.delete(
|
|
92
|
+
unregister(sessionId: string): void {
|
|
93
|
+
this.sessions.delete(sessionId);
|
|
86
94
|
}
|
|
87
95
|
|
|
88
|
-
/** Return the registered info for `
|
|
89
|
-
get(
|
|
90
|
-
return this.sessions.get(
|
|
96
|
+
/** Return the registered info for `sessionId`, or `undefined` if absent. */
|
|
97
|
+
get(sessionId: string): SubagentSessionInfo | undefined {
|
|
98
|
+
return this.sessions.get(sessionId);
|
|
91
99
|
}
|
|
92
100
|
|
|
93
|
-
/** Return `true` when `
|
|
94
|
-
has(
|
|
95
|
-
return this.sessions.has(
|
|
101
|
+
/** Return `true` when `sessionId` has a registered entry. */
|
|
102
|
+
has(sessionId: string): boolean {
|
|
103
|
+
return this.sessions.has(sessionId);
|
|
96
104
|
}
|
|
97
105
|
}
|