@sovorn/pi-session-memory 0.1.0
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/README.md +224 -0
- package/package.json +17 -0
- package/src/config.ts +140 -0
- package/src/extension.ts +10 -0
- package/src/formation.ts +700 -0
- package/src/hydration.ts +327 -0
- package/src/memory-command.ts +68 -0
- package/src/projection.ts +349 -0
- package/src/supersession.ts +223 -0
- package/worker/__init__.py +1 -0
- package/worker/__main__.py +31 -0
- package/worker/laya_runtime.py +412 -0
- package/worker/protocol.py +141 -0
package/src/hydration.ts
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { supersessionStatuses, type MemoryStatus } from "./supersession.ts";
|
|
3
|
+
|
|
4
|
+
const OBSERVATION_TYPE = "pi-session-memory.observation";
|
|
5
|
+
const REFLECTION_TYPE = "pi-session-memory.reflection";
|
|
6
|
+
const MAX_MEMORY_TEXT_CHARS = 1_000;
|
|
7
|
+
const MAX_RAW_SOURCES = 12;
|
|
8
|
+
const MAX_REFLECTION_SOURCES = 6;
|
|
9
|
+
const MAX_ENTRY_ID_CHARS = 256;
|
|
10
|
+
const MAX_HYDRATION_OUTPUT_CHARS = 16_000;
|
|
11
|
+
const HYDRATION_LIMIT_NOTICE = "Session memory has raw evidence larger than the hydration limit; continuing with available detail.";
|
|
12
|
+
|
|
13
|
+
const HydrationParams = {
|
|
14
|
+
type: "object",
|
|
15
|
+
properties: {
|
|
16
|
+
reflectionId: { type: "string", minLength: 1, maxLength: MAX_ENTRY_ID_CHARS, description: "Exact active-branch reflection entry ID" },
|
|
17
|
+
depth: { type: "string", enum: ["reflection", "observation", "raw"], description: "The detail level to retrieve" },
|
|
18
|
+
entryId: { type: "string", minLength: 1, maxLength: MAX_ENTRY_ID_CHARS, description: "Optional linked observation ID at observation depth or exact linked raw entry ID at raw depth" },
|
|
19
|
+
},
|
|
20
|
+
required: ["reflectionId", "depth"],
|
|
21
|
+
additionalProperties: false,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type HydrationDepth = "reflection" | "observation" | "raw";
|
|
25
|
+
type HydrationPointer = { depth: "observation" | "raw"; entryId?: string };
|
|
26
|
+
type HydrationStatus = "complete" | "partial" | "not_found";
|
|
27
|
+
interface ObservationData {
|
|
28
|
+
schemaVersion: 1;
|
|
29
|
+
text: string;
|
|
30
|
+
sourceEntryIds: string[];
|
|
31
|
+
}
|
|
32
|
+
interface ReflectionData {
|
|
33
|
+
schemaVersion: 1;
|
|
34
|
+
text: string;
|
|
35
|
+
supportingObservationIds: string[];
|
|
36
|
+
}
|
|
37
|
+
interface HydrationProjection {
|
|
38
|
+
status: HydrationStatus;
|
|
39
|
+
depth: HydrationDepth;
|
|
40
|
+
reflection?: { entryId: string; text: string; supportingObservationIds: string[] };
|
|
41
|
+
observations?: Array<{ entryId: string; text: string; sourceEntryIds: string[] }>;
|
|
42
|
+
rawEntries?: SessionEntry[];
|
|
43
|
+
missingIds: string[];
|
|
44
|
+
unavailableDetails?: Array<{ depth: "raw"; entryId: string; reason: "result_exceeds_output_bound" }>;
|
|
45
|
+
exactEvidenceRecovered: boolean;
|
|
46
|
+
nextDetail: HydrationPointer | null;
|
|
47
|
+
memoryStatus?: "current" | "superseded" | "stale" | "unresolved";
|
|
48
|
+
supersededBy?: { entryId: string; replacementEntryId: string };
|
|
49
|
+
}
|
|
50
|
+
interface HydrationParamsValue {
|
|
51
|
+
reflectionId: string;
|
|
52
|
+
depth: HydrationDepth;
|
|
53
|
+
entryId?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function registerHydration(pi: ExtensionAPI): void {
|
|
57
|
+
pi.registerTool({
|
|
58
|
+
name: "hydrate_session_memory",
|
|
59
|
+
label: "Hydrate session memory",
|
|
60
|
+
description: "Retrieve a reflection and progressively reveal its supporting observations or exact linked raw Pi entries when more evidence is needed. Use entryId from nextDetail to continue when the bounded result names one.",
|
|
61
|
+
parameters: HydrationParams,
|
|
62
|
+
async execute(_toolCallId, rawParams, _signal, _onUpdate, ctx) {
|
|
63
|
+
const params = parseHydrationParams(rawParams);
|
|
64
|
+
const projection = hydrate(ctx.sessionManager.getBranch(), params);
|
|
65
|
+
const text = JSON.stringify(projection);
|
|
66
|
+
if (text.length > MAX_HYDRATION_OUTPUT_CHARS) throw new Error("Hydration result exceeded its output bound");
|
|
67
|
+
if (projection.unavailableDetails?.length) reportHydrationLimit(ctx);
|
|
68
|
+
return {
|
|
69
|
+
content: [{ type: "text", text }],
|
|
70
|
+
details: {
|
|
71
|
+
status: projection.status,
|
|
72
|
+
depth: projection.depth,
|
|
73
|
+
exactEvidenceRecovered: projection.exactEvidenceRecovered,
|
|
74
|
+
nextDetail: projection.nextDetail,
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function reportHydrationLimit(ctx: ExtensionContext): void {
|
|
82
|
+
if (ctx.hasUI) {
|
|
83
|
+
try {
|
|
84
|
+
ctx.ui.notify(HYDRATION_LIMIT_NOTICE, "warning");
|
|
85
|
+
return;
|
|
86
|
+
} catch {}
|
|
87
|
+
}
|
|
88
|
+
process.stderr.write(`${HYDRATION_LIMIT_NOTICE}\n`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function hydrate(branch: SessionEntry[], params: HydrationParamsValue): HydrationProjection {
|
|
92
|
+
const entriesById = new Map<string, SessionEntry>();
|
|
93
|
+
for (const entry of branch) {
|
|
94
|
+
if (entriesById.has(entry.id)) throw new Error("Active branch contains duplicate entry IDs");
|
|
95
|
+
entriesById.set(entry.id, entry);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const foundReflection = entriesById.get(params.reflectionId);
|
|
99
|
+
if (!isCustomEntry(foundReflection, REFLECTION_TYPE)) {
|
|
100
|
+
return {
|
|
101
|
+
status: "not_found",
|
|
102
|
+
depth: params.depth,
|
|
103
|
+
missingIds: [params.reflectionId],
|
|
104
|
+
exactEvidenceRecovered: false,
|
|
105
|
+
nextDetail: null,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (!isReflectionData(foundReflection.data)) {
|
|
109
|
+
return {
|
|
110
|
+
status: "partial",
|
|
111
|
+
depth: params.depth,
|
|
112
|
+
missingIds: [foundReflection.id],
|
|
113
|
+
exactEvidenceRecovered: false,
|
|
114
|
+
nextDetail: null,
|
|
115
|
+
memoryStatus: "unresolved",
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const memoryStatus: MemoryStatus = supersessionStatuses(branch).get(foundReflection.id) ?? { status: "unresolved" };
|
|
120
|
+
const projection: HydrationProjection = {
|
|
121
|
+
status: "complete",
|
|
122
|
+
depth: params.depth,
|
|
123
|
+
reflection: {
|
|
124
|
+
entryId: foundReflection.id,
|
|
125
|
+
text: foundReflection.data.text,
|
|
126
|
+
supportingObservationIds: foundReflection.data.supportingObservationIds,
|
|
127
|
+
},
|
|
128
|
+
missingIds: [],
|
|
129
|
+
exactEvidenceRecovered: false,
|
|
130
|
+
nextDetail: params.depth === "reflection" ? { depth: "observation" } : null,
|
|
131
|
+
memoryStatus: memoryStatus.status,
|
|
132
|
+
...(memoryStatus.status === "superseded"
|
|
133
|
+
? { supersededBy: { entryId: memoryStatus.recordEntryId, replacementEntryId: memoryStatus.replacementEntryId } }
|
|
134
|
+
: {}),
|
|
135
|
+
};
|
|
136
|
+
if (params.depth === "reflection") return projection;
|
|
137
|
+
|
|
138
|
+
const observationsById = new Map<string, ObservationData>();
|
|
139
|
+
const linkedObservationIds = foundReflection.data.supportingObservationIds;
|
|
140
|
+
for (const id of linkedObservationIds) {
|
|
141
|
+
const entry = entriesById.get(id);
|
|
142
|
+
if (!isCustomEntry(entry, OBSERVATION_TYPE)) {
|
|
143
|
+
projection.missingIds.push(id);
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
if (!isObservationData(entry.data)) {
|
|
147
|
+
projection.missingIds.push(id);
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
observationsById.set(id, entry.data);
|
|
151
|
+
}
|
|
152
|
+
const requestedRawId = params.depth === "raw" ? params.entryId : undefined;
|
|
153
|
+
const selectedObservationIds = params.depth === "observation" && params.entryId !== undefined
|
|
154
|
+
? linkedObservationIds.includes(params.entryId) ? [params.entryId] : []
|
|
155
|
+
: linkedObservationIds;
|
|
156
|
+
if (params.depth === "observation" && params.entryId !== undefined && !linkedObservationIds.includes(params.entryId)) {
|
|
157
|
+
projection.missingIds.push(params.entryId);
|
|
158
|
+
}
|
|
159
|
+
const observations = selectedObservationIds.flatMap((entryId) => {
|
|
160
|
+
const data = observationsById.get(entryId);
|
|
161
|
+
return data === undefined ? [] : [{ entryId, data }];
|
|
162
|
+
});
|
|
163
|
+
const observationsToProject = requestedRawId === undefined
|
|
164
|
+
? observations
|
|
165
|
+
: observations.filter(({ data }) => data.sourceEntryIds.includes(requestedRawId));
|
|
166
|
+
if (projection.missingIds.length > 0) projection.status = "partial";
|
|
167
|
+
|
|
168
|
+
projection.observations = [];
|
|
169
|
+
for (const observation of observationsToProject) {
|
|
170
|
+
const candidate = { entryId: observation.entryId, text: observation.data.text, sourceEntryIds: observation.data.sourceEntryIds };
|
|
171
|
+
projection.observations.push(candidate);
|
|
172
|
+
projection.nextDetail = { depth: "observation", entryId: observation.entryId };
|
|
173
|
+
if (!fitsOutputBound(projection)) {
|
|
174
|
+
projection.observations.pop();
|
|
175
|
+
projection.status = "partial";
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (projection.observations.length < observationsToProject.length) {
|
|
180
|
+
if (requestedRawId === undefined) {
|
|
181
|
+
const nextObservation = observationsToProject[projection.observations.length];
|
|
182
|
+
projection.nextDetail = { depth: "observation", entryId: nextObservation.entryId };
|
|
183
|
+
return projection;
|
|
184
|
+
}
|
|
185
|
+
projection.observations = [];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
projection.nextDetail = projection.observations.length > 0 ? { depth: "raw" } : null;
|
|
189
|
+
if (params.depth === "observation") return projection;
|
|
190
|
+
|
|
191
|
+
const rawIds = [...new Set(observations.flatMap(({ data }) => data.sourceEntryIds))];
|
|
192
|
+
const selectedRawIds = requestedRawId === undefined
|
|
193
|
+
? rawIds
|
|
194
|
+
: rawIds.includes(requestedRawId) ? [requestedRawId] : [];
|
|
195
|
+
if (requestedRawId !== undefined && selectedRawIds.length === 0) {
|
|
196
|
+
projection.status = "partial";
|
|
197
|
+
projection.missingIds.push(requestedRawId);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const rawEntriesById = new Map<string, SessionEntry>();
|
|
201
|
+
const unavailableRawIds: string[] = [];
|
|
202
|
+
for (const id of rawIds) {
|
|
203
|
+
const entry = entriesById.get(id);
|
|
204
|
+
if (!isMessageEntry(entry)) {
|
|
205
|
+
projection.missingIds.push(id);
|
|
206
|
+
projection.status = "partial";
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
rawEntriesById.set(id, entry);
|
|
210
|
+
}
|
|
211
|
+
const nextRawPointer = (entryId: string): HydrationPointer | null => {
|
|
212
|
+
const nextId = rawIds.slice(rawIds.indexOf(entryId) + 1).find((id) => rawEntriesById.has(id));
|
|
213
|
+
return nextId === undefined ? null : { depth: "raw", entryId: nextId };
|
|
214
|
+
};
|
|
215
|
+
const rawIdsToCheck = params.entryId === undefined ? rawIds : selectedRawIds;
|
|
216
|
+
while (true) {
|
|
217
|
+
const newlyUnavailable = rawIdsToCheck
|
|
218
|
+
.filter((id) => {
|
|
219
|
+
const entry = rawEntriesById.get(id);
|
|
220
|
+
if (entry === undefined) return false;
|
|
221
|
+
const candidate = {
|
|
222
|
+
...projection,
|
|
223
|
+
status: "partial",
|
|
224
|
+
observations: [],
|
|
225
|
+
rawEntries: [entry],
|
|
226
|
+
nextDetail: nextRawPointer(id),
|
|
227
|
+
};
|
|
228
|
+
return !fitsOutputBound(candidate);
|
|
229
|
+
});
|
|
230
|
+
if (newlyUnavailable.length === 0) break;
|
|
231
|
+
for (const id of newlyUnavailable) {
|
|
232
|
+
rawEntriesById.delete(id);
|
|
233
|
+
unavailableRawIds.push(id);
|
|
234
|
+
}
|
|
235
|
+
projection.status = "partial";
|
|
236
|
+
projection.unavailableDetails = unavailableRawIds.map((entryId) => ({
|
|
237
|
+
depth: "raw",
|
|
238
|
+
entryId,
|
|
239
|
+
reason: "result_exceeds_output_bound",
|
|
240
|
+
}));
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
projection.rawEntries = [];
|
|
244
|
+
projection.status = "partial";
|
|
245
|
+
projection.nextDetail = requestedRawId !== undefined && rawIds.includes(requestedRawId)
|
|
246
|
+
? nextRawPointer(requestedRawId) : null;
|
|
247
|
+
for (const id of selectedRawIds) {
|
|
248
|
+
const entry = rawEntriesById.get(id);
|
|
249
|
+
if (entry === undefined) continue;
|
|
250
|
+
projection.rawEntries.push(entry);
|
|
251
|
+
projection.nextDetail = nextRawPointer(id);
|
|
252
|
+
if (!fitsOutputBound(projection)) {
|
|
253
|
+
projection.rawEntries.pop();
|
|
254
|
+
if (params.entryId === id && projection.observations?.length) {
|
|
255
|
+
const supportingObservations = projection.observations;
|
|
256
|
+
projection.observations = [];
|
|
257
|
+
projection.rawEntries.push(entry);
|
|
258
|
+
if (fitsOutputBound(projection)) continue;
|
|
259
|
+
projection.rawEntries.pop();
|
|
260
|
+
projection.observations = supportingObservations;
|
|
261
|
+
}
|
|
262
|
+
projection.status = "partial";
|
|
263
|
+
projection.nextDetail = { depth: "raw", entryId: id };
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (!fitsOutputBound(projection)) projection.observations = [];
|
|
268
|
+
projection.exactEvidenceRecovered = projection.missingIds.length === 0 &&
|
|
269
|
+
unavailableRawIds.length === 0 && projection.rawEntries.length === rawIds.length;
|
|
270
|
+
projection.status = projection.exactEvidenceRecovered ? "complete" : "partial";
|
|
271
|
+
return projection;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function parseHydrationParams(value: unknown): HydrationParamsValue {
|
|
275
|
+
if (!isRecord(value) || typeof value.reflectionId !== "string" || value.reflectionId.length === 0 ||
|
|
276
|
+
value.reflectionId.length > MAX_ENTRY_ID_CHARS || !isHydrationDepth(value.depth) ||
|
|
277
|
+
(value.entryId !== undefined && (typeof value.entryId !== "string" || value.entryId.length === 0 || value.entryId.length > MAX_ENTRY_ID_CHARS))) {
|
|
278
|
+
throw new Error("Invalid hydration parameters");
|
|
279
|
+
}
|
|
280
|
+
return {
|
|
281
|
+
reflectionId: value.reflectionId,
|
|
282
|
+
depth: value.depth,
|
|
283
|
+
...(typeof value.entryId === "string" ? { entryId: value.entryId } : {}),
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function fitsOutputBound(projection: HydrationProjection): boolean {
|
|
288
|
+
return JSON.stringify(projection).length <= MAX_HYDRATION_OUTPUT_CHARS;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function isHydrationDepth(value: unknown): value is HydrationDepth {
|
|
292
|
+
return value === "reflection" || value === "observation" || value === "raw";
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function isCustomEntry(entry: unknown, customType: string): entry is Extract<SessionEntry, { type: "custom" }> {
|
|
296
|
+
return isRecord(entry) && entry.type === "custom" && entry.customType === customType && typeof entry.id === "string";
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function isMessageEntry(entry: unknown): entry is Extract<SessionEntry, { type: "message" }> {
|
|
300
|
+
return isRecord(entry) && entry.type === "message" && typeof entry.id === "string" &&
|
|
301
|
+
isRecord(entry.message) && typeof entry.message.role === "string" &&
|
|
302
|
+
["user", "assistant", "toolResult"].includes(entry.message.role);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function isObservationData(value: unknown): value is ObservationData {
|
|
306
|
+
return isRecord(value) && value.schemaVersion === 1 && isMemoryText(value.text) &&
|
|
307
|
+
Array.isArray(value.sourceEntryIds) && value.sourceEntryIds.length > 0 && value.sourceEntryIds.length <= MAX_RAW_SOURCES &&
|
|
308
|
+
value.sourceEntryIds.every(isEntryId) && new Set(value.sourceEntryIds).size === value.sourceEntryIds.length;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function isReflectionData(value: unknown): value is ReflectionData {
|
|
312
|
+
return isRecord(value) && value.schemaVersion === 1 && isMemoryText(value.text) &&
|
|
313
|
+
Array.isArray(value.supportingObservationIds) && value.supportingObservationIds.length > 0 && value.supportingObservationIds.length <= MAX_REFLECTION_SOURCES &&
|
|
314
|
+
value.supportingObservationIds.every(isEntryId) && new Set(value.supportingObservationIds).size === value.supportingObservationIds.length;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function isMemoryText(value: unknown): value is string {
|
|
318
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_MEMORY_TEXT_CHARS;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isEntryId(value: unknown): value is string {
|
|
322
|
+
return typeof value === "string" && value.length > 0 && value.length <= MAX_ENTRY_ID_CHARS;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
326
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
327
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { resolvePython, type LoadedMemoryConfig } from "./config.ts";
|
|
3
|
+
|
|
4
|
+
export interface MemoryCommandDeps {
|
|
5
|
+
ensure: () => LoadedMemoryConfig;
|
|
6
|
+
env: NodeJS.ProcessEnv;
|
|
7
|
+
sessionId: (ctx: { sessionManager: { getSessionId(): string } }) => string;
|
|
8
|
+
overrides: Map<string, boolean>;
|
|
9
|
+
consented: Set<string>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function generationLabel(config: LoadedMemoryConfig, override: boolean | undefined): string {
|
|
13
|
+
if (override === true) return "on (session)";
|
|
14
|
+
if (override === false) return "off (session)";
|
|
15
|
+
return config.settings.generation ? "on (config)" : "off (config)";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function statusText(config: LoadedMemoryConfig, env: NodeJS.ProcessEnv, override?: boolean): string {
|
|
19
|
+
const python = resolvePython(env, config.settings);
|
|
20
|
+
const availability = config.state === "unusable" ? `unusable: ${config.notice}` : config.state;
|
|
21
|
+
return [
|
|
22
|
+
"pi-session-memory",
|
|
23
|
+
`generation: ${generationLabel(config, override)}`,
|
|
24
|
+
`config: ${config.path} (${availability})`,
|
|
25
|
+
`python: ${python.command} (${python.source})`,
|
|
26
|
+
`cadence: observe ${config.settings.observeAfterTokens}, reflect ${config.settings.reflectAfterTokens} tokens`,
|
|
27
|
+
].join("\n");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function registerMemoryCommand(pi: ExtensionAPI, deps: MemoryCommandDeps): void {
|
|
31
|
+
pi.registerCommand("memory", {
|
|
32
|
+
description: "Show or change session-memory generation for this session",
|
|
33
|
+
getArgumentCompletions: (prefix) => [
|
|
34
|
+
{ value: "status", label: "status", description: "Show memory status" },
|
|
35
|
+
{ value: "on", label: "on", description: "Enable generation for this session" },
|
|
36
|
+
{ value: "off", label: "off", description: "Disable generation for this session" },
|
|
37
|
+
].filter((item) => item.value.startsWith(prefix)),
|
|
38
|
+
handler: async (args, ctx) => {
|
|
39
|
+
await handleMemoryCommand(args, ctx, deps);
|
|
40
|
+
},
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function handleMemoryCommand(args: string, ctx: ExtensionCommandContext, deps: MemoryCommandDeps): Promise<void> {
|
|
45
|
+
const trimmed = args.trim();
|
|
46
|
+
const sessionId = deps.sessionId(ctx);
|
|
47
|
+
if (trimmed === "on") {
|
|
48
|
+
if (!ctx.hasUI) {
|
|
49
|
+
ctx.ui?.notify("generation needs an interactive UI; nothing changed", "info");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
deps.overrides.set(sessionId, true);
|
|
53
|
+
ctx.ui.notify("generation: on (session). The first generation in this session asks for confirmation; nothing is sent until you confirm.", "info");
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (trimmed === "off") {
|
|
57
|
+
deps.overrides.set(sessionId, false);
|
|
58
|
+
deps.consented.delete(sessionId);
|
|
59
|
+
if (ctx.hasUI) ctx.ui.notify("generation: off (session)", "info");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (trimmed !== "" && trimmed !== "status") {
|
|
63
|
+
if (ctx.hasUI) ctx.ui.notify("usage: /memory status|on|off", "info");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!ctx.hasUI) return;
|
|
67
|
+
ctx.ui.notify(statusText(deps.ensure(), deps.env, deps.overrides.get(sessionId)), "info");
|
|
68
|
+
}
|