@oh-my-pi/pi-coding-agent 15.9.1 → 15.9.3
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 +29 -1
- package/dist/types/cli/dry-balance-cli.d.ts +104 -0
- package/dist/types/commands/dry-balance.d.ts +31 -0
- package/dist/types/config/model-registry.d.ts +2 -0
- package/dist/types/config/models-config-schema.d.ts +3 -0
- package/dist/types/config/settings.d.ts +11 -0
- package/dist/types/discovery/helpers.d.ts +1 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +2 -3
- package/dist/types/hindsight/bank.d.ts +17 -9
- package/dist/types/hindsight/mental-models.d.ts +1 -1
- package/dist/types/hindsight/state.d.ts +9 -3
- package/dist/types/mcp/manager.d.ts +1 -1
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +9 -0
- package/dist/types/session/auth-storage.d.ts +2 -2
- package/dist/types/task/types.d.ts +2 -0
- package/dist/types/tools/index.d.ts +16 -0
- package/dist/types/tools/path-utils.d.ts +11 -0
- package/package.json +9 -9
- package/src/cli/dry-balance-cli.ts +823 -0
- package/src/cli-commands.ts +1 -0
- package/src/commands/dry-balance.ts +43 -0
- package/src/config/model-registry.ts +6 -0
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings.ts +38 -0
- package/src/discovery/builtin-rules/ts-no-tiny-functions.md +1 -0
- package/src/discovery/github.ts +37 -1
- package/src/discovery/helpers.ts +3 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +245 -25
- package/src/hindsight/backend.ts +184 -35
- package/src/hindsight/bank.ts +32 -22
- package/src/hindsight/mental-models.ts +1 -1
- package/src/hindsight/state.ts +21 -7
- package/src/internal-urls/docs-index.generated.ts +4 -4
- package/src/internal-urls/omp-protocol.ts +8 -2
- package/src/mcp/manager.ts +40 -21
- package/src/modes/components/transcript-container.ts +14 -3
- package/src/modes/components/tree-selector.ts +29 -2
- package/src/modes/controllers/input-controller.ts +8 -2
- package/src/modes/setup-wizard/scenes/sign-in.ts +27 -7
- package/src/prompts/agents/explore.md +1 -0
- package/src/prompts/agents/librarian.md +1 -0
- package/src/prompts/dry-balance-bench.md +8 -0
- package/src/sdk.ts +82 -9
- package/src/session/agent-session.ts +66 -7
- package/src/session/auth-storage.ts +4 -0
- package/src/task/executor.ts +6 -2
- package/src/task/index.ts +8 -7
- package/src/task/types.ts +2 -0
- package/src/tools/bash.ts +3 -4
- package/src/tools/index.ts +16 -0
- package/src/tools/job.ts +3 -3
- package/src/tools/memory-reflect.ts +2 -2
- package/src/tools/path-utils.ts +21 -0
- package/src/tools/search.ts +18 -1
- package/src/utils/file-mentions.ts +7 -107
- package/src/utils/title-generator.ts +58 -37
package/src/hindsight/backend.ts
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
|
|
10
10
|
import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
|
|
11
11
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
12
|
-
import type
|
|
12
|
+
import { onHindsightScopeChanged, type Settings } from "../config/settings";
|
|
13
13
|
import type { MemoryBackend, MemoryBackendStartOptions } from "../memory-backend/types";
|
|
14
14
|
import type { AgentSession } from "../session/agent-session";
|
|
15
|
-
import { computeBankScope } from "./bank";
|
|
15
|
+
import { type BankScope, computeBankScope } from "./bank";
|
|
16
16
|
import { createHindsightClient } from "./client";
|
|
17
17
|
import { isHindsightConfigured, loadHindsightConfig } from "./config";
|
|
18
18
|
import type { HindsightMessage } from "./content";
|
|
@@ -60,12 +60,16 @@ export const hindsightBackend: MemoryBackend = {
|
|
|
60
60
|
recallTagsMatch: parent.recallTagsMatch,
|
|
61
61
|
config: parent.config,
|
|
62
62
|
session,
|
|
63
|
-
|
|
63
|
+
banksSet: parent.banksSet,
|
|
64
64
|
lastRetainedTurn: 0,
|
|
65
65
|
hasRecalledForFirstTurn: true,
|
|
66
66
|
aliasOf: parent,
|
|
67
67
|
}),
|
|
68
68
|
);
|
|
69
|
+
// Aliases don't run auto-recall/auto-retain, so any pending retain
|
|
70
|
+
// queue belongs to the previous alias and is safe to drop after a
|
|
71
|
+
// best-effort flush (`flushRetainQueue` is no-op when empty).
|
|
72
|
+
await previous?.flushRetainQueue();
|
|
69
73
|
previous?.dispose();
|
|
70
74
|
return;
|
|
71
75
|
}
|
|
@@ -76,38 +80,7 @@ export const hindsightBackend: MemoryBackend = {
|
|
|
76
80
|
return;
|
|
77
81
|
}
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
const scope = computeBankScope(config, session.sessionManager.getCwd());
|
|
81
|
-
|
|
82
|
-
const state = new HindsightSessionState({
|
|
83
|
-
sessionId,
|
|
84
|
-
client,
|
|
85
|
-
bankId: scope.bankId,
|
|
86
|
-
retainTags: scope.retainTags,
|
|
87
|
-
recallTags: scope.recallTags,
|
|
88
|
-
recallTagsMatch: scope.recallTagsMatch,
|
|
89
|
-
config,
|
|
90
|
-
session,
|
|
91
|
-
missionsSet: new Set(),
|
|
92
|
-
lastRetainedTurn: 0,
|
|
93
|
-
hasRecalledForFirstTurn: false,
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Cleanup any stale state for this session (defensive — prevents leaks
|
|
97
|
-
// when a session is reused without going through dispose).
|
|
98
|
-
const previous = session.setHindsightSessionState(state);
|
|
99
|
-
previous?.dispose();
|
|
100
|
-
state.attachSessionListeners();
|
|
101
|
-
|
|
102
|
-
// Kick off mental-model bootstrap. Resolves asynchronously; the first
|
|
103
|
-
// turn races and is covered in `beforeAgentStartPrompt` via
|
|
104
|
-
// `mentalModelsLoadPromise`. Subsequent turns see the populated cache
|
|
105
|
-
// because `runMentalModelLoad` calls `refreshBaseSystemPrompt`.
|
|
106
|
-
if (config.mentalModelsEnabled) {
|
|
107
|
-
state.mentalModelsLoadPromise = state.runMentalModelLoad(scope).catch(err => {
|
|
108
|
-
logger.debug("Hindsight: mental-model bootstrap failed", { bankId: state.bankId, error: String(err) });
|
|
109
|
-
});
|
|
110
|
-
}
|
|
83
|
+
await installPrimaryState(session, settings, new Set());
|
|
111
84
|
},
|
|
112
85
|
|
|
113
86
|
async buildDeveloperInstructions(_agentDir, settings, session): Promise<string | undefined> {
|
|
@@ -173,6 +146,182 @@ export const hindsightBackend: MemoryBackend = {
|
|
|
173
146
|
return await state.recallForCompaction(flat);
|
|
174
147
|
},
|
|
175
148
|
};
|
|
149
|
+
interface PrimaryRebuildTask {
|
|
150
|
+
pending: boolean;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const primaryRebuildTasks = new WeakMap<AgentSession, PrimaryRebuildTask>();
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Coalesce and serialize live scope rebuilds for one session. Cwd reloads fire
|
|
157
|
+
* all settings hooks synchronously; running every callback immediately would
|
|
158
|
+
* let multiple rebuilds capture the same old state and leak the fresh states
|
|
159
|
+
* installed by earlier continuations.
|
|
160
|
+
*/
|
|
161
|
+
function schedulePrimaryStateRebuild(session: AgentSession): void {
|
|
162
|
+
const task = primaryRebuildTasks.get(session);
|
|
163
|
+
if (task) {
|
|
164
|
+
task.pending = true;
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const nextTask: PrimaryRebuildTask = { pending: true };
|
|
169
|
+
primaryRebuildTasks.set(session, nextTask);
|
|
170
|
+
void Promise.resolve()
|
|
171
|
+
.then(async () => {
|
|
172
|
+
while (nextTask.pending) {
|
|
173
|
+
nextTask.pending = false;
|
|
174
|
+
try {
|
|
175
|
+
await rebuildPrimaryStateOnScopeChange(session);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
logger.warn("Hindsight: scope rebuild failed", { error: String(err) });
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
})
|
|
181
|
+
.finally(() => {
|
|
182
|
+
if (primaryRebuildTasks.get(session) === nextTask) {
|
|
183
|
+
primaryRebuildTasks.delete(session);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Build (or rebuild) the primary `HindsightSessionState` for `session` from
|
|
190
|
+
* the current settings and install it. Disposes any previous primary state
|
|
191
|
+
* after flushing its retain queue so in-flight tool-initiated retains land in
|
|
192
|
+
* the bank that was selected when they were enqueued, not in the new bank.
|
|
193
|
+
*
|
|
194
|
+
* The created state takes ownership of the `onHindsightScopeChanged`
|
|
195
|
+
* subscription so subsequent `hindsight.bankId` / `bankIdPrefix` / `scoping`
|
|
196
|
+
* edits trigger another rebuild from the same wiring.
|
|
197
|
+
*/
|
|
198
|
+
async function installPrimaryState(
|
|
199
|
+
session: AgentSession,
|
|
200
|
+
settings: Settings,
|
|
201
|
+
banksSet: Set<string>,
|
|
202
|
+
): Promise<HindsightSessionState | undefined> {
|
|
203
|
+
const sessionId = session.sessionId;
|
|
204
|
+
if (!sessionId) return undefined;
|
|
205
|
+
|
|
206
|
+
const config = loadHindsightConfig(settings);
|
|
207
|
+
if (!isHindsightConfigured(config)) return undefined;
|
|
208
|
+
|
|
209
|
+
const client = createHindsightClient(config);
|
|
210
|
+
const scope = computeBankScope(config, session.sessionManager.getCwd());
|
|
211
|
+
|
|
212
|
+
// Cleanup any stale state for this session (defensive — prevents leaks
|
|
213
|
+
// when a session is reused without going through dispose). Flush the
|
|
214
|
+
// previous state's retain queue BEFORE clearing it, otherwise
|
|
215
|
+
// `HindsightRetainQueue.#doFlush` sees `session.getHindsightSessionState()
|
|
216
|
+
// !== state` and drops the batch. Re-read after the await so a concurrent
|
|
217
|
+
// owner cannot leave the actual current state undisposed.
|
|
218
|
+
let previous = session.getHindsightSessionState();
|
|
219
|
+
if (previous) {
|
|
220
|
+
await previous.flushRetainQueue();
|
|
221
|
+
}
|
|
222
|
+
const latest = session.getHindsightSessionState();
|
|
223
|
+
if (latest && latest !== previous) {
|
|
224
|
+
previous?.dispose();
|
|
225
|
+
previous = latest;
|
|
226
|
+
await previous.flushRetainQueue();
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const state = new HindsightSessionState({
|
|
230
|
+
sessionId,
|
|
231
|
+
client,
|
|
232
|
+
bankId: scope.bankId,
|
|
233
|
+
retainTags: scope.retainTags,
|
|
234
|
+
recallTags: scope.recallTags,
|
|
235
|
+
recallTagsMatch: scope.recallTagsMatch,
|
|
236
|
+
config,
|
|
237
|
+
session,
|
|
238
|
+
banksSet,
|
|
239
|
+
lastRetainedTurn: 0,
|
|
240
|
+
hasRecalledForFirstTurn: false,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Subscribe BEFORE installing: if the operator manages to flip another
|
|
244
|
+
// setting between install and subscribe, we'd miss the edge.
|
|
245
|
+
state.unsubscribeScope = onHindsightScopeChanged(() => {
|
|
246
|
+
schedulePrimaryStateRebuild(session);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const displaced = session.setHindsightSessionState(state);
|
|
250
|
+
if (displaced && displaced !== previous) {
|
|
251
|
+
await displaced.flushRetainQueue();
|
|
252
|
+
displaced.dispose();
|
|
253
|
+
}
|
|
254
|
+
previous?.dispose();
|
|
255
|
+
state.attachSessionListeners();
|
|
256
|
+
|
|
257
|
+
// Kick off mental-model bootstrap. Resolves asynchronously; the first
|
|
258
|
+
// turn races and is covered in `beforeAgentStartPrompt` via
|
|
259
|
+
// `mentalModelsLoadPromise`. Subsequent turns see the populated cache
|
|
260
|
+
// because `runMentalModelLoad` calls `refreshBaseSystemPrompt`.
|
|
261
|
+
if (config.mentalModelsEnabled) {
|
|
262
|
+
state.mentalModelsLoadPromise = state.runMentalModelLoad(scope).catch(err => {
|
|
263
|
+
logger.debug("Hindsight: mental-model bootstrap failed", { bankId: state.bankId, error: String(err) });
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return state;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* `onHindsightScopeChanged` handler: re-evaluate the bank scope from current
|
|
272
|
+
* settings and rebuild the primary state when it has actually drifted. No-op
|
|
273
|
+
* when the scope is unchanged or the session is no longer hosting a primary
|
|
274
|
+
* state (e.g. it was wiped to `undefined`, or this is a subagent alias).
|
|
275
|
+
*/
|
|
276
|
+
async function rebuildPrimaryStateOnScopeChange(session: AgentSession): Promise<void> {
|
|
277
|
+
const current = session.getHindsightSessionState();
|
|
278
|
+
if (!current || current.aliasOf) return;
|
|
279
|
+
|
|
280
|
+
const settings = session.settings;
|
|
281
|
+
const config = loadHindsightConfig(settings);
|
|
282
|
+
if (!isHindsightConfigured(config)) {
|
|
283
|
+
// Hindsight effectively unwired mid-session. Flush before clearing so
|
|
284
|
+
// queued retains don't get dropped by `HindsightRetainQueue.#doFlush`.
|
|
285
|
+
await current.flushRetainQueue();
|
|
286
|
+
const previous = session.setHindsightSessionState(undefined);
|
|
287
|
+
previous?.dispose();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const next = computeBankScope(config, session.sessionManager.getCwd());
|
|
292
|
+
if (bankScopesEqual(next, current)) return;
|
|
293
|
+
|
|
294
|
+
// Preserve the banksSet so we don't re-PUT banks we've already confirmed.
|
|
295
|
+
await installPrimaryState(session, settings, current.banksSet);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/** Tag-array equality: order matters because we never reorder on the way in. */
|
|
299
|
+
function stringArraysEqual(a: string[] | undefined, b: string[] | undefined): boolean {
|
|
300
|
+
if (a === b) return true;
|
|
301
|
+
if (!a || !b) return false;
|
|
302
|
+
if (a.length !== b.length) return false;
|
|
303
|
+
for (let i = 0; i < a.length; i++) {
|
|
304
|
+
if (a[i] !== b[i]) return false;
|
|
305
|
+
}
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Structural compare of a freshly resolved `BankScope` against a live state's
|
|
311
|
+
* bank routing. Used by the scope-change handler to skip rebuilds that don't
|
|
312
|
+
* actually move the bank or its tag filters.
|
|
313
|
+
*/
|
|
314
|
+
function bankScopesEqual(
|
|
315
|
+
scope: BankScope,
|
|
316
|
+
state: Pick<HindsightSessionState, "bankId" | "retainTags" | "recallTags" | "recallTagsMatch">,
|
|
317
|
+
): boolean {
|
|
318
|
+
return (
|
|
319
|
+
scope.bankId === state.bankId &&
|
|
320
|
+
stringArraysEqual(scope.retainTags, state.retainTags) &&
|
|
321
|
+
stringArraysEqual(scope.recallTags, state.recallTags) &&
|
|
322
|
+
scope.recallTagsMatch === state.recallTagsMatch
|
|
323
|
+
);
|
|
324
|
+
}
|
|
176
325
|
|
|
177
326
|
/** Reduce arbitrary AgentMessages into the Hindsight flat-text shape. */
|
|
178
327
|
function flattenMessagesForRecall(messages: AgentMessage[]): HindsightMessage[] {
|
package/src/hindsight/bank.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Bank ID derivation, project-tag scoping, and first-use
|
|
2
|
+
* Bank ID derivation, project-tag scoping, and first-use bank setup.
|
|
3
3
|
*
|
|
4
4
|
* Three scoping modes (`HindsightConfig.scoping`):
|
|
5
5
|
* - `global` — single shared bank, no per-project filter.
|
|
@@ -11,10 +11,13 @@
|
|
|
11
11
|
* The base bank id is `bankIdPrefix-bankId` (default `omp`). Per-project mode
|
|
12
12
|
* appends `-<project>`; tagged mode leaves the bank untouched and uses tags.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
15
|
-
* banks we've already
|
|
16
|
-
* `createBank` call.
|
|
17
|
-
*
|
|
14
|
+
* Bank existence is idempotent at module level — a banksSet keeps track of
|
|
15
|
+
* banks we've already PUT so each session boundary doesn't fire a fresh
|
|
16
|
+
* `createBank` call. The PUT is idempotent server-side, so re-firing on a hot
|
|
17
|
+
* path would only burn round-trips. Failures are swallowed: missing the
|
|
18
|
+
* mission patch is an optimisation, but the bank ITSELF must exist before
|
|
19
|
+
* mental-model bootstrap or the first retain, otherwise the very first POST
|
|
20
|
+
* lands against a missing bank.
|
|
18
21
|
*/
|
|
19
22
|
|
|
20
23
|
import * as path from "node:path";
|
|
@@ -93,39 +96,46 @@ export function deriveBankId(config: HindsightConfig, directory: string): string
|
|
|
93
96
|
}
|
|
94
97
|
|
|
95
98
|
/**
|
|
96
|
-
* Ensure a bank
|
|
99
|
+
* Ensure a bank exists, and patch its reflect/retain mission on first use.
|
|
97
100
|
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
101
|
+
* Idempotent: skips the PUT when the bank id is already in the supplied set.
|
|
102
|
+
* The mission body is optional — when `bankMission` is blank we still PUT to
|
|
103
|
+
* make sure the bank itself is created, so mental-model bootstrap and the
|
|
104
|
+
* first retain don't land against a non-existent bank.
|
|
105
|
+
*
|
|
106
|
+
* The set is capped; on overflow we drop the oldest half so it cannot grow
|
|
107
|
+
* unboundedly across long-lived processes.
|
|
100
108
|
*/
|
|
101
|
-
export async function
|
|
109
|
+
export async function ensureBankExists(
|
|
102
110
|
client: HindsightApi,
|
|
103
111
|
bankId: string,
|
|
104
112
|
config: HindsightConfig,
|
|
105
|
-
|
|
113
|
+
banksSet: Set<string>,
|
|
106
114
|
): Promise<void> {
|
|
115
|
+
if (banksSet.has(bankId)) return;
|
|
116
|
+
|
|
107
117
|
const mission = config.bankMission?.trim();
|
|
108
|
-
|
|
109
|
-
if (missionsSet.has(bankId)) return;
|
|
118
|
+
const retainMission = config.retainMission?.trim();
|
|
110
119
|
|
|
111
120
|
try {
|
|
112
121
|
await client.createBank(bankId, {
|
|
113
|
-
reflectMission: mission,
|
|
114
|
-
retainMission:
|
|
122
|
+
reflectMission: mission || undefined,
|
|
123
|
+
retainMission: retainMission || undefined,
|
|
115
124
|
});
|
|
116
|
-
|
|
117
|
-
if (
|
|
118
|
-
const keys = [...
|
|
125
|
+
banksSet.add(bankId);
|
|
126
|
+
if (banksSet.size > MISSION_SET_CAP) {
|
|
127
|
+
const keys = [...banksSet].sort();
|
|
119
128
|
for (const key of keys.slice(0, keys.length >> 1)) {
|
|
120
|
-
|
|
129
|
+
banksSet.delete(key);
|
|
121
130
|
}
|
|
122
131
|
}
|
|
123
132
|
if (config.debug) {
|
|
124
|
-
logger.debug("Hindsight:
|
|
133
|
+
logger.debug("Hindsight: ensured bank", { bankId, mission: Boolean(mission) });
|
|
125
134
|
}
|
|
126
135
|
} catch (err) {
|
|
127
|
-
//
|
|
128
|
-
// reject the call. Either way, retain/recall
|
|
129
|
-
|
|
136
|
+
// Bank creation is best-effort; the server may already have it, or the
|
|
137
|
+
// API may reject the call. Either way, downstream retain/recall calls
|
|
138
|
+
// will surface a clearer error if the bank really is missing.
|
|
139
|
+
logger.debug("Hindsight: ensureBankExists failed", { bankId, error: String(err) });
|
|
130
140
|
}
|
|
131
141
|
}
|
|
@@ -112,7 +112,7 @@ function dedupe<T>(items: T[]): T[] {
|
|
|
112
112
|
* Idempotently create any seed mental models that don't already exist on the
|
|
113
113
|
* bank. Best-effort: a list/create failure does not throw — mental models are
|
|
114
114
|
* an optimization, not a precondition for retain/recall, and we mirror the
|
|
115
|
-
* swallow-on-failure pattern used by `
|
|
115
|
+
* swallow-on-failure pattern used by `ensureBankExists`.
|
|
116
116
|
*
|
|
117
117
|
* Existing models are NEVER modified. See module docstring.
|
|
118
118
|
*/
|
package/src/hindsight/state.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { logger } from "@oh-my-pi/pi-utils";
|
|
2
2
|
import type { AgentSession } from "../session/agent-session";
|
|
3
|
-
import { type BankScope,
|
|
3
|
+
import { type BankScope, ensureBankExists } from "./bank";
|
|
4
4
|
import type { HindsightApi, MemoryItemInput } from "./client";
|
|
5
5
|
import type { HindsightConfig } from "./config";
|
|
6
6
|
import {
|
|
@@ -45,12 +45,12 @@ export interface HindsightSessionStateOptions {
|
|
|
45
45
|
recallTagsMatch?: "any" | "all" | "any_strict" | "all_strict";
|
|
46
46
|
config: HindsightConfig;
|
|
47
47
|
session: AgentSession;
|
|
48
|
-
|
|
48
|
+
banksSet: Set<string>;
|
|
49
49
|
lastRetainedTurn?: number;
|
|
50
50
|
hasRecalledForFirstTurn?: boolean;
|
|
51
51
|
/**
|
|
52
52
|
* When set, this entry is a subagent alias that reuses the parent's bank,
|
|
53
|
-
* scope, config, client, and
|
|
53
|
+
* scope, config, client, and banksSet. Aliases skip auto-recall and
|
|
54
54
|
* auto-retain — those run on the parent only — but the recall/retain/reflect
|
|
55
55
|
* tools resolve via the alias so they persist to the same bank as the parent.
|
|
56
56
|
*/
|
|
@@ -148,7 +148,7 @@ export class HindsightRetainQueue {
|
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
try {
|
|
151
|
-
await
|
|
151
|
+
await ensureBankExists(state.client, state.bankId, state.config, state.banksSet);
|
|
152
152
|
const batch: MemoryItemInput[] = items.map(item => ({
|
|
153
153
|
content: item.content,
|
|
154
154
|
context: item.context ?? state.config.retainContext,
|
|
@@ -198,7 +198,7 @@ export class HindsightSessionState {
|
|
|
198
198
|
recallTagsMatch?: "any" | "all" | "any_strict" | "all_strict";
|
|
199
199
|
config: HindsightConfig;
|
|
200
200
|
session: AgentSession;
|
|
201
|
-
|
|
201
|
+
banksSet: Set<string>;
|
|
202
202
|
lastRetainedTurn: number;
|
|
203
203
|
hasRecalledForFirstTurn: boolean;
|
|
204
204
|
lastRecallSnippet?: string;
|
|
@@ -213,6 +213,12 @@ export class HindsightSessionState {
|
|
|
213
213
|
*/
|
|
214
214
|
mentalModelsLoadPromise?: Promise<void>;
|
|
215
215
|
unsubscribe?: () => void;
|
|
216
|
+
/**
|
|
217
|
+
* Releases the `onHindsightScopeChanged` subscription that drives live
|
|
218
|
+
* rebuilds when `hindsight.bankId` / `bankIdPrefix` / `scoping` change.
|
|
219
|
+
* Only set on primary states; aliases inherit the parent's subscription.
|
|
220
|
+
*/
|
|
221
|
+
unsubscribeScope?: () => void;
|
|
216
222
|
/** Alias states delegate persistence config to a primary parent state. */
|
|
217
223
|
aliasOf?: HindsightSessionState;
|
|
218
224
|
readonly retainQueue: HindsightRetainQueue;
|
|
@@ -226,7 +232,7 @@ export class HindsightSessionState {
|
|
|
226
232
|
this.recallTagsMatch = options.recallTagsMatch;
|
|
227
233
|
this.config = options.config;
|
|
228
234
|
this.session = options.session;
|
|
229
|
-
this.
|
|
235
|
+
this.banksSet = options.banksSet;
|
|
230
236
|
this.lastRetainedTurn = options.lastRetainedTurn ?? 0;
|
|
231
237
|
this.hasRecalledForFirstTurn = options.hasRecalledForFirstTurn ?? false;
|
|
232
238
|
this.aliasOf = options.aliasOf;
|
|
@@ -291,7 +297,7 @@ export class HindsightSessionState {
|
|
|
291
297
|
const { transcript } = prepareRetentionTranscript(target, true);
|
|
292
298
|
if (!transcript) return;
|
|
293
299
|
|
|
294
|
-
await
|
|
300
|
+
await ensureBankExists(this.client, this.bankId, this.config, this.banksSet);
|
|
295
301
|
await this.client.retain(this.bankId, transcript, {
|
|
296
302
|
documentId,
|
|
297
303
|
context: this.config.retainContext,
|
|
@@ -398,6 +404,12 @@ export class HindsightSessionState {
|
|
|
398
404
|
async runMentalModelLoad(scope: BankScope): Promise<void> {
|
|
399
405
|
if (!this.config.mentalModelsEnabled) return;
|
|
400
406
|
|
|
407
|
+
// Create/ensure the bank BEFORE the first mental-model POST so we don't
|
|
408
|
+
// land `createMentalModel` against a bank the server has never seen —
|
|
409
|
+
// that surfaces as a FK / 404 on Hindsight's side. `ensureBankExists`
|
|
410
|
+
// is idempotent (PUT) and skips after the first call via `banksSet`.
|
|
411
|
+
await ensureBankExists(this.client, this.bankId, this.config, this.banksSet);
|
|
412
|
+
|
|
401
413
|
// Seeding is opt-in (`hindsight.mentalModelAutoSeed`). Default behaviour is
|
|
402
414
|
// read-only: we surface whatever models the operator has curated on the
|
|
403
415
|
// bank, but we do NOT POST to create new ones unless they explicitly
|
|
@@ -456,6 +468,8 @@ export class HindsightSessionState {
|
|
|
456
468
|
dispose(): void {
|
|
457
469
|
this.unsubscribe?.();
|
|
458
470
|
this.unsubscribe = undefined;
|
|
471
|
+
this.unsubscribeScope?.();
|
|
472
|
+
this.unsubscribeScope = undefined;
|
|
459
473
|
this.retainQueue.dispose();
|
|
460
474
|
}
|
|
461
475
|
|