@oyasmi/pipiclaw 0.6.1 → 0.6.2

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.
@@ -1,3 +1,4 @@
1
+ import { randomUUID } from "crypto";
1
2
  import { existsSync, mkdirSync, writeFileSync } from "fs";
2
3
  import { mkdir, readFile, rename, writeFile } from "fs/promises";
3
4
  import { dirname, join } from "path";
@@ -62,7 +63,7 @@ function normalizeContent(content) {
62
63
  }
63
64
  async function writeAtomically(path, content) {
64
65
  await mkdir(dirname(path), { recursive: true });
65
- const tempPath = `${path}.tmp`;
66
+ const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
66
67
  await writeFile(tempPath, content, "utf-8");
67
68
  await rename(tempPath, path);
68
69
  }
@@ -14,7 +14,7 @@ export interface MemoryLifecycleOptions {
14
14
  }
15
15
  export declare class MemoryLifecycle {
16
16
  private options;
17
- private backgroundQueue;
17
+ private durableMemoryQueue;
18
18
  private sessionRefreshQueue;
19
19
  private turnsSinceSessionUpdate;
20
20
  private toolCallsSinceSessionUpdate;
@@ -39,12 +39,14 @@ export declare class MemoryLifecycle {
39
39
  private refreshSessionMemory;
40
40
  private runSessionRefreshSerial;
41
41
  private requestThresholdSessionRefresh;
42
- private enqueueBackgroundJob;
42
+ private runDurableMemoryJobSerial;
43
+ private enqueueDurableMemoryJob;
43
44
  private hasPendingAssistantSnapshot;
44
45
  private markDurableConsolidationCheckpoint;
45
46
  private logConsolidationResult;
46
47
  private scheduleIdleConsolidation;
47
48
  private runPreflightConsolidation;
49
+ private runPreflightConsolidationNow;
48
50
  private handleSessionBeforeCompact;
49
51
  private handleSessionCompact;
50
52
  private handleSessionBeforeSwitch;
@@ -5,7 +5,7 @@ const IDLE_CONSOLIDATION_DELAY_MS = 60_000;
5
5
  export class MemoryLifecycle {
6
6
  constructor(options) {
7
7
  this.options = options;
8
- this.backgroundQueue = Promise.resolve();
8
+ this.durableMemoryQueue = Promise.resolve();
9
9
  this.sessionRefreshQueue = Promise.resolve();
10
10
  this.turnsSinceSessionUpdate = 0;
11
11
  this.toolCallsSinceSessionUpdate = 0;
@@ -75,15 +75,16 @@ export class MemoryLifecycle {
75
75
  }
76
76
  async flushForShutdown() {
77
77
  this.clearIdleConsolidationTimer();
78
- const run = async () => {
78
+ await this.runDurableMemoryJobSerial(async () => {
79
79
  if (!this.hasPendingAssistantSnapshot()) {
80
80
  return;
81
81
  }
82
- await this.runPreflightConsolidation("shutdown");
83
- };
84
- const resultPromise = this.backgroundQueue.then(run, run);
85
- this.backgroundQueue = resultPromise.then(() => undefined, () => undefined);
86
- await resultPromise;
82
+ const messageSnapshot = [...this.options.getMessages()];
83
+ const sessionEntrySnapshot = [...this.options.getSessionEntries()];
84
+ const revisionSnapshot = this.durableRevision;
85
+ const settings = this.options.getSessionMemorySettings();
86
+ await this.runPreflightConsolidationNow("shutdown", messageSnapshot, sessionEntrySnapshot, revisionSnapshot, settings);
87
+ });
87
88
  }
88
89
  clearIdleConsolidationTimer() {
89
90
  if (!this.idleConsolidationTimer) {
@@ -152,8 +153,13 @@ export class MemoryLifecycle {
152
153
  this.thresholdRefreshQueued = false;
153
154
  });
154
155
  }
155
- enqueueBackgroundJob(job, failureMessage) {
156
- this.backgroundQueue = this.backgroundQueue.then(job).catch((error) => {
156
+ runDurableMemoryJobSerial(job) {
157
+ const resultPromise = this.durableMemoryQueue.then(job, job);
158
+ this.durableMemoryQueue = resultPromise.then(() => undefined, () => undefined);
159
+ return resultPromise;
160
+ }
161
+ enqueueDurableMemoryJob(job, failureMessage) {
162
+ void this.runDurableMemoryJobSerial(job).catch((error) => {
157
163
  const message = error instanceof Error ? error.message : String(error);
158
164
  log.logWarning(failureMessage, message);
159
165
  });
@@ -186,7 +192,7 @@ export class MemoryLifecycle {
186
192
  const messageSnapshot = [...this.options.getMessages()];
187
193
  const sessionEntrySnapshot = [...this.options.getSessionEntries()];
188
194
  const revisionSnapshot = this.durableRevision;
189
- this.enqueueBackgroundJob(async () => {
195
+ this.enqueueDurableMemoryJob(async () => {
190
196
  try {
191
197
  log.logInfo(`[${this.options.channelId}] Memory consolidation starting (idle)`);
192
198
  const result = await runInlineConsolidation(this.buildRunOptions(messageSnapshot, sessionEntrySnapshot));
@@ -210,6 +216,11 @@ export class MemoryLifecycle {
210
216
  const sessionEntrySnapshot = sessionEntries ? [...sessionEntries] : [...this.options.getSessionEntries()];
211
217
  const revisionSnapshot = this.durableRevision;
212
218
  const settings = this.options.getSessionMemorySettings();
219
+ await this.runDurableMemoryJobSerial(async () => {
220
+ await this.runPreflightConsolidationNow(reason, messageSnapshot, sessionEntrySnapshot, revisionSnapshot, settings);
221
+ });
222
+ }
223
+ async runPreflightConsolidationNow(reason, messageSnapshot, sessionEntrySnapshot, revisionSnapshot = this.durableRevision, settings = this.options.getSessionMemorySettings()) {
213
224
  if (this.shouldForceRefreshFor(reason, settings)) {
214
225
  await this.runSessionRefreshSerial({
215
226
  reason,
@@ -246,7 +257,7 @@ export class MemoryLifecycle {
246
257
  this.enqueueBackgroundMaintenance();
247
258
  }
248
259
  enqueueBackgroundMaintenance() {
249
- this.enqueueBackgroundJob(async () => {
260
+ this.enqueueDurableMemoryJob(async () => {
250
261
  const result = await runBackgroundMaintenance(this.buildRunOptions([], []));
251
262
  this.logBackgroundResult(result);
252
263
  }, `[${this.options.channelId}] Background memory maintenance failed`);
@@ -1,4 +1,4 @@
1
- import { type SandboxConfig } from "../sandbox.js";
1
+ import { type Executor, type SandboxConfig } from "../sandbox.js";
2
2
  import { DingTalkBot, type DingTalkConfig, type DingTalkHandler } from "./dingtalk.js";
3
3
  import { ChannelStore } from "./store.js";
4
4
  export interface BootstrapPaths {
@@ -55,7 +55,7 @@ interface RuntimeContextOptions {
55
55
  sandbox: SandboxConfig;
56
56
  dingtalkConfig: DingTalkConfig;
57
57
  createBot?: (handler: DingTalkHandler, config: DingTalkConfig) => DingTalkBot;
58
- createEventsWatcher?: (workspaceDir: string, bot: DingTalkBot) => {
58
+ createEventsWatcher?: (workspaceDir: string, bot: DingTalkBot, executor: Executor) => {
59
59
  start(): void;
60
60
  stop(): void;
61
61
  };
@@ -6,7 +6,7 @@ import { resetRunner } from "../agent/runner-factory.js";
6
6
  import * as log from "../log.js";
7
7
  import { ensureChannelMemoryFilesSync } from "../memory/files.js";
8
8
  import { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_CONFIG_PATH, SECURITY_CONFIG_PATH, SETTINGS_CONFIG_PATH, TOOLS_CONFIG_PATH, WORKSPACE_DIR, } from "../paths.js";
9
- import { parseSandboxArg, validateSandbox } from "../sandbox.js";
9
+ import { createExecutor, parseSandboxArg, validateSandbox } from "../sandbox.js";
10
10
  import { loadSecurityConfigWithDiagnostics } from "../security/config.js";
11
11
  import { PipiclawSettingsManager } from "../settings.js";
12
12
  import { formatConfigDiagnostic } from "../shared/config-diagnostics.js";
@@ -450,9 +450,10 @@ export function createRuntimeContext(options) {
450
450
  const bot = options.createBot
451
451
  ? options.createBot(handler, options.dingtalkConfig)
452
452
  : new DingTalkBot(handler, options.dingtalkConfig);
453
+ const executor = createExecutor(options.sandbox);
453
454
  const eventsWatcher = options.createEventsWatcher
454
- ? options.createEventsWatcher(options.paths.workspaceDir, bot)
455
- : createEventsWatcher(options.paths.workspaceDir, bot, loadSecurityConfigWithDiagnostics(options.paths.appHomeDir).config.commandGuard);
455
+ ? options.createEventsWatcher(options.paths.workspaceDir, bot, executor)
456
+ : createEventsWatcher(options.paths.workspaceDir, bot, executor, loadSecurityConfigWithDiagnostics(options.paths.appHomeDir).config.commandGuard);
456
457
  const shutdownWithReason = async (reason = "manual") => {
457
458
  if (shutdownPromise) {
458
459
  return shutdownPromise;
@@ -1,3 +1,4 @@
1
+ import type { Executor } from "../sandbox.js";
1
2
  import type { SecurityConfig } from "../security/types.js";
2
3
  import type { DingTalkBot } from "./dingtalk.js";
3
4
  export interface EventAction {
@@ -30,6 +31,7 @@ export type ScheduledEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;
30
31
  export declare class EventsWatcher {
31
32
  private eventsDir;
32
33
  private bot;
34
+ private executor;
33
35
  private commandGuardConfig?;
34
36
  private timers;
35
37
  private crons;
@@ -37,7 +39,7 @@ export declare class EventsWatcher {
37
39
  private startTime;
38
40
  private watcher;
39
41
  private knownFiles;
40
- constructor(eventsDir: string, bot: DingTalkBot, commandGuardConfig?: SecurityConfig["commandGuard"] | undefined);
42
+ constructor(eventsDir: string, bot: DingTalkBot, executor: Executor, commandGuardConfig?: SecurityConfig["commandGuard"] | undefined);
41
43
  start(): void;
42
44
  stop(): void;
43
45
  private debounce;
@@ -62,4 +64,4 @@ export declare class EventsWatcher {
62
64
  /**
63
65
  * Create and start an events watcher.
64
66
  */
65
- export declare function createEventsWatcher(workspaceDir: string, bot: DingTalkBot, commandGuardConfig?: SecurityConfig["commandGuard"]): EventsWatcher;
67
+ export declare function createEventsWatcher(workspaceDir: string, bot: DingTalkBot, executor: Executor, commandGuardConfig?: SecurityConfig["commandGuard"]): EventsWatcher;
@@ -1,4 +1,3 @@
1
- import { exec } from "child_process";
2
1
  import { Cron } from "croner";
3
2
  import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch, writeFileSync } from "fs";
4
3
  import { readFile } from "fs/promises";
@@ -13,9 +12,10 @@ const MAX_RETRIES = 3;
13
12
  const RETRY_BASE_MS = 100;
14
13
  const MAX_TIMEOUT_MS = 2_147_483_647;
15
14
  export class EventsWatcher {
16
- constructor(eventsDir, bot, commandGuardConfig) {
15
+ constructor(eventsDir, bot, executor, commandGuardConfig) {
17
16
  this.eventsDir = eventsDir;
18
17
  this.bot = bot;
18
+ this.executor = executor;
19
19
  this.commandGuardConfig = commandGuardConfig;
20
20
  this.timers = new Map();
21
21
  this.crons = new Map();
@@ -314,24 +314,20 @@ export class EventsWatcher {
314
314
  }
315
315
  }
316
316
  }
317
- runPreAction(action, filename) {
317
+ async runPreAction(action, filename) {
318
318
  if (this.commandGuardConfig?.enabled) {
319
319
  const guardResult = guardCommand(action.command, this.commandGuardConfig);
320
320
  if (!guardResult.allowed) {
321
321
  log.logWarning(`Pre-action command blocked by guard for ${filename}: ${guardResult.reason}`);
322
- return Promise.reject(new Error(`guard: ${guardResult.reason}`));
322
+ throw new Error(`guard: ${guardResult.reason}`);
323
323
  }
324
324
  }
325
- return new Promise((resolve, reject) => {
326
- const child = exec(action.command, { timeout: action.timeout ?? 10_000 });
327
- child.on("close", (code) => {
328
- if (code === 0)
329
- resolve();
330
- else
331
- reject(new Error(`exit ${code}`));
332
- });
333
- child.on("error", reject);
334
- });
325
+ const timeoutMs = action.timeout ?? 10_000;
326
+ const timeoutSeconds = Math.max(1, Math.ceil(timeoutMs / 1000));
327
+ const result = await this.executor.exec(action.command, { timeout: timeoutSeconds });
328
+ if (result.code !== 0) {
329
+ throw new Error(`exit ${result.code}`);
330
+ }
335
331
  }
336
332
  deleteFile(filename) {
337
333
  const filePath = join(this.eventsDir, filename);
@@ -376,7 +372,7 @@ export class EventsWatcher {
376
372
  /**
377
373
  * Create and start an events watcher.
378
374
  */
379
- export function createEventsWatcher(workspaceDir, bot, commandGuardConfig) {
375
+ export function createEventsWatcher(workspaceDir, bot, executor, commandGuardConfig) {
380
376
  const eventsDir = join(workspaceDir, "events");
381
- return new EventsWatcher(eventsDir, bot, commandGuardConfig);
377
+ return new EventsWatcher(eventsDir, bot, executor, commandGuardConfig);
382
378
  }
package/dist/settings.js CHANGED
@@ -194,13 +194,13 @@ export class PipiclawSettingsManager {
194
194
  }
195
195
  // Compaction details
196
196
  getCompactionReserveTokens() {
197
- return DEFAULT_COMPACTION.reserveTokens;
197
+ return this.getCompactionSettings().reserveTokens;
198
198
  }
199
199
  getCompactionKeepRecentTokens() {
200
- return DEFAULT_COMPACTION.keepRecentTokens;
200
+ return this.getCompactionSettings().keepRecentTokens;
201
201
  }
202
202
  getBranchSummarySettings() {
203
- return { reserveTokens: 16384 };
203
+ return { reserveTokens: this.getCompactionSettings().reserveTokens };
204
204
  }
205
205
  getBranchSummarySkipPrompt() {
206
206
  return false;
@@ -28,6 +28,12 @@ const readSchema = Type.Object({
28
28
  offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
29
29
  limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
30
30
  });
31
+ function countTextLines(content) {
32
+ if (content.length === 0) {
33
+ return 0;
34
+ }
35
+ return content.endsWith("\n") ? content.split("\n").length - 1 : content.split("\n").length;
36
+ }
31
37
  function formatPathBlockMessage(resolvedPath, category, reason) {
32
38
  const lines = [`Path blocked${category ? ` [${category}]` : ""}`];
33
39
  if (reason) {
@@ -84,16 +90,17 @@ export function createReadTool(executor, options = {}) {
84
90
  };
85
91
  }
86
92
  // Get total line count first
87
- const countResult = await executor.exec(`wc -l < ${shellEscapePath(path)}`, { signal });
93
+ const countResult = await executor.exec(`awk 'END { print NR }' ${shellEscapePath(path)}`, { signal });
88
94
  if (countResult.code !== 0) {
89
95
  throw new Error(countResult.stderr || `Failed to read file: ${path}`);
90
96
  }
91
- const totalFileLines = Number.parseInt(countResult.stdout.trim(), 10) + 1; // wc -l counts newlines, not lines
97
+ const totalFileLines = Number.parseInt(countResult.stdout.trim(), 10);
92
98
  // Apply offset if specified (1-indexed)
93
99
  const startLine = offset ? Math.max(1, offset) : 1;
94
100
  const startLineDisplay = startLine;
95
101
  // Check if offset is out of bounds
96
- if (startLine > totalFileLines) {
102
+ if ((totalFileLines === 0 && offset !== undefined && startLine > 1) ||
103
+ (totalFileLines > 0 && startLine > totalFileLines)) {
97
104
  throw new Error(`Offset ${offset} is beyond end of file (${totalFileLines} lines total)`);
98
105
  }
99
106
  // Read content with offset
@@ -113,7 +120,7 @@ export function createReadTool(executor, options = {}) {
113
120
  // Apply user limit if specified
114
121
  if (limit !== undefined) {
115
122
  const lines = selectedContent.split("\n");
116
- const endLine = Math.min(limit, lines.length);
123
+ const endLine = Math.min(limit, countTextLines(selectedContent));
117
124
  selectedContent = lines.slice(0, endLine).join("\n");
118
125
  userLimitedLines = endLine;
119
126
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oyasmi/pipiclaw",
3
- "version": "0.6.1",
3
+ "version": "0.6.2",
4
4
  "description": "An AI assistant runtime for coding and team workflows, with DingTalk AI Cards, sub-agents, memory, and scheduled events.",
5
5
  "type": "module",
6
6
  "bin": {