@oh-my-pi/pi-coding-agent 14.6.3 → 14.6.5

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.
@@ -17,6 +17,15 @@ import { clearClaudePluginRootsCache } from "../../discovery/helpers";
17
17
  import { getGatewayStatus } from "../../eval/py/gateway-coordinator";
18
18
  import { loadCustomShare } from "../../export/custom-share";
19
19
  import type { CompactOptions } from "../../extensibility/extensions/types";
20
+ import {
21
+ diffMentalModelContent,
22
+ type HindsightApi,
23
+ type HindsightSessionState,
24
+ loadHindsightConfig,
25
+ reloadMentalModelsForSession,
26
+ resolveSeedsForScope,
27
+ summarizeMentalModel,
28
+ } from "../../hindsight";
20
29
  import { resolveMemoryBackend } from "../../memory-backend";
21
30
  import { BashExecutionComponent } from "../../modes/components/bash-execution";
22
31
  import { BorderedLoader } from "../../modes/components/bordered-loader";
@@ -573,7 +582,7 @@ export class CommandController {
573
582
  const backend = resolveMemoryBackend(this.ctx.settings);
574
583
 
575
584
  if (action === "view") {
576
- const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings);
585
+ const payload = await backend.buildDeveloperInstructions(agentDir, this.ctx.settings, this.ctx.session);
577
586
  if (!payload) {
578
587
  this.ctx.showWarning("Memory payload is empty (memory backend off, disabled, or no memory available).");
579
588
  return;
@@ -590,7 +599,7 @@ export class CommandController {
590
599
 
591
600
  if (action === "reset" || action === "clear") {
592
601
  try {
593
- await backend.clear(agentDir, this.ctx.sessionManager.getCwd());
602
+ await backend.clear(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
594
603
  await this.ctx.session.refreshBaseSystemPrompt();
595
604
  this.ctx.showStatus("Memory data cleared and system prompt refreshed.");
596
605
  } catch (error) {
@@ -601,7 +610,7 @@ export class CommandController {
601
610
 
602
611
  if (action === "enqueue" || action === "rebuild") {
603
612
  try {
604
- await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd());
613
+ await backend.enqueue(agentDir, this.ctx.sessionManager.getCwd(), this.ctx.session);
605
614
  this.ctx.showStatus("Memory consolidation enqueued.");
606
615
  } catch (error) {
607
616
  this.ctx.showError(`Memory enqueue failed: ${error instanceof Error ? error.message : String(error)}`);
@@ -609,7 +618,257 @@ export class CommandController {
609
618
  return;
610
619
  }
611
620
 
612
- this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild>");
621
+ if (action === "mm") {
622
+ await this.#handleMentalModelsSubcommand(argumentText);
623
+ return;
624
+ }
625
+
626
+ this.ctx.showError("Usage: /memory <view|clear|reset|enqueue|rebuild|mm ...>");
627
+ }
628
+
629
+ async #handleMentalModelsSubcommand(argumentText: string): Promise<void> {
630
+ // Parse: "mm <verb> [arg]"
631
+ const parts = argumentText.split(/\s+/).slice(1);
632
+ const verb = parts[0]?.toLowerCase() ?? "list";
633
+ const arg = parts[1];
634
+
635
+ const state = this.ctx.session.getHindsightSessionState();
636
+ const primary = state && !state.aliasOf ? state : undefined;
637
+ if (!primary) {
638
+ this.ctx.showError("Hindsight backend is not active for this session.");
639
+ return;
640
+ }
641
+ if (!primary.config.mentalModelsEnabled) {
642
+ this.ctx.showError("Mental models are disabled (hindsight.mentalModelsEnabled = false).");
643
+ return;
644
+ }
645
+
646
+ switch (verb) {
647
+ case "list":
648
+ await this.#mmList(primary);
649
+ return;
650
+ case "show":
651
+ if (!arg) return this.ctx.showError("Usage: /memory mm show <id>");
652
+ await this.#mmShow(primary, arg);
653
+ return;
654
+ case "refresh":
655
+ await this.#mmRefresh(primary, arg);
656
+ return;
657
+ case "history":
658
+ if (!arg) return this.ctx.showError("Usage: /memory mm history <id>");
659
+ await this.#mmHistory(primary, arg);
660
+ return;
661
+ case "seed":
662
+ await this.#mmSeed(primary);
663
+ return;
664
+ case "reload":
665
+ await this.#mmReload(primary);
666
+ return;
667
+ case "delete":
668
+ case "remove":
669
+ if (!arg) return this.ctx.showError("Usage: /memory mm delete <id>");
670
+ await this.#mmDelete(primary, arg);
671
+ return;
672
+ default:
673
+ this.ctx.showError("Usage: /memory mm <list|show|refresh|history|seed|reload|delete>");
674
+ }
675
+ }
676
+
677
+ async #mmList(state: HindsightSessionState): Promise<void> {
678
+ const client: HindsightApi = state.client;
679
+ try {
680
+ const response = await client.listMentalModels(state.bankId, { detail: "metadata" });
681
+ const items = response.items ?? [];
682
+ if (items.length === 0) {
683
+ this.ctx.showStatus(`No mental models on bank ${state.bankId}.`);
684
+ return;
685
+ }
686
+ const lines = items
687
+ .slice()
688
+ .sort((a, b) => a.id.localeCompare(b.id))
689
+ .map(summarizeMentalModel);
690
+ showMarkdownPanel(this.ctx, `Mental Models — ${state.bankId}`, lines.join("\n"));
691
+ } catch (error) {
692
+ this.ctx.showError(`mm list failed: ${error instanceof Error ? error.message : String(error)}`);
693
+ }
694
+ }
695
+
696
+ async #mmShow(state: HindsightSessionState, id: string): Promise<void> {
697
+ try {
698
+ const model = await state.client.getMentalModel(state.bankId, id, { detail: "content" });
699
+ if (!model) {
700
+ this.ctx.showError(`Mental model not found: ${id}`);
701
+ return;
702
+ }
703
+ const tags = model.tags && model.tags.length > 0 ? `\n_tags: ${model.tags.join(", ")}_` : "";
704
+ const refreshed = model.last_refreshed_at ? `\n_last refreshed: ${model.last_refreshed_at}_` : "";
705
+ const sourceQuery = model.source_query ? `\n\n**Source query:** ${model.source_query}` : "";
706
+ const content = (model.content ?? "_(empty — background reflect may still be running)_").trim();
707
+ showMarkdownPanel(
708
+ this.ctx,
709
+ model.name,
710
+ `**id:** \`${model.id}\`${tags}${refreshed}${sourceQuery}\n\n${content}`,
711
+ );
712
+ } catch (error) {
713
+ this.ctx.showError(`mm show failed: ${error instanceof Error ? error.message : String(error)}`);
714
+ }
715
+ }
716
+
717
+ async #mmRefresh(state: HindsightSessionState, id: string | undefined): Promise<void> {
718
+ try {
719
+ if (id) {
720
+ // Single-model refresh is explicit operator intent: bypass the
721
+ // auto-refresh filter so curated/manual models can still be
722
+ // refreshed on demand.
723
+ await state.client.refreshMentalModel(state.bankId, id);
724
+ this.ctx.showStatus(`Refresh queued for mental model ${id}.`);
725
+ } else {
726
+ // Bulk refresh: only touch models that opted into automatic
727
+ // refresh via `trigger.refresh_after_consolidation`. Curated
728
+ // models are reviewed before publishing and must not be
729
+ // silently regenerated by a bank-wide refresh sweep. Reading
730
+ // `detail: "content"` here is required because the trigger
731
+ // field is excluded from `detail: "metadata"`.
732
+ const list = await state.client.listMentalModels(state.bankId, { detail: "content" });
733
+ const items = list.items ?? [];
734
+ if (items.length === 0) {
735
+ this.ctx.showStatus(`No mental models on bank ${state.bankId}.`);
736
+ return;
737
+ }
738
+ const targets = items.filter(m => m.trigger?.refresh_after_consolidation === true);
739
+ const skipped = items.length - targets.length;
740
+ if (targets.length === 0) {
741
+ this.ctx.showStatus(
742
+ `No mental models opted into auto-refresh; ${skipped} curated model(s) left untouched. Pass an explicit id to refresh one of them.`,
743
+ );
744
+ return;
745
+ }
746
+ let queued = 0;
747
+ for (const item of targets) {
748
+ try {
749
+ await state.client.refreshMentalModel(state.bankId, item.id);
750
+ queued++;
751
+ } catch (error) {
752
+ this.ctx.showWarning(
753
+ `Refresh failed for ${item.id}: ${error instanceof Error ? error.message : String(error)}`,
754
+ );
755
+ }
756
+ }
757
+ const skippedSuffix = skipped > 0 ? `; skipped ${skipped} curated model(s)` : "";
758
+ this.ctx.showStatus(
759
+ `Refresh queued for ${queued}/${targets.length} auto-refresh model(s)${skippedSuffix}.`,
760
+ );
761
+ }
762
+ // Reload the cache after a brief grace so the new content (if the refresh
763
+ // completes synchronously on the server) flows into the system prompt.
764
+ await Bun.sleep(500);
765
+ await reloadMentalModelsForSession(state.session);
766
+ } catch (error) {
767
+ this.ctx.showError(`mm refresh failed: ${error instanceof Error ? error.message : String(error)}`);
768
+ }
769
+ }
770
+
771
+ async #mmHistory(state: HindsightSessionState, id: string): Promise<void> {
772
+ try {
773
+ const [model, history] = await Promise.all([
774
+ state.client.getMentalModel(state.bankId, id, { detail: "content" }),
775
+ state.client.getMentalModelHistory(state.bankId, id),
776
+ ]);
777
+ if (!model) {
778
+ this.ctx.showError(`Mental model not found: ${id}`);
779
+ return;
780
+ }
781
+ if (history.length === 0) {
782
+ this.ctx.showStatus(`No history recorded for ${id}.`);
783
+ return;
784
+ }
785
+ // History is most-recent first. Each entry stores the content BEFORE that
786
+ // change. To diff "what changed at entry N", compare entry N's
787
+ // previous_content (= state before that change) with entry N-1's
788
+ // previous_content (= state after that change, which was state before
789
+ // the next change). For the most recent change, compare against the
790
+ // model's CURRENT content.
791
+ const sections: string[] = [];
792
+ for (let i = 0; i < history.length; i++) {
793
+ const before = history[i].previous_content ?? "";
794
+ const after = i === 0 ? (model.content ?? "") : (history[i - 1].previous_content ?? "");
795
+ const diff = diffMentalModelContent(before, after);
796
+ sections.push(`### ${history[i].changed_at}\n\n\`\`\`diff\n${diff}\n\`\`\``);
797
+ }
798
+ showMarkdownPanel(this.ctx, `History — ${model.name}`, sections.join("\n\n"));
799
+ } catch (error) {
800
+ this.ctx.showError(`mm history failed: ${error instanceof Error ? error.message : String(error)}`);
801
+ }
802
+ }
803
+
804
+ async #mmSeed(state: HindsightSessionState): Promise<void> {
805
+ try {
806
+ const config = loadHindsightConfig(this.ctx.settings);
807
+ const seeds = resolveSeedsForScope(
808
+ {
809
+ bankId: state.bankId,
810
+ retainTags: state.retainTags,
811
+ recallTags: state.recallTags,
812
+ recallTagsMatch: state.recallTagsMatch,
813
+ },
814
+ config.scoping,
815
+ );
816
+ if (seeds.length === 0) {
817
+ this.ctx.showStatus(`No built-in seeds apply to scoping=${config.scoping}.`);
818
+ return;
819
+ }
820
+ const list = await state.client.listMentalModels(state.bankId, { detail: "metadata" });
821
+ const existing = new Set((list.items ?? []).map(m => m.id));
822
+ let created = 0;
823
+ let skipped = 0;
824
+ for (const seed of seeds) {
825
+ if (existing.has(seed.id)) {
826
+ skipped++;
827
+ continue;
828
+ }
829
+ try {
830
+ await state.client.createMentalModel(state.bankId, seed.name, seed.sourceQuery, {
831
+ id: seed.id,
832
+ tags: seed.tags.length > 0 ? seed.tags : undefined,
833
+ maxTokens: seed.maxTokens,
834
+ trigger: seed.trigger,
835
+ });
836
+ created++;
837
+ } catch (error) {
838
+ this.ctx.showWarning(
839
+ `Seed failed for ${seed.id}: ${error instanceof Error ? error.message : String(error)}`,
840
+ );
841
+ }
842
+ }
843
+ this.ctx.showStatus(`Seeded ${created} new mental model(s); ${skipped} already present.`);
844
+ } catch (error) {
845
+ this.ctx.showError(`mm seed failed: ${error instanceof Error ? error.message : String(error)}`);
846
+ }
847
+ }
848
+
849
+ async #mmReload(state: HindsightSessionState): Promise<void> {
850
+ const ok = await reloadMentalModelsForSession(state.session);
851
+ if (ok) {
852
+ this.ctx.showStatus("Mental-model cache reloaded.");
853
+ } else {
854
+ this.ctx.showError("Reload failed (Hindsight backend not active or mental models disabled).");
855
+ }
856
+ }
857
+
858
+ async #mmDelete(state: HindsightSessionState, id: string): Promise<void> {
859
+ try {
860
+ const removed = await state.client.deleteMentalModel(state.bankId, id);
861
+ if (!removed) {
862
+ this.ctx.showError(`Mental model not found: ${id}`);
863
+ return;
864
+ }
865
+ // Drop the cached snippet so the closing tag does not silently keep
866
+ // stale content in the system prompt until the next agent_end TTL.
867
+ await reloadMentalModelsForSession(state.session);
868
+ this.ctx.showStatus(`Deleted mental model ${id} from bank ${state.bankId}.`);
869
+ } catch (error) {
870
+ this.ctx.showError(`mm delete failed: ${error instanceof Error ? error.message : String(error)}`);
871
+ }
613
872
  }
