@oyasmi/pipiclaw 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. package/README.md +308 -209
  2. package/dist/agent/channel-runner.d.ts +47 -0
  3. package/dist/agent/channel-runner.js +441 -0
  4. package/dist/agent/index.d.ts +3 -0
  5. package/dist/agent/index.js +2 -0
  6. package/dist/agent/progress-formatter.d.ts +4 -0
  7. package/dist/agent/progress-formatter.js +52 -0
  8. package/dist/agent/run-queue.d.ts +7 -0
  9. package/dist/agent/run-queue.js +26 -0
  10. package/dist/agent/runner-factory.d.ts +3 -0
  11. package/dist/agent/runner-factory.js +10 -0
  12. package/dist/agent/session-events.d.ts +14 -0
  13. package/dist/agent/session-events.js +215 -0
  14. package/dist/agent/session-resource-gate.d.ts +10 -0
  15. package/dist/agent/session-resource-gate.js +44 -0
  16. package/dist/agent/type-guards.d.ts +22 -0
  17. package/dist/agent/type-guards.js +106 -0
  18. package/dist/agent/types.d.ts +160 -0
  19. package/dist/agent/types.js +22 -0
  20. package/dist/agent.d.ts +2 -16
  21. package/dist/agent.js +1 -782
  22. package/dist/command-extension.d.ts +0 -1
  23. package/dist/command-extension.js +0 -1
  24. package/dist/commands.d.ts +0 -1
  25. package/dist/commands.js +0 -1
  26. package/dist/config-loader.d.ts +0 -1
  27. package/dist/config-loader.js +1 -2
  28. package/dist/context.d.ts +58 -15
  29. package/dist/context.js +50 -8
  30. package/dist/index.d.ts +12 -13
  31. package/dist/index.js +12 -13
  32. package/dist/log.d.ts +0 -1
  33. package/dist/log.js +0 -1
  34. package/dist/main.d.ts +0 -1
  35. package/dist/main.js +5 -405
  36. package/dist/memory/bootstrap.d.ts +6 -0
  37. package/dist/memory/bootstrap.js +46 -0
  38. package/dist/{memory-candidates.d.ts → memory/candidates.d.ts} +1 -1
  39. package/dist/{memory-candidates.js → memory/candidates.js} +33 -21
  40. package/dist/memory/chinese-words.d.ts +1 -0
  41. package/dist/memory/chinese-words.js +273 -0
  42. package/dist/{memory-consolidation.d.ts → memory/consolidation.d.ts} +0 -1
  43. package/dist/{memory-consolidation.js → memory/consolidation.js} +26 -35
  44. package/dist/{memory-files.d.ts → memory/files.d.ts} +0 -6
  45. package/dist/{memory-files.js → memory/files.js} +11 -36
  46. package/dist/{memory-lifecycle.d.ts → memory/lifecycle.d.ts} +23 -6
  47. package/dist/memory/lifecycle.js +246 -0
  48. package/dist/{memory-recall.d.ts → memory/recall.d.ts} +2 -2
  49. package/dist/memory/recall.js +501 -0
  50. package/dist/{session-memory.d.ts → memory/session.d.ts} +1 -1
  51. package/dist/{session-memory.js → memory/session.js} +31 -62
  52. package/dist/model-utils.d.ts +0 -1
  53. package/dist/model-utils.js +0 -1
  54. package/dist/paths.d.ts +0 -1
  55. package/dist/paths.js +0 -1
  56. package/dist/prompt-builder.d.ts +0 -1
  57. package/dist/prompt-builder.js +0 -1
  58. package/dist/runtime/bootstrap.d.ts +47 -0
  59. package/dist/runtime/bootstrap.js +450 -0
  60. package/dist/{delivery.d.ts → runtime/delivery.d.ts} +0 -1
  61. package/dist/{delivery.js → runtime/delivery.js} +1 -2
  62. package/dist/{dingtalk.d.ts → runtime/dingtalk.d.ts} +10 -1
  63. package/dist/{dingtalk.js → runtime/dingtalk.js} +87 -28
  64. package/dist/{events.d.ts → runtime/events.d.ts} +0 -1
  65. package/dist/{events.js → runtime/events.js} +1 -2
  66. package/dist/{store.d.ts → runtime/store.d.ts} +5 -1
  67. package/dist/{store.js → runtime/store.js} +60 -20
  68. package/dist/sandbox.d.ts +0 -1
  69. package/dist/sandbox.js +1 -2
  70. package/dist/{llm-json.d.ts → shared/llm-json.d.ts} +0 -1
  71. package/dist/{llm-json.js → shared/llm-json.js} +0 -1
  72. package/dist/shared/markdown-sections.d.ts +6 -0
  73. package/dist/{markdown-sections.js → shared/markdown-sections.js} +10 -4
  74. package/dist/{shell-escape.d.ts → shared/shell-escape.d.ts} +0 -1
  75. package/dist/{shell-escape.js → shared/shell-escape.js} +0 -1
  76. package/dist/shared/text-utils.d.ts +9 -0
  77. package/dist/shared/text-utils.js +36 -0
  78. package/dist/shared/type-guards.d.ts +5 -0
  79. package/dist/shared/type-guards.js +12 -0
  80. package/dist/shared/types.d.ts +14 -0
  81. package/dist/shared/types.js +1 -0
  82. package/dist/sidecar-worker.d.ts +0 -1
  83. package/dist/sidecar-worker.js +1 -8
  84. package/dist/{sub-agents.d.ts → subagents/discovery.d.ts} +0 -1
  85. package/dist/{sub-agents.js → subagents/discovery.js} +2 -3
  86. package/dist/{tools/subagent.d.ts → subagents/tool.d.ts} +2 -16
  87. package/dist/{tools/subagent.js → subagents/tool.js} +16 -38
  88. package/dist/tools/attach.d.ts +0 -1
  89. package/dist/tools/attach.js +0 -1
  90. package/dist/tools/bash.d.ts +0 -1
  91. package/dist/tools/bash.js +0 -1
  92. package/dist/tools/edit.d.ts +0 -1
  93. package/dist/tools/edit.js +1 -2
  94. package/dist/tools/index.d.ts +1 -2
  95. package/dist/tools/index.js +1 -2
  96. package/dist/tools/read.d.ts +0 -1
  97. package/dist/tools/read.js +1 -2
  98. package/dist/tools/truncate.d.ts +0 -1
  99. package/dist/tools/truncate.js +0 -1
  100. package/dist/tools/write-content.d.ts +0 -1
  101. package/dist/tools/write-content.js +1 -2
  102. package/dist/tools/write.d.ts +0 -1
  103. package/dist/tools/write.js +0 -1
  104. package/package.json +9 -3
  105. package/CHANGELOG.md +0 -47
  106. package/dist/agent.d.ts.map +0 -1
  107. package/dist/agent.js.map +0 -1
  108. package/dist/command-extension.d.ts.map +0 -1
  109. package/dist/command-extension.js.map +0 -1
  110. package/dist/commands.d.ts.map +0 -1
  111. package/dist/commands.js.map +0 -1
  112. package/dist/config-loader.d.ts.map +0 -1
  113. package/dist/config-loader.js.map +0 -1
  114. package/dist/context.d.ts.map +0 -1
  115. package/dist/context.js.map +0 -1
  116. package/dist/delivery.d.ts.map +0 -1
  117. package/dist/delivery.js.map +0 -1
  118. package/dist/dingtalk.d.ts.map +0 -1
  119. package/dist/dingtalk.js.map +0 -1
  120. package/dist/events.d.ts.map +0 -1
  121. package/dist/events.js.map +0 -1
  122. package/dist/index.d.ts.map +0 -1
  123. package/dist/index.js.map +0 -1
  124. package/dist/llm-json.d.ts.map +0 -1
  125. package/dist/llm-json.js.map +0 -1
  126. package/dist/log.d.ts.map +0 -1
  127. package/dist/log.js.map +0 -1
  128. package/dist/main.d.ts.map +0 -1
  129. package/dist/main.js.map +0 -1
  130. package/dist/markdown-sections.d.ts +0 -6
  131. package/dist/markdown-sections.d.ts.map +0 -1
  132. package/dist/markdown-sections.js.map +0 -1
  133. package/dist/memory-candidates.d.ts.map +0 -1
  134. package/dist/memory-candidates.js.map +0 -1
  135. package/dist/memory-consolidation.d.ts.map +0 -1
  136. package/dist/memory-consolidation.js.map +0 -1
  137. package/dist/memory-files.d.ts.map +0 -1
  138. package/dist/memory-files.js.map +0 -1
  139. package/dist/memory-lifecycle.d.ts.map +0 -1
  140. package/dist/memory-lifecycle.js +0 -150
  141. package/dist/memory-lifecycle.js.map +0 -1
  142. package/dist/memory-recall.d.ts.map +0 -1
  143. package/dist/memory-recall.js +0 -218
  144. package/dist/memory-recall.js.map +0 -1
  145. package/dist/model-utils.d.ts.map +0 -1
  146. package/dist/model-utils.js.map +0 -1
  147. package/dist/paths.d.ts.map +0 -1
  148. package/dist/paths.js.map +0 -1
  149. package/dist/prompt-builder.d.ts.map +0 -1
  150. package/dist/prompt-builder.js.map +0 -1
  151. package/dist/sandbox.d.ts.map +0 -1
  152. package/dist/sandbox.js.map +0 -1
  153. package/dist/session-memory-files.d.ts +0 -2
  154. package/dist/session-memory-files.d.ts.map +0 -1
  155. package/dist/session-memory-files.js +0 -2
  156. package/dist/session-memory-files.js.map +0 -1
  157. package/dist/session-memory.d.ts.map +0 -1
  158. package/dist/session-memory.js.map +0 -1
  159. package/dist/shell-escape.d.ts.map +0 -1
  160. package/dist/shell-escape.js.map +0 -1
  161. package/dist/sidecar-worker.d.ts.map +0 -1
  162. package/dist/sidecar-worker.js.map +0 -1
  163. package/dist/store.d.ts.map +0 -1
  164. package/dist/store.js.map +0 -1
  165. package/dist/sub-agents.d.ts.map +0 -1
  166. package/dist/sub-agents.js.map +0 -1
  167. package/dist/tools/attach.d.ts.map +0 -1
  168. package/dist/tools/attach.js.map +0 -1
  169. package/dist/tools/bash.d.ts.map +0 -1
  170. package/dist/tools/bash.js.map +0 -1
  171. package/dist/tools/edit.d.ts.map +0 -1
  172. package/dist/tools/edit.js.map +0 -1
  173. package/dist/tools/index.d.ts.map +0 -1
  174. package/dist/tools/index.js.map +0 -1
  175. package/dist/tools/read.d.ts.map +0 -1
  176. package/dist/tools/read.js.map +0 -1
  177. package/dist/tools/subagent.d.ts.map +0 -1
  178. package/dist/tools/subagent.js.map +0 -1
  179. package/dist/tools/truncate.d.ts.map +0 -1
  180. package/dist/tools/truncate.js.map +0 -1
  181. package/dist/tools/write-content.d.ts.map +0 -1
  182. package/dist/tools/write-content.js.map +0 -1
  183. package/dist/tools/write.d.ts.map +0 -1
  184. package/dist/tools/write.js.map +0 -1
  185. package/docs/improve-memory/design.md +0 -537
  186. package/docs/improve-memory/interfaces-and-tests.md +0 -473
  187. package/docs/improve-memory/spec.md +0 -357
  188. package/docs/memory-rfc.md +0 -297
  189. package/docs/proj-review.md +0 -188
  190. package/docs/subagent/pi-subagent-analyse.txt +0 -190
  191. package/docs/subagent/pi-subagent-design.txt +0 -266
  192. package/docs/subagent/pi-subagent-phase1-plan.txt +0 -529
  193. package/docs/test-supplementation-plan.md +0 -553
