@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
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [14.6.4] - 2026-05-03
|
|
6
|
+
### Added
|
|
7
|
+
|
|
8
|
+
- Added `hindsight.mentalModelsEnabled`, `hindsight.mentalModelAutoSeed`, `hindsight.mentalModelRefreshIntervalMs`, and `hindsight.mentalModelMaxRenderChars` settings to control curated Hindsight mental-model activation, seeding, refresh cadence, and prompt render budget
|
|
9
|
+
- Added `<mental_models>` injection to developer instructions, loading bank-level curated summaries as stable background context
|
|
10
|
+
- Added built-in `/memory mm` commands (`list`, `show`, `refresh`, `history`, `seed`, `reload`, `delete`) to inspect and manage mental models on the active bank
|
|
11
|
+
- Added scope-aware mental-model seeding for `global`, `per-project`, and `per-project-tagged` banks, including built-in seed models like user preferences, project conventions, and project decisions
|
|
12
|
+
- Added warning output when hashline block replacements auto-absorbed duplicate boundary lines
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
|
|
16
|
+
- Changed `/memory clear` and `/memory enqueue` to apply only to the current agent session’s Hindsight cache instead of all live Hindsight sessions
|
|
17
|
+
- Changed the prompt assembly order so `<mental_models>` blocks are appended before `<memories>` recall blocks in developer instructions
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- Fixed Hindsight memory prompt injection and recall/retain tool execution to resolve against the active session state, preventing context from an unrelated session from being used
|
|
22
|
+
- Fixed subagent `/task` sessions to persist memories into the parent agent’s Hindsight bank by explicit parent state wiring
|
|
23
|
+
- Fixed per-session memory retention behavior when switching or resuming sessions by rekeying Hindsight state and resetting conversation-tracking counters so first-turn recall and nth-turn retain cadence no longer leak across conversations
|
|
24
|
+
- Fixed the first-turn startup race so `<mental_models>` appears in the opening system prompt when mental-model loading is enabled
|
|
25
|
+
- Fixed retention hygiene by stripping `<mental_models>` blocks from retained content to prevent curated summaries from feeding back into future memory writes
|
|
26
|
+
- Fixed `<mental_models>` rendering to honor the configured character budget and truncate with an explicit truncation marker when the snapshot exceeds limits
|
|
27
|
+
- Fixed hashline replacements so duplicated payload boundary lines adjacent to a replaced block are absorbed into the replacement range instead of being duplicated
|
|
28
|
+
|
|
5
29
|
## [14.6.3] - 2026-05-03
|
|
6
30
|
|
|
7
31
|
### Breaking Changes
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@oh-my-pi/pi-coding-agent",
|
|
4
|
-
"version": "14.6.
|
|
4
|
+
"version": "14.6.5",
|
|
5
5
|
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
|
|
6
6
|
"homepage": "https://github.com/can1357/oh-my-pi",
|
|
7
7
|
"author": "Can Boluk",
|
|
@@ -46,12 +46,12 @@
|
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@agentclientprotocol/sdk": "0.20.0",
|
|
48
48
|
"@mozilla/readability": "^0.6.0",
|
|
49
|
-
"@oh-my-pi/omp-stats": "14.6.
|
|
50
|
-
"@oh-my-pi/pi-agent-core": "14.6.
|
|
51
|
-
"@oh-my-pi/pi-ai": "14.6.
|
|
52
|
-
"@oh-my-pi/pi-natives": "14.6.
|
|
53
|
-
"@oh-my-pi/pi-tui": "14.6.
|
|
54
|
-
"@oh-my-pi/pi-utils": "14.6.
|
|
49
|
+
"@oh-my-pi/omp-stats": "14.6.5",
|
|
50
|
+
"@oh-my-pi/pi-agent-core": "14.6.5",
|
|
51
|
+
"@oh-my-pi/pi-ai": "14.6.5",
|
|
52
|
+
"@oh-my-pi/pi-natives": "14.6.5",
|
|
53
|
+
"@oh-my-pi/pi-tui": "14.6.5",
|
|
54
|
+
"@oh-my-pi/pi-utils": "14.6.5",
|
|
55
55
|
"@puppeteer/browsers": "^2.13.0",
|
|
56
56
|
"@sinclair/typebox": "^0.34.49",
|
|
57
57
|
"@xterm/headless": "^6.0.0",
|
|
@@ -1275,6 +1275,31 @@ export const SETTINGS_SCHEMA = {
|
|
|
1275
1275
|
|
|
1276
1276
|
"hindsight.debug": { type: "boolean", default: false },
|
|
1277
1277
|
|
|
1278
|
+
"hindsight.mentalModelsEnabled": {
|
|
1279
|
+
type: "boolean",
|
|
1280
|
+
default: true,
|
|
1281
|
+
ui: {
|
|
1282
|
+
tab: "memory",
|
|
1283
|
+
label: "Hindsight Mental Models",
|
|
1284
|
+
description:
|
|
1285
|
+
"Read curated reflect summaries (mental models) into developer instructions at boot. Loads existing models on the bank — does not write. Pair with hindsight.mentalModelAutoSeed to also auto-create the built-in seed set.",
|
|
1286
|
+
condition: "hindsightActive",
|
|
1287
|
+
},
|
|
1288
|
+
},
|
|
1289
|
+
"hindsight.mentalModelAutoSeed": {
|
|
1290
|
+
type: "boolean",
|
|
1291
|
+
default: true,
|
|
1292
|
+
ui: {
|
|
1293
|
+
tab: "memory",
|
|
1294
|
+
label: "Hindsight Mental Model Auto-Seed",
|
|
1295
|
+
description:
|
|
1296
|
+
"At session start, create any built-in mental models (project-conventions, project-decisions, user-preferences) that do not yet exist on the bank.",
|
|
1297
|
+
condition: "hindsightActive",
|
|
1298
|
+
},
|
|
1299
|
+
},
|
|
1300
|
+
"hindsight.mentalModelRefreshIntervalMs": { type: "number", default: 5 * 60 * 1000 },
|
|
1301
|
+
"hindsight.mentalModelMaxRenderChars": { type: "number", default: 16_000 },
|
|
1302
|
+
|
|
1278
1303
|
// TTSR
|
|
1279
1304
|
"ttsr.enabled": {
|
|
1280
1305
|
type: "boolean",
|
|
@@ -793,6 +793,16 @@ interface IndexedEdit {
|
|
|
793
793
|
idx: number;
|
|
794
794
|
}
|
|
795
795
|
|
|
796
|
+
type HashlineDeleteEdit = Extract<HashlineEdit, { kind: "delete" }>;
|
|
797
|
+
|
|
798
|
+
interface HashlineReplacementGroup {
|
|
799
|
+
startIndex: number;
|
|
800
|
+
endIndex: number;
|
|
801
|
+
sourceLineNum: number;
|
|
802
|
+
replacement: string[];
|
|
803
|
+
deletes: HashlineDeleteEdit[];
|
|
804
|
+
}
|
|
805
|
+
|
|
796
806
|
function getHashlineEditAnchors(edit: HashlineEdit): Anchor[] {
|
|
797
807
|
if (edit.kind === "delete") return [edit.anchor];
|
|
798
808
|
if (edit.kind === "modify") return [edit.anchor];
|
|
@@ -889,6 +899,183 @@ function insertAtEnd(fileLines: string[], lineOrigins: HashlineLineOrigin[], lin
|
|
|
889
899
|
}
|
|
890
900
|
|
|
891
901
|
/** Bucket edits by the line they target so we can apply each line's group in one splice. */
|
|
902
|
+
|
|
903
|
+
function getAnchorTargetLine(edit: HashlineEdit): number | undefined {
|
|
904
|
+
if (edit.kind === "delete" || edit.kind === "modify") return edit.anchor.line;
|
|
905
|
+
if (edit.cursor.kind === "before_anchor" || edit.cursor.kind === "after_anchor") return edit.cursor.anchor.line;
|
|
906
|
+
return undefined;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function collectAnchorTargetLines(edits: HashlineEdit[]): Set<number> {
|
|
910
|
+
const lines = new Set<number>();
|
|
911
|
+
for (const edit of edits) {
|
|
912
|
+
const line = getAnchorTargetLine(edit);
|
|
913
|
+
if (line !== undefined) lines.add(line);
|
|
914
|
+
}
|
|
915
|
+
return lines;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function findReplacementGroup(edits: HashlineEdit[], startIndex: number): HashlineReplacementGroup | undefined {
|
|
919
|
+
const first = edits[startIndex];
|
|
920
|
+
if (first?.kind !== "insert" || first.cursor.kind !== "before_anchor") return undefined;
|
|
921
|
+
|
|
922
|
+
const sourceLineNum = first.lineNum;
|
|
923
|
+
const replacement: string[] = [];
|
|
924
|
+
let index = startIndex;
|
|
925
|
+
while (index < edits.length) {
|
|
926
|
+
const edit = edits[index];
|
|
927
|
+
if (edit.kind !== "insert" || edit.lineNum !== sourceLineNum || edit.cursor.kind !== "before_anchor") break;
|
|
928
|
+
replacement.push(edit.text);
|
|
929
|
+
index++;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
const deletes: HashlineDeleteEdit[] = [];
|
|
933
|
+
while (index < edits.length) {
|
|
934
|
+
const edit = edits[index];
|
|
935
|
+
if (edit.kind !== "delete" || edit.lineNum !== sourceLineNum) break;
|
|
936
|
+
deletes.push(edit);
|
|
937
|
+
index++;
|
|
938
|
+
}
|
|
939
|
+
if (deletes.length === 0) return undefined;
|
|
940
|
+
|
|
941
|
+
const startLine = deletes[0].anchor.line;
|
|
942
|
+
for (let offset = 0; offset < deletes.length; offset++) {
|
|
943
|
+
if (deletes[offset].anchor.line !== startLine + offset) return undefined;
|
|
944
|
+
}
|
|
945
|
+
const cursorLine = first.cursor.anchor.line;
|
|
946
|
+
if (cursorLine !== startLine) return undefined;
|
|
947
|
+
|
|
948
|
+
return { startIndex, endIndex: index - 1, sourceLineNum, replacement, deletes };
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
function countMatchingPrefixBlock(fileLines: string[], startLine: number, replacement: string[]): number {
|
|
952
|
+
const max = Math.min(replacement.length, startLine - 1);
|
|
953
|
+
for (let count = max; count >= 2; count--) {
|
|
954
|
+
let matches = true;
|
|
955
|
+
for (let offset = 0; offset < count; offset++) {
|
|
956
|
+
if (fileLines[startLine - count - 1 + offset] !== replacement[offset]) {
|
|
957
|
+
matches = false;
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
if (matches) return count;
|
|
962
|
+
}
|
|
963
|
+
return 0;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function countMatchingSuffixBlock(fileLines: string[], endLine: number, replacement: string[]): number {
|
|
967
|
+
const max = Math.min(replacement.length, fileLines.length - endLine);
|
|
968
|
+
for (let count = max; count >= 2; count--) {
|
|
969
|
+
let matches = true;
|
|
970
|
+
for (let offset = 0; offset < count; offset++) {
|
|
971
|
+
if (fileLines[endLine + offset] !== replacement[replacement.length - count + offset]) {
|
|
972
|
+
matches = false;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
if (matches) return count;
|
|
977
|
+
}
|
|
978
|
+
return 0;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
function hasExternalTargets(lines: Iterable<number>, externalTargetLines: Set<number>): boolean {
|
|
982
|
+
for (const line of lines) {
|
|
983
|
+
if (externalTargetLines.has(line)) return true;
|
|
984
|
+
}
|
|
985
|
+
return false;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
function contiguousRange(start: number, count: number): number[] {
|
|
989
|
+
return Array.from({ length: count }, (_, offset) => start + offset);
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function deleteEditForAutoAbsorbedLine(
|
|
993
|
+
line: number,
|
|
994
|
+
sourceLineNum: number,
|
|
995
|
+
index: number,
|
|
996
|
+
fileLines: string[],
|
|
997
|
+
): HashlineEdit {
|
|
998
|
+
return {
|
|
999
|
+
kind: "delete",
|
|
1000
|
+
anchor: { line, hash: computeLineHash(line, fileLines[line - 1] ?? "") },
|
|
1001
|
+
lineNum: sourceLineNum,
|
|
1002
|
+
index,
|
|
1003
|
+
};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
function absorbReplacementBoundaryDuplicates(
|
|
1007
|
+
edits: HashlineEdit[],
|
|
1008
|
+
fileLines: string[],
|
|
1009
|
+
warnings: string[],
|
|
1010
|
+
): HashlineEdit[] {
|
|
1011
|
+
let nextSyntheticIndex = edits.length;
|
|
1012
|
+
const absorbed: HashlineEdit[] = [];
|
|
1013
|
+
|
|
1014
|
+
// Anchor targets are stable across the loop because we only ever append
|
|
1015
|
+
// synthetic deletes (never mutate originals). A line in this set that
|
|
1016
|
+
// falls outside the current group's range is necessarily owned by another
|
|
1017
|
+
// op, so absorbing it would silently steal its target.
|
|
1018
|
+
const allTargetLines = collectAnchorTargetLines(edits);
|
|
1019
|
+
const emittedAbsorbKeys = new Set<string>();
|
|
1020
|
+
|
|
1021
|
+
for (let index = 0; index < edits.length; index++) {
|
|
1022
|
+
const group = findReplacementGroup(edits, index);
|
|
1023
|
+
if (!group) {
|
|
1024
|
+
absorbed.push(edits[index]);
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
const startLine = group.deletes[0].anchor.line;
|
|
1029
|
+
const endLine = group.deletes[group.deletes.length - 1].anchor.line;
|
|
1030
|
+
|
|
1031
|
+
const prefixCount = countMatchingPrefixBlock(fileLines, startLine, group.replacement);
|
|
1032
|
+
const suffixCount = countMatchingSuffixBlock(fileLines, endLine, group.replacement);
|
|
1033
|
+
const prefixLines = contiguousRange(startLine - prefixCount, prefixCount);
|
|
1034
|
+
const suffixLines = contiguousRange(endLine + 1, suffixCount);
|
|
1035
|
+
const safePrefixCount = hasExternalTargets(prefixLines, allTargetLines) ? 0 : prefixCount;
|
|
1036
|
+
const safeSuffixCount = hasExternalTargets(suffixLines, allTargetLines) ? 0 : suffixCount;
|
|
1037
|
+
|
|
1038
|
+
if (safePrefixCount > 0) {
|
|
1039
|
+
const absorbStart = startLine - safePrefixCount;
|
|
1040
|
+
const key = `prefix:${absorbStart}..${startLine - 1}`;
|
|
1041
|
+
if (!emittedAbsorbKeys.has(key)) {
|
|
1042
|
+
emittedAbsorbKeys.add(key);
|
|
1043
|
+
warnings.push(
|
|
1044
|
+
`Auto-absorbed ${safePrefixCount} duplicate line(s) above replacement at line ${group.sourceLineNum} ` +
|
|
1045
|
+
`(file lines ${absorbStart}..${startLine - 1} matched the payload's leading lines; ` +
|
|
1046
|
+
`widened the deletion to absorb them).`,
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
if (safeSuffixCount > 0) {
|
|
1051
|
+
const absorbEnd = endLine + safeSuffixCount;
|
|
1052
|
+
const key = `suffix:${endLine + 1}..${absorbEnd}`;
|
|
1053
|
+
if (!emittedAbsorbKeys.has(key)) {
|
|
1054
|
+
emittedAbsorbKeys.add(key);
|
|
1055
|
+
warnings.push(
|
|
1056
|
+
`Auto-absorbed ${safeSuffixCount} duplicate line(s) below replacement at line ${group.sourceLineNum} ` +
|
|
1057
|
+
`(file lines ${endLine + 1}..${absorbEnd} matched the payload's trailing lines; ` +
|
|
1058
|
+
`widened the deletion to absorb them).`,
|
|
1059
|
+
);
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
for (const line of contiguousRange(startLine - safePrefixCount, safePrefixCount)) {
|
|
1064
|
+
absorbed.push(deleteEditForAutoAbsorbedLine(line, group.sourceLineNum, nextSyntheticIndex++, fileLines));
|
|
1065
|
+
}
|
|
1066
|
+
for (let groupIndex = group.startIndex; groupIndex <= group.endIndex; groupIndex++) {
|
|
1067
|
+
absorbed.push(edits[groupIndex]);
|
|
1068
|
+
}
|
|
1069
|
+
for (const line of contiguousRange(endLine + 1, safeSuffixCount)) {
|
|
1070
|
+
absorbed.push(deleteEditForAutoAbsorbedLine(line, group.sourceLineNum, nextSyntheticIndex++, fileLines));
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
index = group.endIndex;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return absorbed;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
892
1079
|
function bucketAnchorEditsByLine(edits: IndexedEdit[]): Map<number, IndexedEdit[]> {
|
|
893
1080
|
const byLine = new Map<number, IndexedEdit[]>();
|
|
894
1081
|
for (const entry of edits) {
|
|
@@ -922,10 +1109,12 @@ export function applyHashlineEdits(text: string, edits: HashlineEdit[]): Hashlin
|
|
|
922
1109
|
const mismatches = validateHashlineAnchors(edits, fileLines, warnings);
|
|
923
1110
|
if (mismatches.length > 0) throw new HashlineMismatchError(mismatches, fileLines);
|
|
924
1111
|
|
|
1112
|
+
const normalizedEdits = absorbReplacementBoundaryDuplicates(edits, fileLines, warnings);
|
|
1113
|
+
|
|
925
1114
|
// Normalize after_anchor inserts to before_anchor of the next line, or EOF
|
|
926
1115
|
// when the anchor is the final line. This keeps the bucketing logic below
|
|
927
1116
|
// (which only knows about before_anchor / bof / eof) untouched.
|
|
928
|
-
for (const edit of
|
|
1117
|
+
for (const edit of normalizedEdits) {
|
|
929
1118
|
if (edit.kind !== "insert" || edit.cursor.kind !== "after_anchor") continue;
|
|
930
1119
|
const anchorLine = edit.cursor.anchor.line;
|
|
931
1120
|
if (anchorLine >= fileLines.length) {
|
|
@@ -944,7 +1133,7 @@ export function applyHashlineEdits(text: string, edits: HashlineEdit[]): Hashlin
|
|
|
944
1133
|
const bofLines: string[] = [];
|
|
945
1134
|
const eofLines: string[] = [];
|
|
946
1135
|
const anchorEdits: IndexedEdit[] = [];
|
|
947
|
-
|
|
1136
|
+
normalizedEdits.forEach((edit, idx) => {
|
|
948
1137
|
if (edit.kind === "insert" && edit.cursor.kind === "bof") {
|
|
949
1138
|
bofLines.push(edit.text);
|
|
950
1139
|
} else if (edit.kind === "insert" && edit.cursor.kind === "eof") {
|