@oyasmi/pipiclaw 0.6.1 → 0.6.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.
- package/dist/log.js +25 -22
- 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 +18 -3
- package/dist/runtime/dingtalk.d.ts +6 -0
- package/dist/runtime/dingtalk.js +104 -7
- 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 +3 -6
package/dist/log.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
|
+
function color(style, text) {
|
|
3
|
+
return styleText(style, text);
|
|
4
|
+
}
|
|
2
5
|
function timestamp() {
|
|
3
6
|
const now = new Date();
|
|
4
7
|
const hh = String(now.getHours()).padStart(2, "0");
|
|
@@ -50,91 +53,91 @@ function formatToolArgs(args) {
|
|
|
50
53
|
}
|
|
51
54
|
// User messages
|
|
52
55
|
export function logUserMessage(ctx, text) {
|
|
53
|
-
console.log(
|
|
56
|
+
console.log(color("green", `${timestamp()} ${formatContext(ctx)} ${text}`));
|
|
54
57
|
}
|
|
55
58
|
// Tool execution
|
|
56
59
|
export function logToolStart(ctx, toolName, label, args) {
|
|
57
60
|
const formattedArgs = formatToolArgs(args);
|
|
58
|
-
console.log(
|
|
61
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} ↳ ${toolName}: ${label}`));
|
|
59
62
|
if (formattedArgs) {
|
|
60
63
|
const indented = formattedArgs
|
|
61
64
|
.split("\n")
|
|
62
65
|
.map((line) => ` ${line}`)
|
|
63
66
|
.join("\n");
|
|
64
|
-
console.log(
|
|
67
|
+
console.log(color("dim", indented));
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
70
|
export function logToolSuccess(ctx, toolName, durationMs, result) {
|
|
68
71
|
const duration = (durationMs / 1000).toFixed(1);
|
|
69
|
-
console.log(
|
|
72
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} ✓ ${toolName} (${duration}s)`));
|
|
70
73
|
const truncated = truncate(result, 1000);
|
|
71
74
|
if (truncated) {
|
|
72
75
|
const indented = truncated
|
|
73
76
|
.split("\n")
|
|
74
77
|
.map((line) => ` ${line}`)
|
|
75
78
|
.join("\n");
|
|
76
|
-
console.log(
|
|
79
|
+
console.log(color("dim", indented));
|
|
77
80
|
}
|
|
78
81
|
}
|
|
79
82
|
export function logToolError(ctx, toolName, durationMs, error) {
|
|
80
83
|
const duration = (durationMs / 1000).toFixed(1);
|
|
81
|
-
console.log(
|
|
84
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} ✗ ${toolName} (${duration}s)`));
|
|
82
85
|
const truncated = truncate(error, 1000);
|
|
83
86
|
const indented = truncated
|
|
84
87
|
.split("\n")
|
|
85
88
|
.map((line) => ` ${line}`)
|
|
86
89
|
.join("\n");
|
|
87
|
-
console.log(
|
|
90
|
+
console.log(color("dim", indented));
|
|
88
91
|
}
|
|
89
92
|
// Response streaming
|
|
90
93
|
export function logResponseStart(ctx) {
|
|
91
|
-
console.log(
|
|
94
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} → Streaming response...`));
|
|
92
95
|
}
|
|
93
96
|
export function logThinking(ctx, thinking) {
|
|
94
|
-
console.log(
|
|
97
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} 💭 Thinking`));
|
|
95
98
|
const truncated = truncate(thinking, 1000);
|
|
96
99
|
const indented = truncated
|
|
97
100
|
.split("\n")
|
|
98
101
|
.map((line) => ` ${line}`)
|
|
99
102
|
.join("\n");
|
|
100
|
-
console.log(
|
|
103
|
+
console.log(color("dim", indented));
|
|
101
104
|
}
|
|
102
105
|
export function logResponse(ctx, text) {
|
|
103
|
-
console.log(
|
|
106
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} 💬 Response`));
|
|
104
107
|
const truncated = truncate(text, 1000);
|
|
105
108
|
const indented = truncated
|
|
106
109
|
.split("\n")
|
|
107
110
|
.map((line) => ` ${line}`)
|
|
108
111
|
.join("\n");
|
|
109
|
-
console.log(
|
|
112
|
+
console.log(color("dim", indented));
|
|
110
113
|
}
|
|
111
114
|
// Control
|
|
112
115
|
export function logStopRequest(ctx) {
|
|
113
|
-
console.log(
|
|
114
|
-
console.log(
|
|
116
|
+
console.log(color("green", `${timestamp()} ${formatContext(ctx)} stop`));
|
|
117
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} ⊗ Stop requested - aborting`));
|
|
115
118
|
}
|
|
116
119
|
// System
|
|
117
120
|
export function logInfo(message) {
|
|
118
|
-
console.log(
|
|
121
|
+
console.log(color("blue", `${timestamp()} [system] ${message}`));
|
|
119
122
|
}
|
|
120
123
|
export function logWarning(message, details) {
|
|
121
|
-
console.log(
|
|
124
|
+
console.log(color("yellow", `${timestamp()} [system] ⚠ ${message}`));
|
|
122
125
|
if (details) {
|
|
123
126
|
const indented = details
|
|
124
127
|
.split("\n")
|
|
125
128
|
.map((line) => ` ${line}`)
|
|
126
129
|
.join("\n");
|
|
127
|
-
console.log(
|
|
130
|
+
console.log(color("dim", indented));
|
|
128
131
|
}
|
|
129
132
|
}
|
|
130
133
|
export function logAgentError(ctx, error) {
|
|
131
134
|
const context = ctx === "system" ? "[system]" : formatContext(ctx);
|
|
132
|
-
console.log(
|
|
135
|
+
console.log(color("yellow", `${timestamp()} ${context} ✗ Agent error`));
|
|
133
136
|
const indented = error
|
|
134
137
|
.split("\n")
|
|
135
138
|
.map((line) => ` ${line}`)
|
|
136
139
|
.join("\n");
|
|
137
|
-
console.log(
|
|
140
|
+
console.log(color("dim", indented));
|
|
138
141
|
}
|
|
139
142
|
// Usage summary
|
|
140
143
|
export function logUsageSummary(ctx, usage, contextTokens, contextWindow) {
|
|
@@ -164,8 +167,8 @@ export function logUsageSummary(ctx, usage, contextTokens, contextWindow) {
|
|
|
164
167
|
lines.push(`**Total: $${usage.cost.total.toFixed(4)}**`);
|
|
165
168
|
const summary = lines.join("\n");
|
|
166
169
|
// Log to console
|
|
167
|
-
console.log(
|
|
168
|
-
console.log(
|
|
170
|
+
console.log(color("yellow", `${timestamp()} ${formatContext(ctx)} 💰 Usage`));
|
|
171
|
+
console.log(color("dim", ` ${usage.input.toLocaleString()} in + ${usage.output.toLocaleString()} out` +
|
|
169
172
|
(usage.cacheRead > 0 || usage.cacheWrite > 0
|
|
170
173
|
? ` (${usage.cacheRead.toLocaleString()} cache read, ${usage.cacheWrite.toLocaleString()} cache write)`
|
|
171
174
|
: "") +
|
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";
|
|
@@ -160,6 +160,15 @@ export class BootstrapExitError extends Error {
|
|
|
160
160
|
export function isBootstrapExitError(error) {
|
|
161
161
|
return error instanceof BootstrapExitError;
|
|
162
162
|
}
|
|
163
|
+
function readCliVersion() {
|
|
164
|
+
try {
|
|
165
|
+
const raw = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf-8"));
|
|
166
|
+
return typeof raw.version === "string" && raw.version.trim() ? raw.version : "0.0.0";
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
return "0.0.0";
|
|
170
|
+
}
|
|
171
|
+
}
|
|
163
172
|
function writeTextFileIfMissing(path, content, label, created) {
|
|
164
173
|
if (existsSync(path)) {
|
|
165
174
|
return false;
|
|
@@ -283,11 +292,16 @@ export function parseArgs(argv, paths = DEFAULT_BOOTSTRAP_PATHS, io = console) {
|
|
|
283
292
|
io.log("Options:");
|
|
284
293
|
io.log(" --sandbox=host Run tools on host (default)");
|
|
285
294
|
io.log(" --sandbox=docker:<name> Run tools in Docker container");
|
|
295
|
+
io.log(" --version Print the current version and exit");
|
|
286
296
|
io.log("");
|
|
287
297
|
io.log(`Config: ${paths.channelConfigPath}`);
|
|
288
298
|
io.log(`Workspace: ${paths.workspaceDir}`);
|
|
289
299
|
throw new BootstrapExitError(0);
|
|
290
300
|
}
|
|
301
|
+
else if (arg === "--version") {
|
|
302
|
+
io.log(readCliVersion());
|
|
303
|
+
throw new BootstrapExitError(0);
|
|
304
|
+
}
|
|
291
305
|
}
|
|
292
306
|
return { sandbox };
|
|
293
307
|
}
|
|
@@ -450,9 +464,10 @@ export function createRuntimeContext(options) {
|
|
|
450
464
|
const bot = options.createBot
|
|
451
465
|
? options.createBot(handler, options.dingtalkConfig)
|
|
452
466
|
: new DingTalkBot(handler, options.dingtalkConfig);
|
|
467
|
+
const executor = createExecutor(options.sandbox);
|
|
453
468
|
const eventsWatcher = options.createEventsWatcher
|
|
454
|
-
? options.createEventsWatcher(options.paths.workspaceDir, bot)
|
|
455
|
-
: createEventsWatcher(options.paths.workspaceDir, bot, loadSecurityConfigWithDiagnostics(options.paths.appHomeDir).config.commandGuard);
|
|
469
|
+
? options.createEventsWatcher(options.paths.workspaceDir, bot, executor)
|
|
470
|
+
: createEventsWatcher(options.paths.workspaceDir, bot, executor, loadSecurityConfigWithDiagnostics(options.paths.appHomeDir).config.commandGuard);
|
|
456
471
|
const shutdownWithReason = async (reason = "manual") => {
|
|
457
472
|
if (shutdownPromise) {
|
|
458
473
|
return shutdownPromise;
|
|
@@ -78,7 +78,13 @@ export declare class DingTalkBot {
|
|
|
78
78
|
private clearKeepAliveTimer;
|
|
79
79
|
private clearReconnectTimer;
|
|
80
80
|
private clearAllTimers;
|
|
81
|
+
private sleep;
|
|
81
82
|
private waitForDelay;
|
|
83
|
+
private waitForSocketState;
|
|
84
|
+
private markClientDisconnected;
|
|
85
|
+
private clearClientSocketReference;
|
|
86
|
+
private cleanupSocket;
|
|
87
|
+
private connectWithTimeout;
|
|
82
88
|
private scheduleReconnect;
|
|
83
89
|
start(): Promise<void>;
|
|
84
90
|
private handleRawMessage;
|
package/dist/runtime/dingtalk.js
CHANGED
|
@@ -54,6 +54,13 @@ class ChannelQueue {
|
|
|
54
54
|
// ============================================================================
|
|
55
55
|
const DINGTALK_API = "https://api.dingtalk.com";
|
|
56
56
|
const TOKEN_REFRESH_SECS = 90 * 60; // 1.5 hours (tokens expire after 2 hours)
|
|
57
|
+
const CONNECT_ATTEMPT_TIMEOUT_MS = 10_000;
|
|
58
|
+
const SOCKET_CLOSE_GRACE_MS = 1_000;
|
|
59
|
+
const SOCKET_TERMINATE_GRACE_MS = 250;
|
|
60
|
+
const SOCKET_STATE_CONNECTING = 0;
|
|
61
|
+
const SOCKET_STATE_OPEN = 1;
|
|
62
|
+
const SOCKET_STATE_CLOSING = 2;
|
|
63
|
+
const SOCKET_STATE_CLOSED = 3;
|
|
57
64
|
// ============================================================================
|
|
58
65
|
// DingTalkBot
|
|
59
66
|
// ============================================================================
|
|
@@ -140,6 +147,12 @@ export class DingTalkBot {
|
|
|
140
147
|
this.clearKeepAliveTimer();
|
|
141
148
|
this.clearReconnectTimer();
|
|
142
149
|
}
|
|
150
|
+
async sleep(delayMs) {
|
|
151
|
+
await new Promise((resolve) => {
|
|
152
|
+
const timer = setTimeout(resolve, delayMs);
|
|
153
|
+
timer.unref?.();
|
|
154
|
+
});
|
|
155
|
+
}
|
|
143
156
|
async waitForDelay(delayMs) {
|
|
144
157
|
await new Promise((resolve) => {
|
|
145
158
|
this.reconnectTimer = this.setTrackedTimeout(() => {
|
|
@@ -148,6 +161,81 @@ export class DingTalkBot {
|
|
|
148
161
|
}, delayMs);
|
|
149
162
|
});
|
|
150
163
|
}
|
|
164
|
+
async waitForSocketState(socket, expectedState, timeoutMs) {
|
|
165
|
+
const deadline = Date.now() + timeoutMs;
|
|
166
|
+
while ((socket.readyState ?? SOCKET_STATE_CLOSED) !== expectedState && Date.now() < deadline) {
|
|
167
|
+
await this.sleep(25);
|
|
168
|
+
}
|
|
169
|
+
return (socket.readyState ?? SOCKET_STATE_CLOSED) === expectedState;
|
|
170
|
+
}
|
|
171
|
+
markClientDisconnected() {
|
|
172
|
+
if (!this.client) {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
Reflect.set(this.client, "connected", false);
|
|
176
|
+
Reflect.set(this.client, "registered", false);
|
|
177
|
+
Reflect.set(this.client, "reconnecting", false);
|
|
178
|
+
}
|
|
179
|
+
clearClientSocketReference() {
|
|
180
|
+
if (!this.client) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
Reflect.set(this.client, "socket", undefined);
|
|
184
|
+
}
|
|
185
|
+
async cleanupSocket(reason) {
|
|
186
|
+
const socket = this.getSocket();
|
|
187
|
+
this.markClientDisconnected();
|
|
188
|
+
if (!socket) {
|
|
189
|
+
this.clearClientSocketReference();
|
|
190
|
+
return;
|
|
191
|
+
}
|
|
192
|
+
socket.removeAllListeners?.();
|
|
193
|
+
if ((socket.readyState ?? SOCKET_STATE_CLOSED) !== SOCKET_STATE_CLOSED) {
|
|
194
|
+
try {
|
|
195
|
+
socket.close?.();
|
|
196
|
+
}
|
|
197
|
+
catch (err) {
|
|
198
|
+
log.logWarning(`DingTalk: socket close failed during ${reason}`, err instanceof Error ? err.message : String(err));
|
|
199
|
+
}
|
|
200
|
+
const closed = await this.waitForSocketState(socket, SOCKET_STATE_CLOSED, SOCKET_CLOSE_GRACE_MS);
|
|
201
|
+
if (!closed) {
|
|
202
|
+
log.logWarning(`DingTalk: forcing socket termination during ${reason}`);
|
|
203
|
+
try {
|
|
204
|
+
socket.terminate?.();
|
|
205
|
+
}
|
|
206
|
+
catch (err) {
|
|
207
|
+
log.logWarning(`DingTalk: socket terminate failed during ${reason}`, err instanceof Error ? err.message : String(err));
|
|
208
|
+
}
|
|
209
|
+
await this.waitForSocketState(socket, SOCKET_STATE_CLOSED, SOCKET_TERMINATE_GRACE_MS);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
this.clearClientSocketReference();
|
|
213
|
+
}
|
|
214
|
+
async connectWithTimeout() {
|
|
215
|
+
if (!this.client) {
|
|
216
|
+
throw new Error("DingTalk client is not initialized");
|
|
217
|
+
}
|
|
218
|
+
const connectPromise = Promise.resolve(this.client.connect());
|
|
219
|
+
let timeoutHandle = null;
|
|
220
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
221
|
+
timeoutHandle = setTimeout(() => {
|
|
222
|
+
reject(new Error(`connect timed out after ${CONNECT_ATTEMPT_TIMEOUT_MS}ms`));
|
|
223
|
+
}, CONNECT_ATTEMPT_TIMEOUT_MS);
|
|
224
|
+
timeoutHandle.unref?.();
|
|
225
|
+
});
|
|
226
|
+
try {
|
|
227
|
+
await Promise.race([connectPromise, timeoutPromise]);
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
if (timeoutHandle) {
|
|
231
|
+
clearTimeout(timeoutHandle);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const socket = this.getSocket();
|
|
235
|
+
if (!socket || socket.readyState !== SOCKET_STATE_OPEN) {
|
|
236
|
+
throw new Error("stream socket did not reach open state");
|
|
237
|
+
}
|
|
238
|
+
}
|
|
151
239
|
scheduleReconnect(delayMs, immediate) {
|
|
152
240
|
if (this.isStopped) {
|
|
153
241
|
return;
|
|
@@ -173,11 +261,13 @@ export class DingTalkBot {
|
|
|
173
261
|
}
|
|
174
262
|
log.logInfo(`DingTalk: initializing stream (clientId=${this.config.clientId.substring(0, 8)}…)`);
|
|
175
263
|
this.clearAllTimers();
|
|
176
|
-
|
|
264
|
+
const clientOptions = {
|
|
177
265
|
clientId: this.config.clientId,
|
|
178
266
|
clientSecret: this.config.clientSecret,
|
|
267
|
+
autoReconnect: false,
|
|
179
268
|
keepAlive: false,
|
|
180
|
-
}
|
|
269
|
+
};
|
|
270
|
+
this.client = new DWClient(clientOptions);
|
|
181
271
|
this.client.registerCallbackListener(TOPIC_ROBOT, (msg) => {
|
|
182
272
|
return this.handleRawMessage(msg);
|
|
183
273
|
});
|
|
@@ -220,6 +310,8 @@ export class DingTalkBot {
|
|
|
220
310
|
this.isReconnecting = true;
|
|
221
311
|
let connectionFailed = false;
|
|
222
312
|
let connected = false;
|
|
313
|
+
this.clearReconnectTimer();
|
|
314
|
+
this.clearKeepAliveTimer();
|
|
223
315
|
if (!immediate && this.reconnectAttempts > 0) {
|
|
224
316
|
const delay = Math.min(1000 * 2 ** this.reconnectAttempts + Math.random() * 1000, 30000);
|
|
225
317
|
log.logInfo(`DingTalk: waiting ${Math.round(delay / 1000)}s before reconnecting...`);
|
|
@@ -231,10 +323,14 @@ export class DingTalkBot {
|
|
|
231
323
|
}
|
|
232
324
|
try {
|
|
233
325
|
const socket = this.getSocket();
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
326
|
+
const readyState = socket?.readyState;
|
|
327
|
+
if (readyState === SOCKET_STATE_CONNECTING ||
|
|
328
|
+
readyState === SOCKET_STATE_OPEN ||
|
|
329
|
+
readyState === SOCKET_STATE_CLOSING ||
|
|
330
|
+
readyState === SOCKET_STATE_CLOSED) {
|
|
331
|
+
await this.cleanupSocket("reconnect");
|
|
332
|
+
}
|
|
333
|
+
await this.connectWithTimeout();
|
|
238
334
|
this.lastSocketAvailableTime = Date.now();
|
|
239
335
|
this.reconnectAttempts = 0; // Success, reset backoff
|
|
240
336
|
log.logInfo("DingTalk: connected to stream.");
|
|
@@ -289,6 +385,7 @@ export class DingTalkBot {
|
|
|
289
385
|
});
|
|
290
386
|
}
|
|
291
387
|
catch (err) {
|
|
388
|
+
await this.cleanupSocket("reconnect failure");
|
|
292
389
|
this.reconnectAttempts++;
|
|
293
390
|
connectionFailed = true;
|
|
294
391
|
log.logWarning("DingTalk: connection failed", err instanceof Error ? err.message : String(err));
|
|
@@ -311,7 +408,7 @@ export class DingTalkBot {
|
|
|
311
408
|
}
|
|
312
409
|
if (this.client) {
|
|
313
410
|
try {
|
|
314
|
-
await
|
|
411
|
+
await this.cleanupSocket("stop");
|
|
315
412
|
}
|
|
316
413
|
catch (err) {
|
|
317
414
|
log.logWarning("DingTalk: failed to disconnect cleanly", err instanceof Error ? err.message : String(err));
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oyasmi/pipiclaw",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
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": {
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
"LICENSE"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"clean": "
|
|
25
|
-
"build": "tsc -p tsconfig.build.json &&
|
|
24
|
+
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true }); require('node:fs').rmSync('coverage', { recursive: true, force: true });\"",
|
|
25
|
+
"build": "tsc -p tsconfig.build.json && node -e \"require('node:fs').chmodSync('dist/main.js', 0o755)\"",
|
|
26
26
|
"dev": "tsc -p tsconfig.build.json --watch --preserveWatchOutput",
|
|
27
27
|
"lint": "biome check .",
|
|
28
28
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"@mozilla/readability": "^0.6.0",
|
|
40
40
|
"@sinclair/typebox": "^0.34.0",
|
|
41
41
|
"axios": "^1.7.0",
|
|
42
|
-
"chalk": "^5.6.2",
|
|
43
42
|
"croner": "^9.1.0",
|
|
44
43
|
"diff": "^8.0.2",
|
|
45
44
|
"dingtalk-stream": "^2.1.4",
|
|
@@ -51,11 +50,9 @@
|
|
|
51
50
|
},
|
|
52
51
|
"devDependencies": {
|
|
53
52
|
"@biomejs/biome": "2.3.5",
|
|
54
|
-
"@types/diff": "^7.0.2",
|
|
55
53
|
"@types/jsdom": "^28.0.1",
|
|
56
54
|
"@types/node": "^24.3.0",
|
|
57
55
|
"@vitest/coverage-v8": "^3.2.4",
|
|
58
|
-
"shx": "^0.4.0",
|
|
59
56
|
"typescript": "^5.7.3",
|
|
60
57
|
"vitest": "^3.2.4"
|
|
61
58
|
},
|