@byte5ai/palaia 2.0.13 → 2.1.0
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/package.json +1 -1
- package/src/hooks.ts +104 -25
- package/src/runner.ts +51 -10
package/package.json
CHANGED
package/src/hooks.ts
CHANGED
|
@@ -293,10 +293,14 @@ export async function sendReaction(
|
|
|
293
293
|
|
|
294
294
|
/** Cached Slack bot token resolved from env or OpenClaw config. */
|
|
295
295
|
let _cachedSlackToken: string | null | undefined;
|
|
296
|
+
/** Timestamp when the token was cached (for TTL expiry). */
|
|
297
|
+
let _slackTokenCachedAt = 0;
|
|
298
|
+
/** TTL for cached Slack bot token in milliseconds (5 minutes). */
|
|
299
|
+
const SLACK_TOKEN_CACHE_TTL_MS = 5 * 60 * 1000;
|
|
296
300
|
|
|
297
301
|
/**
|
|
298
302
|
* Resolve the Slack bot token from environment or OpenClaw config file.
|
|
299
|
-
* Caches the result
|
|
303
|
+
* Caches the result with a 5-minute TTL — re-resolves after expiry.
|
|
300
304
|
*
|
|
301
305
|
* Resolution order:
|
|
302
306
|
* 1. SLACK_BOT_TOKEN env var (explicit override)
|
|
@@ -306,12 +310,15 @@ let _cachedSlackToken: string | null | undefined;
|
|
|
306
310
|
* Config path: OPENCLAW_CONFIG env var → ~/.openclaw/openclaw.json
|
|
307
311
|
*/
|
|
308
312
|
async function resolveSlackBotToken(): Promise<string | null> {
|
|
309
|
-
if (_cachedSlackToken !== undefined)
|
|
313
|
+
if (_cachedSlackToken !== undefined && (Date.now() - _slackTokenCachedAt) < SLACK_TOKEN_CACHE_TTL_MS) {
|
|
314
|
+
return _cachedSlackToken;
|
|
315
|
+
}
|
|
310
316
|
|
|
311
317
|
// 1) Environment variable
|
|
312
318
|
const envToken = process.env.SLACK_BOT_TOKEN?.trim();
|
|
313
319
|
if (envToken) {
|
|
314
320
|
_cachedSlackToken = envToken;
|
|
321
|
+
_slackTokenCachedAt = Date.now();
|
|
315
322
|
return envToken;
|
|
316
323
|
}
|
|
317
324
|
|
|
@@ -330,6 +337,7 @@ async function resolveSlackBotToken(): Promise<string | null> {
|
|
|
330
337
|
const directToken = config?.channels?.slack?.botToken?.trim();
|
|
331
338
|
if (directToken) {
|
|
332
339
|
_cachedSlackToken = directToken;
|
|
340
|
+
_slackTokenCachedAt = Date.now();
|
|
333
341
|
return directToken;
|
|
334
342
|
}
|
|
335
343
|
|
|
@@ -337,6 +345,7 @@ async function resolveSlackBotToken(): Promise<string | null> {
|
|
|
337
345
|
const accountToken = config?.channels?.slack?.accounts?.default?.botToken?.trim();
|
|
338
346
|
if (accountToken) {
|
|
339
347
|
_cachedSlackToken = accountToken;
|
|
348
|
+
_slackTokenCachedAt = Date.now();
|
|
340
349
|
return accountToken;
|
|
341
350
|
}
|
|
342
351
|
} catch {
|
|
@@ -345,12 +354,14 @@ async function resolveSlackBotToken(): Promise<string | null> {
|
|
|
345
354
|
}
|
|
346
355
|
|
|
347
356
|
_cachedSlackToken = null;
|
|
357
|
+
_slackTokenCachedAt = Date.now();
|
|
348
358
|
return null;
|
|
349
359
|
}
|
|
350
360
|
|
|
351
361
|
/** Reset cached token (for testing). */
|
|
352
362
|
export function resetSlackTokenCache(): void {
|
|
353
363
|
_cachedSlackToken = undefined;
|
|
364
|
+
_slackTokenCachedAt = 0;
|
|
354
365
|
}
|
|
355
366
|
|
|
356
367
|
async function sendSlackReaction(
|
|
@@ -733,6 +744,13 @@ For each piece of knowledge, return a JSON array of objects:
|
|
|
733
744
|
- "project": which project this belongs to (from known projects list, or null if unclear)
|
|
734
745
|
- "scope": "private" (personal preference, agent-specific), "team" (shared knowledge), or "public" (documentation)
|
|
735
746
|
|
|
747
|
+
STRICT TASK CLASSIFICATION RULES — a "task" MUST have ALL three of:
|
|
748
|
+
1. A clear, completable action (not just an observation or idea)
|
|
749
|
+
2. An identifiable responsible party (explicitly named or unambiguously inferable from context)
|
|
750
|
+
3. A concrete deliverable or measurable end state
|
|
751
|
+
If ANY of these is missing, classify as "memory" instead of "task". When in doubt, use "memory".
|
|
752
|
+
Observations, learnings, insights, opinions, and general knowledge are ALWAYS "memory", never "task".
|
|
753
|
+
|
|
736
754
|
Only extract genuinely significant knowledge. Skip small talk, acknowledgments, routine exchanges.
|
|
737
755
|
Do NOT extract if similar knowledge was likely captured in a recent exchange. Prefer quality over quantity. Skip routine status updates and acknowledgments.
|
|
738
756
|
Return empty array [] if nothing is worth remembering.
|
|
@@ -914,7 +932,25 @@ export async function extractWithLLM(
|
|
|
914
932
|
|
|
915
933
|
let tmpDir: string | null = null;
|
|
916
934
|
try {
|
|
917
|
-
|
|
935
|
+
// Use a fixed base directory for extraction temp dirs and clean up stale ones
|
|
936
|
+
const extractBaseDir = path.join(os.tmpdir(), "palaia-extractions");
|
|
937
|
+
await fs.mkdir(extractBaseDir, { recursive: true });
|
|
938
|
+
// Clean up stale extraction dirs (older than 5 minutes)
|
|
939
|
+
try {
|
|
940
|
+
const entries = await fs.readdir(extractBaseDir, { withFileTypes: true });
|
|
941
|
+
const now = Date.now();
|
|
942
|
+
for (const entry of entries) {
|
|
943
|
+
if (entry.isDirectory()) {
|
|
944
|
+
try {
|
|
945
|
+
const stat = await fs.stat(path.join(extractBaseDir, entry.name));
|
|
946
|
+
if (now - stat.mtimeMs > 5 * 60 * 1000) {
|
|
947
|
+
await fs.rm(path.join(extractBaseDir, entry.name), { recursive: true, force: true });
|
|
948
|
+
}
|
|
949
|
+
} catch { /* ignore individual cleanup errors */ }
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
} catch { /* ignore cleanup errors */ }
|
|
953
|
+
tmpDir = await fs.mkdtemp(path.join(extractBaseDir, "ext-"));
|
|
918
954
|
const sessionId = `palaia-extract-${Date.now()}`;
|
|
919
955
|
const sessionFile = path.join(tmpDir, "session.json");
|
|
920
956
|
|
|
@@ -1268,11 +1304,29 @@ export function buildRecallQuery(messages: unknown[]): string {
|
|
|
1268
1304
|
);
|
|
1269
1305
|
|
|
1270
1306
|
// Fallback: if no messages without provenance, use all user messages
|
|
1271
|
-
const
|
|
1307
|
+
const allUserMsgs = candidates.length > 0
|
|
1272
1308
|
? candidates
|
|
1273
1309
|
: texts.filter(t => t.role === "user");
|
|
1274
1310
|
|
|
1275
|
-
if (
|
|
1311
|
+
if (allUserMsgs.length === 0) return "";
|
|
1312
|
+
|
|
1313
|
+
// Early exit: only scan the last 3 user messages or 2000 chars, whichever comes first
|
|
1314
|
+
const MAX_SCAN_MSGS = 3;
|
|
1315
|
+
const MAX_SCAN_CHARS = 2000;
|
|
1316
|
+
let userMsgs: typeof allUserMsgs;
|
|
1317
|
+
if (allUserMsgs.length <= MAX_SCAN_MSGS) {
|
|
1318
|
+
userMsgs = allUserMsgs;
|
|
1319
|
+
} else {
|
|
1320
|
+
userMsgs = allUserMsgs.slice(-MAX_SCAN_MSGS);
|
|
1321
|
+
// Extend backwards if total chars < MAX_SCAN_CHARS and more messages available
|
|
1322
|
+
let totalChars = userMsgs.reduce((sum, m) => sum + m.text.length, 0);
|
|
1323
|
+
let startIdx = allUserMsgs.length - MAX_SCAN_MSGS;
|
|
1324
|
+
while (startIdx > 0 && totalChars < MAX_SCAN_CHARS) {
|
|
1325
|
+
startIdx--;
|
|
1326
|
+
totalChars += allUserMsgs[startIdx].text.length;
|
|
1327
|
+
userMsgs = allUserMsgs.slice(startIdx);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1276
1330
|
|
|
1277
1331
|
// Step 2: Strip envelopes from the last user message(s)
|
|
1278
1332
|
let lastText = stripSystemPrefix(stripChannelEnvelope(userMsgs[userMsgs.length - 1].text.trim()));
|
|
@@ -1341,10 +1395,22 @@ export function rerankByTypeWeight(
|
|
|
1341
1395
|
// Hook helpers
|
|
1342
1396
|
// ============================================================================
|
|
1343
1397
|
|
|
1344
|
-
|
|
1398
|
+
/**
|
|
1399
|
+
* Resolve per-agent workspace and agentId from hook context.
|
|
1400
|
+
* Fallback chain: ctx.workspaceDir → config.workspace → cwd
|
|
1401
|
+
* Agent chain: ctx.agentId → PALAIA_AGENT env var → undefined
|
|
1402
|
+
*/
|
|
1403
|
+
export function resolvePerAgentContext(ctx: any, config: PalaiaPluginConfig) {
|
|
1404
|
+
return {
|
|
1405
|
+
workspace: ctx?.workspaceDir || config.workspace,
|
|
1406
|
+
agentId: ctx?.agentId || process.env.PALAIA_AGENT || undefined,
|
|
1407
|
+
};
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
function buildRunnerOpts(config: PalaiaPluginConfig, overrides?: { workspace?: string }): RunnerOpts {
|
|
1345
1411
|
return {
|
|
1346
1412
|
binaryPath: config.binaryPath,
|
|
1347
|
-
workspace: config.workspace,
|
|
1413
|
+
workspace: overrides?.workspace || config.workspace,
|
|
1348
1414
|
timeoutMs: config.timeoutMs,
|
|
1349
1415
|
};
|
|
1350
1416
|
}
|
|
@@ -1538,6 +1604,10 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1538
1604
|
// Prune stale entries to prevent memory leaks from crashed sessions (C-2)
|
|
1539
1605
|
pruneStaleEntries();
|
|
1540
1606
|
|
|
1607
|
+
// Per-agent workspace resolution (Issue #111)
|
|
1608
|
+
const resolved = resolvePerAgentContext(ctx, config);
|
|
1609
|
+
const hookOpts = buildRunnerOpts(config, { workspace: resolved.workspace });
|
|
1610
|
+
|
|
1541
1611
|
try {
|
|
1542
1612
|
const maxChars = config.maxInjectedChars || 4000;
|
|
1543
1613
|
const limit = Math.min(config.maxResults || 10, 20);
|
|
@@ -1553,15 +1623,22 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1553
1623
|
let serverQueried = false;
|
|
1554
1624
|
if (config.embeddingServer) {
|
|
1555
1625
|
try {
|
|
1556
|
-
const mgr = getEmbedServerManager(
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1626
|
+
const mgr = getEmbedServerManager(hookOpts);
|
|
1627
|
+
// If embed server workspace differs from resolved workspace, skip server and use CLI
|
|
1628
|
+
const serverWorkspace = hookOpts.workspace;
|
|
1629
|
+
const embedOpts = buildRunnerOpts(config);
|
|
1630
|
+
if (serverWorkspace !== embedOpts.workspace) {
|
|
1631
|
+
logger.info(`[palaia] Embed server workspace mismatch (agent=${resolved.workspace}), falling back to CLI`);
|
|
1632
|
+
} else {
|
|
1633
|
+
const resp = await mgr.query({
|
|
1634
|
+
text: userMessage,
|
|
1635
|
+
top_k: limit,
|
|
1636
|
+
include_cold: config.tier === "all",
|
|
1637
|
+
}, config.timeoutMs || 3000);
|
|
1638
|
+
if (resp?.result?.results && Array.isArray(resp.result.results)) {
|
|
1639
|
+
entries = resp.result.results;
|
|
1640
|
+
serverQueried = true;
|
|
1641
|
+
}
|
|
1565
1642
|
}
|
|
1566
1643
|
} catch (serverError) {
|
|
1567
1644
|
logger.warn(`[palaia] Embed server query failed, falling back to CLI: ${serverError}`);
|
|
@@ -1575,7 +1652,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1575
1652
|
if (config.tier === "all") {
|
|
1576
1653
|
queryArgs.push("--all");
|
|
1577
1654
|
}
|
|
1578
|
-
const result = await runJson<QueryResult>(queryArgs, { ...
|
|
1655
|
+
const result = await runJson<QueryResult>(queryArgs, { ...hookOpts, timeoutMs: 15000 });
|
|
1579
1656
|
if (result && Array.isArray(result.results)) {
|
|
1580
1657
|
entries = result.results;
|
|
1581
1658
|
}
|
|
@@ -1597,7 +1674,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1597
1674
|
} else {
|
|
1598
1675
|
listArgs.push("--tier", config.tier || "hot");
|
|
1599
1676
|
}
|
|
1600
|
-
const result = await runJson<QueryResult>(listArgs,
|
|
1677
|
+
const result = await runJson<QueryResult>(listArgs, hookOpts);
|
|
1601
1678
|
if (result && Array.isArray(result.results)) {
|
|
1602
1679
|
entries = result.results;
|
|
1603
1680
|
}
|
|
@@ -1643,7 +1720,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1643
1720
|
// Update recall counter for satisfaction/transparency nudges (Issue #87)
|
|
1644
1721
|
let nudgeContext = "";
|
|
1645
1722
|
try {
|
|
1646
|
-
const pluginState = await loadPluginState(
|
|
1723
|
+
const pluginState = await loadPluginState(resolved.workspace);
|
|
1647
1724
|
pluginState.successfulRecalls++;
|
|
1648
1725
|
if (!pluginState.firstRecallTimestamp) {
|
|
1649
1726
|
pluginState.firstRecallTimestamp = new Date().toISOString();
|
|
@@ -1652,7 +1729,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1652
1729
|
if (nudges.length > 0) {
|
|
1653
1730
|
nudgeContext = "\n\n## Agent Nudge (Palaia)\n\n" + nudges.join("\n\n");
|
|
1654
1731
|
}
|
|
1655
|
-
await savePluginState(pluginState,
|
|
1732
|
+
await savePluginState(pluginState, resolved.workspace);
|
|
1656
1733
|
} catch {
|
|
1657
1734
|
// Non-fatal
|
|
1658
1735
|
}
|
|
@@ -1714,14 +1791,16 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1714
1791
|
// Resolve session key for turn state
|
|
1715
1792
|
const sessionKey = resolveSessionKeyFromCtx(ctx);
|
|
1716
1793
|
|
|
1717
|
-
//
|
|
1794
|
+
// Per-agent workspace resolution (Issue #111)
|
|
1795
|
+
const resolved = resolvePerAgentContext(ctx, config);
|
|
1796
|
+
const hookOpts = buildRunnerOpts(config, { workspace: resolved.workspace });
|
|
1718
1797
|
|
|
1719
1798
|
if (!event.success || !event.messages || event.messages.length === 0) {
|
|
1720
1799
|
return;
|
|
1721
1800
|
}
|
|
1722
1801
|
|
|
1723
1802
|
try {
|
|
1724
|
-
const agentName =
|
|
1803
|
+
const agentName = resolved.agentId;
|
|
1725
1804
|
|
|
1726
1805
|
const allTexts = extractMessageTexts(event.messages);
|
|
1727
1806
|
|
|
@@ -1762,7 +1841,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1762
1841
|
return;
|
|
1763
1842
|
}
|
|
1764
1843
|
|
|
1765
|
-
const knownProjects = await loadProjects(
|
|
1844
|
+
const knownProjects = await loadProjects(hookOpts);
|
|
1766
1845
|
|
|
1767
1846
|
// Helper: build CLI args with metadata
|
|
1768
1847
|
const buildWriteArgs = (
|
|
@@ -1830,7 +1909,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1830
1909
|
validatedProject,
|
|
1831
1910
|
effectiveScope,
|
|
1832
1911
|
);
|
|
1833
|
-
await run(args, { ...
|
|
1912
|
+
await run(args, { ...hookOpts, timeoutMs: 10_000 });
|
|
1834
1913
|
logger.info(
|
|
1835
1914
|
`[palaia] LLM auto-captured: type=${r.type}, significance=${r.significance}, tags=${tags.join(",")}, project=${validatedProject || "none"}, scope=${effectiveScope || "team"}`
|
|
1836
1915
|
);
|
|
@@ -1914,7 +1993,7 @@ export function registerHooks(api: any, config: PalaiaPluginConfig): void {
|
|
|
1914
1993
|
hintForScope?.scope,
|
|
1915
1994
|
);
|
|
1916
1995
|
|
|
1917
|
-
await run(args, { ...
|
|
1996
|
+
await run(args, { ...hookOpts, timeoutMs: 10_000 });
|
|
1918
1997
|
logger.info(
|
|
1919
1998
|
`[palaia] Rule-based auto-captured: type=${captureData.type}, tags=${captureData.tags.join(",")}`
|
|
1920
1999
|
);
|
package/src/runner.ts
CHANGED
|
@@ -361,6 +361,11 @@ export class EmbedServerManager {
|
|
|
361
361
|
clearTimeout(this.pendingRequest.timer);
|
|
362
362
|
this.pendingRequest = null;
|
|
363
363
|
}
|
|
364
|
+
// Reject all queued requests
|
|
365
|
+
for (const queued of this.requestQueue) {
|
|
366
|
+
queued.reject(new Error(`Embed server exited with code ${code}`));
|
|
367
|
+
}
|
|
368
|
+
this.requestQueue = [];
|
|
364
369
|
});
|
|
365
370
|
|
|
366
371
|
// Register cleanup
|
|
@@ -456,6 +461,16 @@ export class EmbedServerManager {
|
|
|
456
461
|
await this.start();
|
|
457
462
|
}
|
|
458
463
|
|
|
464
|
+
/** Maximum number of queued requests before rejecting */
|
|
465
|
+
private maxQueueSize = 10;
|
|
466
|
+
/** Queue of pending requests waiting to be sent */
|
|
467
|
+
private requestQueue: Array<{
|
|
468
|
+
request: Record<string, unknown>;
|
|
469
|
+
timeoutMs: number;
|
|
470
|
+
resolve: (value: any) => void;
|
|
471
|
+
reject: (reason: any) => void;
|
|
472
|
+
}> = [];
|
|
473
|
+
|
|
459
474
|
private sendRequest(request: Record<string, unknown>, timeoutMs: number): Promise<any> {
|
|
460
475
|
return new Promise((resolve, reject) => {
|
|
461
476
|
if (!this.proc?.stdin?.writable) {
|
|
@@ -463,22 +478,46 @@ export class EmbedServerManager {
|
|
|
463
478
|
return;
|
|
464
479
|
}
|
|
465
480
|
|
|
466
|
-
//
|
|
481
|
+
// If a request is already in flight, queue this one
|
|
467
482
|
if (this.pendingRequest) {
|
|
468
|
-
|
|
483
|
+
if (this.requestQueue.length >= this.maxQueueSize) {
|
|
484
|
+
reject(new Error("Embed server queue full"));
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
this.requestQueue.push({ request, timeoutMs, resolve, reject });
|
|
469
488
|
return;
|
|
470
489
|
}
|
|
471
490
|
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
491
|
+
this._sendImmediate(request, timeoutMs, resolve, reject);
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
private _sendImmediate(
|
|
496
|
+
request: Record<string, unknown>,
|
|
497
|
+
timeoutMs: number,
|
|
498
|
+
resolve: (value: any) => void,
|
|
499
|
+
reject: (reason: any) => void,
|
|
500
|
+
): void {
|
|
501
|
+
const timer = setTimeout(() => {
|
|
502
|
+
this.pendingRequest = null;
|
|
503
|
+
reject(new Error(`Embed server request timed out after ${timeoutMs}ms`));
|
|
504
|
+
this._drainQueue();
|
|
505
|
+
}, timeoutMs);
|
|
476
506
|
|
|
477
|
-
|
|
507
|
+
this.pendingRequest = { resolve, reject, timer };
|
|
478
508
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
509
|
+
const line = JSON.stringify(request) + "\n";
|
|
510
|
+
this.proc!.stdin!.write(line);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
private _drainQueue(): void {
|
|
514
|
+
if (this.pendingRequest || this.requestQueue.length === 0) return;
|
|
515
|
+
const next = this.requestQueue.shift()!;
|
|
516
|
+
if (!this.proc?.stdin?.writable) {
|
|
517
|
+
next.reject(new Error("Embed server not running"));
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
this._sendImmediate(next.request, next.timeoutMs, next.resolve, next.reject);
|
|
482
521
|
}
|
|
483
522
|
|
|
484
523
|
private handleLine(line: string): void {
|
|
@@ -493,6 +532,8 @@ export class EmbedServerManager {
|
|
|
493
532
|
} else {
|
|
494
533
|
pending.resolve(msg);
|
|
495
534
|
}
|
|
535
|
+
// Process next queued request
|
|
536
|
+
this._drainQueue();
|
|
496
537
|
} catch {
|
|
497
538
|
// Ignore non-JSON lines
|
|
498
539
|
}
|