614
873
 
615
874
  async #runNewSessionFlow(options?: NewSessionOptions, label: string = "New session started"): Promise<void> {
@@ -344,8 +344,11 @@ export class InputController {
344
344
  // (a user-role `message_start` event) leaves any draft the user has
345
345
  // typed since queuing intact. Same protection as #783, applied to
346
346
  // the streaming/queue path.
347
- this.ctx.locallySubmittedUserSignatures.add(`${text}\u0000${images?.length ?? 0}`);
348
- await this.ctx.session.prompt(text, { streamingBehavior: "steer", images });
347
+ await this.ctx.withLocalSubmission(
348
+ text,
349
+ () => this.ctx.session.prompt(text, { streamingBehavior: "steer", images }),
350
+ { imageCount: images?.length ?? 0 },
351
+ );
349
352
  this.ctx.updatePendingMessagesDisplay();
350
353
  this.ctx.ui.requestRender();
351
354
  return;
@@ -440,7 +443,9 @@ export class InputController {
440
443
  if (this.ctx.session.isStreaming) {
441
444
  this.ctx.editor.addToHistory(text);
442
445
  this.ctx.editor.setText("");
443
- await this.ctx.session.prompt(text, { streamingBehavior: "followUp" });
446
+ await this.ctx.withLocalSubmission(text, () =>
447
+ this.ctx.session.prompt(text, { streamingBehavior: "followUp" }),
448
+ );
444
449
  this.ctx.updatePendingMessagesDisplay();
445
450
  this.ctx.ui.requestRender();
446
451
  return;
@@ -449,7 +454,7 @@ export class InputController {
449
454
  // Not streaming — just submit normally
450
455
  this.ctx.editor.addToHistory(text);
451
456
  this.ctx.editor.setText("");
452
- await this.ctx.session.prompt(text);
457
+ await this.ctx.withLocalSubmission(text, () => this.ctx.session.prompt(text));
453
458
  }
454
459
 
455
460
  restoreQueuedMessagesToEditor(options?: { abort?: boolean; currentText?: string }): number {
@@ -183,6 +183,7 @@ export class InteractiveMode implements InteractiveModeContext {
183
183
  optimisticUserMessageSignature: string | undefined = undefined;
184
184
  locallySubmittedUserSignatures: Set<string> = new Set();
185
185
  #pendingSubmittedInput: SubmittedUserInput | undefined;
186
+ #pendingSubmissionDispose: (() => void) | undefined;
186
187
  lastSigintTime = 0;
187
188
  lastEscapeTime = 0;
188
189
  shutdownRequested = false;
@@ -567,6 +568,30 @@ export class InteractiveMode implements InteractiveModeContext {
567
568
  );
568
569
  }
569
570
 
571
+ recordLocalSubmission(text: string, imageCount = 0): () => void {
572
+ if (this.isKnownSlashCommand(text)) {
573
+ return () => {};
574
+ }
575
+ const signature = `${text}\u0000${imageCount}`;
576
+ this.locallySubmittedUserSignatures.add(signature);
577
+ let disposed = false;
578
+ return () => {
579
+ if (disposed) return;
580
+ disposed = true;
581
+ this.locallySubmittedUserSignatures.delete(signature);
582
+ };
583
+ }
584
+
585
+ async withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T> {
586
+ const dispose = this.recordLocalSubmission(text, options?.imageCount ?? 0);
587
+ try {
588
+ return await fn();
589
+ } catch (err) {
590
+ dispose();
591
+ throw err;
592
+ }
593
+ }
594
+
570
595
  startPendingSubmission(input: { text: string; images?: ImageContent[] }): SubmittedUserInput {
571
596
  const submission: SubmittedUserInput = {
572
597
  text: input.text,
@@ -575,8 +600,9 @@ export class InteractiveMode implements InteractiveModeContext {
575
600
  started: false,
576
601
  };
577
602
  this.#pendingSubmittedInput = submission;
578
- this.optimisticUserMessageSignature = `${submission.text}\u0000${submission.images?.length ?? 0}`;
579
- this.locallySubmittedUserSignatures.add(this.optimisticUserMessageSignature);
603
+ const imageCount = submission.images?.length ?? 0;
604
+ this.optimisticUserMessageSignature = `${submission.text}\u0000${imageCount}`;
605
+ this.#pendingSubmissionDispose = this.recordLocalSubmission(submission.text, imageCount);
580
606
  this.addMessageToChat({
581
607
  role: "user",
582
608
  content: [{ type: "text", text: submission.text }, ...(submission.images ?? [])],
@@ -598,7 +624,8 @@ export class InteractiveMode implements InteractiveModeContext {
598
624
  submission.cancelled = true;
599
625
  this.#pendingSubmittedInput = undefined;
600
626
  this.optimisticUserMessageSignature = undefined;
601
- this.locallySubmittedUserSignatures.delete(`${submission.text}\u0000${submission.images?.length ?? 0}`);
627
+ this.#pendingSubmissionDispose?.();
628
+ this.#pendingSubmissionDispose = undefined;
602
629
  this.#pendingWorkingMessage = undefined;
603
630
  if (this.loadingAnimation) {
604
631
  this.loadingAnimation.stop();
@@ -624,6 +651,7 @@ export class InteractiveMode implements InteractiveModeContext {
624
651
  finishPendingSubmission(input: SubmittedUserInput): void {
625
652
  if (this.#pendingSubmittedInput === input) {
626
653
  this.#pendingSubmittedInput = undefined;
654
+ this.#pendingSubmissionDispose = undefined;
627
655
  }
628
656
  }
629
657
 
@@ -1223,6 +1251,8 @@ export class InteractiveMode implements InteractiveModeContext {
1223
1251
  showError(message: string): void {
1224
1252
  this.#pendingSubmittedInput = undefined;
1225
1253
  this.optimisticUserMessageSignature = undefined;
1254
+ this.#pendingSubmissionDispose?.();
1255
+ this.#pendingSubmissionDispose = undefined;
1226
1256
  this.#pendingWorkingMessage = undefined;
1227
1257
  if (this.loadingAnimation) {
1228
1258
  this.loadingAnimation.stop();
@@ -151,6 +151,19 @@ export interface InteractiveModeContext {
151
151
  cancelPendingSubmission(): boolean;
152
152
  markPendingSubmissionStarted(input: SubmittedUserInput): boolean;
153
153
  finishPendingSubmission(input: SubmittedUserInput): void;
154
+ /**
155
+ * Marks a locally-initiated user submission so the eventual `message_start`
156
+ * event for that user message does not clobber the editor draft (see #783).
157
+ * Returns a dispose function that removes the signature; call it on
158
+ * delivery failure so a retry can be re-marked cleanly.
159
+ */
160
+ recordLocalSubmission(text: string, imageCount?: number): () => void;
161
+ /**
162
+ * Wraps `fn` in a `recordLocalSubmission` marker that is automatically
163
+ * removed if `fn` rejects. Use this for the common case where a thrown
164
+ * delivery error should leave the signature set untouched.
165
+ */
166
+ withLocalSubmission<T>(text: string, fn: () => Promise<T>, options?: { imageCount?: number }): Promise<T>;
154
167
  isKnownSlashCommand(text: string): boolean;
155
168
  addMessageToChat(message: AgentMessage, options?: { populateHistory?: boolean }): void;
156
169
  renderSessionContext(
@@ -534,6 +534,16 @@ export class UiHelpers {
534
534
  this.ctx.showStatus("Queued message for after compaction");
535
535
  }
536
536
 
537
+ async #deliverQueuedMessage(message: CompactionQueuedMessage): Promise<void> {
538
+ if (this.ctx.isKnownSlashCommand(message.text)) {
539
+ await this.ctx.session.prompt(message.text);
540
+ return;
541
+ }
542
+ await this.ctx.withLocalSubmission(message.text, () =>
543
+ message.mode === "followUp" ? this.ctx.session.followUp(message.text) : this.ctx.session.steer(message.text),
544
+ );
545
+ }
546
+
537
547
  isKnownSlashCommand(text: string): boolean {
538
548
  if (!text.startsWith("/")) return false;
539
549
  const spaceIndex = text.indexOf(" ");
@@ -576,13 +586,7 @@ export class UiHelpers {
576
586
  try {
577
587
  if (options?.willRetry) {
578
588
  for (const message of queuedMessages) {
579
- if (this.ctx.isKnownSlashCommand(message.text)) {
580
- await this.ctx.session.prompt(message.text);
581
- } else if (message.mode === "followUp") {
582
- await this.ctx.session.followUp(message.text);
583
- } else {
584
- await this.ctx.session.steer(message.text);
585
- }
589
+ await this.#deliverQueuedMessage(message);
586
590
  }
587
591
  this.ctx.updatePendingMessagesDisplay();
588
592
  return;
@@ -607,7 +611,10 @@ export class UiHelpers {
607
611
  const rest = queuedMessages.slice(firstPromptIndex + 1);
608
612
 
609
613
  for (const message of preCommands) {
610
- await this.ctx.session.prompt(message.text);
614
+ // preCommands are all slash commands; #deliverQueuedMessage handles
615
+ // that branch (no local-submission marking needed since slash
616
+ // commands don't generate a matching user message_start).
617
+ await this.#deliverQueuedMessage(message);
611
618
  }
612
619
 
613
620
  // Pass streamingBehavior so that if the session is still streaming when
@@ -619,22 +626,22 @@ export class UiHelpers {
619
626
  // deferred, the message lands in the same queue every other consumer
620
627
  // (Alt+Up dequeue, post-stream drain) already drains, instead of being
621
628
  // stranded in compactionQueuedMessages with no drainer.
629
+ //
630
+ // firstPrompt is fire-and-forget — its rejection is funneled through
631
+ // `restoreQueue` rather than rethrown, so we use the primitive
632
+ // recordLocalSubmission and dispose manually in the catch.
633
+ const disposeFirstPrompt = this.ctx.recordLocalSubmission(firstPrompt.text);
622
634
  const promptPromise = this.ctx.session
623
635
  .prompt(firstPrompt.text, {
624
636
  streamingBehavior: firstPrompt.mode === "followUp" ? "followUp" : "steer",
625
637
  })
626
638
  .catch((error: unknown) => {
639
+ disposeFirstPrompt();
627
640
  restoreQueue(error);
628
641
  });
629
642
 
630
643
  for (const message of rest) {
631
- if (this.ctx.isKnownSlashCommand(message.text)) {
632
- await this.ctx.session.prompt(message.text);
633
- } else if (message.mode === "followUp") {
634
- await this.ctx.session.followUp(message.text);
635
- } else {
636
- await this.ctx.session.steer(message.text);
637
- }
644
+ await this.#deliverQueuedMessage(message);
638
645
  }
639
646
  this.ctx.updatePendingMessagesDisplay();
640
647
  void promptPromise;
@@ -79,4 +79,5 @@ This format is purely textual. The tool has NO awareness of language, indentatio
79
79
  - Every inserted/replacement content line **MUST** start with `{{hsep}}`; raw content lines are invalid.
80
80
  - Do not write unified diff syntax (`@@`, `-OLD`, `+NEW`).
81
81
  - To replace a block, use one `= A..B` op followed by all replacement `{{hsep}}TEXT` payload lines.
82
+ - `= A..B` deletes the range; payload is what's written. If a payload edge line already exists immediately outside `A..B`, widen the range to cover it — otherwise it duplicates.
82
83
  </critical>
package/src/sdk.ts CHANGED
@@ -61,6 +61,7 @@ import {
61
61
  } from "./extensibility/extensions";
62
62
  import { loadSkills as loadSkillsInternal, type Skill, type SkillWarning } from "./extensibility/skills";
63
63
  import { type FileSlashCommand, loadSlashCommands as loadSlashCommandsInternal } from "./extensibility/slash-commands";
64
+ import type { HindsightSessionState } from "./hindsight/state";
64
65
  import {
65
66
  AgentProtocolHandler,
66
67
  ArtifactProtocolHandler,
@@ -216,6 +217,8 @@ export interface CreateAgentSessionOptions {
216
217
  requireYieldTool?: boolean;
217
218
  /** Task recursion depth (for subagent sessions). Default: 0 */
218
219
  taskDepth?: number;
220
+ /** Parent Hindsight state to alias for subagent memory tools. */
221
+ parentHindsightSessionState?: HindsightSessionState;
219
222
  /** Pre-allocated agent identity for IRC routing. Default: "0-Main" for top-level, parentTaskPrefix-derived for sub. */
220
223
  agentId?: string;
221
224
  /** Display name for the agent in IRC. Default: "main" or "sub". */
@@ -968,6 +971,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
968
971
  trackEvalExecution: (execution, abortController) =>
969
972
  session ? session.trackEvalExecution(execution, abortController) : execution,
970
973
  getSessionId: () => sessionManager.getSessionId?.() ?? null,
974
+ getHindsightSessionState: () => session?.getHindsightSessionState(),
971
975
  getAgentId: () => resolvedAgentId,
972
976
  getToolByName: name => session?.getToolByName(name),
973
977
  agentRegistry,
@@ -1335,7 +1339,11 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1335
1339
  const promptTools = buildSystemPromptToolMetadata(tools, {
1336
1340
  search_tool_bm25: { description: renderSearchToolBm25Description(discoverableMCPTools) },
1337
1341
  });
1338
- const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(agentDir, settings);
1342
+ const memoryInstructions = await resolveMemoryBackend(settings).buildDeveloperInstructions(
1343
+ agentDir,
1344
+ settings,
1345
+ session,
1346
+ );
1339
1347
 
1340
1348
  // Build combined append prompt: memory instructions + MCP server instructions
1341
1349
  const serverInstructions = mcpManager?.getServerInstructions();
@@ -1755,6 +1763,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
1755
1763
  modelRegistry,
1756
1764
  agentDir,
1757
1765
  taskDepth,
1766
+ parentHindsightSessionState: options.parentHindsightSessionState,
1758
1767
  }),
1759
1768
  ),
1760
1769
  );