@oyasmi/pipiclaw 0.3.5 → 0.5.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.
Files changed (122) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/LICENSE +184 -0
  3. package/README.md +267 -230
  4. package/dist/agent.d.ts.map +1 -1
  5. package/dist/agent.js +158 -76
  6. package/dist/agent.js.map +1 -1
  7. package/dist/command-extension.d.ts.map +1 -1
  8. package/dist/command-extension.js.map +1 -1
  9. package/dist/commands.d.ts.map +1 -1
  10. package/dist/commands.js.map +1 -1
  11. package/dist/config-loader.d.ts.map +1 -1
  12. package/dist/config-loader.js.map +1 -1
  13. package/dist/context.d.ts +18 -0
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +26 -2
  16. package/dist/context.js.map +1 -1
  17. package/dist/delivery.d.ts.map +1 -1
  18. package/dist/delivery.js +11 -14
  19. package/dist/delivery.js.map +1 -1
  20. package/dist/dingtalk.d.ts.map +1 -1
  21. package/dist/dingtalk.js +26 -26
  22. package/dist/dingtalk.js.map +1 -1
  23. package/dist/events.d.ts.map +1 -1
  24. package/dist/events.js +5 -8
  25. package/dist/events.js.map +1 -1
  26. package/dist/index.d.ts +24 -0
  27. package/dist/index.d.ts.map +1 -0
  28. package/dist/index.js +24 -0
  29. package/dist/index.js.map +1 -0
  30. package/dist/llm-json.d.ts +7 -0
  31. package/dist/llm-json.d.ts.map +1 -0
  32. package/dist/llm-json.js +77 -0
  33. package/dist/llm-json.js.map +1 -0
  34. package/dist/log.d.ts.map +1 -1
  35. package/dist/log.js.map +1 -1
  36. package/dist/main.d.ts.map +1 -1
  37. package/dist/main.js.map +1 -1
  38. package/dist/markdown-sections.d.ts +6 -0
  39. package/dist/markdown-sections.d.ts.map +1 -0
  40. package/dist/markdown-sections.js +34 -0
  41. package/dist/markdown-sections.js.map +1 -0
  42. package/dist/memory-candidates.d.ts +21 -0
  43. package/dist/memory-candidates.d.ts.map +1 -0
  44. package/dist/memory-candidates.js +126 -0
  45. package/dist/memory-candidates.js.map +1 -0
  46. package/dist/memory-consolidation.d.ts.map +1 -1
  47. package/dist/memory-consolidation.js +28 -49
  48. package/dist/memory-consolidation.js.map +1 -1
  49. package/dist/memory-files.d.ts +3 -0
  50. package/dist/memory-files.d.ts.map +1 -1
  51. package/dist/memory-files.js +51 -0
  52. package/dist/memory-files.js.map +1 -1
  53. package/dist/memory-lifecycle.d.ts +9 -0
  54. package/dist/memory-lifecycle.d.ts.map +1 -1
  55. package/dist/memory-lifecycle.js +67 -2
  56. package/dist/memory-lifecycle.js.map +1 -1
  57. package/dist/memory-recall.d.ts +29 -0
  58. package/dist/memory-recall.d.ts.map +1 -0
  59. package/dist/memory-recall.js +218 -0
  60. package/dist/memory-recall.js.map +1 -0
  61. package/dist/model-utils.d.ts.map +1 -1
  62. package/dist/model-utils.js.map +1 -1
  63. package/dist/paths.d.ts.map +1 -1
  64. package/dist/prompt-builder.d.ts.map +1 -1
  65. package/dist/prompt-builder.js +7 -2
  66. package/dist/prompt-builder.js.map +1 -1
  67. package/dist/sandbox.d.ts.map +1 -1
  68. package/dist/sandbox.js +0 -1
  69. package/dist/sandbox.js.map +1 -1
  70. package/dist/session-memory-files.d.ts +2 -0
  71. package/dist/session-memory-files.d.ts.map +1 -0
  72. package/dist/session-memory-files.js +2 -0
  73. package/dist/session-memory-files.js.map +1 -0
  74. package/dist/session-memory.d.ts +22 -0
  75. package/dist/session-memory.d.ts.map +1 -0
  76. package/dist/session-memory.js +274 -0
  77. package/dist/session-memory.js.map +1 -0
  78. package/dist/shell-escape.d.ts.map +1 -1
  79. package/dist/shell-escape.js.map +1 -1
  80. package/dist/sidecar-worker.d.ts +27 -0
  81. package/dist/sidecar-worker.d.ts.map +1 -0
  82. package/dist/sidecar-worker.js +105 -0
  83. package/dist/sidecar-worker.js.map +1 -0
  84. package/dist/store.d.ts.map +1 -1
  85. package/dist/store.js +2 -3
  86. package/dist/store.js.map +1 -1
  87. package/dist/sub-agents.d.ts +10 -0
  88. package/dist/sub-agents.d.ts.map +1 -1
  89. package/dist/sub-agents.js +132 -10
  90. package/dist/sub-agents.js.map +1 -1
  91. package/dist/tools/attach.d.ts.map +1 -1
  92. package/dist/tools/attach.js.map +1 -1
  93. package/dist/tools/bash.d.ts.map +1 -1
  94. package/dist/tools/bash.js.map +1 -1
  95. package/dist/tools/edit.d.ts.map +1 -1
  96. package/dist/tools/edit.js.map +1 -1
  97. package/dist/tools/index.d.ts +3 -0
  98. package/dist/tools/index.d.ts.map +1 -1
  99. package/dist/tools/index.js +2 -0
  100. package/dist/tools/index.js.map +1 -1
  101. package/dist/tools/read.d.ts.map +1 -1
  102. package/dist/tools/read.js.map +1 -1
  103. package/dist/tools/subagent.d.ts +6 -0
  104. package/dist/tools/subagent.d.ts.map +1 -1
  105. package/dist/tools/subagent.js +127 -12
  106. package/dist/tools/subagent.js.map +1 -1
  107. package/dist/tools/truncate.d.ts.map +1 -1
  108. package/dist/tools/truncate.js.map +1 -1
  109. package/dist/tools/write-content.d.ts.map +1 -1
  110. package/dist/tools/write-content.js.map +1 -1
  111. package/dist/tools/write.d.ts.map +1 -1
  112. package/dist/tools/write.js.map +1 -1
  113. package/docs/improve-memory/design.md +537 -0
  114. package/docs/improve-memory/interfaces-and-tests.md +473 -0
  115. package/docs/improve-memory/spec.md +357 -0
  116. package/docs/memory-rfc.md +297 -0
  117. package/docs/proj-review.md +188 -0
  118. package/docs/subagent/pi-subagent-analyse.txt +190 -0
  119. package/docs/subagent/pi-subagent-design.txt +266 -0
  120. package/docs/subagent/pi-subagent-phase1-plan.txt +529 -0
  121. package/docs/test-supplementation-plan.md +553 -0
  122. package/package.json +71 -53
