@byte5ai/palaia 2.3.6 → 2.7.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.
@@ -0,0 +1,465 @@
1
+ /**
2
+ * Session lifecycle hooks for palaia v3.
3
+ *
4
+ * Handles session_start, session_end, before_reset, llm_input, llm_output,
5
+ * and after_tool_call hooks to provide:
6
+ * - Session briefing on session start (context restoration)
7
+ * - Automatic session summaries on session end/reset
8
+ * - LLM model switch detection
9
+ * - Tool observation tracking
10
+ * - Token usage tracking
11
+ */
12
+
13
+ import type { OpenClawPluginApi } from "../types.js";
14
+ import type { PalaiaPluginConfig } from "../config.js";
15
+ import { run, type RunnerOpts } from "../runner.js";
16
+ import {
17
+ getOrCreateSessionState,
18
+ deleteSessionState,
19
+ type PendingBriefing,
20
+ type ToolObservation,
21
+ } from "./state.js";
22
+ import {
23
+ strippalaiaInjectedContext,
24
+ stripPrivateBlocks,
25
+ trimToRecentExchanges,
26
+ extractWithLLM,
27
+ } from "./capture.js";
28
+ import { extractMessageTexts } from "./recall.js";
29
+ import { loadPriorities, resolvePriorities } from "../priorities.js";
30
+
31
+ /**
32
+ * Resolve effective capture scope from priorities (per-agent override)
33
+ * falling back to plugin config, then to "team".
34
+ */
35
+ async function getEffectiveCaptureScope(config: PalaiaPluginConfig): Promise<string> {
36
+ try {
37
+ const prio = await loadPriorities(config.workspace || "");
38
+ const agentId = process.env.PALAIA_AGENT || undefined;
39
+ const resolved = resolvePriorities(prio, {
40
+ recallTypeWeight: config.recallTypeWeight,
41
+ recallMinScore: config.recallMinScore,
42
+ maxInjectedChars: config.maxInjectedChars,
43
+ tier: config.tier,
44
+ }, agentId);
45
+ if (resolved.captureScope) return resolved.captureScope;
46
+ } catch {
47
+ // Fall through to config default
48
+ }
49
+ return config.captureScope || "team";
50
+ }
51
+
52
+ function buildRunnerOpts(config: PalaiaPluginConfig): RunnerOpts {
53
+ return {
54
+ binaryPath: config.binaryPath,
55
+ workspace: config.workspace,
56
+ timeoutMs: config.timeoutMs,
57
+ };
58
+ }
59
+
60
+ // ── Session Briefing ────────────────────────────────────────���───────────
61
+
62
+ /**
63
+ * Load session briefing data (last session summary + open tasks).
64
+ * Called during session_start to prepare context for the first turn.
65
+ */
66
+ export async function loadSessionBriefing(
67
+ config: PalaiaPluginConfig,
68
+ logger: { info(...a: unknown[]): void; warn(...a: unknown[]): void },
69
+ ): Promise<PendingBriefing> {
70
+ const opts = buildRunnerOpts(config);
71
+ let summary: string | null = null;
72
+ let summaryCreated: number = Date.now();
73
+ const openTasks: string[] = [];
74
+
75
+ // Load last session summary and open tasks in parallel
76
+ const { runJson } = await import("../runner.js");
77
+ const [summaryResult, tasksResult] = await Promise.allSettled([
78
+ runJson<{ results: Array<{ title: string; body: string; created?: string }> }>(
79
+ ["query", "session-summary", "--limit", "1", "--tags", "session-summary"],
80
+ { ...opts, timeoutMs: 5000 },
81
+ ),
82
+ runJson<{ results: Array<{ title: string; body: string; priority?: string }> }>(
83
+ ["list", "--type", "task", "--status", "open", "--limit", "5"],
84
+ { ...opts, timeoutMs: 5000 },
85
+ ),
86
+ ]);
87
+
88
+ if (summaryResult.status === "fulfilled" && summaryResult.value?.results?.[0]) {
89
+ summary = summaryResult.value.results[0].body;
90
+ const created = summaryResult.value.results[0].created;
91
+ if (created) {
92
+ const parsed = new Date(created).getTime();
93
+ if (!isNaN(parsed)) summaryCreated = parsed;
94
+ }
95
+ }
96
+
97
+ if (tasksResult.status === "fulfilled" && tasksResult.value?.results) {
98
+ for (const task of tasksResult.value.results) {
99
+ const prio = task.priority ? ` (${task.priority})` : "";
100
+ openTasks.push(`${task.title}${prio}`);
101
+ }
102
+ }
103
+
104
+ return { summary, openTasks, timestamp: summaryCreated };
105
+ }
106
+
107
+ /**
108
+ * Format a pending briefing into injectable text.
109
+ */
110
+ export function formatBriefing(briefing: PendingBriefing, maxChars: number): string {
111
+ if (maxChars <= 0) return "";
112
+ if (!briefing.summary && briefing.openTasks.length === 0) return "";
113
+
114
+ const parts: string[] = ["## Session Briefing (palaia)\n"];
115
+
116
+ if (briefing.summary) {
117
+ const agoMs = Date.now() - briefing.timestamp;
118
+ const agoMin = Math.round(agoMs / 60_000);
119
+ const agoStr = agoMin < 1 ? "just now" :
120
+ agoMin < 60 ? `${agoMin}m ago` :
121
+ `${Math.round(agoMin / 60)}h ago`;
122
+ parts.push(`Last session (${agoStr}):`);
123
+ parts.push(briefing.summary);
124
+ parts.push("");
125
+ }
126
+
127
+ if (briefing.openTasks.length > 0) {
128
+ parts.push("Open tasks:");
129
+ for (const task of briefing.openTasks) {
130
+ parts.push(`- ${task}`);
131
+ }
132
+ parts.push("");
133
+ }
134
+
135
+ const text = parts.join("\n");
136
+ return text.length <= maxChars ? text : text.slice(0, maxChars - 3) + "...";
137
+ }
138
+
139
+ // ── Session Summary Capture ─────────────────────────────────────────────
140
+
141
+ /**
142
+ * Extract and save a session summary from conversation messages.
143
+ * Called during before_reset (with messages) or session_end (without).
144
+ */
145
+ export async function captureSessionSummary(
146
+ messages: unknown[] | undefined,
147
+ sessionKey: string,
148
+ api: OpenClawPluginApi,
149
+ config: PalaiaPluginConfig,
150
+ logger: { info(...a: unknown[]): void; warn(...a: unknown[]): void },
151
+ ): Promise<void> {
152
+ const opts = buildRunnerOpts(config);
153
+ const state = getOrCreateSessionState(sessionKey);
154
+
155
+ // Guard: prevent double-save (before_reset + session_end race)
156
+ if (state.summarySaved) return;
157
+ let summaryText: string | null = null;
158
+
159
+ if (messages && messages.length > 0) {
160
+ // Try LLM-based summary extraction
161
+ try {
162
+ const results = await extractWithLLM(messages, api.config, {
163
+ captureModel: config.captureModel,
164
+ workspace: config.workspace,
165
+ }, []);
166
+
167
+ if (results.length > 0) {
168
+ summaryText = results[0].content;
169
+ }
170
+ } catch {
171
+ // Fall through to rule-based
172
+ }
173
+
174
+ // Rule-based fallback: last 3 user+assistant pairs
175
+ if (!summaryText) {
176
+ const allTexts = extractMessageTexts(messages);
177
+ const cleaned = allTexts.map((t: { role: string; text: string }) => ({
178
+ ...t,
179
+ text: stripPrivateBlocks(
180
+ t.role === "user" ? strippalaiaInjectedContext(t.text) : t.text
181
+ ),
182
+ }));
183
+ const recent = trimToRecentExchanges(cleaned, 3);
184
+ if (recent.length > 0) {
185
+ summaryText = recent
186
+ .map(t => `[${t.role}]: ${t.text.slice(0, 200)}`)
187
+ .join("\n")
188
+ .slice(0, 500);
189
+ }
190
+ }
191
+ } else {
192
+ // No messages available (session_end without before_reset).
193
+ // Use accumulated tool observations as summary.
194
+ if (state.toolObservations.length > 0) {
195
+ summaryText = state.toolObservations
196
+ .map(o => `${o.toolName}: ${o.resultSummary}`)
197
+ .join("\n")
198
+ .slice(0, 500);
199
+ }
200
+ }
201
+
202
+ if (!summaryText) return;
203
+
204
+ // Save session summary
205
+ try {
206
+ const scope = await getEffectiveCaptureScope(config);
207
+ const args: string[] = [
208
+ "write", summaryText,
209
+ "--type", "memory",
210
+ "--tags", "session-summary,auto-capture",
211
+ "--scope", scope,
212
+ ];
213
+ if (state.autoSessionId) args.push("--instance", state.autoSessionId);
214
+ const agentName = process.env.PALAIA_AGENT || undefined;
215
+ if (agentName) args.push("--agent", agentName);
216
+
217
+ await run(args, { ...opts, timeoutMs: 10_000 });
218
+ state.summarySaved = true; // Only mark after successful write
219
+ logger.info(`[palaia] Session summary saved (${summaryText.length} chars)`);
220
+ } catch (error) {
221
+ // Don't set summarySaved — allow retry from session_end if before_reset failed
222
+ logger.warn(`[palaia] Failed to save session summary: ${error}`);
223
+ }
224
+ }
225
+
226
+ // ── Tool Observations ─────────────────────────────────────────────────���─
227
+
228
+ /** Tools worth tracking for observations. */
229
+ const TRACKED_TOOLS = new Set([
230
+ // File operations
231
+ "memory_search", "memory_get", "memory_write",
232
+ "read_file", "write_file", "edit_file",
233
+ "search_files", "list_files",
234
+ // Shell
235
+ "bash", "shell", "terminal",
236
+ // Web
237
+ "web_search", "fetch_url",
238
+ ]);
239
+
240
+ /**
241
+ * Process an after_tool_call event into a tool observation.
242
+ * Returns null if the tool isn't worth tracking.
243
+ */
244
+ export function processToolCall(
245
+ toolName: string,
246
+ params: Record<string, unknown>,
247
+ result: unknown,
248
+ durationMs: number,
249
+ ): ToolObservation | null {
250
+ // Only track relevant tools (skip internal/framework tools)
251
+ if (!TRACKED_TOOLS.has(toolName) && !toolName.startsWith("palaia_")) {
252
+ return null;
253
+ }
254
+
255
+ // Skip errored tool calls (only null/undefined, not falsy 0/"")
256
+ if (result === undefined || result === null) return null;
257
+
258
+ // Summarize params (keep it short)
259
+ const paramKeys = Object.keys(params).slice(0, 3);
260
+ const paramsSummary = paramKeys
261
+ .map(k => {
262
+ const v = params[k];
263
+ try {
264
+ const s = typeof v === "string" ? v.slice(0, 80) : JSON.stringify(v)?.slice(0, 80) ?? "";
265
+ return `${k}=${s}`;
266
+ } catch {
267
+ return `${k}=[object]`;
268
+ }
269
+ })
270
+ .join(", ");
271
+
272
+ // Summarize result
273
+ let resultSummary: string;
274
+ if (typeof result === "string") {
275
+ resultSummary = result.slice(0, 200);
276
+ } else if (typeof result === "object" && result !== null) {
277
+ resultSummary = JSON.stringify(result).slice(0, 200);
278
+ } else {
279
+ resultSummary = String(result).slice(0, 200);
280
+ }
281
+
282
+ return {
283
+ toolName,
284
+ paramsSummary,
285
+ resultSummary,
286
+ durationMs,
287
+ timestamp: Date.now(),
288
+ };
289
+ }
290
+
291
+ // ── Hook Registration ───────────────────────────────────────────────────
292
+
293
+ /**
294
+ * Register all session lifecycle hooks.
295
+ * Called from hooks/index.ts during plugin registration.
296
+ */
297
+ export function registerSessionHooks(
298
+ api: OpenClawPluginApi,
299
+ config: PalaiaPluginConfig,
300
+ ): void {
301
+ const logger = api.logger;
302
+
303
+ // ── session_start: Load briefing for context restoration ──────────
304
+ if (config.sessionBriefing) {
305
+ api.on("session_start", async (event: any, ctx: any) => {
306
+ const sessionKey = ctx?.sessionKey || ctx?.sessionId;
307
+ if (!sessionKey) return;
308
+ const state = getOrCreateSessionState(sessionKey);
309
+
310
+ // Create a promise that before_prompt_build can await
311
+ let resolve: () => void;
312
+ state.briefingReady = new Promise<void>(r => { resolve = r; });
313
+ state.briefingReadyResolve = resolve!;
314
+
315
+ try {
316
+ state.pendingBriefing = await loadSessionBriefing(config, logger);
317
+ if (state.pendingBriefing.summary || state.pendingBriefing.openTasks.length > 0) {
318
+ logger.info(`[palaia] Session briefing loaded for ${sessionKey}`);
319
+ }
320
+ } catch (error) {
321
+ logger.warn(`[palaia] Failed to load session briefing: ${error}`);
322
+ } finally {
323
+ state.briefingReadyResolve?.();
324
+ }
325
+ });
326
+ }
327
+
328
+ // ── session_end: Save session summary ─────────────────────────────
329
+ if (config.sessionSummary) {
330
+ api.on("session_end", async (event: any, ctx: any) => {
331
+ const sessionKey = ctx?.sessionKey || ctx?.sessionId;
332
+ if (!sessionKey) return;
333
+
334
+ try {
335
+ // Only save summary if session had meaningful interaction.
336
+ // Guard: if OpenClaw omits messageCount, fall back to tool observations
337
+ // as evidence of interaction rather than silently skipping.
338
+ const messageCount = (event as any)?.messageCount;
339
+ const state = getOrCreateSessionState(sessionKey);
340
+ const hasInteraction = (typeof messageCount === "number" && messageCount >= 4)
341
+ || (messageCount === undefined && state.toolObservations.length > 0);
342
+ if (hasInteraction) {
343
+ await captureSessionSummary(undefined, sessionKey, api, config, logger);
344
+ }
345
+ } catch (error) {
346
+ logger.warn(`[palaia] session_end summary failed: ${error}`);
347
+ } finally {
348
+ deleteSessionState(sessionKey);
349
+ }
350
+ });
351
+ }
352
+
353
+ // ── before_reset: Save session summary with messages (priority) ───
354
+ if (config.sessionSummary) {
355
+ api.on("before_reset", async (event: any, ctx: any) => {
356
+ const sessionKey = ctx?.sessionKey || ctx?.sessionId;
357
+ if (!sessionKey) return;
358
+
359
+ try {
360
+ const messages = (event as any)?.messages;
361
+ if (messages && Array.isArray(messages) && messages.length >= 4) {
362
+ await captureSessionSummary(messages, sessionKey, api, config, logger);
363
+ }
364
+ } catch (error) {
365
+ logger.warn(`[palaia] before_reset summary failed: ${error}`);
366
+ }
367
+ });
368
+ }
369
+
370
+ // ── llm_input: Model switch detection ─────────────────────────────
371
+ api.on("llm_input", (event: any, ctx: any) => {
372
+ const sessionKey = ctx?.sessionKey || ctx?.sessionId;
373
+ if (!sessionKey) return;
374
+ const state = getOrCreateSessionState(sessionKey);
375
+
376
+ const model = (event as any)?.model;
377
+ if (!model) return;
378
+
379
+ if (state.lastModel && model !== state.lastModel) {
380
+ state.modelSwitchDetected = true;
381
+ logger.info(`[palaia] Model switch detected: ${state.lastModel} → ${model}`);
382
+
383
+ // Re-load briefing for next assemble
384
+ if (config.sessionBriefing && !state.pendingBriefing) {
385
+ loadSessionBriefing(config, logger).then(briefing => {
386
+ state.pendingBriefing = briefing;
387
+ state.briefingDelivered = false;
388
+ }).catch(() => {});
389
+ }
390
+ }
391
+ state.lastModel = model;
392
+ });
393
+
394
+ // ── after_tool_call: Tool observation tracking ────────────────��───
395
+ if (config.captureToolObservations) {
396
+ api.on("after_tool_call", (event: any, ctx: any) => {
397
+ const sessionKey = ctx?.sessionKey || ctx?.sessionId;
398
+ if (!sessionKey) return;
399
+ const state = getOrCreateSessionState(sessionKey);
400
+
401
+ const obs = processToolCall(
402
+ (event as any)?.toolName || "",
403
+ (event as any)?.params || {},
404
+ (event as any)?.result,
405
+ (event as any)?.durationMs || 0,
406
+ );
407
+
408
+ if (obs) {
409
+ state.toolObservations.push(obs);
410
+ // Keep max 50 observations per session to prevent memory bloat
411
+ if (state.toolObservations.length > 50) {
412
+ state.toolObservations.shift();
413
+ }
414
+ }
415
+ });
416
+ }
417
+
418
+ // ── subagent_spawning: Log sub-agent spawn for context tracking ─
419
+ api.on("subagent_spawning", async (event: any, _ctx: any) => {
420
+ try {
421
+ const childKey = (event as any)?.childSessionKey;
422
+ if (!childKey) return;
423
+ logger.info(`[palaia] Sub-agent spawning: ${childKey}`);
424
+ } catch {
425
+ // Non-fatal
426
+ }
427
+ });
428
+
429
+ // ── subagent_ended: Capture sub-agent results ─────────────────
430
+ if (config.autoCapture) {
431
+ api.on("subagent_ended", async (event: any, _ctx: any) => {
432
+ try {
433
+ const outcome = (event as any)?.outcome;
434
+ if (outcome !== "ok") return;
435
+
436
+ const childKey = (event as any)?.targetSessionKey;
437
+ if (!childKey || !api.runtime?.subagent?.getSessionMessages) return;
438
+
439
+ const result = await api.runtime.subagent.getSessionMessages({
440
+ sessionKey: childKey,
441
+ limit: 10,
442
+ });
443
+ if (!result?.messages?.length) return;
444
+
445
+ const texts = extractMessageTexts(result.messages);
446
+ const summaryParts = texts.slice(-4).map(
447
+ (t: { role: string; text: string }) => `[${t.role}]: ${t.text.slice(0, 200)}`
448
+ );
449
+ if (summaryParts.length === 0) return;
450
+
451
+ const summaryText = `Sub-agent result:\n${summaryParts.join("\n")}`.slice(0, 500);
452
+ const subagentScope = await getEffectiveCaptureScope(config);
453
+ await run([
454
+ "write", summaryText,
455
+ "--type", "memory",
456
+ "--tags", "subagent-result,auto-capture",
457
+ "--scope", subagentScope,
458
+ ], { ...buildRunnerOpts(config), timeoutMs: 10_000 });
459
+ logger.info(`[palaia] Sub-agent result captured (${summaryText.length} chars)`);
460
+ } catch (error) {
461
+ logger.warn(`[palaia] Sub-agent result capture failed: ${error}`);
462
+ }
463
+ });
464
+ }
465
+ }
@@ -127,6 +127,8 @@ export function pruneStaleEntries(): void {
127
127
  lastInboundMessageByChannel.delete(key);
128
128
  }
