@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
|
@@ -102,6 +102,7 @@ import { ExtensionToolWrapper } from "../extensibility/extensions/wrapper";
|
|
|
102
102
|
import type { HookCommandContext } from "../extensibility/hooks/types";
|
|
103
103
|
import type { Skill, SkillWarning } from "../extensibility/skills";
|
|
104
104
|
import { expandSlashCommand, type FileSlashCommand } from "../extensibility/slash-commands";
|
|
105
|
+
import type { HindsightSessionState } from "../hindsight/state";
|
|
105
106
|
import { type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
106
107
|
import {
|
|
107
108
|
buildDiscoverableMCPSearchIndex,
|
|
@@ -563,6 +564,7 @@ export class AgentSession {
|
|
|
563
564
|
#lastSuccessfulYieldToolCallId: string | undefined = undefined;
|
|
564
565
|
#promptGeneration = 0;
|
|
565
566
|
#providerSessionState = new Map<string, ProviderSessionState>();
|
|
567
|
+
#hindsightSessionState: HindsightSessionState | undefined = undefined;
|
|
566
568
|
|
|
567
569
|
#startPowerAssertion(): void {
|
|
568
570
|
if (process.platform !== "darwin") {
|
|
@@ -702,6 +704,16 @@ export class AgentSession {
|
|
|
702
704
|
return this.#providerSessionState;
|
|
703
705
|
}
|
|
704
706
|
|
|
707
|
+
getHindsightSessionState(): HindsightSessionState | undefined {
|
|
708
|
+
return this.#hindsightSessionState;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
setHindsightSessionState(state: HindsightSessionState | undefined): HindsightSessionState | undefined {
|
|
712
|
+
const previous = this.#hindsightSessionState;
|
|
713
|
+
this.#hindsightSessionState = state;
|
|
714
|
+
return previous;
|
|
715
|
+
}
|
|
716
|
+
|
|
705
717
|
/** TTSR manager for time-traveling stream rules */
|
|
706
718
|
get ttsrManager(): TtsrManager | undefined {
|
|
707
719
|
return this.#ttsrManager;
|
|
@@ -1964,6 +1976,22 @@ export class AgentSession {
|
|
|
1964
1976
|
this.#unsubscribeAgent = this.agent.subscribe(this.#handleAgentEvent);
|
|
1965
1977
|
}
|
|
1966
1978
|
|
|
1979
|
+
/** Keep Hindsight metadata aligned when the underlying agent session id changes. */
|
|
1980
|
+
#rekeyHindsightMemoryForCurrentSessionId(): void {
|
|
1981
|
+
if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
|
|
1982
|
+
const sid = this.agent.sessionId;
|
|
1983
|
+
if (!sid) return;
|
|
1984
|
+
this.getHindsightSessionState()?.setSessionId(sid);
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
/** New session file: reset auto-recall / retain-threshold counters for the new transcript. */
|
|
1988
|
+
#resetHindsightConversationTrackingIfHindsight(): void {
|
|
1989
|
+
if (resolveMemoryBackend(this.settings).id !== "hindsight") return;
|
|
1990
|
+
const state = this.getHindsightSessionState();
|
|
1991
|
+
if (!state || state.aliasOf) return;
|
|
1992
|
+
state.resetConversationTracking();
|
|
1993
|
+
}
|
|
1994
|
+
|
|
1967
1995
|
/**
|
|
1968
1996
|
* Remove all listeners, flush pending writes, and disconnect from agent.
|
|
1969
1997
|
* Call this when completely done with the session.
|
|
@@ -1994,6 +2022,9 @@ export class AgentSession {
|
|
|
1994
2022
|
this.#stopPowerAssertion();
|
|
1995
2023
|
await this.sessionManager.close();
|
|
1996
2024
|
this.#closeAllProviderSessions("dispose");
|
|
2025
|
+
const hindsightState = this.setHindsightSessionState(undefined);
|
|
2026
|
+
await hindsightState?.flushRetainQueue();
|
|
2027
|
+
hindsightState?.dispose();
|
|
1997
2028
|
this.#disconnectFromAgent();
|
|
1998
2029
|
this.#eventListeners = [];
|
|
1999
2030
|
}
|
|
@@ -3626,6 +3657,8 @@ export class AgentSession {
|
|
|
3626
3657
|
await this.sessionManager.newSession(options);
|
|
3627
3658
|
this.setTodoPhases([]);
|
|
3628
3659
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
3660
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
3661
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
3629
3662
|
this.#steeringMessages = [];
|
|
3630
3663
|
this.#followUpMessages = [];
|
|
3631
3664
|
this.#pendingNextTurnMessages = [];
|
|
@@ -3719,6 +3752,7 @@ export class AgentSession {
|
|
|
3719
3752
|
|
|
3720
3753
|
// Update agent session ID
|
|
3721
3754
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
3755
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
3722
3756
|
|
|
3723
3757
|
// Emit session_switch event with reason "fork" to hooks
|
|
3724
3758
|
if (this.#extensionRunner) {
|
|
@@ -4261,7 +4295,7 @@ export class AgentSession {
|
|
|
4261
4295
|
if (!backend.preCompactionContext) return undefined;
|
|
4262
4296
|
const messages = preparation.messagesToSummarize.concat(preparation.turnPrefixMessages);
|
|
4263
4297
|
try {
|
|
4264
|
-
return await backend.preCompactionContext(messages, this.settings);
|
|
4298
|
+
return await backend.preCompactionContext(messages, this.settings, this);
|
|
4265
4299
|
} catch (err) {
|
|
4266
4300
|
logger.debug("Memory backend preCompactionContext failed", {
|
|
4267
4301
|
backend: backend.id,
|
|
@@ -4425,6 +4459,8 @@ export class AgentSession {
|
|
|
4425
4459
|
await this.sessionManager.newSession(previousSessionFile ? { parentSession: previousSessionFile } : undefined);
|
|
4426
4460
|
this.agent.reset();
|
|
4427
4461
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
4462
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
4463
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
4428
4464
|
this.#steeringMessages = [];
|
|
4429
4465
|
this.#followUpMessages = [];
|
|
4430
4466
|
this.#pendingNextTurnMessages = [];
|
|
@@ -6571,6 +6607,7 @@ export class AgentSession {
|
|
|
6571
6607
|
try {
|
|
6572
6608
|
await this.sessionManager.setSessionFile(sessionPath);
|
|
6573
6609
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
6610
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6574
6611
|
|
|
6575
6612
|
const sessionContext = this.buildDisplaySessionContext();
|
|
6576
6613
|
const didReloadConversationChange =
|
|
@@ -6640,11 +6677,15 @@ export class AgentSession {
|
|
|
6640
6677
|
? undefined
|
|
6641
6678
|
: configuredServiceTier;
|
|
6642
6679
|
|
|
6680
|
+
if (switchingToDifferentSession) {
|
|
6681
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
6682
|
+
}
|
|
6643
6683
|
this.#reconnectToAgent();
|
|
6644
6684
|
return true;
|
|
6645
6685
|
} catch (error) {
|
|
6646
6686
|
this.sessionManager.restoreState(previousSessionState);
|
|
6647
6687
|
this.agent.sessionId = previousSessionState.sessionId;
|
|
6688
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6648
6689
|
let restoreMcpError: unknown;
|
|
6649
6690
|
try {
|
|
6650
6691
|
await this.#restoreMCPSelectionsForSessionContext(previousSessionContext, {
|
|
@@ -6736,6 +6777,8 @@ export class AgentSession {
|
|
|
6736
6777
|
}
|
|
6737
6778
|
this.#syncTodoPhasesFromBranch();
|
|
6738
6779
|
this.agent.sessionId = this.sessionManager.getSessionId();
|
|
6780
|
+
this.#rekeyHindsightMemoryForCurrentSessionId();
|
|
6781
|
+
this.#resetHindsightConversationTrackingIfHindsight();
|
|
6739
6782
|
|
|
6740
6783
|
// Reload messages from entries (works for both file and in-memory mode)
|
|
6741
6784
|
const sessionContext = this.buildDisplaySessionContext();
|
|
@@ -598,6 +598,16 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<BuiltinSlashCommandSpec> = [
|
|
|
598
598
|
{ name: "reset", description: "Alias for clear" },
|
|
599
599
|
{ name: "enqueue", description: "Enqueue memory consolidation maintenance" },
|
|
600
600
|
{ name: "rebuild", description: "Alias for enqueue" },
|
|
601
|
+
{ name: "mm list", description: "List mental models on the active bank" },
|
|
602
|
+
{ name: "mm show", description: "Show one mental model (id required)" },
|
|
603
|
+
{
|
|
604
|
+
name: "mm refresh",
|
|
605
|
+
description: "Refresh auto-refresh models bank-wide, or one model by id",
|
|
606
|
+
},
|
|
607
|
+
{ name: "mm history", description: "Diff the change history of a mental model" },
|
|
608
|
+
{ name: "mm seed", description: "Create any built-in mental models that are missing" },
|
|
609
|
+
{ name: "mm delete", description: "Delete a mental model from the bank (id required)" },
|
|
610
|
+
{ name: "mm reload", description: "Re-pull the cached <mental_models> block" },
|
|
601
611
|
],
|
|
602
612
|
allowArgs: true,
|
|
603
613
|
handle: async (command, runtime) => {
|
package/src/task/executor.ts
CHANGED
|
@@ -17,6 +17,7 @@ import { SETTINGS_SCHEMA, type SettingPath } from "../config/settings-schema";
|
|
|
17
17
|
import type { CustomTool } from "../extensibility/custom-tools/types";
|
|
18
18
|
import { runExtensionCompact, runExtensionSetModel } from "../extensibility/extensions/compact-handler";
|
|
19
19
|
import type { Skill } from "../extensibility/skills";
|
|
20
|
+
import type { HindsightSessionState } from "../hindsight/state";
|
|
20
21
|
import type { LocalProtocolOptions } from "../internal-urls";
|
|
21
22
|
import { callTool } from "../mcp/client";
|
|
22
23
|
import type { MCPManager } from "../mcp/manager";
|
|
@@ -163,6 +164,7 @@ export interface ExecutorOptions {
|
|
|
163
164
|
settings?: Settings;
|
|
164
165
|
/** Override local:// protocol options so subagent shares parent's local:// root */
|
|
165
166
|
localProtocolOptions?: LocalProtocolOptions;
|
|
167
|
+
parentHindsightSessionState?: HindsightSessionState;
|
|
166
168
|
}
|
|
167
169
|
|
|
168
170
|
function parseStringifiedJson(value: unknown): unknown {
|
|
@@ -979,6 +981,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
979
981
|
hasUI: false,
|
|
980
982
|
spawns: spawnsEnv,
|
|
981
983
|
taskDepth: childDepth,
|
|
984
|
+
parentHindsightSessionState: options.parentHindsightSessionState,
|
|
982
985
|
parentTaskPrefix: id,
|
|
983
986
|
agentId: id,
|
|
984
987
|
agentDisplayName: agent.name,
|
package/src/task/index.ts
CHANGED
|
@@ -864,6 +864,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
864
864
|
skills: availableSkills,
|
|
865
865
|
promptTemplates,
|
|
866
866
|
localProtocolOptions,
|
|
867
|
+
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
867
868
|
});
|
|
868
869
|
}
|
|
869
870
|
|
|
@@ -918,6 +919,7 @@ export class TaskTool implements AgentTool<TSchema, TaskToolDetails, Theme> {
|
|
|
918
919
|
skills: availableSkills,
|
|
919
920
|
promptTemplates,
|
|
920
921
|
localProtocolOptions,
|
|
922
|
+
parentHindsightSessionState: this.session.getHindsightSessionState?.(),
|
|
921
923
|
});
|
|
922
924
|
if (mergeMode === "branch" && result.exitCode === 0) {
|
|
923
925
|
try {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
-
import { getHindsightSessionState } from "../hindsight/backend";
|
|
5
4
|
import { formatCurrentTime, formatMemories } from "../hindsight/content";
|
|
6
5
|
import recallDescription from "../prompts/tools/recall.md" with { type: "text" };
|
|
7
6
|
import type { ToolSession } from ".";
|
|
@@ -30,8 +29,7 @@ export class HindsightRecallTool implements AgentTool<typeof hindsightRecallSche
|
|
|
30
29
|
|
|
31
30
|
async execute(_id: string, params: HindsightRecallParams, signal?: AbortSignal): Promise<AgentToolResult> {
|
|
32
31
|
return untilAborted(signal, async () => {
|
|
33
|
-
const
|
|
34
|
-
const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
|
|
32
|
+
const state = this.session.getHindsightSessionState?.();
|
|
35
33
|
if (!state) {
|
|
36
34
|
throw new Error("Hindsight backend is not initialised for this session.");
|
|
37
35
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { logger, untilAborted } from "@oh-my-pi/pi-utils";
|
|
3
3
|
import { type Static, Type } from "@sinclair/typebox";
|
|
4
|
-
import { getHindsightSessionState } from "../hindsight/backend";
|
|
5
4
|
import { ensureBankMission } from "../hindsight/bank";
|
|
6
5
|
import reflectDescription from "../prompts/tools/reflect.md" with { type: "text" };
|
|
7
6
|
import type { ToolSession } from ".";
|
|
@@ -29,8 +28,7 @@ export class HindsightReflectTool implements AgentTool<typeof hindsightReflectSc
|
|
|
29
28
|
|
|
30
29
|
async execute(_id: string, params: HindsightReflectParams, signal?: AbortSignal): Promise<AgentToolResult> {
|
|
31
30
|
return untilAborted(signal, async () => {
|
|
32
|
-
const
|
|
33
|
-
const state = sessionId ? getHindsightSessionState(sessionId) : undefined;
|
|
31
|
+
const state = this.session.getHindsightSessionState?.();
|
|
34
32
|
if (!state) {
|
|
35
33
|
throw new Error("Hindsight backend is not initialised for this session.");
|
|
36
34
|
}
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { AgentTool, AgentToolResult } from "@oh-my-pi/pi-agent-core";
|
|
2
2
|
import { type Static, Type } from "@sinclair/typebox";
|
|
3
|
-
import { getHindsightSessionState } from "../hindsight/backend";
|
|
4
|
-
import { enqueueRetain } from "../hindsight/retain-queue";
|
|
5
3
|
import retainDescription from "../prompts/tools/retain.md" with { type: "text" };
|
|
6
4
|
import type { ToolSession } from ".";
|
|
7
5
|
|
|
@@ -39,18 +37,17 @@ export class HindsightRetainTool implements AgentTool<typeof hindsightRetainSche
|
|
|
39
37
|
}
|
|
40
38
|
|
|
41
39
|
async execute(_id: string, params: HindsightRetainParams): Promise<AgentToolResult> {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
if (!state || !sessionId) {
|
|
40
|
+
const state = this.session.getHindsightSessionState?.();
|
|
41
|
+
if (!state) {
|
|
45
42
|
throw new Error("Hindsight backend is not initialised for this session.");
|
|
46
43
|
}
|
|
47
44
|
|
|
48
|
-
// Push every item onto the
|
|
49
|
-
// queue flushes either when it reaches its batch threshold or when
|
|
50
|
-
// debounce timer fires. If the eventual batch fails, the queue
|
|
45
|
+
// Push every item onto the session-owned queue and return immediately.
|
|
46
|
+
// The queue flushes either when it reaches its batch threshold or when
|
|
47
|
+
// its debounce timer fires. If the eventual batch fails, the queue
|
|
51
48
|
// surfaces a UI-only warning notice — the LLM is not informed.
|
|
52
49
|
for (const item of params.items) {
|
|
53
|
-
enqueueRetain(
|
|
50
|
+
state.enqueueRetain(item.content, item.context);
|
|
54
51
|
}
|
|
55
52
|
|
|
56
53
|
const count = params.items.length;
|
package/src/tools/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type { Settings } from "../config/settings";
|
|
|
7
7
|
import { EditTool } from "../edit";
|
|
8
8
|
import { checkPythonKernelAvailability } from "../eval/py/kernel";
|
|
9
9
|
import type { Skill } from "../extensibility/skills";
|
|
10
|
+
import type { HindsightSessionState } from "../hindsight/state";
|
|
10
11
|
import type { InternalUrlRouter } from "../internal-urls";
|
|
11
12
|
import { LspTool } from "../lsp";
|
|
12
13
|
import type { DiscoverableMCPSearchIndex, DiscoverableMCPTool } from "../mcp/discoverable-tool-metadata";
|
|
@@ -141,6 +142,8 @@ export interface ToolSession {
|
|
|
141
142
|
trackEvalExecution?<T>(execution: Promise<T>, abortController: AbortController): Promise<T>;
|
|
142
143
|
/** Get session ID */
|
|
143
144
|
getSessionId?: () => string | null;
|
|
145
|
+
/** Get Hindsight runtime state for this agent session. */
|
|
146
|
+
getHindsightSessionState?: () => HindsightSessionState | undefined;
|
|
144
147
|
/** Agent identity used for IRC routing. Returns the registry id (e.g. "0-Main", "0-AuthLoader"). */
|
|
145
148
|
getAgentId?: () => string | null;
|
|
146
149
|
/** Look up a registered tool by name (used by the eval js backend's tool bridge). */
|
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Global, debounced batch queue for tool-initiated `retain` calls.
|
|
3
|
-
*
|
|
4
|
-
* The `retain` tool used to block on a single-item HTTP round trip per
|
|
5
|
-
* invocation. Now it pushes onto a per-session queue and returns immediately;
|
|
6
|
-
* a flush fires when:
|
|
7
|
-
* 1. the queue reaches `FLUSH_BATCH_SIZE`, or
|
|
8
|
-
* 2. `FLUSH_INTERVAL_MS` elapses since the queue first became non-empty.
|
|
9
|
-
*
|
|
10
|
-
* On batch failure we surface a UI-only notice via `session.emitNotice` —
|
|
11
|
-
* a single yellow "Hindsight: memory retention failed …" line in the TUI.
|
|
12
|
-
* The LLM is NOT told; the agent already received "Memory queued" and has
|
|
13
|
-
* moved on. This is purely so the user knows their facts didn't persist.
|
|
14
|
-
*
|
|
15
|
-
* Auto-retain (`retainSession` in backend.ts) is intentionally NOT routed
|
|
16
|
-
* through this queue — it submits a full transcript as one large item and
|
|
17
|
-
* already runs `async: true` server-side.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
import { logger } from "@oh-my-pi/pi-utils";
|
|
21
|
-
import { getHindsightSessionState, type HindsightSessionState } from "./backend";
|
|
22
|
-
import { ensureBankMission } from "./bank";
|
|
23
|
-
import type { MemoryItemInput } from "./client";
|
|
24
|
-
|
|
25
|
-
const FLUSH_BATCH_SIZE = 16;
|
|
26
|
-
const FLUSH_INTERVAL_MS = 5_000;
|
|
27
|
-
|
|
28
|
-
interface PendingItem {
|
|
29
|
-
content: string;
|
|
30
|
-
context?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
interface SessionQueue {
|
|
34
|
-
items: PendingItem[];
|
|
35
|
-
timer?: NodeJS.Timeout;
|
|
36
|
-
/** Currently in-flight flush; subsequent flushes await it before running. */
|
|
37
|
-
flushing?: Promise<void>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const QUEUES = new Map<string, SessionQueue>();
|
|
41
|
-
|
|
42
|
-
/** Push a memory item onto the session's retain queue. Returns immediately. */
|
|
43
|
-
export function enqueueRetain(sessionId: string, content: string, context?: string): void {
|
|
44
|
-
const queue = QUEUES.get(sessionId) ?? createQueue(sessionId);
|
|
45
|
-
queue.items.push({ content, context });
|
|
46
|
-
|
|
47
|
-
if (queue.items.length >= FLUSH_BATCH_SIZE) {
|
|
48
|
-
void flushSessionQueue(sessionId);
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
if (!queue.timer) {
|
|
52
|
-
queue.timer = setTimeout(() => {
|
|
53
|
-
void flushSessionQueue(sessionId);
|
|
54
|
-
}, FLUSH_INTERVAL_MS);
|
|
55
|
-
// Don't pin the event loop alive just for a pending retain flush.
|
|
56
|
-
queue.timer.unref?.();
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** Flush a single session's queue. Safe to call when empty or already in flight. */
|
|
61
|
-
export async function flushSessionQueue(sessionId: string): Promise<void> {
|
|
62
|
-
const queue = QUEUES.get(sessionId);
|
|
63
|
-
if (!queue) return;
|
|
64
|
-
|
|
65
|
-
if (queue.timer) {
|
|
66
|
-
clearTimeout(queue.timer);
|
|
67
|
-
queue.timer = undefined;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
if (queue.flushing) {
|
|
71
|
-
// Coalesce: wait for the in-flight flush, then drain anything that
|
|
72
|
-
// landed after it started so we don't strand items.
|
|
73
|
-
await queue.flushing;
|
|
74
|
-
if (queue.items.length > 0) {
|
|
75
|
-
await flushSessionQueue(sessionId);
|
|
76
|
-
}
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (queue.items.length === 0) {
|
|
81
|
-
QUEUES.delete(sessionId);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const items = queue.items.splice(0);
|
|
86
|
-
const flushPromise = doFlush(sessionId, items);
|
|
87
|
-
queue.flushing = flushPromise;
|
|
88
|
-
try {
|
|
89
|
-
await flushPromise;
|
|
90
|
-
} finally {
|
|
91
|
-
queue.flushing = undefined;
|
|
92
|
-
if (queue.items.length === 0 && !queue.timer) {
|
|
93
|
-
QUEUES.delete(sessionId);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/** Flush every pending session queue. Called from `clear`/`enqueue` backend hooks. */
|
|
99
|
-
export async function flushAllRetainQueues(): Promise<void> {
|
|
100
|
-
const ids = [...QUEUES.keys()];
|
|
101
|
-
await Promise.all(ids.map(id => flushSessionQueue(id)));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/** Test helper: clear timers and pending items without triggering flushes. */
|
|
105
|
-
export function clearRetainQueueForTest(): void {
|
|
106
|
-
for (const queue of QUEUES.values()) {
|
|
107
|
-
if (queue.timer) clearTimeout(queue.timer);
|
|
108
|
-
}
|
|
109
|
-
QUEUES.clear();
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/** Test helper: peek at queued count for a session. */
|
|
113
|
-
export function getRetainQueueDepthForTest(sessionId: string): number {
|
|
114
|
-
return QUEUES.get(sessionId)?.items.length ?? 0;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
async function doFlush(sessionId: string, items: PendingItem[]): Promise<void> {
|
|
118
|
-
const state = getHindsightSessionState(sessionId);
|
|
119
|
-
if (!state) {
|
|
120
|
-
// Session went away before we could flush. We can't notify anyone, so
|
|
121
|
-
// log and drop — these are best-effort facts, not transactional writes.
|
|
122
|
-
logger.warn("Hindsight retain queue: session vanished, dropping batch", {
|
|
123
|
-
sessionId,
|
|
124
|
-
items: items.length,
|
|
125
|
-
});
|
|
126
|
-
return;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
await ensureBankMission(state.client, state.bankId, state.config, state.missionsSet);
|
|
131
|
-
const batch: MemoryItemInput[] = items.map(item => ({
|
|
132
|
-
content: item.content,
|
|
133
|
-
context: item.context ?? state.config.retainContext,
|
|
134
|
-
metadata: { session_id: sessionId },
|
|
135
|
-
tags: state.retainTags,
|
|
136
|
-
}));
|
|
137
|
-
await state.client.retainBatch(state.bankId, batch, { async: true });
|
|
138
|
-
if (state.config.debug) {
|
|
139
|
-
logger.debug("Hindsight retain queue: batch flushed", {
|
|
140
|
-
sessionId,
|
|
141
|
-
bankId: state.bankId,
|
|
142
|
-
items: items.length,
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
} catch (err) {
|
|
146
|
-
const errorText = err instanceof Error ? err.message : String(err);
|
|
147
|
-
logger.warn("Hindsight retain queue: batch flush failed", {
|
|
148
|
-
sessionId,
|
|
149
|
-
bankId: state.bankId,
|
|
150
|
-
items: items.length,
|
|
151
|
-
error: errorText,
|
|
152
|
-
});
|
|
153
|
-
notifyRetainFailure(state, items.length, errorText);
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function notifyRetainFailure(state: HindsightSessionState, count: number, errorText: string): void {
|
|
158
|
-
const noun = count === 1 ? "memory" : "memories";
|
|
159
|
-
state.session.emitNotice("warning", `Memory retention failed for ${count} ${noun}: ${errorText}`, "Hindsight");
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
function createQueue(sessionId: string): SessionQueue {
|
|
163
|
-
const queue: SessionQueue = { items: [] };
|
|
164
|
-
QUEUES.set(sessionId, queue);
|
|
165
|
-
return queue;
|
|
166
|
-
}
|