@@ -0,0 +1,274 @@
1
+ import { serializeConversation } from "@mariozechner/pi-coding-agent";
2
+ import { writeFile } from "fs/promises";
3
+ import { join } from "path";
4
+ import { parseJsonObject } from "./llm-json.js";
5
+ import { splitLevelOneSections } from "./markdown-sections.js";
6
+ import { readChannelMemory } from "./memory-files.js";
7
+ import { readChannelSession, rewriteChannelSession } from "./session-memory-files.js";
8
+ import { runSidecarTask, SidecarParseError } from "./sidecar-worker.js";
9
+ const SESSION_TRANSCRIPT_MAX_CHARS = 20_000;
10
+ const SESSION_MEMORY_MAX_CHARS = 4_000;
11
+ const SESSION_ITEM_LIMIT = 12;
12
+ const SESSION_ITEM_MAX_CHARS = 300;
13
+ const SESSION_MEMORY_TIMEOUT_MS = 10_000;
14
+ const SESSION_MEMORY_SYSTEM_PROMPT = `You maintain a Pipiclaw SESSION.md file.
15
+
16
+ Return strict JSON only. Do not use Markdown fences.
17
+
18
+ Output schema:
19
+ {
20
+ "title": "string",
21
+ "currentState": ["string"],
22
+ "userIntent": ["string"],
23
+ "activeFiles": ["string"],
24
+ "decisions": ["string"],
25
+ "constraints": ["string"],
26
+ "errorsAndCorrections": ["string"],
27
+ "nextSteps": ["string"],
28
+ "worklog": ["string"]
29
+ }
30
+
31
+ Rules:
32
+ - Prefer short, information-dense bullet-sized items.
33
+ - Capture only the current active work state, not the full conversation history.
34
+ - Keep durable facts out unless they are directly relevant to the current work.
35
+ - "activeFiles" should list only files, directories, or work areas currently in focus.
36
+ - "errorsAndCorrections" should record recent failures, fixes, or things to avoid repeating.
37
+ - "nextSteps" should reflect the most likely immediate follow-up actions.
38
+ - "worklog" must stay terse and recent.
39
+ - If a field has nothing useful, return an empty string or empty array.`;
40
+ function clipText(text, maxChars) {
41
+ const normalized = text.replace(/\r/g, "").trim();
42
+ if (normalized.length <= maxChars) {
43
+ return normalized;
44
+ }
45
+ const headChars = Math.floor(maxChars * 0.45);
46
+ const tailChars = maxChars - headChars;
47
+ return `${normalized.slice(0, headChars)}\n\n[... omitted middle section ...]\n\n${normalized.slice(-tailChars)}`;
48
+ }
49
+ function normalizeItem(value) {
50
+ if (typeof value !== "string") {
51
+ return null;
52
+ }
53
+ const normalized = value.replace(/\s+/g, " ").trim();
54
+ if (!normalized) {
55
+ return null;
56
+ }
57
+ return normalized.length > SESSION_ITEM_MAX_CHARS
58
+ ? `${normalized.slice(0, SESSION_ITEM_MAX_CHARS - 3)}...`
59
+ : normalized;
60
+ }
61
+ function normalizeItems(value) {
62
+ if (!Array.isArray(value)) {
63
+ return [];
64
+ }
65
+ return value
66
+ .map(normalizeItem)
67
+ .filter((item) => !!item)
68
+ .slice(0, SESSION_ITEM_LIMIT);
69
+ }
70
+ function normalizeTitle(value) {
71
+ return typeof value === "string" ? value.trim().slice(0, 120) : "";
72
+ }
73
+ function isRecord(value) {
74
+ return typeof value === "object" && value !== null;
75
+ }
76
+ function parseStateUpdate(text) {
77
+ const parsed = parseJsonObject(text);
78
+ if (!isRecord(parsed)) {
79
+ throw new Error("Session memory response was not a JSON object");
80
+ }
81
+ const next = {};
82
+ if ("title" in parsed)
83
+ next.title = normalizeTitle(parsed.title);
84
+ if ("currentState" in parsed)
85
+ next.currentState = normalizeItems(parsed.currentState);
86
+ if ("userIntent" in parsed)
87
+ next.userIntent = normalizeItems(parsed.userIntent);
88
+ if ("activeFiles" in parsed)
89
+ next.activeFiles = normalizeItems(parsed.activeFiles);
90
+ if ("decisions" in parsed)
91
+ next.decisions = normalizeItems(parsed.decisions);
92
+ if ("constraints" in parsed)
93
+ next.constraints = normalizeItems(parsed.constraints);
94
+ if ("errorsAndCorrections" in parsed)
95
+ next.errorsAndCorrections = normalizeItems(parsed.errorsAndCorrections);
96
+ if ("nextSteps" in parsed)
97
+ next.nextSteps = normalizeItems(parsed.nextSteps);
98
+ if ("worklog" in parsed)
99
+ next.worklog = normalizeItems(parsed.worklog);
100
+ return next;
101
+ }
102
+ function createEmptySessionMemoryState() {
103
+ return {
104
+ title: "",
105
+ currentState: [],
106
+ userIntent: [],
107
+ activeFiles: [],
108
+ decisions: [],
109
+ constraints: [],
110
+ errorsAndCorrections: [],
111
+ nextSteps: [],
112
+ worklog: [],
113
+ };
114
+ }
115
+ function stripHtmlComments(text) {
116
+ return text.replace(/<!--[\s\S]*?-->/g, "").trim();
117
+ }
118
+ function parseSectionItems(content) {
119
+ const normalized = stripHtmlComments(content);
120
+ if (!normalized) {
121
+ return [];
122
+ }
123
+ const lines = normalized
124
+ .split("\n")
125
+ .map((line) => line.trim())
126
+ .filter(Boolean);
127
+ const bulletItems = lines
128
+ .filter((line) => line.startsWith("- "))
129
+ .map((line) => normalizeItem(line.slice(2)))
130
+ .filter((item) => !!item);
131
+ if (bulletItems.length > 0) {
132
+ return bulletItems.slice(0, SESSION_ITEM_LIMIT);
133
+ }
134
+ return lines
135
+ .map(normalizeItem)
136
+ .filter((item) => !!item)
137
+ .slice(0, SESSION_ITEM_LIMIT);
138
+ }
139
+ function parseRenderedSessionMemory(markdown) {
140
+ const state = createEmptySessionMemoryState();
141
+ for (const section of splitLevelOneSections(markdown)) {
142
+ switch (section.heading.toLowerCase()) {
143
+ case "session title":
144
+ state.title = stripHtmlComments(section.content).split("\n")[0]?.trim().slice(0, 120) || "";
145
+ break;
146
+ case "current state":
147
+ state.currentState = parseSectionItems(section.content);
148
+ break;
149
+ case "user intent":
150
+ state.userIntent = parseSectionItems(section.content);
151
+ break;
152
+ case "active files":
153
+ state.activeFiles = parseSectionItems(section.content);
154
+ break;
155
+ case "decisions":
156
+ state.decisions = parseSectionItems(section.content);
157
+ break;
158
+ case "constraints":
159
+ state.constraints = parseSectionItems(section.content);
160
+ break;
161
+ case "errors & corrections":
162
+ state.errorsAndCorrections = parseSectionItems(section.content);
163
+ break;
164
+ case "next steps":
165
+ state.nextSteps = parseSectionItems(section.content);
166
+ break;
167
+ case "worklog":
168
+ state.worklog = parseSectionItems(section.content);
169
+ break;
170
+ }
171
+ }
172
+ return state;
173
+ }
174
+ function mergeSessionMemoryState(current, update) {
175
+ return {
176
+ title: typeof update.title === "string" ? update.title : current.title,
177
+ currentState: Array.isArray(update.currentState) ? update.currentState : current.currentState,
178
+ userIntent: Array.isArray(update.userIntent) ? update.userIntent : current.userIntent,
179
+ activeFiles: Array.isArray(update.activeFiles) ? update.activeFiles : current.activeFiles,
180
+ decisions: Array.isArray(update.decisions) ? update.decisions : current.decisions,
181
+ constraints: Array.isArray(update.constraints) ? update.constraints : current.constraints,
182
+ errorsAndCorrections: Array.isArray(update.errorsAndCorrections)
183
+ ? update.errorsAndCorrections
184
+ : current.errorsAndCorrections,
185
+ nextSteps: Array.isArray(update.nextSteps) ? update.nextSteps : current.nextSteps,
186
+ worklog: Array.isArray(update.worklog) ? update.worklog : current.worklog,
187
+ };
188
+ }
189
+ async function writeSessionMemoryDebugFile(channelDir, error, rawText) {
190
+ const debugPath = join(channelDir, "SESSION.invalid-response.txt");
191
+ const header = [
192
+ `timestamp: ${new Date().toISOString()}`,
193
+ `error: ${error instanceof Error ? error.message : String(error)}`,
194
+ "",
195
+ "raw response:",
196
+ "",
197
+ ].join("\n");
198
+ await writeFile(debugPath, `${header}${rawText}\n`, "utf-8");
199
+ }
200
+ function renderSection(heading, items) {
201
+ return [`# ${heading}`, "", ...items.map((item) => `- ${item}`)].join("\n");
202
+ }
203
+ function isStandardAgentMessage(message) {
204
+ return (typeof message === "object" &&
205
+ message !== null &&
206
+ "role" in message &&
207
+ (message.role === "user" || message.role === "assistant" || message.role === "toolResult"));
208
+ }
209
+ function buildMessagesForSessionMemory(messages) {
210
+ return messages.filter(isStandardAgentMessage);
211
+ }
212
+ export function renderSessionMemory(state) {
213
+ const sections = [];
214
+ if (state.title.trim()) {
215
+ sections.push("# Session Title", "", state.title.trim());
216
+ }
217
+ const orderedSections = [
218
+ ["Current State", state.currentState],
219
+ ["User Intent", state.userIntent],
220
+ ["Active Files", state.activeFiles],
221
+ ["Decisions", state.decisions],
222
+ ["Constraints", state.constraints],
223
+ ["Errors & Corrections", state.errorsAndCorrections],
224
+ ["Next Steps", state.nextSteps],
225
+ ["Worklog", state.worklog],
226
+ ];
227
+ for (const [heading, items] of orderedSections) {
228
+ if (items.length === 0 && heading === "Worklog") {
229
+ continue;
230
+ }
231
+ sections.push(renderSection(heading, items));
232
+ }
233
+ return `${sections.join("\n\n").trim()}\n`;
234
+ }
235
+ function buildSessionPrompt(currentSession, currentMemory, messages) {
236
+ const transcript = clipText(serializeConversation(messages), SESSION_TRANSCRIPT_MAX_CHARS);
237
+ return `Current SESSION.md:
238
+ ${currentSession || "(empty)"}
239
+
240
+ Current channel MEMORY.md:
241
+ ${clipText(currentMemory, SESSION_MEMORY_MAX_CHARS) || "(empty)"}
242
+
243
+ Recent conversation:
244
+ ${transcript || "(empty)"}`;
245
+ }
246
+ export async function updateChannelSessionMemory(options) {
247
+ const currentSession = await readChannelSession(options.channelDir);
248
+ const currentMemory = await readChannelMemory(options.channelDir);
249
+ const messages = buildMessagesForSessionMemory(options.messages);
250
+ const currentState = parseRenderedSessionMemory(currentSession);
251
+ let update;
252
+ try {
253
+ const result = await runSidecarTask({
254
+ name: "session-memory-update",
255
+ model: options.model,
256
+ resolveApiKey: options.resolveApiKey,
257
+ systemPrompt: SESSION_MEMORY_SYSTEM_PROMPT,
258
+ prompt: buildSessionPrompt(currentSession, currentMemory, messages),
259
+ parse: parseStateUpdate,
260
+ timeoutMs: SESSION_MEMORY_TIMEOUT_MS,
261
+ });
262
+ update = result.output;
263
+ }
264
+ catch (error) {
265
+ if (error instanceof SidecarParseError) {
266
+ await writeSessionMemoryDebugFile(options.channelDir, error.cause ?? error, error.rawText);
267
+ }
268
+ throw error;
269
+ }
270
+ const rendered = renderSessionMemory(mergeSessionMemoryState(currentState, update));
271
+ await rewriteChannelSession(options.channelDir, rendered);
272
+ return parseRenderedSessionMemory(rendered);
273
+ }
274
+ //# sourceMappingURL=session-memory.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session-memory.js","sourceRoot":"","sources":["../src/session-memory.ts"],"names":[],"mappings":"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","sourcesContent":["import type { AgentMessage } from \"@mariozechner/pi-agent-core\";\nimport type { Api, Message, Model } from \"@mariozechner/pi-ai\";\nimport { serializeConversation } from \"@mariozechner/pi-coding-agent\";\nimport { writeFile } from \"fs/promises\";\nimport { join } from \"path\";\nimport { parseJsonObject } from \"./llm-json.js\";\nimport { splitLevelOneSections } from \"./markdown-sections.js\";\nimport { readChannelMemory } from \"./memory-files.js\";\nimport { readChannelSession, rewriteChannelSession } from \"./session-memory-files.js\";\nimport { runSidecarTask, SidecarParseError } from \"./sidecar-worker.js\";\n\nconst SESSION_TRANSCRIPT_MAX_CHARS = 20_000;\nconst SESSION_MEMORY_MAX_CHARS = 4_000;\nconst SESSION_ITEM_LIMIT = 12;\nconst SESSION_ITEM_MAX_CHARS = 300;\nconst SESSION_MEMORY_TIMEOUT_MS = 10_000;\n\nconst SESSION_MEMORY_SYSTEM_PROMPT = `You maintain a Pipiclaw SESSION.md file.\n\nReturn strict JSON only. Do not use Markdown fences.\n\nOutput schema:\n{\n \"title\": \"string\",\n \"currentState\": [\"string\"],\n \"userIntent\": [\"string\"],\n \"activeFiles\": [\"string\"],\n \"decisions\": [\"string\"],\n \"constraints\": [\"string\"],\n \"errorsAndCorrections\": [\"string\"],\n \"nextSteps\": [\"string\"],\n \"worklog\": [\"string\"]\n}\n\nRules:\n- Prefer short, information-dense bullet-sized items.\n- Capture only the current active work state, not the full conversation history.\n- Keep durable facts out unless they are directly relevant to the current work.\n- \"activeFiles\" should list only files, directories, or work areas currently in focus.\n- \"errorsAndCorrections\" should record recent failures, fixes, or things to avoid repeating.\n- \"nextSteps\" should reflect the most likely immediate follow-up actions.\n- \"worklog\" must stay terse and recent.\n- If a field has nothing useful, return an empty string or empty array.`;\n\nexport interface SessionMemoryState {\n\ttitle: string;\n\tcurrentState: string[];\n\tuserIntent: string[];\n\tactiveFiles: string[];\n\tdecisions: string[];\n\tconstraints: string[];\n\terrorsAndCorrections: string[];\n\tnextSteps: string[];\n\tworklog: string[];\n}\n\nexport interface SessionMemoryUpdateOptions {\n\tchannelDir: string;\n\tmessages: AgentMessage[];\n\tmodel: Model<Api>;\n\tresolveApiKey: (model: Model<Api>) => Promise<string>;\n}\n\ntype SessionMemoryStateUpdate = Partial<Record<keyof SessionMemoryState, string[] | string>>;\n\nfunction clipText(text: string, maxChars: number): string {\n\tconst normalized = text.replace(/\\r/g, \"\").trim();\n\tif (normalized.length <= maxChars) {\n\t\treturn normalized;\n\t}\n\tconst headChars = Math.floor(maxChars * 0.45);\n\tconst tailChars = maxChars - headChars;\n\treturn `${normalized.slice(0, headChars)}\\n\\n[... omitted middle section ...]\\n\\n${normalized.slice(-tailChars)}`;\n}\n\nfunction normalizeItem(value: unknown): string | null {\n\tif (typeof value !== \"string\") {\n\t\treturn null;\n\t}\n\tconst normalized = value.replace(/\\s+/g, \" \").trim();\n\tif (!normalized) {\n\t\treturn null;\n\t}\n\treturn normalized.length > SESSION_ITEM_MAX_CHARS\n\t\t? `${normalized.slice(0, SESSION_ITEM_MAX_CHARS - 3)}...`\n\t\t: normalized;\n}\n\nfunction normalizeItems(value: unknown): string[] {\n\tif (!Array.isArray(value)) {\n\t\treturn [];\n\t}\n\treturn value\n\t\t.map(normalizeItem)\n\t\t.filter((item): item is string => !!item)\n\t\t.slice(0, SESSION_ITEM_LIMIT);\n}\n\nfunction normalizeTitle(value: unknown): string {\n\treturn typeof value === \"string\" ? value.trim().slice(0, 120) : \"\";\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n\treturn typeof value === \"object\" && value !== null;\n}\n\nfunction parseStateUpdate(text: string): SessionMemoryStateUpdate {\n\tconst parsed = parseJsonObject(text);\n\tif (!isRecord(parsed)) {\n\t\tthrow new Error(\"Session memory response was not a JSON object\");\n\t}\n\n\tconst next: SessionMemoryStateUpdate = {};\n\tif (\"title\" in parsed) next.title = normalizeTitle(parsed.title);\n\tif (\"currentState\" in parsed) next.currentState = normalizeItems(parsed.currentState);\n\tif (\"userIntent\" in parsed) next.userIntent = normalizeItems(parsed.userIntent);\n\tif (\"activeFiles\" in parsed) next.activeFiles = normalizeItems(parsed.activeFiles);\n\tif (\"decisions\" in parsed) next.decisions = normalizeItems(parsed.decisions);\n\tif (\"constraints\" in parsed) next.constraints = normalizeItems(parsed.constraints);\n\tif (\"errorsAndCorrections\" in parsed) next.errorsAndCorrections = normalizeItems(parsed.errorsAndCorrections);\n\tif (\"nextSteps\" in parsed) next.nextSteps = normalizeItems(parsed.nextSteps);\n\tif (\"worklog\" in parsed) next.worklog = normalizeItems(parsed.worklog);\n\treturn next;\n}\n\nfunction createEmptySessionMemoryState(): SessionMemoryState {\n\treturn {\n\t\ttitle: \"\",\n\t\tcurrentState: [],\n\t\tuserIntent: [],\n\t\tactiveFiles: [],\n\t\tdecisions: [],\n\t\tconstraints: [],\n\t\terrorsAndCorrections: [],\n\t\tnextSteps: [],\n\t\tworklog: [],\n\t};\n}\n\nfunction stripHtmlComments(text: string): string {\n\treturn text.replace(/<!--[\\s\\S]*?-->/g, \"\").trim();\n}\n\nfunction parseSectionItems(content: string): string[] {\n\tconst normalized = stripHtmlComments(content);\n\tif (!normalized) {\n\t\treturn [];\n\t}\n\n\tconst lines = normalized\n\t\t.split(\"\\n\")\n\t\t.map((line) => line.trim())\n\t\t.filter(Boolean);\n\tconst bulletItems = lines\n\t\t.filter((line) => line.startsWith(\"- \"))\n\t\t.map((line) => normalizeItem(line.slice(2)))\n\t\t.filter((item): item is string => !!item);\n\tif (bulletItems.length > 0) {\n\t\treturn bulletItems.slice(0, SESSION_ITEM_LIMIT);\n\t}\n\treturn lines\n\t\t.map(normalizeItem)\n\t\t.filter((item): item is string => !!item)\n\t\t.slice(0, SESSION_ITEM_LIMIT);\n}\n\nfunction parseRenderedSessionMemory(markdown: string): SessionMemoryState {\n\tconst state = createEmptySessionMemoryState();\n\tfor (const section of splitLevelOneSections(markdown)) {\n\t\tswitch (section.heading.toLowerCase()) {\n\t\t\tcase \"session title\":\n\t\t\t\tstate.title = stripHtmlComments(section.content).split(\"\\n\")[0]?.trim().slice(0, 120) || \"\";\n\t\t\t\tbreak;\n\t\t\tcase \"current state\":\n\t\t\t\tstate.currentState = parseSectionItems(section.content);\n\t\t\t\tbreak;\n\t\t\tcase \"user intent\":\n\t\t\t\tstate.userIntent = parseSectionItems(section.content);\n\t\t\t\tbreak;\n\t\t\tcase \"active files\":\n\t\t\t\tstate.activeFiles = parseSectionItems(section.content);\n\t\t\t\tbreak;\n\t\t\tcase \"decisions\":\n\t\t\t\tstate.decisions = parseSectionItems(section.content);\n\t\t\t\tbreak;\n\t\t\tcase \"constraints\":\n\t\t\t\tstate.constraints = parseSectionItems(section.content);\n\t\t\t\tbreak;\n\t\t\tcase \"errors & corrections\":\n\t\t\t\tstate.errorsAndCorrections = parseSectionItems(section.content);\n\t\t\t\tbreak;\n\t\t\tcase \"next steps\":\n\t\t\t\tstate.nextSteps = parseSectionItems(section.content);\n\t\t\t\tbreak;\n\t\t\tcase \"worklog\":\n\t\t\t\tstate.worklog = parseSectionItems(section.content);\n\t\t\t\tbreak;\n\t\t}\n\t}\n\treturn state;\n}\n\nfunction mergeSessionMemoryState(current: SessionMemoryState, update: SessionMemoryStateUpdate): SessionMemoryState {\n\treturn {\n\t\ttitle: typeof update.title === \"string\" ? update.title : current.title,\n\t\tcurrentState: Array.isArray(update.currentState) ? update.currentState : current.currentState,\n\t\tuserIntent: Array.isArray(update.userIntent) ? update.userIntent : current.userIntent,\n\t\tactiveFiles: Array.isArray(update.activeFiles) ? update.activeFiles : current.activeFiles,\n\t\tdecisions: Array.isArray(update.decisions) ? update.decisions : current.decisions,\n\t\tconstraints: Array.isArray(update.constraints) ? update.constraints : current.constraints,\n\t\terrorsAndCorrections: Array.isArray(update.errorsAndCorrections)\n\t\t\t? update.errorsAndCorrections\n\t\t\t: current.errorsAndCorrections,\n\t\tnextSteps: Array.isArray(update.nextSteps) ? update.nextSteps : current.nextSteps,\n\t\tworklog: Array.isArray(update.worklog) ? update.worklog : current.worklog,\n\t};\n}\n\nasync function writeSessionMemoryDebugFile(channelDir: string, error: unknown, rawText: string): Promise<void> {\n\tconst debugPath = join(channelDir, \"SESSION.invalid-response.txt\");\n\tconst header = [\n\t\t`timestamp: ${new Date().toISOString()}`,\n\t\t`error: ${error instanceof Error ? error.message : String(error)}`,\n\t\t\"\",\n\t\t\"raw response:\",\n\t\t\"\",\n\t].join(\"\\n\");\n\tawait writeFile(debugPath, `${header}${rawText}\\n`, \"utf-8\");\n}\n\nfunction renderSection(heading: string, items: string[]): string {\n\treturn [`# ${heading}`, \"\", ...items.map((item) => `- ${item}`)].join(\"\\n\");\n}\n\nfunction isStandardAgentMessage(message: AgentMessage): message is Message {\n\treturn (\n\t\ttypeof message === \"object\" &&\n\t\tmessage !== null &&\n\t\t\"role\" in message &&\n\t\t(message.role === \"user\" || message.role === \"assistant\" || message.role === \"toolResult\")\n\t);\n}\n\nfunction buildMessagesForSessionMemory(messages: AgentMessage[]): Message[] {\n\treturn messages.filter(isStandardAgentMessage);\n}\n\nexport function renderSessionMemory(state: SessionMemoryState): string {\n\tconst sections: string[] = [];\n\tif (state.title.trim()) {\n\t\tsections.push(\"# Session Title\", \"\", state.title.trim());\n\t}\n\n\tconst orderedSections: Array<[string, string[]]> = [\n\t\t[\"Current State\", state.currentState],\n\t\t[\"User Intent\", state.userIntent],\n\t\t[\"Active Files\", state.activeFiles],\n\t\t[\"Decisions\", state.decisions],\n\t\t[\"Constraints\", state.constraints],\n\t\t[\"Errors & Corrections\", state.errorsAndCorrections],\n\t\t[\"Next Steps\", state.nextSteps],\n\t\t[\"Worklog\", state.worklog],\n\t];\n\n\tfor (const [heading, items] of orderedSections) {\n\t\tif (items.length === 0 && heading === \"Worklog\") {\n\t\t\tcontinue;\n\t\t}\n\t\tsections.push(renderSection(heading, items));\n\t}\n\n\treturn `${sections.join(\"\\n\\n\").trim()}\\n`;\n}\n\nfunction buildSessionPrompt(currentSession: string, currentMemory: string, messages: Message[]): string {\n\tconst transcript = clipText(serializeConversation(messages), SESSION_TRANSCRIPT_MAX_CHARS);\n\treturn `Current SESSION.md:\n${currentSession || \"(empty)\"}\n\nCurrent channel MEMORY.md:\n${clipText(currentMemory, SESSION_MEMORY_MAX_CHARS) || \"(empty)\"}\n\nRecent conversation:\n${transcript || \"(empty)\"}`;\n}\n\nexport async function updateChannelSessionMemory(options: SessionMemoryUpdateOptions): Promise<SessionMemoryState> {\n\tconst currentSession = await readChannelSession(options.channelDir);\n\tconst currentMemory = await readChannelMemory(options.channelDir);\n\tconst messages = buildMessagesForSessionMemory(options.messages);\n\tconst currentState = parseRenderedSessionMemory(currentSession);\n\n\tlet update: SessionMemoryStateUpdate;\n\ttry {\n\t\tconst result = await runSidecarTask({\n\t\t\tname: \"session-memory-update\",\n\t\t\tmodel: options.model,\n\t\t\tresolveApiKey: options.resolveApiKey,\n\t\t\tsystemPrompt: SESSION_MEMORY_SYSTEM_PROMPT,\n\t\t\tprompt: buildSessionPrompt(currentSession, currentMemory, messages),\n\t\t\tparse: parseStateUpdate,\n\t\t\ttimeoutMs: SESSION_MEMORY_TIMEOUT_MS,\n\t\t});\n\t\tupdate = result.output;\n\t} catch (error) {\n\t\tif (error instanceof SidecarParseError) {\n\t\t\tawait writeSessionMemoryDebugFile(options.channelDir, error.cause ?? error, error.rawText);\n\t\t}\n\t\tthrow error;\n\t}\n\n\tconst rendered = renderSessionMemory(mergeSessionMemoryState(currentState, update));\n\tawait rewriteChannelSession(options.channelDir, rendered);\n\treturn parseRenderedSessionMemory(rendered);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"shell-escape.d.ts","sourceRoot":"","sources":["../src/shell-escape.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C","sourcesContent":["/**\n * Shell-escape a string for safe use in sh -c commands.\n * Wraps in single quotes and escapes internal single quotes.\n */\nexport function shellEscape(s: string): string {\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
1
+ {"version":3,"file":"shell-escape.d.ts","sourceRoot":"","sources":["../src/shell-escape.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE7C"}
@@ -1 +1 @@
1
- {"version":3,"file":"shell-escape.js","sourceRoot":"","sources":["../src/shell-escape.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,CAAS,EAAU;IAC9C,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AAAA,CACvC","sourcesContent":["/**\n * Shell-escape a string for safe use in sh -c commands.\n * Wraps in single quotes and escapes internal single quotes.\n */\nexport function shellEscape(s: string): string {\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
1
+ {"version":3,"file":"shell-escape.js","sourceRoot":"","sources":["../src/shell-escape.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,UAAU,WAAW,CAAC,CAAS;IACpC,OAAO,IAAI,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC;AACxC,CAAC","sourcesContent":["/**\n * Shell-escape a string for safe use in sh -c commands.\n * Wraps in single quotes and escapes internal single quotes.\n */\nexport function shellEscape(s: string): string {\n\treturn `'${s.replace(/'/g, \"'\\\\''\")}'`;\n}\n"]}
@@ -0,0 +1,27 @@
1
+ import type { Api, Model } from "@mariozechner/pi-ai";
2
+ export interface SidecarTask<T> {
3
+ name: string;
4
+ model: Model<Api>;
5
+ resolveApiKey: (model: Model<Api>) => Promise<string>;
6
+ systemPrompt: string;
7
+ prompt: string;
8
+ parse: (text: string) => T;
9
+ timeoutMs?: number;
10
+ signal?: AbortSignal;
11
+ }
12
+ export interface SidecarResult<T> {
13
+ output: T;
14
+ rawText: string;
15
+ }
16
+ export declare class SidecarTimeoutError extends Error {
17
+ readonly taskName: string;
18
+ readonly timeoutMs: number;
19
+ constructor(taskName: string, timeoutMs: number);
20
+ }
21
+ export declare class SidecarParseError extends Error {
22
+ readonly taskName: string;
23
+ readonly rawText: string;
24
+ constructor(taskName: string, rawText: string, cause: unknown);
25
+ }
26
+ export declare function runSidecarTask<T>(task: SidecarTask<T>): Promise<SidecarResult<T>>;
27
+ //# sourceMappingURL=sidecar-worker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar-worker.d.ts","sourceRoot":"","sources":["../src/sidecar-worker.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,GAAG,EAAoB,KAAK,EAAE,MAAM,qBAAqB,CAAC;AAGxE,MAAM,WAAW,WAAW,CAAC,CAAC;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAClB,aAAa,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,GAAG,CAAC,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IACtD,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,CAAC,CAAC;IAC3B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED,MAAM,WAAW,aAAa,CAAC,CAAC;IAC/B,MAAM,EAAE,CAAC,CAAC;IACV,OAAO,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,mBAAoB,SAAQ,KAAK;IAC7C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;gBAEf,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM;CAM/C;AAED,qBAAa,iBAAkB,SAAQ,KAAK;IAC3C,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO;CAO7D;AAaD,wBAAsB,cAAc,CAAC,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CA2FvF"}
@@ -0,0 +1,105 @@
1
+ import { Agent } from "@mariozechner/pi-agent-core";
2
+ import { convertToLlm } from "@mariozechner/pi-coding-agent";
3
+ export class SidecarTimeoutError extends Error {
4
+ constructor(taskName, timeoutMs) {
5
+ super(`Sidecar task "${taskName}" timed out after ${timeoutMs}ms`);
6
+ this.name = "SidecarTimeoutError";
7
+ this.taskName = taskName;
8
+ this.timeoutMs = timeoutMs;
9
+ }
10
+ }
11
+ export class SidecarParseError extends Error {
12
+ constructor(taskName, rawText, cause) {
13
+ super(`Sidecar task "${taskName}" returned invalid output`);
14
+ this.name = "SidecarParseError";
15
+ this.taskName = taskName;
16
+ this.rawText = rawText;
17
+ this.cause = cause;
18
+ }
19
+ }
20
+ function extractAssistantText(message) {
21
+ return message.content
22
+ .filter((part) => part.type === "text")
23
+ .map((part) => part.text)
24
+ .join("\n")
25
+ .trim();
26
+ }
27
+ export async function runSidecarTask(task) {
28
+ const apiKey = await task.resolveApiKey(task.model);
29
+ const worker = new Agent({
30
+ initialState: {
31
+ systemPrompt: task.systemPrompt,
32
+ model: task.model,
33
+ thinkingLevel: "off",
34
+ tools: [],
35
+ },
36
+ convertToLlm,
37
+ getApiKey: async () => apiKey,
38
+ });
39
+ const abortWorker = () => {
40
+ try {
41
+ worker.abort();
42
+ }
43
+ catch {
44
+ /* ignore */
45
+ }
46
+ };
47
+ let removeAbortListener = () => { };
48
+ let timeoutHandle = null;
49
+ const runPromise = (async () => {
50
+ await worker.prompt(task.prompt);
51
+ await worker.waitForIdle();
52
+ const lastMessage = worker.state.messages[worker.state.messages.length - 1];
53
+ if (!lastMessage || lastMessage.role !== "assistant") {
54
+ throw new Error(`Sidecar task "${task.name}" returned no assistant message`);
55
+ }
56
+ if (lastMessage.stopReason === "error" || lastMessage.stopReason === "aborted") {
57
+ throw new Error(lastMessage.errorMessage || `Sidecar task "${task.name}" failed`);
58
+ }
59
+ const rawText = extractAssistantText(lastMessage);
60
+ try {
61
+ return {
62
+ output: task.parse(rawText),
63
+ rawText,
64
+ };
65
+ }
66
+ catch (error) {
67
+ throw new SidecarParseError(task.name, rawText, error);
68
+ }
69
+ })();
70
+ void runPromise.catch(() => { });
71
+ const blockers = [];
72
+ if (task.timeoutMs && task.timeoutMs > 0) {
73
+ blockers.push(new Promise((_, reject) => {
74
+ timeoutHandle = setTimeout(() => {
75
+ abortWorker();
76
+ reject(new SidecarTimeoutError(task.name, task.timeoutMs));
77
+ }, task.timeoutMs);
78
+ }));
79
+ }
80
+ if (task.signal) {
81
+ const signal = task.signal;
82
+ blockers.push(new Promise((_, reject) => {
83
+ const abort = () => {
84
+ abortWorker();
85
+ reject(signal.reason instanceof Error ? signal.reason : new Error(`Sidecar task "${task.name}" aborted`));
86
+ };
87
+ if (signal.aborted) {
88
+ abort();
89
+ return;
90
+ }
91
+ signal.addEventListener("abort", abort, { once: true });
92
+ removeAbortListener = () => signal.removeEventListener("abort", abort);
93
+ }));
94
+ }
95
+ try {
96
+ return blockers.length > 0 ? await Promise.race([runPromise, ...blockers]) : await runPromise;
97
+ }
98
+ finally {
99
+ if (timeoutHandle) {
100
+ clearTimeout(timeoutHandle);
101
+ }
102
+ removeAbortListener();
103
+ }
104
+ }
105
+ //# sourceMappingURL=sidecar-worker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sidecar-worker.js","sourceRoot":"","sources":["../src/sidecar-worker.ts"],"names":[],"mappings":"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","sourcesContent":["import { Agent } from \"@mariozechner/pi-agent-core\";\nimport type { Api, AssistantMessage, Model } from \"@mariozechner/pi-ai\";\nimport { convertToLlm } from \"@mariozechner/pi-coding-agent\";\n\nexport interface SidecarTask<T> {\n\tname: string;\n\tmodel: Model<Api>;\n\tresolveApiKey: (model: Model<Api>) => Promise<string>;\n\tsystemPrompt: string;\n\tprompt: string;\n\tparse: (text: string) => T;\n\ttimeoutMs?: number;\n\tsignal?: AbortSignal;\n}\n\nexport interface SidecarResult<T> {\n\toutput: T;\n\trawText: string;\n}\n\nexport class SidecarTimeoutError extends Error {\n\treadonly taskName: string;\n\treadonly timeoutMs: number;\n\n\tconstructor(taskName: string, timeoutMs: number) {\n\t\tsuper(`Sidecar task \"${taskName}\" timed out after ${timeoutMs}ms`);\n\t\tthis.name = \"SidecarTimeoutError\";\n\t\tthis.taskName = taskName;\n\t\tthis.timeoutMs = timeoutMs;\n\t}\n}\n\nexport class SidecarParseError extends Error {\n\treadonly taskName: string;\n\treadonly rawText: string;\n\n\tconstructor(taskName: string, rawText: string, cause: unknown) {\n\t\tsuper(`Sidecar task \"${taskName}\" returned invalid output`);\n\t\tthis.name = \"SidecarParseError\";\n\t\tthis.taskName = taskName;\n\t\tthis.rawText = rawText;\n\t\tthis.cause = cause;\n\t}\n}\n\nfunction extractAssistantText(message: AssistantMessage): string {\n\treturn message.content\n\t\t.filter(\n\t\t\t(part): part is Extract<AssistantMessage[\"content\"][number], { type: \"text\"; text: string }> =>\n\t\t\t\tpart.type === \"text\",\n\t\t)\n\t\t.map((part) => part.text)\n\t\t.join(\"\\n\")\n\t\t.trim();\n}\n\nexport async function runSidecarTask<T>(task: SidecarTask<T>): Promise<SidecarResult<T>> {\n\tconst apiKey = await task.resolveApiKey(task.model);\n\tconst worker = new Agent({\n\t\tinitialState: {\n\t\t\tsystemPrompt: task.systemPrompt,\n\t\t\tmodel: task.model,\n\t\t\tthinkingLevel: \"off\",\n\t\t\ttools: [],\n\t\t},\n\t\tconvertToLlm,\n\t\tgetApiKey: async () => apiKey,\n\t});\n\n\tconst abortWorker = () => {\n\t\ttry {\n\t\t\tworker.abort();\n\t\t} catch {\n\t\t\t/* ignore */\n\t\t}\n\t};\n\n\tlet removeAbortListener = () => {};\n\tlet timeoutHandle: NodeJS.Timeout | null = null;\n\n\tconst runPromise = (async () => {\n\t\tawait worker.prompt(task.prompt);\n\t\tawait worker.waitForIdle();\n\n\t\tconst lastMessage = worker.state.messages[worker.state.messages.length - 1];\n\t\tif (!lastMessage || lastMessage.role !== \"assistant\") {\n\t\t\tthrow new Error(`Sidecar task \"${task.name}\" returned no assistant message`);\n\t\t}\n\n\t\tif (lastMessage.stopReason === \"error\" || lastMessage.stopReason === \"aborted\") {\n\t\t\tthrow new Error(lastMessage.errorMessage || `Sidecar task \"${task.name}\" failed`);\n\t\t}\n\n\t\tconst rawText = extractAssistantText(lastMessage);\n\t\ttry {\n\t\t\treturn {\n\t\t\t\toutput: task.parse(rawText),\n\t\t\t\trawText,\n\t\t\t};\n\t\t} catch (error) {\n\t\t\tthrow new SidecarParseError(task.name, rawText, error);\n\t\t}\n\t})();\n\tvoid runPromise.catch(() => {});\n\n\tconst blockers: Array<Promise<never>> = [];\n\tif (task.timeoutMs && task.timeoutMs > 0) {\n\t\tblockers.push(\n\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\ttimeoutHandle = setTimeout(() => {\n\t\t\t\t\tabortWorker();\n\t\t\t\t\treject(new SidecarTimeoutError(task.name, task.timeoutMs!));\n\t\t\t\t}, task.timeoutMs);\n\t\t\t}),\n\t\t);\n\t}\n\n\tif (task.signal) {\n\t\tconst signal = task.signal;\n\t\tblockers.push(\n\t\t\tnew Promise<never>((_, reject) => {\n\t\t\t\tconst abort = () => {\n\t\t\t\t\tabortWorker();\n\t\t\t\t\treject(\n\t\t\t\t\t\tsignal.reason instanceof Error ? signal.reason : new Error(`Sidecar task \"${task.name}\" aborted`),\n\t\t\t\t\t);\n\t\t\t\t};\n\n\t\t\t\tif (signal.aborted) {\n\t\t\t\t\tabort();\n\t\t\t\t\treturn;\n\t\t\t\t}\n\n\t\t\t\tsignal.addEventListener(\"abort\", abort, { once: true });\n\t\t\t\tremoveAbortListener = () => signal.removeEventListener(\"abort\", abort);\n\t\t\t}),\n\t\t);\n\t}\n\n\ttry {\n\t\treturn blockers.length > 0 ? await Promise.race([runPromise, ...blockers]) : await runPromise;\n\t} finally {\n\t\tif (timeoutHandle) {\n\t\t\tclearTimeout(timeoutHandle);\n\t\t}\n\t\tremoveAbortListener();\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE;YACL,KAAK,EAAE,MAAM,CAAC;YACd,MAAM,EAAE,MAAM,CAAC;YACf,SAAS,EAAE,MAAM,CAAC;YAClB,UAAU,EAAE,MAAM,CAAC;YACnB,KAAK,EAAE,MAAM,CAAC;SACd,CAAC;KACF,CAAC;CACF;AAED,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,cAAc,CAA6B;IAEnD,YAAY,MAAM,EAAE,kBAAkB,EAOrC;IAED;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAMvC;IAED;;;;OAIG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC,CAwB5E;IAEK,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAK7E;IAED;;;OAGG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAQ/E;IAED;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAiEjD;CACD","sourcesContent":["import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n\tdeliveryMode?: \"steer\" | \"followUp\";\n\tskipContextSync?: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport interface LoggedSubAgentRun {\n\tdate: string;\n\ttoolCallId: string;\n\tlabel: string;\n\tagent: string;\n\tsource: \"predefined\" | \"inline\";\n\tmodel: string;\n\ttools: string[];\n\tturns: number;\n\ttoolCalls: number;\n\tdurationMs: number;\n\tfailed: boolean;\n\tfailureReason?: string;\n\toutput: string;\n\toutputTruncated: boolean;\n\tusage: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t\tcost: {\n\t\t\tinput: number;\n\t\t\toutput: number;\n\t\t\tcacheRead: number;\n\t\t\tcacheWrite: number;\n\t\t\ttotal: number;\n\t\t};\n\t};\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl raw archive.\n\t * This file is cold storage and is not proactively loaded into memory context.\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\tasync logSubAgentRun(channelId: string, run: LoggedSubAgentRun): Promise<void> {\n\t\tconst logPath = join(this.getChannelDir(channelId), \"subagent-runs.jsonl\");\n\t\tthis.rotateIfNeeded(logPath);\n\t\tconst line = `${JSON.stringify(run)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst fd = openSync(logPath, \"r\");\n\t\t\ttry {\n\t\t\t\tlet end = stats.size;\n\t\t\t\tconst trailing = Buffer.alloc(1);\n\t\t\t\twhile (end > 0) {\n\t\t\t\t\treadSync(fd, trailing, 0, 1, end - 1);\n\t\t\t\t\tif (trailing[0] !== 0x0a && trailing[0] !== 0x0d) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tend--;\n\t\t\t\t}\n\n\t\t\t\tif (end === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst chunkSize = 4096;\n\t\t\t\tconst buffer = Buffer.alloc(chunkSize);\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tlet position = end;\n\n\t\t\t\twhile (position > 0) {\n\t\t\t\t\tconst bytesToRead = Math.min(chunkSize, position);\n\t\t\t\t\tposition -= bytesToRead;\n\t\t\t\t\treadSync(fd, buffer, 0, bytesToRead, position);\n\n\t\t\t\t\tconst newlineIndex = buffer.subarray(0, bytesToRead).lastIndexOf(0x0a);\n\t\t\t\t\tif (newlineIndex !== -1) {\n\t\t\t\t\t\tlineStart = position + newlineIndex + 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst lineLength = end - lineStart;\n\t\t\t\tif (lineLength <= 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst lineBuffer = Buffer.alloc(lineLength);\n\t\t\t\treadSync(fd, lineBuffer, 0, lineLength, lineStart);\n\t\t\t\tconst lastLine = lineBuffer.toString(\"utf-8\").replace(/\\r+$/, \"\");\n\t\t\t\tif (!lastLine) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\t\treturn message.ts;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,aAAa;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,YAAY,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC;IACpC,eAAe,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,UAAU,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,GAAG,QAAQ,CAAC;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,eAAe,EAAE,OAAO,CAAC;IACzB,KAAK,EAAE;QACN,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;QACnB,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE;YACL,KAAK,EAAE,MAAM,CAAC;YACd,MAAM,EAAE,MAAM,CAAC;YACf,SAAS,EAAE,MAAM,CAAC;YAClB,UAAU,EAAE,MAAM,CAAC;YACnB,KAAK,EAAE,MAAM,CAAC;SACd,CAAC;KACF,CAAC;CACF;AAED,qBAAa,YAAY;IACxB,OAAO,CAAC,UAAU,CAAS;IAE3B,OAAO,CAAC,cAAc,CAA6B;gBAEvC,MAAM,EAAE,kBAAkB;IAStC;;OAEG;IACH,aAAa,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAQxC;;;;OAIG;IACG,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,OAAO,CAAC;IA0BvE,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9E;;;OAGG;IACH,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACG,cAAc,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAUhF;;;OAGG;IACH,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI;CAkElD"}
package/dist/store.js CHANGED
@@ -2,10 +2,9 @@ import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statS
2
2
  import { appendFile, writeFile } from "fs/promises";
3
3
  import { dirname, join } from "path";
4
4
  export class ChannelStore {
5
- workingDir;
6
- // Track recently logged message timestamps to prevent duplicates
7
- recentlyLogged = new Map();
8
5
  constructor(config) {
6
+ // Track recently logged message timestamps to prevent duplicates
7
+ this.recentlyLogged = new Map();
9
8
  this.workingDir = config.workingDir;
10
9
  // Ensure working directory exists
11
10
  if (!existsSync(this.workingDir)) {
package/dist/store.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n\tdeliveryMode?: \"steer\" | \"followUp\";\n\tskipContextSync?: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport interface LoggedSubAgentRun {\n\tdate: string;\n\ttoolCallId: string;\n\tlabel: string;\n\tagent: string;\n\tsource: \"predefined\" | \"inline\";\n\tmodel: string;\n\ttools: string[];\n\tturns: number;\n\ttoolCalls: number;\n\tdurationMs: number;\n\tfailed: boolean;\n\tfailureReason?: string;\n\toutput: string;\n\toutputTruncated: boolean;\n\tusage: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t\tcost: {\n\t\t\tinput: number;\n\t\t\toutput: number;\n\t\t\tcacheRead: number;\n\t\t\tcacheWrite: number;\n\t\t\ttotal: number;\n\t\t};\n\t};\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl raw archive.\n\t * This file is cold storage and is not proactively loaded into memory context.\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\tasync logSubAgentRun(channelId: string, run: LoggedSubAgentRun): Promise<void> {\n\t\tconst logPath = join(this.getChannelDir(channelId), \"subagent-runs.jsonl\");\n\t\tthis.rotateIfNeeded(logPath);\n\t\tconst line = `${JSON.stringify(run)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst fd = openSync(logPath, \"r\");\n\t\t\ttry {\n\t\t\t\tlet end = stats.size;\n\t\t\t\tconst trailing = Buffer.alloc(1);\n\t\t\t\twhile (end > 0) {\n\t\t\t\t\treadSync(fd, trailing, 0, 1, end - 1);\n\t\t\t\t\tif (trailing[0] !== 0x0a && trailing[0] !== 0x0d) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tend--;\n\t\t\t\t}\n\n\t\t\t\tif (end === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst chunkSize = 4096;\n\t\t\t\tconst buffer = Buffer.alloc(chunkSize);\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tlet position = end;\n\n\t\t\t\twhile (position > 0) {\n\t\t\t\t\tconst bytesToRead = Math.min(chunkSize, position);\n\t\t\t\t\tposition -= bytesToRead;\n\t\t\t\t\treadSync(fd, buffer, 0, bytesToRead, position);\n\n\t\t\t\t\tconst newlineIndex = buffer.subarray(0, bytesToRead).lastIndexOf(0x0a);\n\t\t\t\t\tif (newlineIndex !== -1) {\n\t\t\t\t\t\tlineStart = position + newlineIndex + 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst lineLength = end - lineStart;\n\t\t\t\tif (lineLength <= 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst lineBuffer = Buffer.alloc(lineLength);\n\t\t\t\treadSync(fd, lineBuffer, 0, lineLength, lineStart);\n\t\t\t\tconst lastLine = lineBuffer.toString(\"utf-8\").replace(/\\r+$/, \"\");\n\t\t\t\tif (!lastLine) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\t\treturn message.ts;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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","sourcesContent":["import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync } from \"fs\";\nimport { appendFile, writeFile } from \"fs/promises\";\nimport { dirname, join } from \"path\";\n\nexport interface LoggedMessage {\n\tdate: string;\n\tts: string;\n\tuser: string;\n\tuserName?: string;\n\tdisplayName?: string;\n\ttext: string;\n\tisBot: boolean;\n\tdeliveryMode?: \"steer\" | \"followUp\";\n\tskipContextSync?: boolean;\n}\n\nexport interface ChannelStoreConfig {\n\tworkingDir: string;\n}\n\nexport interface LoggedSubAgentRun {\n\tdate: string;\n\ttoolCallId: string;\n\tlabel: string;\n\tagent: string;\n\tsource: \"predefined\" | \"inline\";\n\tmodel: string;\n\ttools: string[];\n\tturns: number;\n\ttoolCalls: number;\n\tdurationMs: number;\n\tfailed: boolean;\n\tfailureReason?: string;\n\toutput: string;\n\toutputTruncated: boolean;\n\tusage: {\n\t\tinput: number;\n\t\toutput: number;\n\t\tcacheRead: number;\n\t\tcacheWrite: number;\n\t\ttotal: number;\n\t\tcost: {\n\t\t\tinput: number;\n\t\t\toutput: number;\n\t\t\tcacheRead: number;\n\t\t\tcacheWrite: number;\n\t\t\ttotal: number;\n\t\t};\n\t};\n}\n\nexport class ChannelStore {\n\tprivate workingDir: string;\n\t// Track recently logged message timestamps to prevent duplicates\n\tprivate recentlyLogged = new Map<string, number>();\n\n\tconstructor(config: ChannelStoreConfig) {\n\t\tthis.workingDir = config.workingDir;\n\n\t\t// Ensure working directory exists\n\t\tif (!existsSync(this.workingDir)) {\n\t\t\tmkdirSync(this.workingDir, { recursive: true });\n\t\t}\n\t}\n\n\t/**\n\t * Get or create the directory for a channel/DM\n\t */\n\tgetChannelDir(channelId: string): string {\n\t\tconst dir = join(this.workingDir, channelId);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true });\n\t\t}\n\t\treturn dir;\n\t}\n\n\t/**\n\t * Log a message to the channel's log.jsonl raw archive.\n\t * This file is cold storage and is not proactively loaded into memory context.\n\t * Returns false if message was already logged (duplicate)\n\t */\n\tasync logMessage(channelId: string, message: LoggedMessage): Promise<boolean> {\n\t\t// Check for duplicate (same channel + timestamp)\n\t\tconst dedupeKey = `${channelId}:${message.ts}`;\n\t\tif (this.recentlyLogged.has(dedupeKey)) {\n\t\t\treturn false; // Already logged\n\t\t}\n\n\t\t// Mark as logged and schedule cleanup after 60 seconds\n\t\tthis.recentlyLogged.set(dedupeKey, Date.now());\n\t\tsetTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);\n\n\t\tconst logPath = join(this.getChannelDir(channelId), \"log.jsonl\");\n\n\t\t// Rotate if file exceeds size limit\n\t\tthis.rotateIfNeeded(logPath);\n\n\t\t// Ensure message has a date field\n\t\tif (!message.date) {\n\t\t\tmessage.date = new Date().toISOString();\n\t\t}\n\n\t\tconst line = `${JSON.stringify(message)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t\treturn true;\n\t}\n\n\tasync logSubAgentRun(channelId: string, run: LoggedSubAgentRun): Promise<void> {\n\t\tconst logPath = join(this.getChannelDir(channelId), \"subagent-runs.jsonl\");\n\t\tthis.rotateIfNeeded(logPath);\n\t\tconst line = `${JSON.stringify(run)}\\n`;\n\t\tawait appendFile(logPath, line, \"utf-8\");\n\t}\n\n\t/**\n\t * Rotate log file if it exceeds 1MB.\n\t * Keeps one backup (log.jsonl.1) and resets the sync offset.\n\t */\n\tprivate rotateIfNeeded(logPath: string): void {\n\t\ttry {\n\t\t\tif (!existsSync(logPath)) return;\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size > 1_000_000) {\n\t\t\t\trenameSync(logPath, `${logPath}.1`);\n\t\t\t\t// Reset sync offset since log.jsonl was replaced\n\t\t\t\tconst syncOffsetPath = join(dirname(logPath), \".sync-offset\");\n\t\t\t\ttry {\n\t\t\t\t\twriteFile(syncOffsetPath, \"0\", \"utf-8\").catch(() => {});\n\t\t\t\t} catch {\n\t\t\t\t\t/* ignore */\n\t\t\t\t}\n\t\t\t}\n\t\t} catch {\n\t\t\t// Ignore rotation errors\n\t\t}\n\t}\n\n\t/**\n\t * Log a bot response\n\t */\n\tasync logBotResponse(channelId: string, text: string, ts: string): Promise<void> {\n\t\tawait this.logMessage(channelId, {\n\t\t\tdate: new Date().toISOString(),\n\t\t\tts,\n\t\t\tuser: \"bot\",\n\t\t\ttext,\n\t\t\tisBot: true,\n\t\t});\n\t}\n\n\t/**\n\t * Get the timestamp of the last logged message for a channel\n\t * Returns null if no log exists\n\t */\n\tgetLastTimestamp(channelId: string): string | null {\n\t\tconst logPath = join(this.workingDir, channelId, \"log.jsonl\");\n\t\tif (!existsSync(logPath)) {\n\t\t\treturn null;\n\t\t}\n\n\t\ttry {\n\t\t\tconst stats = statSync(logPath);\n\t\t\tif (stats.size === 0) {\n\t\t\t\treturn null;\n\t\t\t}\n\n\t\t\tconst fd = openSync(logPath, \"r\");\n\t\t\ttry {\n\t\t\t\tlet end = stats.size;\n\t\t\t\tconst trailing = Buffer.alloc(1);\n\t\t\t\twhile (end > 0) {\n\t\t\t\t\treadSync(fd, trailing, 0, 1, end - 1);\n\t\t\t\t\tif (trailing[0] !== 0x0a && trailing[0] !== 0x0d) {\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t\tend--;\n\t\t\t\t}\n\n\t\t\t\tif (end === 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst chunkSize = 4096;\n\t\t\t\tconst buffer = Buffer.alloc(chunkSize);\n\t\t\t\tlet lineStart = 0;\n\t\t\t\tlet position = end;\n\n\t\t\t\twhile (position > 0) {\n\t\t\t\t\tconst bytesToRead = Math.min(chunkSize, position);\n\t\t\t\t\tposition -= bytesToRead;\n\t\t\t\t\treadSync(fd, buffer, 0, bytesToRead, position);\n\n\t\t\t\t\tconst newlineIndex = buffer.subarray(0, bytesToRead).lastIndexOf(0x0a);\n\t\t\t\t\tif (newlineIndex !== -1) {\n\t\t\t\t\t\tlineStart = position + newlineIndex + 1;\n\t\t\t\t\t\tbreak;\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tconst lineLength = end - lineStart;\n\t\t\t\tif (lineLength <= 0) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst lineBuffer = Buffer.alloc(lineLength);\n\t\t\t\treadSync(fd, lineBuffer, 0, lineLength, lineStart);\n\t\t\t\tconst lastLine = lineBuffer.toString(\"utf-8\").replace(/\\r+$/, \"\");\n\t\t\t\tif (!lastLine) {\n\t\t\t\t\treturn null;\n\t\t\t\t}\n\n\t\t\t\tconst message = JSON.parse(lastLine) as LoggedMessage;\n\t\t\t\treturn message.ts;\n\t\t\t} finally {\n\t\t\t\tcloseSync(fd);\n\t\t\t}\n\t\t} catch {\n\t\t\treturn null;\n\t\t}\n\t}\n}\n"]}
@@ -1,6 +1,10 @@
1
1
  import type { Api, Model } from "@mariozechner/pi-ai";
2
2
  declare const ALLOWED_SUB_AGENT_TOOLS: readonly ["read", "bash", "edit", "write"];
3
+ declare const ALLOWED_CONTEXT_MODES: readonly ["isolated", "contextual"];
4
+ declare const ALLOWED_MEMORY_MODES: readonly ["none", "session", "relevant"];
3
5
  export type SubAgentToolName = (typeof ALLOWED_SUB_AGENT_TOOLS)[number];
6
+ export type SubAgentContextMode = (typeof ALLOWED_CONTEXT_MODES)[number];
7
+ export type SubAgentMemoryMode = (typeof ALLOWED_MEMORY_MODES)[number];
4
8
  export interface SubAgentConfig {
5
9
  name: string;
6
10
  description: string;
@@ -12,6 +16,9 @@ export interface SubAgentConfig {
12
16
  maxToolCalls: number;
13
17
  maxWallTimeSec: number;
14
18
  bashTimeoutSec: number;
19
+ contextMode: SubAgentContextMode;
20
+ memory: SubAgentMemoryMode;
21
+ paths: string[];
15
22
  filePath?: string;
16
23
  source: "predefined" | "inline";
17
24
  }
@@ -34,6 +41,9 @@ export interface SubAgentInvocationOverrides {
34
41
  maxToolCalls?: number;
35
42
  maxWallTimeSec?: number;
36
43
  bashTimeoutSec?: number;
44
+ contextMode?: string;
45
+ memory?: string;
46
+ paths?: string[];
37
47
  }
38
48
  export declare function validateSubAgentTask(task: string): string | undefined;
39
49
  export declare function getSubAgentsDir(workspaceDir: string): string;