@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.
- package/dist/memory/files.js +2 -1
- package/dist/memory/lifecycle.d.ts +4 -2
- package/dist/memory/lifecycle.js +22 -11
- package/dist/runtime/bootstrap.d.ts +2 -2
- package/dist/runtime/bootstrap.js +4 -3
- package/dist/runtime/events.d.ts +4 -2
- package/dist/runtime/events.js +12 -16
- package/dist/settings.js +3 -3
- package/dist/tools/read.js +11 -4
- package/package.json +1 -1
package/dist/memory/files.js
CHANGED
|
@@ -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
|
|
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
|
|
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;
|
package/dist/memory/lifecycle.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
78
|
+
await this.runDurableMemoryJobSerial(async () => {
|
|
79
79
|
if (!this.hasPendingAssistantSnapshot()) {
|
|
80
80
|
return;
|
|
81
81
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
156
|
-
|
|
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.
|
|
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.
|
|
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;
|
package/dist/runtime/events.d.ts
CHANGED
|
@@ -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;
|
package/dist/runtime/events.js
CHANGED
|
@@ -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
|
-
|
|
322
|
+
throw new Error(`guard: ${guardResult.reason}`);
|
|
323
323
|
}
|
|
324
324
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
197
|
+
return this.getCompactionSettings().reserveTokens;
|
|
198
198
|
}
|
|
199
199
|
getCompactionKeepRecentTokens() {
|
|
200
|
-
return
|
|
200
|
+
return this.getCompactionSettings().keepRecentTokens;
|
|
201
201
|
}
|
|
202
202
|
getBranchSummarySettings() {
|
|
203
|
-
return { reserveTokens:
|
|
203
|
+
return { reserveTokens: this.getCompactionSettings().reserveTokens };
|
|
204
204
|
}
|
|
205
205
|
getBranchSummarySkipPrompt() {
|
|
206
206
|
return false;
|
package/dist/tools/read.js
CHANGED
|
@@ -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(`
|
|
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)
|
|
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 >
|
|
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,
|
|
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