@oyasmi/pipiclaw 0.6.0 → 0.6.1
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/agent/channel-runner.d.ts +3 -0
- package/dist/agent/channel-runner.js +75 -11
- package/dist/agent/context-budget.d.ts +9 -0
- package/dist/agent/context-budget.js +31 -0
- package/dist/agent/session-events.js +11 -4
- package/dist/agent/type-guards.js +4 -2
- package/dist/agent/types.d.ts +10 -3
- package/dist/agent/types.js +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/memory/candidates.d.ts +8 -5
- package/dist/memory/candidates.js +92 -42
- package/dist/memory/consolidation.js +2 -2
- package/dist/memory/recall.d.ts +2 -2
- package/dist/memory/recall.js +2 -3
- package/dist/runtime/bootstrap.js +2 -2
- package/dist/runtime/delivery.js +47 -9
- package/dist/runtime/dingtalk.d.ts +9 -1
- package/dist/runtime/dingtalk.js +55 -20
- package/dist/runtime/events.d.ts +14 -2
- package/dist/runtime/events.js +77 -14
- package/dist/subagents/tool.d.ts +2 -0
- package/dist/subagents/tool.js +2 -3
- package/dist/tools/index.d.ts +2 -0
- package/dist/tools/index.js +1 -0
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ export declare class ChannelRunner implements AgentRunner {
|
|
|
16
16
|
private readonly settingsManager;
|
|
17
17
|
private readonly modelRegistry;
|
|
18
18
|
private readonly memoryLifecycle;
|
|
19
|
+
private readonly memoryCandidateStore;
|
|
19
20
|
private readonly sessionResourceGate;
|
|
20
21
|
private readonly sessionReady;
|
|
21
22
|
private subAgentDiscovery;
|
|
@@ -42,7 +43,9 @@ export declare class ChannelRunner implements AgentRunner {
|
|
|
42
43
|
private refreshSessionResources;
|
|
43
44
|
private initializeSession;
|
|
44
45
|
private reloadSessionResources;
|
|
46
|
+
private bindSessionExtensions;
|
|
45
47
|
private ensureSessionReady;
|
|
48
|
+
private maybeRunPreventiveCompactionForIncomingText;
|
|
46
49
|
private refreshSubAgentDiscovery;
|
|
47
50
|
private reportSettingsDiagnostics;
|
|
48
51
|
private reportConfigDiagnostics;
|
|
@@ -4,7 +4,7 @@ import { mkdir, readFile, writeFile } from "fs/promises";
|
|
|
4
4
|
import { dirname, join, resolve } from "path";
|
|
5
5
|
import * as log from "../log.js";
|
|
6
6
|
import { buildFirstTurnMemoryBootstrap as renderFirstTurnMemoryBootstrap } from "../memory/bootstrap.js";
|
|
7
|
-
import {
|
|
7
|
+
import { createMemoryCandidateStore } from "../memory/candidates.js";
|
|
8
8
|
import { getChannelMemoryPath } from "../memory/files.js";
|
|
9
9
|
import { MemoryLifecycle } from "../memory/lifecycle.js";
|
|
10
10
|
import { recallRelevantMemory } from "../memory/recall.js";
|
|
@@ -22,6 +22,7 @@ import { loadToolsConfigWithDiagnostics } from "../tools/config.js";
|
|
|
22
22
|
import { createPipiclawTools } from "../tools/index.js";
|
|
23
23
|
import { createCommandExtension } from "./command-extension.js";
|
|
24
24
|
import { renderBuiltInHelp } from "./commands.js";
|
|
25
|
+
import { estimateIncomingMessageTokens, getPreventiveCompactionDecision } from "./context-budget.js";
|
|
25
26
|
import { clipUserInput } from "./progress-formatter.js";
|
|
26
27
|
import { buildAppendSystemPrompt } from "./prompt-builder.js";
|
|
27
28
|
import { createRunQueue } from "./run-queue.js";
|
|
@@ -68,6 +69,7 @@ export class ChannelRunner {
|
|
|
68
69
|
this.sessionManager = SessionManager.open(contextFile, channelDir);
|
|
69
70
|
this.settingsManager = new PipiclawSettingsManager(APP_HOME_DIR);
|
|
70
71
|
this.reportSettingsDiagnostics();
|
|
72
|
+
this.memoryCandidateStore = createMemoryCandidateStore();
|
|
71
73
|
// Create AuthStorage and ModelRegistry
|
|
72
74
|
const authStorage = AuthStorage.create(AUTH_CONFIG_PATH);
|
|
73
75
|
this.modelRegistry = createModelRegistry(authStorage, MODELS_CONFIG_PATH);
|
|
@@ -167,16 +169,17 @@ export class ChannelRunner {
|
|
|
167
169
|
this.runState.queue = runQueue.queue;
|
|
168
170
|
try {
|
|
169
171
|
await this.ensureSessionReady();
|
|
170
|
-
|
|
171
|
-
await mkdir(this.channelDir, { recursive: true });
|
|
172
|
-
const candidateCache = createMemoryCandidateCache();
|
|
172
|
+
this.memoryLifecycle.noteUserTurnStarted();
|
|
173
173
|
const clippedInput = clipUserInput(ctx.message.text, MAX_USER_MESSAGE_CHARS);
|
|
174
174
|
const userMessage = this.formatUserMessage(clippedInput, ctx.message.userName);
|
|
175
|
-
|
|
175
|
+
const preserveRawInput = this.shouldPreserveRawInput(ctx.message.text);
|
|
176
|
+
await this.maybeRunPreventiveCompactionForIncomingText(preserveRawInput ? clippedInput : userMessage);
|
|
177
|
+
// Ensure channel directory exists
|
|
178
|
+
await mkdir(this.channelDir, { recursive: true });
|
|
179
|
+
let promptText = preserveRawInput ? clippedInput : userMessage;
|
|
176
180
|
let recalledContextText = "";
|
|
177
181
|
let durableMemoryBootstrapText = "";
|
|
178
|
-
|
|
179
|
-
if (!this.shouldPreserveRawInput(ctx.message.text)) {
|
|
182
|
+
if (!preserveRawInput) {
|
|
180
183
|
const recallSettings = this.settingsManager.getMemoryRecallSettings();
|
|
181
184
|
if (recallSettings.enabled) {
|
|
182
185
|
const recall = await recallRelevantMemory({
|
|
@@ -190,7 +193,7 @@ export class ChannelRunner {
|
|
|
190
193
|
autoRerank: HAN_REGEX.test(clippedInput),
|
|
191
194
|
model: this.session.model ?? this.activeModel,
|
|
192
195
|
resolveApiKey: async (model) => getApiKeyForModel(this.modelRegistry, model),
|
|
193
|
-
|
|
196
|
+
candidateStore: this.memoryCandidateStore,
|
|
194
197
|
});
|
|
195
198
|
if (recall.renderedText) {
|
|
196
199
|
recalledContextText = recall.renderedText;
|
|
@@ -234,10 +237,20 @@ export class ChannelRunner {
|
|
|
234
237
|
this.runState.errorMessage &&
|
|
235
238
|
!this.runState.finalResponseDelivered) {
|
|
236
239
|
try {
|
|
237
|
-
const
|
|
240
|
+
const baseErrorSummary = this.runState.errorMessage.length > 240
|
|
238
241
|
? `${this.runState.errorMessage.slice(0, 237)}...`
|
|
239
242
|
: this.runState.errorMessage;
|
|
240
|
-
|
|
243
|
+
const compactionSummary = this.runState.lastCompactionError &&
|
|
244
|
+
this.runState.lastCompactionError !== this.runState.errorMessage
|
|
245
|
+
? this.runState.lastCompactionError.length > 240
|
|
246
|
+
? `${this.runState.lastCompactionError.slice(0, 237)}...`
|
|
247
|
+
: this.runState.lastCompactionError
|
|
248
|
+
: undefined;
|
|
249
|
+
const detailLines = [`\`${baseErrorSummary}\``];
|
|
250
|
+
if (compactionSummary) {
|
|
251
|
+
detailLines.push(`Recovery: \`${compactionSummary}\``);
|
|
252
|
+
}
|
|
253
|
+
await ctx.replaceMessage(`_Sorry, something went wrong._\n\n${detailLines.join("\n\n")}`);
|
|
241
254
|
}
|
|
242
255
|
catch (err) {
|
|
243
256
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
@@ -370,8 +383,10 @@ export class ChannelRunner {
|
|
|
370
383
|
if (clippedText !== text.trim()) {
|
|
371
384
|
log.logWarning(`[${this.channelId}] Queued message exceeded ${MAX_USER_MESSAGE_CHARS} chars and was clipped`);
|
|
372
385
|
}
|
|
386
|
+
const queuedMessage = this.formatUserMessage(clippedText, userName);
|
|
387
|
+
await this.maybeRunPreventiveCompactionForIncomingText(queuedMessage);
|
|
373
388
|
await this.sessionResourceGate.runPrompt(async () => {
|
|
374
|
-
await this.session.prompt(
|
|
389
|
+
await this.session.prompt(queuedMessage, {
|
|
375
390
|
streamingBehavior: delivery,
|
|
376
391
|
});
|
|
377
392
|
});
|
|
@@ -392,6 +407,7 @@ export class ChannelRunner {
|
|
|
392
407
|
}
|
|
393
408
|
async initializeSession() {
|
|
394
409
|
await this.reloadSessionResources();
|
|
410
|
+
await this.bindSessionExtensions();
|
|
395
411
|
}
|
|
396
412
|
async reloadSessionResources() {
|
|
397
413
|
this.settingsManager.reload();
|
|
@@ -402,9 +418,56 @@ export class ChannelRunner {
|
|
|
402
418
|
this.rebuildSessionTools();
|
|
403
419
|
await this.session.reload();
|
|
404
420
|
}
|
|
421
|
+
async bindSessionExtensions() {
|
|
422
|
+
await this.session.bindExtensions({
|
|
423
|
+
commandContextActions: {
|
|
424
|
+
waitForIdle: () => this.session.agent.waitForIdle(),
|
|
425
|
+
newSession: async (options) => {
|
|
426
|
+
const success = await this.session.newSession(options);
|
|
427
|
+
return { cancelled: !success };
|
|
428
|
+
},
|
|
429
|
+
fork: async (entryId) => {
|
|
430
|
+
const result = await this.session.fork(entryId);
|
|
431
|
+
return { cancelled: result.cancelled };
|
|
432
|
+
},
|
|
433
|
+
navigateTree: async (targetId, options) => {
|
|
434
|
+
const result = await this.session.navigateTree(targetId, options);
|
|
435
|
+
return { cancelled: result.cancelled };
|
|
436
|
+
},
|
|
437
|
+
switchSession: async (sessionPath) => {
|
|
438
|
+
const success = await this.session.switchSession(sessionPath);
|
|
439
|
+
return { cancelled: !success };
|
|
440
|
+
},
|
|
441
|
+
reload: async () => {
|
|
442
|
+
await this.refreshSessionResources();
|
|
443
|
+
},
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
}
|
|
405
447
|
async ensureSessionReady() {
|
|
406
448
|
await this.sessionReady;
|
|
407
449
|
}
|
|
450
|
+
async maybeRunPreventiveCompactionForIncomingText(incomingText) {
|
|
451
|
+
const currentModel = this.session.model ?? this.activeModel;
|
|
452
|
+
const contextUsage = this.session.getContextUsage();
|
|
453
|
+
const contextTokens = contextUsage?.tokens;
|
|
454
|
+
const incomingTokens = estimateIncomingMessageTokens(incomingText);
|
|
455
|
+
const decision = getPreventiveCompactionDecision(contextTokens, incomingTokens, currentModel.contextWindow);
|
|
456
|
+
if (!decision.shouldCompact) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const currentTokens = contextTokens ?? 0;
|
|
460
|
+
const startedAt = Date.now();
|
|
461
|
+
log.logInfo(`[${this.channelId}] Preventive compaction triggered: projected ${decision.projectedTokens}/${currentModel.contextWindow} tokens (current=${currentTokens}, incoming≈${incomingTokens}), threshold=${decision.thresholdTokens}`);
|
|
462
|
+
try {
|
|
463
|
+
await this.session.compact();
|
|
464
|
+
log.logInfo(`[${this.channelId}] Preventive compaction complete in ${Date.now() - startedAt}ms`);
|
|
465
|
+
}
|
|
466
|
+
catch (error) {
|
|
467
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
468
|
+
log.logWarning(`[${this.channelId}] Preventive compaction failed`, message);
|
|
469
|
+
}
|
|
470
|
+
}
|
|
408
471
|
refreshSubAgentDiscovery() {
|
|
409
472
|
this.modelRegistry.refresh();
|
|
410
473
|
const discovery = discoverSubAgents(this.workspaceDir, this.modelRegistry.getAvailable());
|
|
@@ -439,6 +502,7 @@ export class ChannelRunner {
|
|
|
439
502
|
sandboxConfig: this.sandboxConfig,
|
|
440
503
|
getSubAgentDiscovery: () => this.subAgentDiscovery,
|
|
441
504
|
getMemoryRecallSettings: () => this.settingsManager.getMemoryRecallSettings(),
|
|
505
|
+
memoryCandidateStore: this.memoryCandidateStore,
|
|
442
506
|
securityConfig: securityLoad.config,
|
|
443
507
|
toolsConfig: toolsLoad.config,
|
|
444
508
|
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export declare const PREVENTIVE_COMPACTION_THRESHOLD_RATIO = 0.75;
|
|
2
|
+
export interface PreventiveCompactionDecision {
|
|
3
|
+
shouldCompact: boolean;
|
|
4
|
+
projectedTokens: number | null;
|
|
5
|
+
thresholdTokens: number;
|
|
6
|
+
ratio: number;
|
|
7
|
+
}
|
|
8
|
+
export declare function estimateIncomingMessageTokens(text: string): number;
|
|
9
|
+
export declare function getPreventiveCompactionDecision(contextTokens: number | null | undefined, incomingTokens: number, contextWindow: number, thresholdRatio?: number): PreventiveCompactionDecision;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export const PREVENTIVE_COMPACTION_THRESHOLD_RATIO = 0.75;
|
|
2
|
+
const ESTIMATED_CHARS_PER_TOKEN = 3;
|
|
3
|
+
export function estimateIncomingMessageTokens(text) {
|
|
4
|
+
if (!text) {
|
|
5
|
+
return 0;
|
|
6
|
+
}
|
|
7
|
+
return Math.ceil(text.length / ESTIMATED_CHARS_PER_TOKEN);
|
|
8
|
+
}
|
|
9
|
+
export function getPreventiveCompactionDecision(contextTokens, incomingTokens, contextWindow, thresholdRatio = PREVENTIVE_COMPACTION_THRESHOLD_RATIO) {
|
|
10
|
+
const normalizedContextWindow = Number.isFinite(contextWindow) ? Math.max(0, Math.floor(contextWindow)) : 0;
|
|
11
|
+
const normalizedIncomingTokens = Number.isFinite(incomingTokens) && incomingTokens > 0 ? Math.floor(incomingTokens) : 0;
|
|
12
|
+
const normalizedRatio = Number.isFinite(thresholdRatio) && thresholdRatio > 0
|
|
13
|
+
? Math.min(thresholdRatio, 1)
|
|
14
|
+
: PREVENTIVE_COMPACTION_THRESHOLD_RATIO;
|
|
15
|
+
const thresholdTokens = Math.floor(normalizedContextWindow * normalizedRatio);
|
|
16
|
+
if (contextTokens === null || contextTokens === undefined || !Number.isFinite(contextTokens) || contextTokens < 0) {
|
|
17
|
+
return {
|
|
18
|
+
shouldCompact: false,
|
|
19
|
+
projectedTokens: null,
|
|
20
|
+
thresholdTokens,
|
|
21
|
+
ratio: normalizedRatio,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const projectedTokens = Math.floor(contextTokens) + normalizedIncomingTokens;
|
|
25
|
+
return {
|
|
26
|
+
shouldCompact: normalizedContextWindow > 0 && projectedTokens >= thresholdTokens,
|
|
27
|
+
projectedTokens,
|
|
28
|
+
thresholdTokens,
|
|
29
|
+
ratio: normalizedRatio,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -195,16 +195,23 @@ export async function handleSessionEvent(event, context) {
|
|
|
195
195
|
return;
|
|
196
196
|
}
|
|
197
197
|
if (isAutoCompactionStartEvent(event)) {
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
const label = event.reason === "manual" ? "Compacting context..." : "Compacting context...";
|
|
199
|
+
log.logInfo(`Compaction started (reason: ${event.reason})`);
|
|
200
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("assistant", label), false), "compaction start");
|
|
200
201
|
return;
|
|
201
202
|
}
|
|
202
203
|
if (isAutoCompactionEndEvent(event)) {
|
|
203
204
|
if (event.result) {
|
|
204
|
-
|
|
205
|
+
runState.lastCompactionError = undefined;
|
|
206
|
+
log.logInfo(`Compaction complete: ${event.result.tokensBefore} tokens compacted`);
|
|
205
207
|
}
|
|
206
208
|
else if (event.aborted) {
|
|
207
|
-
log.logInfo("
|
|
209
|
+
log.logInfo("Compaction aborted");
|
|
210
|
+
}
|
|
211
|
+
else if (event.errorMessage) {
|
|
212
|
+
runState.lastCompactionError = event.errorMessage;
|
|
213
|
+
log.logWarning("Compaction failed", event.errorMessage);
|
|
214
|
+
queue.enqueue(() => ctx.respond(formatProgressEntry("error", truncate(event.errorMessage ?? "Compaction failed", 200)), false), "compaction error");
|
|
208
215
|
}
|
|
209
216
|
return;
|
|
210
217
|
}
|
|
@@ -65,10 +65,12 @@ export function isTurnEndEvent(value) {
|
|
|
65
65
|
return hasEventType(value, "turn_end") && "message" in value && Array.isArray(value.toolResults);
|
|
66
66
|
}
|
|
67
67
|
export function isAutoCompactionStartEvent(value) {
|
|
68
|
-
return hasEventType(value, "auto_compaction_start") && (value.reason === "threshold" || value.reason === "overflow")
|
|
68
|
+
return ((hasEventType(value, "auto_compaction_start") && (value.reason === "threshold" || value.reason === "overflow")) ||
|
|
69
|
+
(hasEventType(value, "compaction_start") &&
|
|
70
|
+
(value.reason === "threshold" || value.reason === "overflow" || value.reason === "manual")));
|
|
69
71
|
}
|
|
70
72
|
export function isAutoCompactionEndEvent(value) {
|
|
71
|
-
return hasEventType(value, "auto_compaction_end");
|
|
73
|
+
return hasEventType(value, "auto_compaction_end") || hasEventType(value, "compaction_end");
|
|
72
74
|
}
|
|
73
75
|
export function isAutoRetryStartEvent(value) {
|
|
74
76
|
return (hasEventType(value, "auto_retry_start") &&
|
package/dist/agent/types.d.ts
CHANGED
|
@@ -44,6 +44,7 @@ export interface RunState {
|
|
|
44
44
|
totalUsage: UsageTotals;
|
|
45
45
|
stopReason: string;
|
|
46
46
|
errorMessage: string | undefined;
|
|
47
|
+
lastCompactionError: string | undefined;
|
|
47
48
|
finalOutcome: FinalOutcome;
|
|
48
49
|
finalResponseDelivered: boolean;
|
|
49
50
|
}
|
|
@@ -118,11 +119,17 @@ export type SessionEvent = {
|
|
|
118
119
|
type: "auto_compaction_start";
|
|
119
120
|
reason: "threshold" | "overflow";
|
|
120
121
|
} | {
|
|
121
|
-
type: "
|
|
122
|
+
type: "compaction_start";
|
|
123
|
+
reason: "manual" | "threshold" | "overflow";
|
|
124
|
+
} | {
|
|
125
|
+
type: "auto_compaction_end" | "compaction_end";
|
|
126
|
+
reason?: "manual" | "threshold" | "overflow";
|
|
122
127
|
result?: {
|
|
123
128
|
tokensBefore: number;
|
|
124
129
|
};
|
|
125
130
|
aborted?: boolean;
|
|
131
|
+
errorMessage?: string;
|
|
132
|
+
willRetry?: boolean;
|
|
126
133
|
} | {
|
|
127
134
|
type: "auto_retry_start";
|
|
128
135
|
attempt: number;
|
|
@@ -149,10 +156,10 @@ export type TurnEndEvent = Extract<SessionEvent, {
|
|
|
149
156
|
type: "turn_end";
|
|
150
157
|
}>;
|
|
151
158
|
export type AutoCompactionStartEvent = Extract<SessionEvent, {
|
|
152
|
-
type: "auto_compaction_start";
|
|
159
|
+
type: "auto_compaction_start" | "compaction_start";
|
|
153
160
|
}>;
|
|
154
161
|
export type AutoCompactionEndEvent = Extract<SessionEvent, {
|
|
155
|
-
type: "auto_compaction_end";
|
|
162
|
+
type: "auto_compaction_end" | "compaction_end";
|
|
156
163
|
}>;
|
|
157
164
|
export type AutoRetryStartEvent = Extract<SessionEvent, {
|
|
158
165
|
type: "auto_retry_start";
|
package/dist/agent/types.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export { type BuiltInCommand, type BuiltInCommandName, parseBuiltInCommand, rend
|
|
|
3
3
|
export { type AgentRunner, getOrCreateRunner } from "./agent/index.js";
|
|
4
4
|
export { type AppendSystemPromptOptions, buildAppendSystemPrompt } from "./agent/prompt-builder.js";
|
|
5
5
|
export { getAgentConfig, getSoul, loadPipiclawSkills, } from "./agent/workspace-resources.js";
|
|
6
|
-
export { type BuildMemoryCandidatesOptions, buildMemoryCandidates, type MemoryCandidate, } from "./memory/candidates.js";
|
|
6
|
+
export { type BuildMemoryCandidatesOptions, buildMemoryCandidates, createMemoryCandidateStore, type MemoryCandidate, type MemoryCandidateStore, } from "./memory/candidates.js";
|
|
7
7
|
export { type BackgroundMaintenanceResult, type ConsolidationRunOptions, type InlineConsolidationResult, runBackgroundMaintenance, runInlineConsolidation, } from "./memory/consolidation.js";
|
|
8
8
|
export { ensureChannelMemoryFiles, ensureChannelMemoryFilesSync, getChannelSessionPath, readChannelSession, rewriteChannelSession, } from "./memory/files.js";
|
|
9
9
|
export { type ConsolidationReason, MemoryLifecycle, type MemoryLifecycleOptions } from "./memory/lifecycle.js";
|
|
@@ -16,7 +16,7 @@ export { APP_HOME_DIR, APP_NAME, AUTH_CONFIG_PATH, CHANNEL_CONFIG_PATH, MODELS_C
|
|
|
16
16
|
export { ensureChannelDir, getChannelDir, getChannelDirName, } from "./runtime/channel-paths.js";
|
|
17
17
|
export { createDingTalkContext } from "./runtime/delivery.js";
|
|
18
18
|
export { type BusyMessageMode, DingTalkBot, type DingTalkConfig, type DingTalkContext, type DingTalkEvent, type DingTalkHandler, } from "./runtime/dingtalk.js";
|
|
19
|
-
export { createEventsWatcher, EventsWatcher, type ImmediateEvent, type OneShotEvent, type PeriodicEvent, type ScheduledEvent, } from "./runtime/events.js";
|
|
19
|
+
export { createEventsWatcher, type EventAction, EventsWatcher, type ImmediateEvent, type OneShotEvent, type PeriodicEvent, type ScheduledEvent, } from "./runtime/events.js";
|
|
20
20
|
export { ChannelStore, type LoggedMessage, type LoggedSubAgentRun } from "./runtime/store.js";
|
|
21
21
|
export { createExecutor, type ExecOptions, type ExecResult, type Executor, parseSandboxArg, type SandboxConfig, validateSandbox, } from "./sandbox.js";
|
|
22
22
|
export { type PipiclawMemoryRecallSettings, type PipiclawSessionMemorySettings, type PipiclawSettings, PipiclawSettingsManager, } from "./settings.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ export { parseBuiltInCommand, renderBuiltInHelp, } from "./agent/commands.js";
|
|
|
3
3
|
export { getOrCreateRunner } from "./agent/index.js";
|
|
4
4
|
export { buildAppendSystemPrompt } from "./agent/prompt-builder.js";
|
|
5
5
|
export { getAgentConfig, getSoul, loadPipiclawSkills, } from "./agent/workspace-resources.js";
|
|
6
|
-
export { buildMemoryCandidates, } from "./memory/candidates.js";
|
|
6
|
+
export { buildMemoryCandidates, createMemoryCandidateStore, } from "./memory/candidates.js";
|
|
7
7
|
export { runBackgroundMaintenance, runInlineConsolidation, } from "./memory/consolidation.js";
|
|
8
8
|
export { ensureChannelMemoryFiles, ensureChannelMemoryFilesSync, getChannelSessionPath, readChannelSession, rewriteChannelSession, } from "./memory/files.js";
|
|
9
9
|
export { MemoryLifecycle } from "./memory/lifecycle.js";
|
|
@@ -12,10 +12,13 @@ export interface MemoryCandidate {
|
|
|
12
12
|
export interface BuildMemoryCandidatesOptions {
|
|
13
13
|
workspaceDir: string;
|
|
14
14
|
channelDir: string;
|
|
15
|
-
cache?: MemoryCandidateCache;
|
|
16
15
|
}
|
|
17
|
-
export
|
|
18
|
-
|
|
16
|
+
export declare class MemoryCandidateStore {
|
|
17
|
+
private files;
|
|
18
|
+
private inflight;
|
|
19
|
+
invalidate(path?: string): void;
|
|
20
|
+
getCandidates(options: BuildMemoryCandidatesOptions): Promise<MemoryCandidate[]>;
|
|
21
|
+
private loadFileCandidates;
|
|
19
22
|
}
|
|
20
|
-
export declare function
|
|
21
|
-
export declare function buildMemoryCandidates(options: BuildMemoryCandidatesOptions): Promise<MemoryCandidate[]>;
|
|
23
|
+
export declare function createMemoryCandidateStore(): MemoryCandidateStore;
|
|
24
|
+
export declare function buildMemoryCandidates(options: BuildMemoryCandidatesOptions, store?: MemoryCandidateStore): Promise<MemoryCandidate[]>;
|
|
@@ -1,12 +1,7 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
1
|
+
import { readFile, stat } from "fs/promises";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { splitH1Sections, splitH2Sections } from "../shared/markdown-sections.js";
|
|
4
4
|
import { getChannelHistoryPath, getChannelMemoryPath, getChannelSessionPath } from "./files.js";
|
|
5
|
-
export function createMemoryCandidateCache() {
|
|
6
|
-
return {
|
|
7
|
-
entries: new Map(),
|
|
8
|
-
};
|
|
9
|
-
}
|
|
10
5
|
function normalizeContent(content) {
|
|
11
6
|
return content.replace(/\r/g, "").trim();
|
|
12
7
|
}
|
|
@@ -24,6 +19,9 @@ function slugify(value) {
|
|
|
24
19
|
.replace(/[^a-z0-9]+/g, "-")
|
|
25
20
|
.replace(/^-+|-+$/g, "") || "section");
|
|
26
21
|
}
|
|
22
|
+
function sameFingerprint(a, b) {
|
|
23
|
+
return a.exists === b.exists && a.mtimeMs === b.mtimeMs && a.ctimeMs === b.ctimeMs && a.size === b.size;
|
|
24
|
+
}
|
|
27
25
|
function inferPriority(source, title) {
|
|
28
26
|
const normalizedTitle = title.trim().toLowerCase();
|
|
29
27
|
if (source === "channel-session") {
|
|
@@ -76,9 +74,6 @@ function buildCandidate(source, path, title, content, timestamp, searchText) {
|
|
|
76
74
|
priority: inferPriority(source, title),
|
|
77
75
|
};
|
|
78
76
|
}
|
|
79
|
-
function buildCacheKey(options) {
|
|
80
|
-
return `${options.workspaceDir}\u0000${options.channelDir}`;
|
|
81
|
-
}
|
|
82
77
|
function buildWorkspaceOrChannelMemoryCandidates(source, path, content) {
|
|
83
78
|
const sections = splitH2Sections(content);
|
|
84
79
|
if (sections.length === 0 && content) {
|
|
@@ -98,41 +93,96 @@ function buildSessionCandidates(path, content) {
|
|
|
98
93
|
: `${sessionTitle.trim()}\n${section.content}`));
|
|
99
94
|
}
|
|
100
95
|
function buildHistoryCandidates(path, content) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
96
|
+
const sections = splitH2Sections(content).filter((section) => section.content.trim());
|
|
97
|
+
if (sections.length === 0) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
const foldedSections = sections.filter((section) => section.heading.startsWith("Folded History Through "));
|
|
101
|
+
const recentSectionLimit = 8;
|
|
102
|
+
const recentSections = sections.slice(-recentSectionLimit);
|
|
103
|
+
const selectedSections = Array.from(new Set([...foldedSections, ...recentSections]));
|
|
104
|
+
return selectedSections.map((section) => buildCandidate("channel-history", path, section.heading, section.content, section.heading));
|
|
104
105
|
}
|
|
105
|
-
async function
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
106
|
+
async function readFingerprint(path) {
|
|
107
|
+
try {
|
|
108
|
+
const stats = await stat(path);
|
|
109
|
+
return {
|
|
110
|
+
exists: true,
|
|
111
|
+
mtimeMs: stats.mtimeMs,
|
|
112
|
+
ctimeMs: stats.ctimeMs,
|
|
113
|
+
size: stats.size,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
return {
|
|
118
|
+
exists: false,
|
|
119
|
+
mtimeMs: 0,
|
|
120
|
+
ctimeMs: 0,
|
|
121
|
+
size: 0,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
122
124
|
}
|
|
123
|
-
export
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
export class MemoryCandidateStore {
|
|
126
|
+
constructor() {
|
|
127
|
+
this.files = new Map();
|
|
128
|
+
this.inflight = new Map();
|
|
129
|
+
}
|
|
130
|
+
invalidate(path) {
|
|
131
|
+
if (!path) {
|
|
132
|
+
this.files.clear();
|
|
133
|
+
this.inflight.clear();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
this.files.delete(path);
|
|
137
|
+
this.inflight.delete(path);
|
|
126
138
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
139
|
+
async getCandidates(options) {
|
|
140
|
+
const definitions = [
|
|
141
|
+
{
|
|
142
|
+
path: getChannelSessionPath(options.channelDir),
|
|
143
|
+
build: buildSessionCandidates,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
path: getChannelMemoryPath(options.channelDir),
|
|
147
|
+
build: (path, content) => buildWorkspaceOrChannelMemoryCandidates("channel-memory", path, content),
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
path: join(options.workspaceDir, "MEMORY.md"),
|
|
151
|
+
build: (path, content) => buildWorkspaceOrChannelMemoryCandidates("workspace-memory", path, content),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
path: getChannelHistoryPath(options.channelDir),
|
|
155
|
+
build: buildHistoryCandidates,
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
const candidateGroups = await Promise.all(definitions.map(async (definition) => this.loadFileCandidates(definition.path, definition.build)));
|
|
159
|
+
return candidateGroups.flat();
|
|
160
|
+
}
|
|
161
|
+
async loadFileCandidates(path, build) {
|
|
162
|
+
const pending = this.inflight.get(path);
|
|
163
|
+
if (pending) {
|
|
164
|
+
return pending;
|
|
165
|
+
}
|
|
166
|
+
const work = (async () => {
|
|
167
|
+
const fingerprint = await readFingerprint(path);
|
|
168
|
+
const cached = this.files.get(path);
|
|
169
|
+
if (cached && sameFingerprint(cached.fingerprint, fingerprint)) {
|
|
170
|
+
return cached.candidates;
|
|
171
|
+
}
|
|
172
|
+
const content = fingerprint.exists ? await readOptionalFile(path) : "";
|
|
173
|
+
const candidates = build(path, content);
|
|
174
|
+
this.files.set(path, { fingerprint, candidates });
|
|
175
|
+
return candidates;
|
|
176
|
+
})().finally(() => {
|
|
177
|
+
this.inflight.delete(path);
|
|
178
|
+
});
|
|
179
|
+
this.inflight.set(path, work);
|
|
180
|
+
return work;
|
|
131
181
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
return
|
|
182
|
+
}
|
|
183
|
+
export function createMemoryCandidateStore() {
|
|
184
|
+
return new MemoryCandidateStore();
|
|
185
|
+
}
|
|
186
|
+
export async function buildMemoryCandidates(options, store = createMemoryCandidateStore()) {
|
|
187
|
+
return store.getCandidates(options);
|
|
138
188
|
}
|
|
@@ -12,8 +12,8 @@ const HISTORY_LENGTH_THRESHOLD = 8_000;
|
|
|
12
12
|
const HISTORY_BLOCK_THRESHOLD = 5;
|
|
13
13
|
const HISTORY_RECENT_BLOCKS_TO_KEEP = 3;
|
|
14
14
|
const INLINE_CONSOLIDATION_TIMEOUT_MS = 20_000;
|
|
15
|
-
const MEMORY_CLEANUP_TIMEOUT_MS =
|
|
16
|
-
const HISTORY_FOLDING_TIMEOUT_MS =
|
|
15
|
+
const MEMORY_CLEANUP_TIMEOUT_MS = 120_000;
|
|
16
|
+
const HISTORY_FOLDING_TIMEOUT_MS = 120_000;
|
|
17
17
|
const INLINE_CONSOLIDATION_SYSTEM_PROMPT = `You are a runtime memory consolidation worker for Pipiclaw.
|
|
18
18
|
|
|
19
19
|
Return strict JSON only. Do not wrap in Markdown fences.
|
package/dist/memory/recall.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
2
|
-
import { type MemoryCandidate, type
|
|
2
|
+
import { type MemoryCandidate, type MemoryCandidateStore } from "./candidates.js";
|
|
3
3
|
export interface RecallRequest {
|
|
4
4
|
query: string;
|
|
5
5
|
workspaceDir: string;
|
|
@@ -12,7 +12,7 @@ export interface RecallRequest {
|
|
|
12
12
|
autoRerank?: boolean;
|
|
13
13
|
model: Model<Api>;
|
|
14
14
|
resolveApiKey: (model: Model<Api>) => Promise<string>;
|
|
15
|
-
|
|
15
|
+
candidateStore?: MemoryCandidateStore;
|
|
16
16
|
}
|
|
17
17
|
export interface RecalledMemory {
|
|
18
18
|
source: MemoryCandidate["source"];
|
package/dist/memory/recall.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { parseJsonObject } from "../shared/llm-json.js";
|
|
2
2
|
import { HAN_REGEX } from "../shared/text-utils.js";
|
|
3
|
-
import { buildMemoryCandidates } from "./candidates.js";
|
|
3
|
+
import { buildMemoryCandidates, createMemoryCandidateStore, } from "./candidates.js";
|
|
4
4
|
import { COMMON_CHINESE_WORDS } from "./chinese-words.js";
|
|
5
5
|
import { runSidecarTask } from "./sidecar-worker.js";
|
|
6
6
|
const RERANK_SYSTEM_PROMPT = `You are selecting which memory snippets are most relevant to the current user turn.
|
|
@@ -466,8 +466,7 @@ export async function recallRelevantMemory(request) {
|
|
|
466
466
|
const candidates = await buildMemoryCandidates({
|
|
467
467
|
workspaceDir: request.workspaceDir,
|
|
468
468
|
channelDir: request.channelDir,
|
|
469
|
-
|
|
470
|
-
});
|
|
469
|
+
}, request.candidateStore ?? createMemoryCandidateStore());
|
|
471
470
|
const filteredCandidates = request.allowedSources?.length
|
|
472
471
|
? candidates.filter((candidate) => request.allowedSources?.includes(candidate.source))
|
|
473
472
|
: candidates;
|
|
@@ -137,7 +137,7 @@ const SECURITY_CONFIG_TEMPLATE = {
|
|
|
137
137
|
},
|
|
138
138
|
};
|
|
139
139
|
const SHUTDOWN_WAIT_MS = 15000;
|
|
140
|
-
const SHUTDOWN_FLUSH_WAIT_MS =
|
|
140
|
+
const SHUTDOWN_FLUSH_WAIT_MS = 45000;
|
|
141
141
|
const SHUTDOWN_ABORT_WAIT_MS = 5000;
|
|
142
142
|
export const DEFAULT_BOOTSTRAP_PATHS = {
|
|
143
143
|
appName: APP_NAME,
|
|
@@ -452,7 +452,7 @@ export function createRuntimeContext(options) {
|
|
|
452
452
|
: new DingTalkBot(handler, options.dingtalkConfig);
|
|
453
453
|
const eventsWatcher = options.createEventsWatcher
|
|
454
454
|
? options.createEventsWatcher(options.paths.workspaceDir, bot)
|
|
455
|
-
: createEventsWatcher(options.paths.workspaceDir, bot);
|
|
455
|
+
: createEventsWatcher(options.paths.workspaceDir, bot, loadSecurityConfigWithDiagnostics(options.paths.appHomeDir).config.commandGuard);
|
|
456
456
|
const shutdownWithReason = async (reason = "manual") => {
|
|
457
457
|
if (shutdownPromise) {
|
|
458
458
|
return shutdownPromise;
|
package/dist/runtime/delivery.js
CHANGED
|
@@ -6,7 +6,9 @@ class ChannelDeliveryController {
|
|
|
6
6
|
this.event = event;
|
|
7
7
|
this.bot = bot;
|
|
8
8
|
this.store = store;
|
|
9
|
-
this.
|
|
9
|
+
this.progressSegments = [];
|
|
10
|
+
this.cachedProgressText = "";
|
|
11
|
+
this.progressTextDirty = false;
|
|
10
12
|
this.mode = "progress";
|
|
11
13
|
this.desiredRevision = 0;
|
|
12
14
|
this.appliedRevision = 0;
|
|
@@ -20,6 +22,9 @@ class ChannelDeliveryController {
|
|
|
20
22
|
this.timer = null;
|
|
21
23
|
this.cardWarmupTimer = null;
|
|
22
24
|
this.flushWaiters = [];
|
|
25
|
+
this.sentProgressChars = 0;
|
|
26
|
+
this.replayRequired = false;
|
|
27
|
+
this.finalReplacementText = "";
|
|
23
28
|
}
|
|
24
29
|
buildContext() {
|
|
25
30
|
return {
|
|
@@ -86,7 +91,11 @@ class ChannelDeliveryController {
|
|
|
86
91
|
if (this.closed || this.finalResponseDelivered || !text.trim())
|
|
87
92
|
return;
|
|
88
93
|
this.clearCardWarmup();
|
|
89
|
-
this.
|
|
94
|
+
if (this.progressSegments.length > 0) {
|
|
95
|
+
this.progressSegments.push("\n\n");
|
|
96
|
+
}
|
|
97
|
+
this.progressSegments.push(text);
|
|
98
|
+
this.progressTextDirty = true;
|
|
90
99
|
if (this.progressWindowStartedAt === 0) {
|
|
91
100
|
this.progressWindowStartedAt = Date.now();
|
|
92
101
|
}
|
|
@@ -116,7 +125,7 @@ class ChannelDeliveryController {
|
|
|
116
125
|
if (this.closed || this.finalResponseDelivered)
|
|
117
126
|
return;
|
|
118
127
|
this.clearCardWarmup();
|
|
119
|
-
this.
|
|
128
|
+
this.finalReplacementText = text;
|
|
120
129
|
this.mode = "finalize-with-fallback";
|
|
121
130
|
this.bumpRevision(true);
|
|
122
131
|
}
|
|
@@ -159,6 +168,7 @@ class ChannelDeliveryController {
|
|
|
159
168
|
try {
|
|
160
169
|
while (this.appliedRevision < this.desiredRevision) {
|
|
161
170
|
const mode = this.mode;
|
|
171
|
+
const progressText = this.getProgressText();
|
|
162
172
|
const throttleBaseAt = this.lastDeliveredAt > 0 ? this.lastDeliveredAt : this.progressWindowStartedAt;
|
|
163
173
|
if (mode === "progress" && throttleBaseAt > 0) {
|
|
164
174
|
const remaining = MIN_UPDATE_INTERVAL_MS - (Date.now() - throttleBaseAt);
|
|
@@ -171,31 +181,48 @@ class ChannelDeliveryController {
|
|
|
171
181
|
}
|
|
172
182
|
}
|
|
173
183
|
const revision = this.desiredRevision;
|
|
174
|
-
const content =
|
|
184
|
+
const content = progressText.trim();
|
|
185
|
+
const replacementText = this.finalReplacementText;
|
|
175
186
|
let touchedRemote = false;
|
|
176
187
|
try {
|
|
177
188
|
if (mode === "progress") {
|
|
178
189
|
if (content) {
|
|
179
|
-
|
|
190
|
+
const nextSentChars = progressText.length;
|
|
191
|
+
if (this.replayRequired) {
|
|
192
|
+
touchedRemote = await this.bot.replaceCard(this.event.channelId, progressText);
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
const delta = progressText.slice(this.sentProgressChars);
|
|
196
|
+
touchedRemote = delta ? await this.bot.appendToCard(this.event.channelId, delta) : true;
|
|
197
|
+
}
|
|
180
198
|
if (!touchedRemote) {
|
|
181
199
|
this.bot.discardCard(this.event.channelId);
|
|
200
|
+
this.replayRequired = true;
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
this.sentProgressChars = nextSentChars;
|
|
204
|
+
this.replayRequired = false;
|
|
182
205
|
}
|
|
183
206
|
}
|
|
184
207
|
}
|
|
185
208
|
else if (mode === "finalize-existing") {
|
|
186
209
|
if (content || this.cardWarmupTriggered) {
|
|
187
|
-
touchedRemote = await this.bot.
|
|
210
|
+
touchedRemote = await this.bot.replaceCard(this.event.channelId, content ? progressText : NO_CONTENT, true);
|
|
188
211
|
if (!touchedRemote) {
|
|
189
212
|
this.bot.discardCard(this.event.channelId);
|
|
190
213
|
}
|
|
214
|
+
else {
|
|
215
|
+
this.sentProgressChars = progressText.length;
|
|
216
|
+
this.replayRequired = false;
|
|
217
|
+
}
|
|
191
218
|
}
|
|
192
219
|
else {
|
|
193
220
|
this.bot.discardCard(this.event.channelId);
|
|
194
221
|
}
|
|
195
222
|
}
|
|
196
223
|
else if (mode === "finalize-with-fallback") {
|
|
197
|
-
if (
|
|
198
|
-
touchedRemote = await this.bot.finalizeCard(this.event.channelId,
|
|
224
|
+
if (replacementText.trim()) {
|
|
225
|
+
touchedRemote = await this.bot.finalizeCard(this.event.channelId, replacementText);
|
|
199
226
|
if (!touchedRemote) {
|
|
200
227
|
this.bot.discardCard(this.event.channelId);
|
|
201
228
|
}
|
|
@@ -206,7 +233,7 @@ class ChannelDeliveryController {
|
|
|
206
233
|
}
|
|
207
234
|
else if (mode === "silent") {
|
|
208
235
|
if (this.cardWarmupTriggered) {
|
|
209
|
-
touchedRemote = await this.bot.
|
|
236
|
+
touchedRemote = await this.bot.replaceCard(this.event.channelId, NO_CONTENT, true);
|
|
210
237
|
}
|
|
211
238
|
if (!touchedRemote) {
|
|
212
239
|
this.bot.discardCard(this.event.channelId);
|
|
@@ -216,6 +243,9 @@ class ChannelDeliveryController {
|
|
|
216
243
|
catch (err) {
|
|
217
244
|
log.logWarning(`[${this.event.channelId}] Delivery sync failed`, err instanceof Error ? err.message : String(err));
|
|
218
245
|
this.bot.discardCard(this.event.channelId);
|
|
246
|
+
if (mode === "progress") {
|
|
247
|
+
this.replayRequired = true;
|
|
248
|
+
}
|
|
219
249
|
}
|
|
220
250
|
if (touchedRemote) {
|
|
221
251
|
this.lastDeliveredAt = Date.now();
|
|
@@ -262,6 +292,14 @@ class ChannelDeliveryController {
|
|
|
262
292
|
this.clearCardWarmup();
|
|
263
293
|
await this.flush();
|
|
264
294
|
}
|
|
295
|
+
getProgressText() {
|
|
296
|
+
if (!this.progressTextDirty) {
|
|
297
|
+
return this.cachedProgressText;
|
|
298
|
+
}
|
|
299
|
+
this.cachedProgressText = this.progressSegments.join("");
|
|
300
|
+
this.progressTextDirty = false;
|
|
301
|
+
return this.cachedProgressText;
|
|
302
|
+
}
|
|
265
303
|
}
|
|
266
304
|
export function createDingTalkContext(event, bot, store) {
|
|
267
305
|
return new ChannelDeliveryController(event, bot, store).buildContext();
|
|
@@ -94,7 +94,15 @@ export declare class DingTalkBot {
|
|
|
94
94
|
*/
|
|
95
95
|
ensureCard(channelId: string): Promise<void>;
|
|
96
96
|
/**
|
|
97
|
-
*
|
|
97
|
+
* Replace the active card content with a full snapshot.
|
|
98
|
+
*/
|
|
99
|
+
replaceCard(channelId: string, content: string, finalize?: boolean, failed?: boolean): Promise<boolean>;
|
|
100
|
+
/**
|
|
101
|
+
* Append a delta to the active card transcript.
|
|
102
|
+
*/
|
|
103
|
+
appendToCard(channelId: string, content: string, finalize?: boolean, failed?: boolean): Promise<boolean>;
|
|
104
|
+
/**
|
|
105
|
+
* Stream content to the active AI Card for a channel using full replacement semantics.
|
|
98
106
|
*/
|
|
99
107
|
streamToCard(channelId: string, content: string, finalize?: boolean): Promise<boolean>;
|
|
100
108
|
/**
|
package/dist/runtime/dingtalk.js
CHANGED
|
@@ -362,48 +362,80 @@ export class DingTalkBot {
|
|
|
362
362
|
await this.createCard(channelId);
|
|
363
363
|
}
|
|
364
364
|
/**
|
|
365
|
-
*
|
|
365
|
+
* Replace the active card content with a full snapshot.
|
|
366
366
|
*/
|
|
367
|
-
async
|
|
367
|
+
async replaceCard(channelId, content, finalize = false, failed = false) {
|
|
368
368
|
let card = this.activeCards.get(channelId);
|
|
369
|
-
if ((!card || card.finished) &&
|
|
369
|
+
if ((!card || card.finished) && this.config.cardTemplateId && (content.trim() || !finalize || failed)) {
|
|
370
370
|
await this.ensureCard(channelId);
|
|
371
371
|
card = this.activeCards.get(channelId);
|
|
372
372
|
}
|
|
373
373
|
if (!card || card.finished) {
|
|
374
|
-
if (finalize) {
|
|
374
|
+
if (finalize && content.trim()) {
|
|
375
375
|
return this.sendPlain(channelId, content);
|
|
376
376
|
}
|
|
377
377
|
return false;
|
|
378
378
|
}
|
|
379
|
-
const streamed = await this.streamCard(card, content,
|
|
380
|
-
|
|
379
|
+
const streamed = await this.streamCard(card, content, {
|
|
380
|
+
append: false,
|
|
381
|
+
finalize,
|
|
382
|
+
failed,
|
|
383
|
+
});
|
|
384
|
+
if (!streamed || finalize || failed) {
|
|
381
385
|
this.activeCards.delete(channelId);
|
|
382
386
|
}
|
|
383
387
|
return streamed;
|
|
384
388
|
}
|
|
385
389
|
/**
|
|
386
|
-
*
|
|
387
|
-
* Returns true if a card was finalized, false if no active card existed.
|
|
390
|
+
* Append a delta to the active card transcript.
|
|
388
391
|
*/
|
|
389
|
-
async
|
|
392
|
+
async appendToCard(channelId, content, finalize = false, failed = false) {
|
|
393
|
+
if (!content && !finalize && !failed) {
|
|
394
|
+
return true;
|
|
395
|
+
}
|
|
390
396
|
let card = this.activeCards.get(channelId);
|
|
391
|
-
if ((!card || card.finished) && this.config.cardTemplateId && content.trim()) {
|
|
397
|
+
if ((!card || card.finished) && !finalize && !failed && this.config.cardTemplateId && content.trim()) {
|
|
392
398
|
await this.ensureCard(channelId);
|
|
393
399
|
card = this.activeCards.get(channelId);
|
|
394
400
|
}
|
|
395
401
|
if (!card || card.finished) {
|
|
402
|
+
if (finalize && content.trim()) {
|
|
403
|
+
return this.sendPlain(channelId, content);
|
|
404
|
+
}
|
|
396
405
|
return false;
|
|
397
406
|
}
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
407
|
+
const streamed = await this.streamCard(card, content, {
|
|
408
|
+
append: true,
|
|
409
|
+
finalize,
|
|
410
|
+
failed,
|
|
411
|
+
});
|
|
412
|
+
if (!streamed || finalize || failed) {
|
|
413
|
+
this.activeCards.delete(channelId);
|
|
414
|
+
}
|
|
415
|
+
return streamed;
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Stream content to the active AI Card for a channel using full replacement semantics.
|
|
419
|
+
*/
|
|
420
|
+
async streamToCard(channelId, content, finalize = false) {
|
|
421
|
+
return this.replaceCard(channelId, content, finalize, false);
|
|
422
|
+
}
|
|
423
|
+
/**
|
|
424
|
+
* Finalize the active card for a channel without falling back to a plain message.
|
|
425
|
+
* Returns true if a card was finalized, false if no active card existed.
|
|
426
|
+
*/
|
|
427
|
+
async finalizeExistingCard(channelId, content) {
|
|
428
|
+
const finalized = await this.replaceCard(channelId, content, true, false);
|
|
429
|
+
if (!finalized) {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
return true;
|
|
401
433
|
}
|
|
402
434
|
/**
|
|
403
435
|
* Finalize and remove the active card for a channel.
|
|
404
436
|
*/
|
|
405
437
|
async finalizeCard(channelId, content) {
|
|
406
|
-
const finalized = await this.
|
|
438
|
+
const finalized = await this.replaceCard(channelId, content, true, false);
|
|
407
439
|
if (!finalized) {
|
|
408
440
|
return this.sendPlain(channelId, content);
|
|
409
441
|
}
|
|
@@ -529,7 +561,7 @@ export class DingTalkBot {
|
|
|
529
561
|
this.activeCards.set(channelId, card);
|
|
530
562
|
return card;
|
|
531
563
|
}
|
|
532
|
-
async streamCard(card, content,
|
|
564
|
+
async streamCard(card, content, options) {
|
|
533
565
|
// Refresh token if needed
|
|
534
566
|
const ageSecs = Date.now() / 1000 - card.createdAt;
|
|
535
567
|
if (ageSecs > TOKEN_REFRESH_SECS) {
|
|
@@ -543,9 +575,12 @@ export class DingTalkBot {
|
|
|
543
575
|
guid: `${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
|
|
544
576
|
key: card.templateKey,
|
|
545
577
|
content,
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
578
|
+
append: options.append,
|
|
579
|
+
finished: options.finalize,
|
|
580
|
+
failed: options.failed,
|
|
581
|
+
isFull: !options.append,
|
|
582
|
+
isFinalize: options.finalize,
|
|
583
|
+
isError: options.failed,
|
|
549
584
|
};
|
|
550
585
|
const start = Date.now();
|
|
551
586
|
try {
|
|
@@ -560,8 +595,8 @@ export class DingTalkBot {
|
|
|
560
595
|
log.logWarning(`DingTalk Card: streaming request took ${duration}ms (slow)`);
|
|
561
596
|
}
|
|
562
597
|
card.lastUpdated = Date.now() / 1000;
|
|
563
|
-
card.content = content;
|
|
564
|
-
if (finalize) {
|
|
598
|
+
card.content = options.append ? `${card.content}${content}` : content;
|
|
599
|
+
if (options.finalize || options.failed) {
|
|
565
600
|
card.finished = true;
|
|
566
601
|
}
|
|
567
602
|
return true;
|
package/dist/runtime/events.d.ts
CHANGED
|
@@ -1,14 +1,22 @@
|
|
|
1
|
+
import type { SecurityConfig } from "../security/types.js";
|
|
1
2
|
import type { DingTalkBot } from "./dingtalk.js";
|
|
3
|
+
export interface EventAction {
|
|
4
|
+
type: "bash";
|
|
5
|
+
command: string;
|
|
6
|
+
timeout?: number;
|
|
7
|
+
}
|
|
2
8
|
export interface ImmediateEvent {
|
|
3
9
|
type: "immediate";
|
|
4
10
|
channelId: string;
|
|
5
11
|
text: string;
|
|
12
|
+
preAction?: EventAction;
|
|
6
13
|
}
|
|
7
14
|
export interface OneShotEvent {
|
|
8
15
|
type: "one-shot";
|
|
9
16
|
channelId: string;
|
|
10
17
|
text: string;
|
|
11
18
|
at: string;
|
|
19
|
+
preAction?: EventAction;
|
|
12
20
|
}
|
|
13
21
|
export interface PeriodicEvent {
|
|
14
22
|
type: "periodic";
|
|
@@ -16,18 +24,20 @@ export interface PeriodicEvent {
|
|
|
16
24
|
text: string;
|
|
17
25
|
schedule: string;
|
|
18
26
|
timezone: string;
|
|
27
|
+
preAction?: EventAction;
|
|
19
28
|
}
|
|
20
29
|
export type ScheduledEvent = ImmediateEvent | OneShotEvent | PeriodicEvent;
|
|
21
30
|
export declare class EventsWatcher {
|
|
22
31
|
private eventsDir;
|
|
23
32
|
private bot;
|
|
33
|
+
private commandGuardConfig?;
|
|
24
34
|
private timers;
|
|
25
35
|
private crons;
|
|
26
36
|
private debounceTimers;
|
|
27
37
|
private startTime;
|
|
28
38
|
private watcher;
|
|
29
39
|
private knownFiles;
|
|
30
|
-
constructor(eventsDir: string, bot: DingTalkBot);
|
|
40
|
+
constructor(eventsDir: string, bot: DingTalkBot, commandGuardConfig?: SecurityConfig["commandGuard"] | undefined);
|
|
31
41
|
start(): void;
|
|
32
42
|
stop(): void;
|
|
33
43
|
private debounce;
|
|
@@ -36,11 +46,13 @@ export declare class EventsWatcher {
|
|
|
36
46
|
private handleDelete;
|
|
37
47
|
private cancelScheduled;
|
|
38
48
|
private handleFile;
|
|
49
|
+
private parsePreAction;
|
|
39
50
|
private parseEvent;
|
|
40
51
|
private handleImmediate;
|
|
41
52
|
private handleOneShot;
|
|
42
53
|
private handlePeriodic;
|
|
43
54
|
private execute;
|
|
55
|
+
private runPreAction;
|
|
44
56
|
private deleteFile;
|
|
45
57
|
private getInvalidMarkerPath;
|
|
46
58
|
private markInvalid;
|
|
@@ -50,4 +62,4 @@ export declare class EventsWatcher {
|
|
|
50
62
|
/**
|
|
51
63
|
* Create and start an events watcher.
|
|
52
64
|
*/
|
|
53
|
-
export declare function createEventsWatcher(workspaceDir: string, bot: DingTalkBot): EventsWatcher;
|
|
65
|
+
export declare function createEventsWatcher(workspaceDir: string, bot: DingTalkBot, commandGuardConfig?: SecurityConfig["commandGuard"]): EventsWatcher;
|
package/dist/runtime/events.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { exec } from "child_process";
|
|
1
2
|
import { Cron } from "croner";
|
|
2
3
|
import { existsSync, mkdirSync, readdirSync, statSync, unlinkSync, watch, writeFileSync } from "fs";
|
|
3
4
|
import { readFile } from "fs/promises";
|
|
4
5
|
import { join } from "path";
|
|
5
6
|
import * as log from "../log.js";
|
|
7
|
+
import { guardCommand } from "../security/command-guard.js";
|
|
6
8
|
// ============================================================================
|
|
7
9
|
// EventsWatcher
|
|
8
10
|
// ============================================================================
|
|
@@ -11,9 +13,10 @@ const MAX_RETRIES = 3;
|
|
|
11
13
|
const RETRY_BASE_MS = 100;
|
|
12
14
|
const MAX_TIMEOUT_MS = 2_147_483_647;
|
|
13
15
|
export class EventsWatcher {
|
|
14
|
-
constructor(eventsDir, bot) {
|
|
16
|
+
constructor(eventsDir, bot, commandGuardConfig) {
|
|
15
17
|
this.eventsDir = eventsDir;
|
|
16
18
|
this.bot = bot;
|
|
19
|
+
this.commandGuardConfig = commandGuardConfig;
|
|
17
20
|
this.timers = new Map();
|
|
18
21
|
this.crons = new Map();
|
|
19
22
|
this.debounceTimers = new Map();
|
|
@@ -145,19 +148,39 @@ export class EventsWatcher {
|
|
|
145
148
|
break;
|
|
146
149
|
}
|
|
147
150
|
}
|
|
151
|
+
parsePreAction(data, filename) {
|
|
152
|
+
if (!data.preAction)
|
|
153
|
+
return undefined;
|
|
154
|
+
if (typeof data.preAction !== "object" || data.preAction === null) {
|
|
155
|
+
throw new Error(`Invalid 'preAction' field in ${filename}, expected an object`);
|
|
156
|
+
}
|
|
157
|
+
const action = data.preAction;
|
|
158
|
+
if (action.type !== "bash") {
|
|
159
|
+
throw new Error(`Unsupported preAction type '${String(action.type)}' in ${filename}, only 'bash' is supported`);
|
|
160
|
+
}
|
|
161
|
+
if (typeof action.command !== "string" || action.command.trim().length === 0) {
|
|
162
|
+
throw new Error(`Missing or empty 'preAction.command' in ${filename}`);
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
type: "bash",
|
|
166
|
+
command: action.command,
|
|
167
|
+
...(typeof action.timeout === "number" ? { timeout: action.timeout } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
148
170
|
parseEvent(content, filename) {
|
|
149
171
|
const data = JSON.parse(content);
|
|
150
172
|
if (!data.type || !data.channelId || !data.text) {
|
|
151
173
|
throw new Error(`Missing required fields (type, channelId, text) in ${filename}`);
|
|
152
174
|
}
|
|
175
|
+
const preAction = this.parsePreAction(data, filename);
|
|
153
176
|
switch (data.type) {
|
|
154
177
|
case "immediate":
|
|
155
|
-
return { type: "immediate", channelId: data.channelId, text: data.text };
|
|
178
|
+
return { type: "immediate", channelId: data.channelId, text: data.text, preAction };
|
|
156
179
|
case "one-shot":
|
|
157
180
|
if (!data.at) {
|
|
158
181
|
throw new Error(`Missing 'at' field for one-shot event in ${filename}`);
|
|
159
182
|
}
|
|
160
|
-
return { type: "one-shot", channelId: data.channelId, text: data.text, at: data.at };
|
|
183
|
+
return { type: "one-shot", channelId: data.channelId, text: data.text, at: data.at, preAction };
|
|
161
184
|
case "periodic":
|
|
162
185
|
if (!data.schedule) {
|
|
163
186
|
throw new Error(`Missing 'schedule' field for periodic event in ${filename}`);
|
|
@@ -171,12 +194,13 @@ export class EventsWatcher {
|
|
|
171
194
|
text: data.text,
|
|
172
195
|
schedule: data.schedule,
|
|
173
196
|
timezone: data.timezone,
|
|
197
|
+
preAction,
|
|
174
198
|
};
|
|
175
199
|
default:
|
|
176
200
|
throw new Error(`Unknown event type '${data.type}' in ${filename}`);
|
|
177
201
|
}
|
|
178
202
|
}
|
|
179
|
-
handleImmediate(filename, event) {
|
|
203
|
+
async handleImmediate(filename, event) {
|
|
180
204
|
const filePath = join(this.eventsDir, filename);
|
|
181
205
|
try {
|
|
182
206
|
const stat = statSync(filePath);
|
|
@@ -190,7 +214,7 @@ export class EventsWatcher {
|
|
|
190
214
|
return;
|
|
191
215
|
}
|
|
192
216
|
log.logInfo(`Executing immediate event: ${filename}`);
|
|
193
|
-
this.execute(filename, event);
|
|
217
|
+
await this.execute(filename, event);
|
|
194
218
|
}
|
|
195
219
|
handleOneShot(filename, event) {
|
|
196
220
|
const atTime = new Date(event.at).getTime();
|
|
@@ -212,18 +236,28 @@ export class EventsWatcher {
|
|
|
212
236
|
return;
|
|
213
237
|
}
|
|
214
238
|
log.logInfo(`Scheduling one-shot event: ${filename} in ${Math.round(delay / 1000)}s`);
|
|
215
|
-
const timer = setTimeout(() => {
|
|
239
|
+
const timer = setTimeout(async () => {
|
|
216
240
|
this.timers.delete(filename);
|
|
217
|
-
|
|
218
|
-
|
|
241
|
+
try {
|
|
242
|
+
log.logInfo(`Executing one-shot event: ${filename}`);
|
|
243
|
+
await this.execute(filename, event);
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
log.logWarning(`One-shot event execution failed: ${filename}`, String(err));
|
|
247
|
+
}
|
|
219
248
|
}, delay);
|
|
220
249
|
this.timers.set(filename, timer);
|
|
221
250
|
}
|
|
222
251
|
handlePeriodic(filename, event) {
|
|
223
252
|
try {
|
|
224
|
-
const cron = new Cron(event.schedule, { timezone: event.timezone }, () => {
|
|
225
|
-
|
|
226
|
-
|
|
253
|
+
const cron = new Cron(event.schedule, { timezone: event.timezone }, async () => {
|
|
254
|
+
try {
|
|
255
|
+
log.logInfo(`Executing periodic event: ${filename}`);
|
|
256
|
+
await this.execute(filename, event, false);
|
|
257
|
+
}
|
|
258
|
+
catch (err) {
|
|
259
|
+
log.logWarning(`Periodic event execution failed: ${filename}`, String(err));
|
|
260
|
+
}
|
|
227
261
|
});
|
|
228
262
|
this.crons.set(filename, cron);
|
|
229
263
|
const next = cron.nextRun();
|
|
@@ -234,7 +268,17 @@ export class EventsWatcher {
|
|
|
234
268
|
this.markInvalid(filename, `Invalid cron schedule: ${event.schedule}\n${String(err)}`);
|
|
235
269
|
}
|
|
236
270
|
}
|
|
237
|
-
execute(filename, event, deleteAfter = true) {
|
|
271
|
+
async execute(filename, event, deleteAfter = true) {
|
|
272
|
+
if (event.preAction) {
|
|
273
|
+
try {
|
|
274
|
+
await this.runPreAction(event.preAction, filename);
|
|
275
|
+
}
|
|
276
|
+
catch (err) {
|
|
277
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
278
|
+
log.logInfo(`Pre-action gate blocked event: ${filename} (${reason})`);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
238
282
|
let scheduleInfo;
|
|
239
283
|
switch (event.type) {
|
|
240
284
|
case "immediate":
|
|
@@ -270,6 +314,25 @@ export class EventsWatcher {
|
|
|
270
314
|
}
|
|
271
315
|
}
|
|
272
316
|
}
|
|
317
|
+
runPreAction(action, filename) {
|
|
318
|
+
if (this.commandGuardConfig?.enabled) {
|
|
319
|
+
const guardResult = guardCommand(action.command, this.commandGuardConfig);
|
|
320
|
+
if (!guardResult.allowed) {
|
|
321
|
+
log.logWarning(`Pre-action command blocked by guard for ${filename}: ${guardResult.reason}`);
|
|
322
|
+
return Promise.reject(new Error(`guard: ${guardResult.reason}`));
|
|
323
|
+
}
|
|
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
|
+
});
|
|
335
|
+
}
|
|
273
336
|
deleteFile(filename) {
|
|
274
337
|
const filePath = join(this.eventsDir, filename);
|
|
275
338
|
try {
|
|
@@ -313,7 +376,7 @@ export class EventsWatcher {
|
|
|
313
376
|
/**
|
|
314
377
|
* Create and start an events watcher.
|
|
315
378
|
*/
|
|
316
|
-
export function createEventsWatcher(workspaceDir, bot) {
|
|
379
|
+
export function createEventsWatcher(workspaceDir, bot, commandGuardConfig) {
|
|
317
380
|
const eventsDir = join(workspaceDir, "events");
|
|
318
|
-
return new EventsWatcher(eventsDir, bot);
|
|
381
|
+
return new EventsWatcher(eventsDir, bot, commandGuardConfig);
|
|
319
382
|
}
|
package/dist/subagents/tool.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type AgentEvent, type AgentMessage, type AgentTool } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
3
|
+
import type { MemoryCandidateStore } from "../memory/candidates.js";
|
|
3
4
|
import type { Executor } from "../sandbox.js";
|
|
4
5
|
import type { SecurityConfig } from "../security/types.js";
|
|
5
6
|
import type { PipiclawMemoryRecallSettings } from "../settings.js";
|
|
@@ -44,6 +45,7 @@ export interface SubAgentToolOptions {
|
|
|
44
45
|
channelDir: string;
|
|
45
46
|
getSubAgentDiscovery?: () => SubAgentDiscoveryResult;
|
|
46
47
|
getMemoryRecallSettings?: () => PipiclawMemoryRecallSettings;
|
|
48
|
+
memoryCandidateStore?: MemoryCandidateStore;
|
|
47
49
|
securityConfig?: SecurityConfig;
|
|
48
50
|
webConfig?: PipiclawWebToolsConfig;
|
|
49
51
|
runtimeContext: {
|
package/dist/subagents/tool.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { Agent } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import { convertToLlm } from "@mariozechner/pi-coding-agent";
|
|
3
3
|
import { Type } from "@sinclair/typebox";
|
|
4
|
-
import { createMemoryCandidateCache } from "../memory/candidates.js";
|
|
5
4
|
import { readChannelSession } from "../memory/files.js";
|
|
6
5
|
import { recallRelevantMemory } from "../memory/recall.js";
|
|
7
6
|
import { formatModelReference } from "../models/utils.js";
|
|
@@ -187,7 +186,7 @@ function stripRuntimeContextWrapper(renderedText) {
|
|
|
187
186
|
.replace(/\s*<\/runtime_context>$/i, "")
|
|
188
187
|
.trim();
|
|
189
188
|
}
|
|
190
|
-
async function buildContextualBlocks(task, config, options, currentModel
|
|
189
|
+
async function buildContextualBlocks(task, config, options, currentModel) {
|
|
191
190
|
if (config.contextMode !== "contextual") {
|
|
192
191
|
return [];
|
|
193
192
|
}
|
|
@@ -226,7 +225,7 @@ async function buildContextualBlocks(task, config, options, currentModel, candid
|
|
|
226
225
|
model: currentModel,
|
|
227
226
|
resolveApiKey: options.resolveApiKey,
|
|
228
227
|
allowedSources: ["workspace-memory", "channel-memory", "channel-history"],
|
|
229
|
-
|
|
228
|
+
candidateStore: options.memoryCandidateStore,
|
|
230
229
|
});
|
|
231
230
|
const recalledText = stripRuntimeContextWrapper(recalled.renderedText);
|
|
232
231
|
if (recalledText) {
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AgentTool } from "@mariozechner/pi-agent-core";
|
|
2
2
|
import type { Api, Model } from "@mariozechner/pi-ai";
|
|
3
|
+
import type { MemoryCandidateStore } from "../memory/candidates.js";
|
|
3
4
|
import type { Executor, SandboxConfig } from "../sandbox.js";
|
|
4
5
|
import type { SecurityConfig, SecurityRuntimeContext } from "../security/types.js";
|
|
5
6
|
import type { PipiclawMemoryRecallSettings } from "../settings.js";
|
|
@@ -17,6 +18,7 @@ export interface CreatePipiclawToolsOptions {
|
|
|
17
18
|
sandboxConfig: SandboxConfig;
|
|
18
19
|
getSubAgentDiscovery: () => SubAgentDiscoveryResult;
|
|
19
20
|
getMemoryRecallSettings: () => PipiclawMemoryRecallSettings;
|
|
21
|
+
memoryCandidateStore: MemoryCandidateStore;
|
|
20
22
|
securityConfig?: SecurityConfig;
|
|
21
23
|
toolsConfig?: PipiclawToolsConfig;
|
|
22
24
|
}
|
package/dist/tools/index.js
CHANGED
|
@@ -65,6 +65,7 @@ export function createPipiclawTools(options) {
|
|
|
65
65
|
channelDir: options.channelDir,
|
|
66
66
|
getSubAgentDiscovery: options.getSubAgentDiscovery,
|
|
67
67
|
getMemoryRecallSettings: options.getMemoryRecallSettings,
|
|
68
|
+
memoryCandidateStore: options.memoryCandidateStore,
|
|
68
69
|
securityConfig,
|
|
69
70
|
webConfig: toolsConfig.tools.web,
|
|
70
71
|
runtimeContext: {
|
package/package.json
CHANGED