@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.
- package/CHANGELOG.md +24 -0
- package/package.json +7 -7
- package/src/config/settings-schema.ts +25 -0
- package/src/edit/modes/hashline.ts +191 -2
- package/src/hindsight/backend.ts +85 -324
- package/src/hindsight/client.ts +153 -0
- package/src/hindsight/config.ts +10 -0
- package/src/hindsight/content.ts +9 -4
- package/src/hindsight/index.ts +2 -0
- package/src/hindsight/mental-models.ts +382 -0
- package/src/hindsight/seeds.json +32 -0
- package/src/hindsight/state.ts +469 -0
- package/src/memory-backend/types.ts +14 -4
- package/src/modes/controllers/command-controller.ts +263 -4
- package/src/modes/controllers/input-controller.ts +9 -4
- package/src/modes/interactive-mode.ts +33 -3
- package/src/modes/types.ts +13 -0
- package/src/modes/utils/ui-helpers.ts +22 -15
- package/src/prompts/tools/hashline.md +1 -0
- package/src/sdk.ts +10 -1
- package/src/session/agent-session.ts +44 -1
- package/src/slash-commands/builtin-registry.ts +10 -0
- package/src/task/executor.ts +3 -0
- package/src/task/index.ts +2 -0
- package/src/tools/hindsight-recall.ts +1 -3
- package/src/tools/hindsight-reflect.ts +1 -3
- package/src/tools/hindsight-retain.ts +6 -9
- package/src/tools/index.ts +3 -0
- package/src/hindsight/retain-queue.ts +0 -166
|
@@ -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
|
-
|
|
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.
|
|
348
|
-
|
|
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.
|
|
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
|
-
|
|
579
|
-
this.
|
|
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
|
|
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();
|
package/src/modes/types.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
);
|