129
129
  }
130
+ // Also prune stale session state (orphaned sessions)
131
+ pruneStaleSessionState();
130
132
  }
131
133
 
132
134
  /**
@@ -154,6 +156,106 @@ export function resetTurnState(): void {
154
156
  lastInboundMessageByChannel.clear();
155
157
  }
156
158
 
159
+ // ============================================================================
160
+ // Session Continuity State (v3.0)
161
+ // ============================================================================
162
+
163
+ /** Accumulated tool observation from after_tool_call hooks. */
164
+ export interface ToolObservation {
165
+ toolName: string;
166
+ paramsSummary: string;
167
+ resultSummary: string;
168
+ durationMs: number;
169
+ timestamp: number;
170
+ }
171
+
172
+ /** Pending session briefing, cached between session_start and first before_prompt_build. */
173
+ export interface PendingBriefing {
174
+ summary: string | null;
175
+ openTasks: string[];
176
+ timestamp: number;
177
+ }
178
+
179
+ /** Session-level state that persists across turns within a session. */
180
+ export interface SessionState {
181
+ /** Auto-generated session ID for instance tracking. */
182
+ autoSessionId: string;
183
+ /** Last known LLM model (for switch detection). */
184
+ lastModel: string | null;
185
+ /** True when model switch detected, cleared after briefing re-injection. */
186
+ modelSwitchDetected: boolean;
187
+ /** Pending briefing to inject on next before_prompt_build. */
188
+ pendingBriefing: PendingBriefing | null;
189
+ /** Accumulated tool observations for the session (used in session summary). */
190
+ toolObservations: ToolObservation[];
191
+ /** Timestamp of session start. */
192
+ startedAt: number;
193
+ /** Whether the first turn briefing has been delivered. */
194
+ briefingDelivered: boolean;
195
+ /** Whether a session summary has already been saved (prevents double-save). */
196
+ summarySaved: boolean;
197
+ /** Promise that resolves when session_start briefing load is complete. */
198
+ briefingReady: Promise<void> | null;
199
+ /** Resolver for briefingReady promise. */
200
+ briefingReadyResolve: (() => void) | null;
201
+ }
202
+
203
+ /** Session state map. Keyed by sessionKey. */
204
+ export const sessionStateByKey = new Map<string, SessionState>();
205
+
206
+ /** Maximum age for session state entries before pruning (1 hour). */
207
+ const SESSION_STATE_TTL_MS = 60 * 60 * 1000;
208
+
209
+ /**
210
+ * Remove stale entries from sessionStateByKey.
211
+ * Called alongside pruneStaleEntries() to prevent memory leaks from
212
+ * sessions that ended without firing session_end (crash/kill).
213
+ */
214
+ export function pruneStaleSessionState(): void {
215
+ const now = Date.now();
216
+ for (const [key, state] of sessionStateByKey) {
217
+ if (now - state.startedAt > SESSION_STATE_TTL_MS) {
218
+ sessionStateByKey.delete(key);
219
+ }
220
+ }
221
+ }
222
+
223
+ /** Generate an auto session ID. */
224
+ export function generateAutoSessionId(): string {
225
+ const now = new Date();
226
+ const pad = (n: number) => String(n).padStart(2, "0");
227
+ const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
228
+ const time = `${pad(now.getHours())}${pad(now.getMinutes())}`;
229
+ const hex = Math.floor(Math.random() * 0xffff).toString(16).padStart(4, "0");
230
+ return `auto-${date}-${time}-${hex}`;
231
+ }
232
+
233
+ /** Get or create session state for a session key. */
234
+ export function getOrCreateSessionState(sessionKey: string): SessionState {
235
+ let state = sessionStateByKey.get(sessionKey);
236
+ if (!state) {
237
+ state = {
238
+ autoSessionId: generateAutoSessionId(),
239
+ lastModel: null,
240
+ modelSwitchDetected: false,
241
+ pendingBriefing: null,
242
+ toolObservations: [],
243
+ startedAt: Date.now(),
244
+ briefingDelivered: false,
245
+ summarySaved: false,
246
+ briefingReady: null,
247
+ briefingReadyResolve: null,
248
+ };
249
+ sessionStateByKey.set(sessionKey, state);
250
+ }
251
+ return state;
252
+ }
253
+
254
+ /** Delete session state (cleanup). */
255
+ export function deleteSessionState(sessionKey: string): void {
256
+ sessionStateByKey.delete(sessionKey);
257
+ }
258
+
157
259
  // ============================================================================