@@ -11,8 +11,9 @@ import axios from "axios";
11
11
  import { DWClient, TOPIC_ROBOT } from "dingtalk-stream";
12
12
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
13
13
  import { dirname, join } from "path";
14
- import { parseBuiltInCommand, renderBuiltInHelp } from "./commands.js";
15
- import * as log from "./log.js";
14
+ import { parseBuiltInCommand, renderBuiltInHelp } from "../commands.js";
15
+ import * as log from "../log.js";
16
+ import { isRecord } from "../shared/type-guards.js";
16
17
  class ChannelQueue {
17
18
  constructor() {
18
19
  this.queue = [];
@@ -72,6 +73,7 @@ export class DingTalkBot {
72
73
  this.lastSocketAvailableTime = Date.now();
73
74
  this.activeMessageProcessing = false;
74
75
  this.keepAliveTimer = null;
76
+ this.reconnectTimer = null;
75
77
  this.isReconnecting = false;
76
78
  this.isStopped = false;
77
79
  this.reconnectAttempts = 0;
@@ -95,6 +97,67 @@ export class DingTalkBot {
95
97
  }
96
98
  return true;
97
99
  }
100
+ getSocket() {
101
+ if (!this.client) {
102
+ return null;
103
+ }
104
+ const socket = Reflect.get(this.client, "socket");
105
+ return this.isSocketLike(socket) ? socket : null;
106
+ }
107
+ isSocketLike(value) {
108
+ if (!isRecord(value)) {
109
+ return false;
110
+ }
111
+ return typeof value.on === "function";
112
+ }
113
+ setTrackedTimeout(callback, delayMs) {
114
+ const timer = setTimeout(() => {
115
+ callback();
116
+ }, delayMs);
117
+ timer.unref?.();
118
+ return timer;
119
+ }
120
+ setTrackedInterval(callback, intervalMs) {
121
+ const timer = setInterval(callback, intervalMs);
122
+ timer.unref?.();
123
+ return timer;
124
+ }
125
+ clearKeepAliveTimer() {
126
+ if (this.keepAliveTimer) {
127
+ clearInterval(this.keepAliveTimer);
128
+ this.keepAliveTimer = null;
129
+ }
130
+ }
131
+ clearReconnectTimer() {
132
+ if (this.reconnectTimer) {
133
+ clearTimeout(this.reconnectTimer);
134
+ this.reconnectTimer = null;
135
+ }
136
+ }
137
+ clearAllTimers() {
138
+ this.clearKeepAliveTimer();
139
+ this.clearReconnectTimer();
140
+ }
141
+ async waitForDelay(delayMs) {
142
+ await new Promise((resolve) => {
143
+ this.reconnectTimer = this.setTrackedTimeout(() => {
144
+ this.reconnectTimer = null;
145
+ resolve();
146
+ }, delayMs);
147
+ });
148
+ }
149
+ scheduleReconnect(delayMs, immediate) {
150
+ if (this.isStopped) {
151
+ return;
152
+ }
153
+ this.clearReconnectTimer();
154
+ this.reconnectTimer = this.setTrackedTimeout(() => {
155
+ this.reconnectTimer = null;
156
+ this.doReconnect(immediate).catch((err) => {
157
+ log.logWarning("DingTalk: reconnect failed", err instanceof Error ? err.message : String(err));
158
+ });
159
+ }, delayMs);
160
+ }
98
161
  // ==========================================================================
99
162
  // Public API
100
163
  // ==========================================================================
@@ -110,10 +173,10 @@ export class DingTalkBot {
110
173
  if (process.env.DINGTALK_FORCE_PROXY !== "true") {
111
174
  axios.defaults.proxy = false;
112
175
  }
176
+ this.clearAllTimers();
113
177
  this.client = new DWClient({
114
178
  clientId: this.config.clientId,
115
179
  clientSecret: this.config.clientSecret,
116
- autoReconnect: false,
117
180
  keepAlive: false,
118
181
  });
119
182
  this.client.registerCallbackListener(TOPIC_ROBOT, (msg) => {
@@ -133,7 +196,8 @@ export class DingTalkBot {
133
196
  return { status: "SUCCESS", message: "OK" };
134
197
  }
135
198
  try {
136
- const data = typeof msg.data === "string" ? JSON.parse(msg.data) : msg.data;
199
+ const parsedData = typeof msg.data === "string" ? JSON.parse(msg.data) : msg.data;
200
+ const data = isRecord(parsedData) ? parsedData : {};
137
201
  // 3. Business logic deduplication
138
202
  const msgId = data.msgId;
139
203
  if (msgId && !this.markProcessed(msgId)) {
@@ -157,21 +221,24 @@ export class DingTalkBot {
157
221
  if (!immediate && this.reconnectAttempts > 0) {
158
222
  const delay = Math.min(1000 * 2 ** this.reconnectAttempts + Math.random() * 1000, 30000);
159
223
  log.logInfo(`DingTalk: waiting ${Math.round(delay / 1000)}s before reconnecting...`);
160
- await new Promise((resolve) => setTimeout(resolve, delay));
224
+ await this.waitForDelay(delay);
225
+ if (this.isStopped || !this.client) {
226
+ this.isReconnecting = false;
227
+ return;
228
+ }
161
229
  }
162
230
  try {
163
- const socket = this.client.socket;
231
+ const socket = this.getSocket();
164
232
  if (socket?.readyState === 1 || socket?.readyState === 3) {
165
- await this.client.disconnect();
233
+ await Promise.resolve(this.client.disconnect());
166
234
  }
167
235
  await this.client.connect();
168
236
  this.lastSocketAvailableTime = Date.now();
169
237
  this.reconnectAttempts = 0; // Success, reset backoff
170
238
  log.logInfo("DingTalk: connected to stream.");
171
239
  // Setup keep alive
172
- if (this.keepAliveTimer)
173
- clearInterval(this.keepAliveTimer);
174
- this.keepAliveTimer = setInterval(() => {
240
+ this.clearKeepAliveTimer();
241
+ this.keepAliveTimer = this.setTrackedInterval(() => {
175
242
  if (this.isStopped)
176
243
  return;
177
244
  const elapsed = Date.now() - this.lastSocketAvailableTime;
@@ -179,9 +246,9 @@ export class DingTalkBot {
179
246
  log.logWarning("DingTalk: connection timeout detected (>90s). Keeping active where possible...");
180
247
  }
181
248
  try {
182
- const s = this.client?.socket;
249
+ const s = this.getSocket();
183
250
  if (s?.readyState === 1) {
184
- s.ping();
251
+ s.ping?.();
185
252
  }
186
253
  }
187
254
  catch (_err) {
@@ -189,7 +256,7 @@ export class DingTalkBot {
189
256
  }
190
257
  }, 30 * 1000);
191
258
  // Setup native socket events
192
- const s = this.client.socket;
259
+ const s = this.getSocket();
193
260
  s?.on("pong", () => {
194
261
  this.lastSocketAvailableTime = Date.now();
195
262
  });
@@ -197,11 +264,7 @@ export class DingTalkBot {
197
264
  log.logWarning(`DingTalk: WebSocket closed: code=${code}, reason=${reason}`);
198
265
  if (this.isStopped)
199
266
  return;
200
- setTimeout(() => {
201
- this.doReconnect(true).catch((err) => {
202
- log.logWarning("DingTalk: reconnect failed", err instanceof Error ? err.message : String(err));
203
- });
204
- }, 1000);
267
+ this.scheduleReconnect(1000, true);
205
268
  });
206
269
  s?.on("message", (raw) => {
207
270
  try {
@@ -228,20 +291,19 @@ export class DingTalkBot {
228
291
  }
229
292
  // Auto-retry on failure with exponential backoff
230
293
  if (connectionFailed && !this.isStopped) {
231
- this.doReconnect().catch(() => { });
294
+ this.scheduleReconnect(0, false);
232
295
  }
233
296
  }
234
297
  async stop() {
235
298
  log.logInfo("DingTalk: stopping bot");
236
299
  this.isStopped = true;
237
- if (this.keepAliveTimer)
238
- clearInterval(this.keepAliveTimer);
300
+ this.clearAllTimers();
239
301
  for (const queue of this.queues.values()) {
240
302
  queue.stop();
241
303
  }
242
304
  if (this.client) {
243
305
  try {
244
- await Promise.resolve(this.client.disconnect?.());
306
+ await Promise.resolve(this.client.disconnect());
245
307
  }
246
308
  catch (err) {
247
309
  log.logWarning("DingTalk: failed to disconnect cleanly", err instanceof Error ? err.message : String(err));
@@ -553,11 +615,9 @@ export class DingTalkBot {
553
615
  if (textContent)
554
616
  return textContent;
555
617
  // 2. richText 类型消息:从 content.richText 列表提取文本片段
556
- const raw = data;
557
- const contentObj = raw.content;
558
- if (contentObj?.richText) {
618
+ if (data.content?.richText) {
559
619
  const parts = [];
560
- for (const item of contentObj.richText) {
620
+ for (const item of data.content.richText) {
561
621
  if (item.text)
562
622
  parts.push(item.text);
563
623
  }
@@ -577,7 +637,7 @@ export class DingTalkBot {
577
637
  const conversationId = data.conversationId || "";
578
638
  const conversationType = data.conversationType || "1";
579
639
  if (!content) {
580
- const msgtype = data.msgtype || "unknown";
640
+ const msgtype = typeof data.msgtype === "string" ? data.msgtype : "unknown";
581
641
  log.logWarning(`DingTalk: empty message (type=${msgtype})`);
582
642
  return;
583
643
  }
@@ -704,4 +764,3 @@ export class DingTalkBot {
704
764
  return join(this.config.stateDir, channelId, ".channel-meta.json");
705
765
  }
706
766
  }
707
- //# sourceMappingURL=dingtalk.js.map
@@ -48,4 +48,3 @@ export declare class EventsWatcher {
48
48
  * Create and start an events watcher.
49
49
  */
50
50
  export declare function createEventsWatcher(workspaceDir: string, bot: DingTalkBot): EventsWatcher;
51
- //# sourceMappingURL=events.d.ts.map
@@ -2,7 +2,7 @@ import { Cron } from "croner";
2
2
  import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch } from "fs";
3
3
  import { readFile } from "fs/promises";
4
4
  import { join } from "path";
5
- import * as log from "./log.js";
5
+ import * as log from "../log.js";
6
6
  // ============================================================================
7
7
  // EventsWatcher
8
8
  // ============================================================================
@@ -292,4 +292,3 @@ export function createEventsWatcher(workspaceDir, bot) {
292
292
  const eventsDir = join(workspaceDir, "events");
293
293
  return new EventsWatcher(eventsDir, bot);
294
294
  }
295
- //# sourceMappingURL=events.js.map
@@ -45,6 +45,8 @@ export interface LoggedSubAgentRun {
45
45
  export declare class ChannelStore {
46
46
  private workingDir;
47
47
  private recentlyLogged;
48
+ private cleanupTimer;
49
+ private writeChains;
48
50
  constructor(config: ChannelStoreConfig);
49
51
  /**
50
52
  * Get or create the directory for a channel/DM
@@ -71,5 +73,7 @@ export declare class ChannelStore {
71
73
  * Returns null if no log exists
72
74
  */
73
75
  getLastTimestamp(channelId: string): string | null;
76
+ private enqueueWrite;
77
+ private startCleanupTimer;
78
+ private cleanupExpiredEntries;
74
79
  }
75
- //# sourceMappingURL=store.d.ts.map
@@ -1,10 +1,14 @@
1
1
  import { closeSync, existsSync, mkdirSync, openSync, readSync, renameSync, statSync } from "fs";
2
2
  import { appendFile, writeFile } from "fs/promises";
3
3
  import { dirname, join } from "path";
4
+ const MAX_LOG_SIZE_BYTES = 1_000_000;
5
+ const DEDUPE_TTL_MS = 60_000;
6
+ const DEDUPE_CLEANUP_INTERVAL_MS = 30_000;
4
7
  export class ChannelStore {
5
8
  constructor(config) {
6
- // Track recently logged message timestamps to prevent duplicates
7
9
  this.recentlyLogged = new Map();
10
+ this.cleanupTimer = null;
11
+ this.writeChains = new Map();
8
12
  this.workingDir = config.workingDir;
9
13
  // Ensure working directory exists
10
14
  if (!existsSync(this.workingDir)) {
@@ -27,46 +31,50 @@ export class ChannelStore {
27
31
  * Returns false if message was already logged (duplicate)
28
32
  */
29
33
  async logMessage(channelId, message) {
30
- // Check for duplicate (same channel + timestamp)
31
34
  const dedupeKey = `${channelId}:${message.ts}`;
32
- if (this.recentlyLogged.has(dedupeKey)) {
33
- return false; // Already logged
35
+ const now = Date.now();
36
+ const previousLogTime = this.recentlyLogged.get(dedupeKey);
37
+ if (previousLogTime !== undefined) {
38
+ if (now - previousLogTime < DEDUPE_TTL_MS) {
39
+ return false;
40
+ }
41
+ this.recentlyLogged.delete(dedupeKey);
34
42
  }
35
- // Mark as logged and schedule cleanup after 60 seconds
36
- this.recentlyLogged.set(dedupeKey, Date.now());
37
- setTimeout(() => this.recentlyLogged.delete(dedupeKey), 60000);
43
+ this.recentlyLogged.set(dedupeKey, now);
44
+ this.startCleanupTimer();
38
45
  const logPath = join(this.getChannelDir(channelId), "log.jsonl");
39
- // Rotate if file exceeds size limit
40
- this.rotateIfNeeded(logPath);
41
- // Ensure message has a date field
42
46
  if (!message.date) {
43
47
  message.date = new Date().toISOString();
44
48
  }
45
- const line = `${JSON.stringify(message)}\n`;
46
- await appendFile(logPath, line, "utf-8");
49
+ await this.enqueueWrite(logPath, async () => {
50
+ await this.rotateIfNeeded(logPath);
51
+ const line = `${JSON.stringify(message)}\n`;
52
+ await appendFile(logPath, line, "utf-8");
53
+ });
47
54
  return true;
48
55
  }
49
56
  async logSubAgentRun(channelId, run) {
50
57
  const logPath = join(this.getChannelDir(channelId), "subagent-runs.jsonl");
51
- this.rotateIfNeeded(logPath);
52
- const line = `${JSON.stringify(run)}\n`;
53
- await appendFile(logPath, line, "utf-8");
58
+ await this.enqueueWrite(logPath, async () => {
59
+ await this.rotateIfNeeded(logPath);
60
+ const line = `${JSON.stringify(run)}\n`;
61
+ await appendFile(logPath, line, "utf-8");
62
+ });
54
63
  }
55
64
  /**
56
65
  * Rotate log file if it exceeds 1MB.
57
66
  * Keeps one backup (log.jsonl.1) and resets the sync offset.
58
67
  */
59
- rotateIfNeeded(logPath) {
68
+ async rotateIfNeeded(logPath) {
60
69
  try {
61
70
  if (!existsSync(logPath))
62
71
  return;
63
72
  const stats = statSync(logPath);
64
- if (stats.size > 1_000_000) {
73
+ if (stats.size > MAX_LOG_SIZE_BYTES) {
65
74
  renameSync(logPath, `${logPath}.1`);
66
- // Reset sync offset since log.jsonl was replaced
67
75
  const syncOffsetPath = join(dirname(logPath), ".sync-offset");
68
76
  try {
69
- writeFile(syncOffsetPath, "0", "utf-8").catch(() => { });
77
+ await writeFile(syncOffsetPath, "0", "utf-8");
70
78
  }
71
79
  catch {
72
80
  /* ignore */
@@ -152,5 +160,37 @@ export class ChannelStore {
152
160
  return null;
153
161
  }
154
162
  }
163
+ enqueueWrite(logPath, work) {
164
+ const previous = this.writeChains.get(logPath) ?? Promise.resolve();
165
+ const result = previous.catch(() => undefined).then(() => work());
166
+ const completion = result.then(() => undefined, () => undefined);
167
+ this.writeChains.set(logPath, completion);
168
+ completion.finally(() => {
169
+ if (this.writeChains.get(logPath) === completion) {
170
+ this.writeChains.delete(logPath);
171
+ }
172
+ });
173
+ return result;
174
+ }
175
+ startCleanupTimer() {
176
+ if (this.cleanupTimer) {
177
+ return;
178
+ }
179
+ this.cleanupTimer = setInterval(() => {
180
+ this.cleanupExpiredEntries();
181
+ }, DEDUPE_CLEANUP_INTERVAL_MS);
182
+ this.cleanupTimer.unref?.();
183
+ }
184
+ cleanupExpiredEntries(now = Date.now()) {
185
+ const cutoff = now - DEDUPE_TTL_MS;
186
+ for (const [key, loggedAt] of this.recentlyLogged) {
187
+ if (loggedAt <= cutoff) {
188
+ this.recentlyLogged.delete(key);
189
+ }
190
+ }
191
+ if (this.recentlyLogged.size === 0 && this.cleanupTimer) {
192
+ clearInterval(this.cleanupTimer);
193
+ this.cleanupTimer = null;
194
+ }
195
+ }
155
196
  }
156
- //# sourceMappingURL=store.js.map
package/dist/sandbox.d.ts CHANGED
@@ -32,4 +32,3 @@ export interface ExecResult {
32
32
  stderr: string;
33
33
  code: number;
34
34
  }
35
- //# sourceMappingURL=sandbox.d.ts.map
package/dist/sandbox.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { spawn } from "child_process";
2
- import { shellEscape } from "./shell-escape.js";
2
+ import { shellEscape } from "./shared/shell-escape.js";
3
3
  export function parseSandboxArg(value) {
4
4
  if (value === "host") {
5
5
  return { type: "host" };
@@ -219,4 +219,3 @@ function killProcessTree(pid) {
219
219
  }
220
220
  }
221
221
  }
222
- //# sourceMappingURL=sandbox.js.map
@@ -4,4 +4,3 @@ export declare class LlmJsonParseError extends Error {
4
4
  }
5
5
  export declare function extractJsonObject(text: string): string;
6
6
  export declare function parseJsonObject(text: string): unknown;
7
- //# sourceMappingURL=llm-json.d.ts.map
@@ -74,4 +74,3 @@ export function extractJsonObject(text) {
74
74
  export function parseJsonObject(text) {
75
75
  return JSON.parse(extractJsonObject(text));
76
76
  }
77
- //# sourceMappingURL=llm-json.js.map
@@ -0,0 +1,6 @@
1
+ export interface MarkdownSection {
2
+ heading: string;
3
+ content: string;
4
+ }
5
+ export declare function splitH1Sections(content: string): MarkdownSection[];
6
+ export declare function splitH2Sections(content: string): MarkdownSection[];
@@ -1,4 +1,4 @@
1
- export function splitLevelOneSections(content) {
1
+ function splitSectionsByHeading(content, headingPrefix) {
2
2
  const normalized = content.replace(/\r/g, "").trim();
3
3
  if (!normalized) {
4
4
  return [];
@@ -7,6 +7,7 @@ export function splitLevelOneSections(content) {
7
7
  const sections = [];
8
8
  let currentHeading = "";
9
9
  let currentLines = [];
10
+ const prefixLength = headingPrefix.length;
10
11
  const flush = () => {
11
12
  if (!currentHeading) {
12
13
  return;
@@ -18,9 +19,9 @@ export function splitLevelOneSections(content) {
18
19
  sections.push({ heading: currentHeading, content: sectionContent });
19
20
  };
20
21
  for (const line of lines) {
21
- if (line.startsWith("# ")) {
22
+ if (line.startsWith(headingPrefix)) {
22
23
  flush();
23
- currentHeading = line.slice(2).trim();
24
+ currentHeading = line.slice(prefixLength).trim();
24
25
  currentLines = [];
25
26
  continue;
26
27
  }
@@ -31,4 +32,9 @@ export function splitLevelOneSections(content) {
31
32
  flush();
32
33
  return sections;
33
34
  }
34
- //# sourceMappingURL=markdown-sections.js.map
35
+ export function splitH1Sections(content) {
36
+ return splitSectionsByHeading(content, "# ");
37
+ }
38
+ export function splitH2Sections(content) {
39
+ return splitSectionsByHeading(content, "## ");
40
+ }
@@ -3,4 +3,3 @@
3
3
  * Wraps in single quotes and escapes internal single quotes.
4
4
  */
5
5
  export declare function shellEscape(s: string): string;
6
- //# sourceMappingURL=shell-escape.d.ts.map
@@ -5,4 +5,3 @@
5
5
  export function shellEscape(s) {
6
6
  return `'${s.replace(/'/g, "'\\''")}'`;
7
7
  }
8
- //# sourceMappingURL=shell-escape.js.map
@@ -0,0 +1,9 @@
1
+ import type { AssistantMessage } from "@mariozechner/pi-ai";
2
+ export declare function clipText(text: string, maxChars: number, opts?: {
3
+ headRatio?: number;
4
+ omitHint?: string;
5
+ }): string;
6
+ export declare function truncate(text: string, maxLen: number): string;
7
+ export declare const HAN_REGEX: RegExp;
8
+ export declare function extractLabelFromArgs(args: unknown): string | null;
9
+ export declare function extractAssistantText(message: AssistantMessage): string;
@@ -0,0 +1,36 @@
1
+ export function clipText(text, maxChars, opts = {}) {
2
+ const normalized = text.replace(/\s+\n/g, "\n").replace(/\r/g, "").trim();
3
+ if (normalized.length <= maxChars) {
4
+ return normalized;
5
+ }
6
+ const headRatio = Math.max(0, Math.min(1, opts.headRatio ?? 0.45));
7
+ const omitHint = opts.omitHint ?? "[... omitted middle section ...]";
8
+ if (headRatio >= 1) {
9
+ const headChars = Math.max(0, maxChars - omitHint.length);
10
+ return `${normalized.slice(0, headChars).trimEnd()}${omitHint}`;
11
+ }
12
+ const headChars = Math.floor(maxChars * headRatio);
13
+ const tailChars = maxChars - headChars;
14
+ return `${normalized.slice(0, headChars)}\n\n${omitHint}\n\n${normalized.slice(-tailChars)}`;
15
+ }
16
+ export function truncate(text, maxLen) {
17
+ if (text.length <= maxLen) {
18
+ return text;
19
+ }
20
+ return `${text.substring(0, maxLen - 3)}...`;
21
+ }
22
+ export const HAN_REGEX = /\p{Script=Han}/u;
23
+ export function extractLabelFromArgs(args) {
24
+ if (!args || typeof args !== "object" || !("label" in args)) {
25
+ return null;
26
+ }
27
+ const label = args.label;
28
+ return typeof label === "string" && label.trim() ? label.trim() : null;
29
+ }
30
+ export function extractAssistantText(message) {
31
+ return message.content
32
+ .filter((part) => part.type === "text")
33
+ .map((part) => part.text)
34
+ .join("\n")
35
+ .trim();
36
+ }
@@ -0,0 +1,5 @@
1
+ import type { AgentMessage } from "@mariozechner/pi-agent-core";
2
+ import type { Message } from "@mariozechner/pi-ai";
3
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
4
+ export declare function isStandardAgentMessage(message: AgentMessage): message is Message;
5
+ export declare function buildStandardMessages(messages: AgentMessage[]): Message[];
@@ -0,0 +1,12 @@
1
+ export function isRecord(value) {
2
+ return typeof value === "object" && value !== null;
3
+ }
4
+ export function isStandardAgentMessage(message) {
5
+ return (typeof message === "object" &&
6
+ message !== null &&
7
+ "role" in message &&
8
+ (message.role === "user" || message.role === "assistant" || message.role === "toolResult"));
9
+ }
10
+ export function buildStandardMessages(messages) {
11
+ return messages.filter(isStandardAgentMessage);
12
+ }
@@ -0,0 +1,14 @@
1
+ export interface UsageTotals {
2
+ input: number;
3
+ output: number;
4
+ cacheRead: number;
5
+ cacheWrite: number;
6
+ total: number;
7
+ cost: {
8
+ input: number;
9
+ output: number;
10
+ cacheRead: number;
11
+ cacheWrite: number;
12
+ total: number;
13
+ };
14
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -24,4 +24,3 @@ export declare class SidecarParseError extends Error {
24
24
  constructor(taskName: string, rawText: string, cause: unknown);
25
25
  }
26
26
  export declare function runSidecarTask<T>(task: SidecarTask<T>): Promise<SidecarResult<T>>;
27
- //# sourceMappingURL=sidecar-worker.d.ts.map
@@ -1,5 +1,6 @@
1
1
  import { Agent } from "@mariozechner/pi-agent-core";
2
2
  import { convertToLlm } from "@mariozechner/pi-coding-agent";
3
+ import { extractAssistantText } from "./shared/text-utils.js";
3
4
  export class SidecarTimeoutError extends Error {
4
5
  constructor(taskName, timeoutMs) {
5
6
  super(`Sidecar task "${taskName}" timed out after ${timeoutMs}ms`);
@@ -17,13 +18,6 @@ export class SidecarParseError extends Error {
17
18
  this.cause = cause;
18
19
  }
19
20
  }
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
21
  export async function runSidecarTask(task) {
28
22
  const apiKey = await task.resolveApiKey(task.model);
29
23
  const worker = new Agent({
@@ -102,4 +96,3 @@ export async function runSidecarTask(task) {
102
96
  removeAbortListener();
103
97
  }
104
98
  }
105
- //# sourceMappingURL=sidecar-worker.js.map
@@ -58,4 +58,3 @@ export declare function resolveSubAgentConfig(availableModels: Model<Api>[], cur
58
58
  };
59
59
  export declare function formatSubAgentList(agents: SubAgentConfig[], maxItems?: number): string;
60
60
  export {};
61
- //# sourceMappingURL=sub-agents.d.ts.map
@@ -1,8 +1,8 @@
1
1
  import { parseFrontmatter } from "@mariozechner/pi-coding-agent";
2
2
  import { existsSync, readdirSync, readFileSync } from "fs";
3
3
  import { join } from "path";
4
- import { findExactModelReferenceMatch, formatModelReference } from "./model-utils.js";
5
- import { SUB_AGENTS_DIR_NAME } from "./paths.js";
4
+ import { findExactModelReferenceMatch, formatModelReference } from "../model-utils.js";
5
+ import { SUB_AGENTS_DIR_NAME } from "../paths.js";
6
6
  const ALLOWED_SUB_AGENT_TOOLS = ["read", "bash", "edit", "write"];
7
7
  const DEFAULT_SUB_AGENT_TOOLS = ["read", "bash"];
8
8
  const DEFAULT_MAX_TURNS = 24;
@@ -366,4 +366,3 @@ export function formatSubAgentList(agents, maxItems = 12) {
366
366
  }
367
367
  return `${listed.join("\n")}\n- ... and ${agents.length - maxItems} more`;
368
368
  }
369
- //# sourceMappingURL=sub-agents.js.map
@@ -2,7 +2,8 @@ import { type AgentEvent, type AgentMessage, type AgentTool } from "@mariozechne
2
2
  import type { Api, Model } from "@mariozechner/pi-ai";
3
3
  import type { PipiclawMemoryRecallSettings } from "../context.js";
4
4
  import type { Executor } from "../sandbox.js";
5
- import { type ResolvedSubAgentConfig, type SubAgentDiscoveryResult } from "../sub-agents.js";
5
+ import type { UsageTotals } from "../shared/types.js";
6
+ import { type ResolvedSubAgentConfig, type SubAgentDiscoveryResult } from "./discovery.js";
6
7
  declare const subagentSchema: import("@sinclair/typebox").TObject<{
7
8
  label: import("@sinclair/typebox").TString;
8
9
  agent: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
@@ -19,20 +20,6 @@ declare const subagentSchema: import("@sinclair/typebox").TObject<{
19
20
  memory: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
20
21
  paths: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TArray<import("@sinclair/typebox").TString>>;
21
22
  }>;
22
- interface UsageTotals {
23
- input: number;
24
- output: number;
25
- cacheRead: number;
26
- cacheWrite: number;
27
- total: number;
28
- cost: {
29
- input: number;
30
- output: number;
31
- cacheRead: number;
32
- cacheWrite: number;
33
- total: number;
34
- };
35
- }
36
23
  export interface SubAgentToolDetails {
37
24
  kind: "subagent";
38
25
  agent: string;
@@ -77,4 +64,3 @@ interface SubAgentWorker {
77
64
  }
78
65
  export declare function createSubAgentTool(options: SubAgentToolOptions): AgentTool<typeof subagentSchema, SubAgentToolDetails>;
79
66
  export {};
80
- //# sourceMappingURL=subagent.d.ts.map