@remembrance-ai/pi-plugin 0.0.0-stage → 0.2.25

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/src/index.mjs ADDED
@@ -0,0 +1,693 @@
1
+ // Pi 1.5.2 adapter. Policy, storage and recovery remain in the canonical core.
2
+ import { readFileSync } from "node:fs";
3
+ import { createHash } from "node:crypto";
4
+ import { fileURLToPath } from "node:url";
5
+ import * as hookCore from "../scripts/hook-core.mjs";
6
+ import { handlePrivateLessonSubmitApproval } from "../scripts/private-lesson-approval.mjs";
7
+ import { PRIVATE_EGRESS_TOOL_LANES } from "../scripts/private-egress-lanes.mjs";
8
+
9
+ const SURFACE = "pi";
10
+ const USER_AGENT = "@remembrance-ai/pi-plugin";
11
+ const POLICY_HOLD =
12
+ "Remembrance cannot confirm this organization's skill policy. Run run_connection_doctor, resolve the reported connection issue, then submit the task again. No private submission was authorized.";
13
+
14
+ export function createRemembranceExtension(options = {}) {
15
+ const core = { ...hookCore, ...options.core };
16
+ // Read live credentials without mutating the host process environment. The
17
+ // overlay also follows credential rotation while child processes get the
18
+ // complete current environment and only these two trusted host markers.
19
+ const sourceEnv = options.env ?? process.env;
20
+ const metadata = {
21
+ REMEMBRANCE_PLUGIN_HOST: SURFACE,
22
+ REMEMBRANCE_HOST_SURFACE: "cli",
23
+ };
24
+ const env = new Proxy(sourceEnv, {
25
+ get: (target, key) =>
26
+ Object.hasOwn(metadata, key) ? metadata[key] : Reflect.get(target, key),
27
+ ownKeys: (target) => [
28
+ ...new Set([...Reflect.ownKeys(target), ...Object.keys(metadata)]),
29
+ ],
30
+ getOwnPropertyDescriptor: (target, key) =>
31
+ Object.hasOwn(metadata, key)
32
+ ? { configurable: true, enumerable: true, value: metadata[key] }
33
+ : Reflect.getOwnPropertyDescriptor(target, key),
34
+ });
35
+ const version =
36
+ options.pluginVersion ??
37
+ JSON.parse(
38
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
39
+ ).version;
40
+ return function remembranceExtension(pi) {
41
+ let state = null;
42
+ let approvalQueue = Promise.resolve();
43
+ const sessionId = (ctx) => ctx?.sessionManager?.getSessionId?.() ?? null;
44
+ const scope = () =>
45
+ createHash("sha256")
46
+ .update(
47
+ JSON.stringify([
48
+ core.resolveApiConfiguration(env)?.apiUrl ?? "",
49
+ core.resolveApiCredential(env)?.apiKey ?? null,
50
+ ]),
51
+ )
52
+ .digest("hex");
53
+ const current = (ctx) => {
54
+ if (state?.id !== sessionId(ctx)) return null;
55
+ if (state.scope !== scope()) {
56
+ state.blocked = true;
57
+ state.scopeChanged = true;
58
+ state.startup = "";
59
+ state.prompt = null;
60
+ state.recovery = null;
61
+ // A running Pi request retains its already-built forced system head.
62
+ // Abort instead of letting that head cross a changed identity.
63
+ ctx.abort?.();
64
+ }
65
+ return state;
66
+ };
67
+ // An await releases the session boundary. Reacquire this predicate before
68
+ // another scoped effect/return, and at every core transport attempt.
69
+ const active = (local, ctx) =>
70
+ Boolean(
71
+ local && state === local && current(ctx) === local && !local.blocked,
72
+ );
73
+ const scopedFetch =
74
+ (local, ctx) =>
75
+ (...args) => {
76
+ if (!active(local, ctx))
77
+ return Promise.reject(
78
+ new Error("Remembrance session scope changed."),
79
+ );
80
+ return (options.fetchImpl ?? fetch)(...args);
81
+ };
82
+ const notify = (ctx, text, severity = "info") => {
83
+ if (ctx?.hasUI && text) ctx.ui.notify(text, severity);
84
+ };
85
+ function health(component, ctx) {
86
+ core.recordPluginLifecycleHealth(
87
+ {
88
+ surface: SURFACE,
89
+ component,
90
+ pluginVersion: version,
91
+ hostVersion: options.hostVersion ?? "",
92
+ sessionId: sessionId(ctx),
93
+ credentialSource: core.resolveApiCredential(env).source,
94
+ },
95
+ env,
96
+ );
97
+ }
98
+ pi.registerMcpServer("remembrance", {
99
+ command: process.execPath,
100
+ args: [
101
+ fileURLToPath(
102
+ new URL("../servers/remembrance-mcp.mjs", import.meta.url),
103
+ ),
104
+ ],
105
+ env: {
106
+ REMEMBRANCE_PLUGIN_HOST: SURFACE,
107
+ REMEMBRANCE_HOST_SURFACE: "cli",
108
+ },
109
+ description:
110
+ "Remembrance organization skills, preferences, feedback and protected learning",
111
+ exposure: "codemode",
112
+ toolExposure: { run_connection_doctor: "direct" },
113
+ });
114
+ pi.on("session_start", async (event, ctx) => {
115
+ const local = {
116
+ id: sessionId(ctx),
117
+ scope: scope(),
118
+ startup: "",
119
+ prompt: null,
120
+ blocked: false,
121
+ delivered: false,
122
+ observed: new Set(),
123
+ completionRequested: false,
124
+ completed: false,
125
+ };
126
+ state = local;
127
+ local.fetchImpl = scopedFetch(local, ctx);
128
+ try {
129
+ health("session_start", ctx);
130
+ await core.warmPrincipalSession(
131
+ {
132
+ runtime: SURFACE,
133
+ hostSurface: "cli",
134
+ clientVersion: version,
135
+ hostVersion: options.hostVersion ?? "",
136
+ fetchImpl: local.fetchImpl,
137
+ },
138
+ env,
139
+ );
140
+ if (!active(local, ctx)) return;
141
+ const [skills, update, snapshot] = await Promise.all([
142
+ core.sessionSkillsDirective(
143
+ {
144
+ source: event.reason,
145
+ nativeSessionId: local.id,
146
+ fetchImpl: local.fetchImpl,
147
+ },
148
+ env,
149
+ ),
150
+ core.checkForClientUpdate(
151
+ {
152
+ surface: SURFACE,
153
+ currentVersion: version,
154
+ fetchImpl: local.fetchImpl,
155
+ },
156
+ env,
157
+ ),
158
+ core.projectPreferenceSnapshot({
159
+ runtime: SURFACE,
160
+ env,
161
+ projectPath: ctx.cwd,
162
+ fetchImpl: local.fetchImpl,
163
+ userAgent: USER_AGENT,
164
+ }),
165
+ ]);
166
+ if (!active(local, ctx)) return;
167
+ const budget =
168
+ core.HOOK_CONTEXT_BUDGETS.pi ?? core.HOOK_CONTEXT_BUDGETS.opencode;
169
+ const preferences = snapshot
170
+ ? core.preferenceSnapshotText(snapshot, budget)
171
+ : null;
172
+ local.startup = core.composeBoundedHookContext(
173
+ "",
174
+ [
175
+ skills && { id: "pi_session_skills", kind: "notice", text: skills },
176
+ update?.notice && {
177
+ id: "pi_client_update",
178
+ kind: "notice",
179
+ text: update.notice,
180
+ },
181
+ preferences && {
182
+ id: "pi_preferences",
183
+ kind: "preference",
184
+ text: preferences,
185
+ },
186
+ ].filter(Boolean),
187
+ budget,
188
+ );
189
+ notify(
190
+ ctx,
191
+ `Remembrance ${version} is active. Run run_connection_doctor to verify the connection.`,
192
+ );
193
+ } catch {
194
+ if (!active(local, ctx)) return;
195
+ local.blocked = Boolean(core.resolveApiCredential(env).apiKey);
196
+ notify(ctx, POLICY_HOLD, "warning");
197
+ }
198
+ });
199
+ pi.on("input", async (event, ctx) => {
200
+ const local = current(ctx);
201
+ if (!local) return { action: "handled" };
202
+ if (local.scopeChanged) {
203
+ notify(
204
+ ctx,
205
+ "Remembrance's organization or registry changed during this session. Start a new Pi session before continuing so earlier organization context and approvals cannot cross the new connection.",
206
+ "warning",
207
+ );
208
+ return { action: "handled" };
209
+ }
210
+ // Custom continuations keep the current required context. They never
211
+ // become a fresh user instruction or a private-source producer.
212
+ if (!["interactive", "rpc"].includes(event.source))
213
+ return { action: "continue" };
214
+ local.prompt = null;
215
+ local.delivered = false;
216
+ local.completed = false;
217
+ local.completionRequested = false;
218
+ local.recovery = null;
219
+ local.requestContext = Boolean(event.streamingBehavior);
220
+ // Provenance labels are not authority. Synthetic input neither captures
221
+ // preferences nor supplies native private source; RPC is discovery only.
222
+ try {
223
+ local.scope = scope();
224
+ health("prompt_hook", ctx);
225
+ const result = await core.runPromptHook(event.text, {
226
+ ...(core.HOOK_CONTEXT_BUDGETS.pi ??
227
+ core.HOOK_CONTEXT_BUDGETS.opencode),
228
+ env,
229
+ fetchImpl: local.fetchImpl,
230
+ projectPath: ctx.cwd,
231
+ identity: { provider: SURFACE, model: ctx.model?.id ?? SURFACE },
232
+ sessionId: local.id,
233
+ nativeSessionId: null,
234
+ nativeContextBridge: options.nativeContextBridge,
235
+ userAgent: USER_AGENT,
236
+ reportDirectiveShown: false,
237
+ deferReminderAcknowledgement: true,
238
+ contextAdditions: local.startup
239
+ ? [{ id: "pi_session_start", kind: "notice", text: local.startup }]
240
+ : [],
241
+ });
242
+ if (!active(local, ctx)) return { action: "handled" };
243
+ local.blocked =
244
+ Boolean(core.resolveApiCredential(env).apiKey) &&
245
+ result?.reason === "query_unavailable";
246
+ local.prompt = result;
247
+ } catch {
248
+ if (!active(local, ctx)) return { action: "handled" };
249
+ local.blocked = Boolean(core.resolveApiCredential(env).apiKey);
250
+ }
251
+ if (local.blocked) {
252
+ notify(ctx, POLICY_HOLD, "warning");
253
+ return { action: "handled" };
254
+ }
255
+ return { action: "continue" };
256
+ });
257
+ async function contextText(local, ctx) {
258
+ const text = local.prompt?.context ?? local.startup;
259
+ if (!text) return "";
260
+ if (!local.delivered && local.prompt) {
261
+ const result = local.prompt;
262
+ local.delivered = true;
263
+ try {
264
+ if (result.eligible) core.recordTaskEligibility(local.id, env);
265
+ if (!result.preferencesOnly) {
266
+ if (result.consumed && result.matched) {
267
+ core.recordRegistryUse(local.id, env);
268
+ core.recordHighMatchSurface(
269
+ local.id,
270
+ result.highMatch ?? null,
271
+ env,
272
+ );
273
+ core.recordValueEpisodeSurface(
274
+ local.id,
275
+ result.valueEpisode ?? null,
276
+ env,
277
+ );
278
+ }
279
+ core.recordQueryFeedbackObligation(
280
+ local.id,
281
+ result.queryFeedback,
282
+ env,
283
+ );
284
+ core.recordDirectiveSurface(
285
+ local.id,
286
+ result.directive ?? null,
287
+ env,
288
+ );
289
+ core.acknowledgePrivateAuthorizationReminders(
290
+ result.context ?? "",
291
+ env,
292
+ local.id,
293
+ );
294
+ if (result.directive)
295
+ await core.reportDirectiveEvent(
296
+ {
297
+ event: "shown",
298
+ directive_id: result.directive.directive_id,
299
+ surface: "plugin_hook",
300
+ runtime: SURFACE,
301
+ trigger_reason: result.directive.trigger_reason,
302
+ },
303
+ { env, userAgent: USER_AGENT, fetchImpl: local.fetchImpl },
304
+ );
305
+ }
306
+ } catch {
307
+ // Reporting cannot erase a required context return: Pi catches hook
308
+ // exceptions and otherwise proceeds without the returned prompt.
309
+ if (active(local, ctx))
310
+ notify(
311
+ ctx,
312
+ "Remembrance context is active; completion tracking needs recovery through run_connection_doctor.",
313
+ "warning",
314
+ );
315
+ }
316
+ }
317
+ return text;
318
+ }
319
+ pi.on("before_agent_start", async (event, ctx) => {
320
+ const local = current(ctx);
321
+ if (!local) return;
322
+ if (local.blocked)
323
+ return { systemPrompt: `${event.systemPrompt}\n\n${POLICY_HOLD}` };
324
+ local.requestContext = false;
325
+ const text = await contextText(local, ctx);
326
+ if (!active(local, ctx))
327
+ return { systemPrompt: `${event.systemPrompt}\n\n${POLICY_HOLD}` };
328
+ if (!text) return;
329
+ return { systemPrompt: `${event.systemPrompt}\n\n${text}` };
330
+ });
331
+ pi.on("context", async (event, ctx) => {
332
+ const local = current(ctx);
333
+ if (!local || local.blocked || (!local.requestContext && !local.recovery))
334
+ return;
335
+ const text = [
336
+ local.requestContext ? await contextText(local, ctx) : null,
337
+ local.recovery,
338
+ ]
339
+ .filter(Boolean)
340
+ .join("\n\n");
341
+ if (!active(local, ctx)) return;
342
+ if (!text) return;
343
+ // Steering bypasses before_agent_start. This non-persisted request copy
344
+ // carries its fresh context until the next normal run rebuilds the head.
345
+ return {
346
+ messages: [
347
+ ...event.messages,
348
+ {
349
+ role: "custom",
350
+ customType: "remembrance-steering-context",
351
+ content: text,
352
+ display: false,
353
+ timestamp: Date.now(),
354
+ },
355
+ ],
356
+ };
357
+ });
358
+ pi.on("tool_call", async (event, ctx) => {
359
+ if (
360
+ current(ctx)?.blocked &&
361
+ event.toolName !== "mcp__remembrance__run_connection_doctor"
362
+ ) {
363
+ return { block: true, reason: POLICY_HOLD };
364
+ }
365
+ if (!event.toolName?.startsWith("mcp__remembrance__")) return;
366
+ const tool = event.toolName.slice("mcp__remembrance__".length);
367
+ if (!PRIVATE_EGRESS_TOOL_LANES[tool]) return;
368
+ const local = current(ctx);
369
+ const approve = async () => {
370
+ if (!active(local, ctx)) return { block: true, reason: POLICY_HOLD };
371
+ const binding = scope();
372
+ const lane = PRIVATE_EGRESS_TOOL_LANES[tool];
373
+ const receiptFile = `pi-egress-${binding}-${lane.replaceAll("_", "-")}.json`;
374
+ const receipt = core.readHookStateFile(receiptFile, { env });
375
+ const approved =
376
+ receipt?.version === 1 &&
377
+ receipt.scope === binding &&
378
+ receipt.lane === lane &&
379
+ receipt.approved === true;
380
+ const descriptor = handlePrivateLessonSubmitApproval(
381
+ { toolName: tool, input: event.input },
382
+ {
383
+ approvedLanes: new Set(approved ? [lane] : []),
384
+ },
385
+ )?.requireApproval;
386
+ if (!descriptor) return;
387
+ const unavailable = {
388
+ block: true,
389
+ reason:
390
+ "Remembrance approval is required for this exact submission lane. This blocked call sent nothing and did not change any existing local draft. Retry the same action in an interactive Pi session and choose its native approval option; existing approval for another lane does not apply.",
391
+ };
392
+ if (!ctx.hasUI || typeof ctx.ui?.select !== "function")
393
+ return unavailable;
394
+ const labels = {
395
+ "allow-once": "Allow this time",
396
+ "allow-always": "Always allow this lane",
397
+ deny: "Deny",
398
+ };
399
+ const originalArgs = JSON.stringify(event.input ?? {});
400
+ let selected;
401
+ try {
402
+ selected = await ctx.ui.select(
403
+ `${descriptor.title}\n\n${descriptor.description}\n\nAction: ${tool}\nArguments: ${originalArgs}`,
404
+ descriptor.allowedDecisions.map((decision) => labels[decision]),
405
+ { timeout: descriptor.timeoutMs },
406
+ );
407
+ } catch {
408
+ active(local, ctx);
409
+ return unavailable;
410
+ }
411
+ if (
412
+ !active(local, ctx) ||
413
+ JSON.stringify(event.input ?? {}) !== originalArgs
414
+ )
415
+ return unavailable;
416
+ const decision = descriptor.allowedDecisions.find(
417
+ (value) => labels[value] === selected,
418
+ );
419
+ if (decision !== "allow-once" && decision !== "allow-always") {
420
+ return {
421
+ block: true,
422
+ reason:
423
+ "Host policy denied this exact Remembrance action before transport. Do not retry through another transport.",
424
+ };
425
+ }
426
+ if (decision === "allow-always") {
427
+ const saved = core.writeHookStateFile(
428
+ receiptFile,
429
+ {
430
+ version: 1,
431
+ scope: binding,
432
+ lane,
433
+ approved: true,
434
+ approved_at: new Date().toISOString(),
435
+ },
436
+ { env },
437
+ );
438
+ if (!saved)
439
+ return {
440
+ block: true,
441
+ reason:
442
+ "Remembrance could not safely save this lane approval. Retry the same action and choose Allow this time, or resolve local state storage before approving the lane again.",
443
+ };
444
+ }
445
+ };
446
+ // Sibling calls are preflighted in order by Pi. Keep dialogs serialized
447
+ // for callers entering through concurrent nested tools as well.
448
+ const pending = approvalQueue.then(approve, approve);
449
+ approvalQueue = pending.then(
450
+ () => undefined,
451
+ () => undefined,
452
+ );
453
+ return pending;
454
+ });
455
+ pi.on("tool_result", async (event, ctx) => {
456
+ const local = current(ctx);
457
+ if (
458
+ !local ||
459
+ local.blocked ||
460
+ !event.toolName?.startsWith("mcp__remembrance__") ||
461
+ !event.toolCallId
462
+ )
463
+ return;
464
+ if (local.observed.has(event.toolCallId)) return;
465
+ // Pi generates nested call IDs and parentToolCallId; payload claims do
466
+ // not supply identity. Bound this in-memory window to the active run.
467
+ local.observed.add(event.toolCallId);
468
+ if (local.observed.size > 512)
469
+ local.observed.delete(local.observed.values().next().value);
470
+ const response = {
471
+ content: event.content,
472
+ structuredContent: event.structuredContent,
473
+ details: event.details,
474
+ isError: event.isError,
475
+ };
476
+ const args = event.input ?? {};
477
+ try {
478
+ health("tool_observer", ctx);
479
+ if (core.toolResponseIndicatesFailure(response)) {
480
+ const denial = core.recordHostPolicyDenial(
481
+ {
482
+ surface: SURFACE,
483
+ sessionId: local.id,
484
+ eventType: "tool_result",
485
+ toolName: event.toolName,
486
+ value: { ...response, tool_input: args },
487
+ pluginVersion: version,
488
+ },
489
+ env,
490
+ );
491
+ if (denial) {
492
+ notify(ctx, core.HOST_POLICY_ALERT_TEXT, "warning");
493
+ core.markHostPolicyAlertDelivered(
494
+ SURFACE,
495
+ local.id,
496
+ denial.id,
497
+ env,
498
+ );
499
+ }
500
+ return;
501
+ }
502
+ const tool = event.toolName.slice("mcp__remembrance__".length);
503
+ if (tool === "query_skills") {
504
+ if (core.queryResponseHasMatches(response))
505
+ core.recordRegistryUse(local.id, env);
506
+ core.recordHighMatchSurface(
507
+ local.id,
508
+ core.highMatchFromResponse(response),
509
+ env,
510
+ );
511
+ core.recordValueEpisodeSurface(
512
+ local.id,
513
+ core.valueEpisodeFromResponse(response),
514
+ env,
515
+ );
516
+ await core.recordDirectiveFollowThroughForTool(
517
+ local.id,
518
+ event.toolName,
519
+ response,
520
+ { env, fetchImpl: local.fetchImpl },
521
+ );
522
+ } else if (tool === "invoke_skill") {
523
+ const selection = core.directSelectionFromResponse(response);
524
+ if (selection) {
525
+ core.recordDirectSelectionUse(local.id, selection, env);
526
+ core.recordValueEpisodeSurface(
527
+ local.id,
528
+ core.valueEpisodeFromResponse(response),
529
+ env,
530
+ );
531
+ core.clearHighMatchSurfaceForExplicitSelection(
532
+ local.id,
533
+ selection.slug,
534
+ env,
535
+ );
536
+ }
537
+ } else {
538
+ core.clearHighMatchSurfaceIfOpened(
539
+ local.id,
540
+ event.toolName,
541
+ args,
542
+ env,
543
+ );
544
+ }
545
+ if (!active(local, ctx)) return;
546
+ await core.bindPrivateSkillNativeCapture(
547
+ event.toolName,
548
+ args,
549
+ response,
550
+ {
551
+ env,
552
+ nativeSessionId: local.id,
553
+ nativeContextBridge: (...bridgeArgs) =>
554
+ active(local, ctx)
555
+ ? (
556
+ options.nativeContextBridge ??
557
+ core.runPrivateSkillNativeContextBridge
558
+ )(...bridgeArgs)
559
+ : Promise.resolve({
560
+ status: "unavailable",
561
+ reason_code: "native_session_changed",
562
+ }),
563
+ },
564
+ );
565
+ if (!active(local, ctx)) return;
566
+ core.observeSuccessfulCompletionTool(
567
+ local.id,
568
+ event.toolName,
569
+ args,
570
+ response,
571
+ env,
572
+ );
573
+ } catch {
574
+ // Source, payloads and exception text never enter diagnostics.
575
+ if (active(local, ctx))
576
+ notify(
577
+ ctx,
578
+ "Remembrance could not record this tool's completion. Run run_connection_doctor before claiming the feedback or recovery loop is complete.",
579
+ "warning",
580
+ );
581
+ }
582
+ });
583
+ pi.on("agent_before_settle", async (event, ctx) => {
584
+ const local = current(ctx);
585
+ if (
586
+ !local ||
587
+ local.blocked ||
588
+ local.completed ||
589
+ event.outcome !== "completed"
590
+ )
591
+ return;
592
+ local.completed = true;
593
+ try {
594
+ health("completion_hook", ctx);
595
+ const messages = event.context?.contextMessages ?? [];
596
+ const last = [...messages]
597
+ .reverse()
598
+ .find((message) => message.role === "assistant");
599
+ const answer = Array.isArray(last?.content)
600
+ ? last.content
601
+ .filter((part) => part.type === "text")
602
+ .map((part) => part.text)
603
+ .join("\n")
604
+ : "";
605
+ const input = { session_id: local.id, last_assistant_message: answer };
606
+ await core.reportTaskOutcomesOnStop(local.id, input, {
607
+ env,
608
+ userAgent: USER_AGENT,
609
+ fetchImpl: local.fetchImpl,
610
+ });
611
+ if (!active(local, ctx)) return;
612
+ const decision = core.decideStop(input, {
613
+ env,
614
+ visibleCompletionRecovery: true,
615
+ });
616
+ if (
617
+ decision?.allow !== false ||
618
+ !decision.reason ||
619
+ local.completionRequested
620
+ )
621
+ return;
622
+ local.completionRequested = true;
623
+ core.writePromptedCount(local.id, decision.useCount ?? 1, env);
624
+ core.acknowledgePrivateAuthorizationReminders(
625
+ decision.reason,
626
+ env,
627
+ local.id,
628
+ );
629
+ local.recovery = core.completionContinuationReason(
630
+ decision.reason,
631
+ input,
632
+ env,
633
+ { answerShown: true },
634
+ );
635
+ // Boundary entries are persisted by Pi. Exact retained payloads stay
636
+ // only in this scoped runtime and the next request's context copy.
637
+ return {
638
+ entries: [
639
+ ...(event.entries ?? []),
640
+ {
641
+ type: "custom_message",
642
+ customType: "remembrance-completion",
643
+ content:
644
+ "Complete the pending Remembrance actions supplied in the current request context, then preserve the task result.",
645
+ display: false,
646
+ },
647
+ ],
648
+ continue: true,
649
+ };
650
+ } catch {
651
+ if (active(local, ctx))
652
+ notify(
653
+ ctx,
654
+ "Remembrance completion tracking is incomplete. Run run_connection_doctor and resume the pending action through its normal recovery instructions.",
655
+ "warning",
656
+ );
657
+ }
658
+ });
659
+ pi.on("agent_settled", async (_event, ctx) => {
660
+ const local = current(ctx);
661
+ if (!local) return;
662
+ const alert = active(local, ctx)
663
+ ? core.readPendingHostPolicyAlert(SURFACE, local.id, env)
664
+ : null;
665
+ if (alert) {
666
+ notify(ctx, alert.text ?? core.HOST_POLICY_ALERT_TEXT, "warning");
667
+ if (ctx.hasUI)
668
+ core.markHostPolicyAlertDelivered(SURFACE, local.id, alert.id, env);
669
+ }
670
+ local.observed.clear();
671
+ local.recovery = null;
672
+ });
673
+ pi.on("session_shutdown", () => {
674
+ state = null;
675
+ });
676
+ pi.on("session_tree", () => {
677
+ if (state) {
678
+ state.prompt = null;
679
+ state.delivered = false;
680
+ }
681
+ });
682
+ pi.registerCommand("remembrance", {
683
+ description: "Show Remembrance connection and recovery help",
684
+ handler: async (_args, ctx) =>
685
+ notify(
686
+ ctx,
687
+ "Run run_connection_doctor for connection health. Private submissions require the normal exact-action approval; command text does not approve them.",
688
+ ),
689
+ });
690
+ };
691
+ }
692
+
693
+ export default createRemembranceExtension();