158
260
  // Session Key Helpers
159
261
  // ============================================================================
@@ -236,10 +338,11 @@ const VALID_SCOPES = ["private", "team", "public"];
236
338
 
237
339
  /**
238
340
  * Check if a scope string is valid for palaia write.
239
- * Valid: "private", "team", "public", or any "shared:*" prefix.
341
+ * Valid: "private", "team", "public".
342
+ * Legacy shared:X is accepted but normalized to "team" by the CLI.
240
343
  */
241
344
  export function isValidScope(s: string): boolean {
242
- return VALID_SCOPES.includes(s) || s.startsWith("shared:");
345
+ return VALID_SCOPES.includes(s);
243
346
  }
244
347
 
245
348
  /**
@@ -293,7 +396,7 @@ export function formatStatusResponse(
293
396
  stats: Record<string, unknown>,
294
397
  config: PalaiaPluginConfig,
295
398
  ): string {
296
- const lines: string[] = ["Palaia Memory Status", ""];
399
+ const lines: string[] = ["palaia Memory Status", ""];
297
400
 
298
401
  // Recall count
299
402
  const sinceDate = state.firstRecallTimestamp
package/src/priorities.ts CHANGED
@@ -32,6 +32,8 @@ export interface AgentPriorityOverride {
32
32
  recallMinScore?: number;
33
33
  maxInjectedChars?: number;
34
34
  tier?: string;
35
+ scopeVisibility?: string[]; // Issue #145: agent isolation
36
+ captureScope?: string; // Issue #147: per-agent write scope
35
37
  }
36
38
 
37
39
  export interface ProjectPriorityOverride {
@@ -45,6 +47,8 @@ export interface ResolvedPriorities {
45
47
  recallMinScore: number;
46
48
  maxInjectedChars: number;
47
49
  tier: string;
50
+ scopeVisibility: string[] | null; // Issue #145: agent isolation
51
+ captureScope: string | null; // Issue #147: per-agent write scope
48
52
  }
49
53
 
50
54
  // ============================================================================
@@ -136,6 +140,8 @@ export function resolvePriorities(
136
140
  recallMinScore: defaults.recallMinScore ?? DEFAULT_MIN_SCORE,
137
141
  maxInjectedChars: defaults.maxInjectedChars ?? DEFAULT_MAX_CHARS,
138
142
  tier: defaults.tier ?? DEFAULT_TIER,
143
+ scopeVisibility: null,
144
+ captureScope: null,
139
145
  };
140
146
 
141
147
  if (!prio) return resolved;
@@ -176,6 +182,12 @@ export function resolvePriorities(
176
182
  if (agentCfg.tier !== undefined) {
177
183
  resolved.tier = agentCfg.tier;
178
184
  }
185
+ if (agentCfg.scopeVisibility) {
186
+ resolved.scopeVisibility = [...agentCfg.scopeVisibility];
187
+ }
188
+ if (agentCfg.captureScope) {
189
+ resolved.captureScope = agentCfg.captureScope;
190
+ }
179
191
  }
180
192
  }
181
193
 
package/src/runner.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Palaia CLI subprocess runner.
2
+ * palaia CLI subprocess runner.
3
3
  *
4
4
  * Executes palaia CLI commands, parses JSON output, handles binary detection
5
5
  * and timeouts. This is the bridge between the OpenClaw plugin and the
@@ -92,7 +92,7 @@ export async function detectBinary(
92
92
  }
93
93
 
94
94
  throw new Error(
95
- "Palaia binary not found. Install with: pip install palaia\n" +
95
+ "palaia binary not found. Install with: pip install palaia\n" +
96
96
  "Or set binaryPath in plugin config."
97
97
  );
98
98
  }
@@ -129,7 +129,7 @@ function execCommand(
129
129
  (error, stdout, stderr) => {
130
130
  if (error && (error as any).killed) {
131
131
  reject(
132
- new Error(`Palaia command timed out after ${timeout}ms: ${cmd} ${args.join(" ")}`)
132
+ new Error(`palaia command timed out after ${timeout}ms: ${cmd} ${args.join(" ")}`)
133
133
  );
134
134
  return;
135
135
  }
@@ -184,7 +184,7 @@ export async function run(
184
184
 
185
185
  if (result.exitCode !== 0) {
186
186
  const errMsg = result.stderr.trim() || result.stdout.trim();
187
- throw new Error(`Palaia CLI error (exit ${result.exitCode}): ${errMsg}`);
187
+ throw new Error(`palaia CLI error (exit ${result.exitCode}): ${errMsg}`);
188
188
  }
189
189
 
190
190
  return result.stdout;