@poolzin/pool-bot 2026.1.29 → 2026.1.31
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/CHANGELOG.md +29 -1
- package/README.md +11 -0
- package/dist/agents/system-prompt.js +16 -16
- package/dist/agents/tools/memory-tool.js +2 -1
- package/dist/build-info.json +3 -3
- package/dist/cli/program/command-registry.js +5 -0
- package/dist/cli/program/register.completion.js +355 -0
- package/dist/gateway/hooks/index.js +49 -0
- package/dist/gateway/hooks/lifecycle-hooks-integration.js +256 -0
- package/dist/gateway/hooks/lifecycle-hooks.js +236 -0
- package/dist/gateway/hooks/progressive-disclosure-details.js +237 -0
- package/dist/gateway/hooks/progressive-disclosure-index.js +354 -0
- package/dist/gateway/hooks/progressive-disclosure-timeline.js +231 -0
- package/dist/gateway/hooks/progressive-disclosure-types.js +65 -0
- package/dist/gateway/hooks/progressive-disclosure.js +242 -0
- package/dist/gateway/hooks/tool-usage-capture.js +253 -0
- package/dist/gateway/hooks/tool-usage-storage.js +144 -0
- package/dist/gateway/server-methods/nodes.js +2 -0
- package/dist/gateway/server.impl.js +4 -0
- package/dist/imessage/monitor/monitor-provider.js +14 -1
- package/dist/media/store.js +37 -1
- package/dist/memory/index.js +5 -0
- package/dist/memory/manager.js +25 -2
- package/docs/WHATSAPP-HEARTBEAT-TROUBLESHOOTING.md +319 -0
- package/package.json +1 -1
- package/skills/webgpu-threejs-tsl/REFERENCE.md +283 -0
- package/skills/webgpu-threejs-tsl/SKILL.md +91 -0
- package/skills/webgpu-threejs-tsl/docs/compute-shaders.md +404 -0
- package/skills/webgpu-threejs-tsl/docs/core-concepts.md +453 -0
- package/skills/webgpu-threejs-tsl/docs/materials.md +353 -0
- package/skills/webgpu-threejs-tsl/docs/post-processing.md +434 -0
- package/skills/webgpu-threejs-tsl/docs/wgsl-integration.md +324 -0
- package/skills/webgpu-threejs-tsl/examples/basic-setup.js +87 -0
- package/skills/webgpu-threejs-tsl/examples/custom-material.js +170 -0
- package/skills/webgpu-threejs-tsl/examples/earth-shader.js +292 -0
- package/skills/webgpu-threejs-tsl/examples/particle-system.js +259 -0
- package/skills/webgpu-threejs-tsl/examples/post-processing.js +199 -0
- package/skills/webgpu-threejs-tsl/templates/compute-shader.js +305 -0
- package/skills/webgpu-threejs-tsl/templates/webgpu-project.js +276 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Usage Capture Hook
|
|
3
|
+
*
|
|
4
|
+
* Captures tool usage automatically via lifecycle hooks.
|
|
5
|
+
* Safe: never throws, async operations, feature-flagged.
|
|
6
|
+
*
|
|
7
|
+
* Integration:
|
|
8
|
+
* - Registers as onToolUse hook
|
|
9
|
+
* - Captures all tool calls automatically
|
|
10
|
+
* - Stores compressed data in SessionEntry
|
|
11
|
+
*/
|
|
12
|
+
import { TOOL_USAGE_ENABLED, TOOL_USAGE_MAX_RECORDS, createToolUsageRecord, updateToolStats, addToHistory, } from "./tool-usage-storage.js";
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Configuration
|
|
15
|
+
// ============================================================================
|
|
16
|
+
const TOOL_USAGE_BUFFER_FLUSH_INTERVAL = 30000; // 30 seconds
|
|
17
|
+
const TOOL_USAGE_BUFFER_MAX_SIZE = 50; // Flush after 50 tool calls
|
|
18
|
+
const toolUsageBuffer = new Map();
|
|
19
|
+
let bufferFlushScheduled = false;
|
|
20
|
+
// ============================================================================
|
|
21
|
+
// SessionEntry Extension (Type-safe, backward compatible)
|
|
22
|
+
// ============================================================================
|
|
23
|
+
/**
|
|
24
|
+
* Load tool usage from SessionEntry
|
|
25
|
+
* Safe: handles missing data, validates structure
|
|
26
|
+
*/
|
|
27
|
+
export async function loadSessionToolUsage(sessionKey) {
|
|
28
|
+
try {
|
|
29
|
+
// TODO: Integrate with actual SessionEntry in Phase 4
|
|
30
|
+
// For now, return in-memory placeholder
|
|
31
|
+
const existing = toolUsageBuffer.get(sessionKey);
|
|
32
|
+
if (existing && existing.length > 0) {
|
|
33
|
+
// Return computed stats from buffer
|
|
34
|
+
const tools = {};
|
|
35
|
+
for (const record of existing) {
|
|
36
|
+
if (!tools[record.toolName]) {
|
|
37
|
+
tools[record.toolName] = {
|
|
38
|
+
count: 0,
|
|
39
|
+
totalMs: 0,
|
|
40
|
+
avgMs: 0,
|
|
41
|
+
lastUsed: record.timestamp,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
tools[record.toolName] = updateToolStats(tools[record.toolName], record.executionTimeMs);
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
version: 2,
|
|
48
|
+
sessionKey,
|
|
49
|
+
sessionId: sessionKey, // Placeholder
|
|
50
|
+
tools,
|
|
51
|
+
history: existing,
|
|
52
|
+
lastCaptured: Date.now(),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Return empty usage
|
|
56
|
+
return {
|
|
57
|
+
version: 2,
|
|
58
|
+
sessionKey,
|
|
59
|
+
sessionId: sessionKey,
|
|
60
|
+
tools: {},
|
|
61
|
+
history: [],
|
|
62
|
+
lastCaptured: Date.now(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
console.error("[tool-usage-capture] Failed to load tool usage:", err);
|
|
67
|
+
// Return empty usage on error
|
|
68
|
+
return {
|
|
69
|
+
version: 2,
|
|
70
|
+
sessionKey,
|
|
71
|
+
sessionId: sessionKey,
|
|
72
|
+
tools: {},
|
|
73
|
+
history: [],
|
|
74
|
+
lastCaptured: Date.now(),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Save tool usage to SessionEntry
|
|
80
|
+
* Safe: validates data, handles errors gracefully
|
|
81
|
+
*/
|
|
82
|
+
export async function saveSessionToolUsage(sessionKey, usage) {
|
|
83
|
+
try {
|
|
84
|
+
// TODO: Integrate with actual SessionEntry in Phase 4
|
|
85
|
+
// For now, just update in-memory buffer
|
|
86
|
+
const existing = toolUsageBuffer.get(sessionKey) || [];
|
|
87
|
+
const newRecords = usage.history.slice(existing.length);
|
|
88
|
+
if (newRecords.length > 0) {
|
|
89
|
+
toolUsageBuffer.set(sessionKey, [...existing, ...newRecords]);
|
|
90
|
+
}
|
|
91
|
+
if (TOOL_USAGE_ENABLED) {
|
|
92
|
+
console.log(`[tool-usage-capture] Saved ${newRecords.length} records for ${sessionKey}`);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
console.error("[tool-usage-capture] Failed to save tool usage:", err);
|
|
97
|
+
// Never throw - errors are logged only
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// ============================================================================
|
|
101
|
+
// Hook Implementation
|
|
102
|
+
// ============================================================================
|
|
103
|
+
/**
|
|
104
|
+
* Main hook: captures tool usage automatically
|
|
105
|
+
* Safe: validates all inputs, never throws, handles all edge cases
|
|
106
|
+
*/
|
|
107
|
+
export const toolUsageCaptureHook = async (ctx) => {
|
|
108
|
+
// Feature flag check (safe exit)
|
|
109
|
+
if (!TOOL_USAGE_ENABLED) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
// Validate inputs (safe defaults)
|
|
114
|
+
const toolName = ctx.toolName || "unknown_tool";
|
|
115
|
+
const executionTimeMs = ctx.executionTimeMs || 0;
|
|
116
|
+
const success = ctx.success !== undefined ? ctx.success : true;
|
|
117
|
+
// Create compressed record
|
|
118
|
+
const record = createToolUsageRecord(toolName, ctx.toolInput, ctx.toolOutput, executionTimeMs, success, ctx.error);
|
|
119
|
+
// Load existing usage
|
|
120
|
+
const usage = await loadSessionToolUsage(ctx.sessionKey);
|
|
121
|
+
// Update stats
|
|
122
|
+
if (!usage.tools[toolName]) {
|
|
123
|
+
usage.tools[toolName] = {
|
|
124
|
+
count: 0,
|
|
125
|
+
totalMs: 0,
|
|
126
|
+
avgMs: 0,
|
|
127
|
+
lastUsed: record.timestamp,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
usage.tools[toolName] = updateToolStats(usage.tools[toolName], executionTimeMs);
|
|
131
|
+
// Add to history (respects max limit)
|
|
132
|
+
usage.history = addToHistory(usage.history, record, TOOL_USAGE_MAX_RECORDS);
|
|
133
|
+
usage.lastCaptured = Date.now();
|
|
134
|
+
// Save (async, non-blocking)
|
|
135
|
+
await saveSessionToolUsage(ctx.sessionKey, usage).catch((err) => {
|
|
136
|
+
// Save errors are logged in saveSessionToolUsage
|
|
137
|
+
// This catch is just for extra safety
|
|
138
|
+
console.error("[tool-usage-capture] Unhandled save error:", err);
|
|
139
|
+
});
|
|
140
|
+
// Debug logging (can be disabled)
|
|
141
|
+
if (success && TOOL_USAGE_ENABLED) {
|
|
142
|
+
console.log(`[tool-usage-capture] ${toolName} (${executionTimeMs}ms) - ${ctx.sessionKey}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
// NEVER throw from hook - would break the tool call
|
|
147
|
+
console.error("[tool-usage-capture] Hook error:", err);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
// ============================================================================
|
|
151
|
+
// Buffer Management (performance optimization)
|
|
152
|
+
// ============================================================================
|
|
153
|
+
/**
|
|
154
|
+
* Flush in-memory buffer to storage
|
|
155
|
+
* Safe: handles errors, never throws
|
|
156
|
+
*/
|
|
157
|
+
export async function flushToolUsageBuffer() {
|
|
158
|
+
try {
|
|
159
|
+
if (toolUsageBuffer.size === 0) {
|
|
160
|
+
return; // Nothing to flush
|
|
161
|
+
}
|
|
162
|
+
let flushed = 0;
|
|
163
|
+
for (const [sessionKey, records] of toolUsageBuffer.entries()) {
|
|
164
|
+
try {
|
|
165
|
+
// TODO: Actually persist to SessionEntry in Phase 4
|
|
166
|
+
// For now, buffer stays in-memory
|
|
167
|
+
flushed += records.length;
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
console.error(`[tool-usage-capture] Failed to flush buffer for ${sessionKey}:`, err);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (flushed > 0 && TOOL_USAGE_ENABLED) {
|
|
174
|
+
console.log(`[tool-usage-capture] Flushed ${flushed} records from buffer`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
catch (err) {
|
|
178
|
+
console.error("[tool-usage-capture] Buffer flush error:", err);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Schedule buffer flush
|
|
183
|
+
* Safe: debounced, only schedules once
|
|
184
|
+
*/
|
|
185
|
+
function scheduleBufferFlush() {
|
|
186
|
+
if (bufferFlushScheduled) {
|
|
187
|
+
return; // Already scheduled
|
|
188
|
+
}
|
|
189
|
+
bufferFlushScheduled = true;
|
|
190
|
+
// Flush after 30 seconds
|
|
191
|
+
setTimeout(async () => {
|
|
192
|
+
await flushToolUsageBuffer();
|
|
193
|
+
bufferFlushScheduled = false;
|
|
194
|
+
}, TOOL_USAGE_BUFFER_FLUSH_INTERVAL);
|
|
195
|
+
}
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// Query API (for future use)
|
|
198
|
+
// ============================================================================
|
|
199
|
+
/**
|
|
200
|
+
* Get tool usage statistics for a session
|
|
201
|
+
* Safe: returns empty object if no data
|
|
202
|
+
*/
|
|
203
|
+
export async function getSessionToolUsage(sessionKey) {
|
|
204
|
+
try {
|
|
205
|
+
const usage = await loadSessionToolUsage(sessionKey);
|
|
206
|
+
return usage;
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
console.error(`[tool-usage-capture] Failed to get usage for ${sessionKey}:`, err);
|
|
210
|
+
return undefined;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get recent tool usage (last N records)
|
|
215
|
+
* Safe: returns empty array if no data
|
|
216
|
+
*/
|
|
217
|
+
export async function getRecentToolUsage(sessionKey, count = 10) {
|
|
218
|
+
try {
|
|
219
|
+
const usage = await loadSessionToolUsage(sessionKey);
|
|
220
|
+
return usage.history.slice(-count);
|
|
221
|
+
}
|
|
222
|
+
catch (err) {
|
|
223
|
+
console.error(`[tool-usage-capture] Failed to get recent usage for ${sessionKey}:`, err);
|
|
224
|
+
return [];
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// ============================================================================
|
|
228
|
+
// Cleanup
|
|
229
|
+
// ============================================================================
|
|
230
|
+
/**
|
|
231
|
+
* Clear tool usage buffer (for testing)
|
|
232
|
+
* Safe: never throws
|
|
233
|
+
*/
|
|
234
|
+
export function clearToolUsageBuffer() {
|
|
235
|
+
try {
|
|
236
|
+
toolUsageBuffer.clear();
|
|
237
|
+
}
|
|
238
|
+
catch (err) {
|
|
239
|
+
console.error("[tool-usage-capture] Buffer clear error:", err);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Clear tool usage for a specific session
|
|
244
|
+
* Safe: never throws
|
|
245
|
+
*/
|
|
246
|
+
export function clearSessionToolUsage(sessionKey) {
|
|
247
|
+
try {
|
|
248
|
+
toolUsageBuffer.delete(sessionKey);
|
|
249
|
+
}
|
|
250
|
+
catch (err) {
|
|
251
|
+
console.error(`[tool-usage-capture] Failed to clear usage for ${sessionKey}:`, err);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Usage Capture - Storage Layer
|
|
3
|
+
*
|
|
4
|
+
* Safe, efficient storage for tool usage data.
|
|
5
|
+
* Phase 2a: Type definitions and interfaces (NO breaking changes)
|
|
6
|
+
*
|
|
7
|
+
* Safety measures:
|
|
8
|
+
* - All fields optional (backward compatible)
|
|
9
|
+
* - Version field for future migrations
|
|
10
|
+
* - No modifications to existing SessionEntry fields
|
|
11
|
+
*/
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Constants
|
|
14
|
+
// ============================================================================
|
|
15
|
+
export const TOOL_USAGE_MAX_RECORDS = 100; // Keep only last 100 records
|
|
16
|
+
export const TOOL_USAGE_MAX_SUMMARY_LEN = 100; // Max 100 chars per summary
|
|
17
|
+
export const TOOL_USAGE_ENABLED = true; // Feature flag (master switch)
|
|
18
|
+
// ============================================================================
|
|
19
|
+
// Compression Functions (safe, no side effects)
|
|
20
|
+
// ============================================================================
|
|
21
|
+
/**
|
|
22
|
+
* Compress a tool input/output value to a short summary
|
|
23
|
+
* Safe: never throws, handles all types gracefully
|
|
24
|
+
*/
|
|
25
|
+
export function compressToolValue(value, maxLen = TOOL_USAGE_MAX_SUMMARY_LEN) {
|
|
26
|
+
try {
|
|
27
|
+
// Handle primitives
|
|
28
|
+
if (value === null)
|
|
29
|
+
return "null";
|
|
30
|
+
if (value === undefined)
|
|
31
|
+
return "undefined";
|
|
32
|
+
if (typeof value === "string") {
|
|
33
|
+
return value.length > maxLen ? value.slice(0, maxLen) + "..." : value;
|
|
34
|
+
}
|
|
35
|
+
if (typeof value === "number")
|
|
36
|
+
return String(value);
|
|
37
|
+
if (typeof value === "boolean")
|
|
38
|
+
return String(value);
|
|
39
|
+
// Handle objects and arrays
|
|
40
|
+
if (typeof value === "object") {
|
|
41
|
+
const json = JSON.stringify(value);
|
|
42
|
+
return json.length > maxLen ? json.slice(0, maxLen) + "..." : json;
|
|
43
|
+
}
|
|
44
|
+
// Fallback
|
|
45
|
+
return String(value);
|
|
46
|
+
}
|
|
47
|
+
catch (err) {
|
|
48
|
+
// If compression fails, return safe fallback
|
|
49
|
+
return "[compression_error]";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a tool usage record from hook context
|
|
54
|
+
* Safe: validates all inputs, never throws
|
|
55
|
+
*/
|
|
56
|
+
export function createToolUsageRecord(toolName, toolInput, toolOutput, executionTimeMs, success, error) {
|
|
57
|
+
// Validate inputs (safe defaults)
|
|
58
|
+
const safeToolName = typeof toolName === "string" && toolName.length > 0 ? toolName : "unknown_tool";
|
|
59
|
+
const safeInput = typeof toolInput === "object" && toolInput !== null ? toolInput : {};
|
|
60
|
+
const safeTime = typeof executionTimeMs === "number" && executionTimeMs >= 0 ? executionTimeMs : 0;
|
|
61
|
+
const safeSuccess = Boolean(success);
|
|
62
|
+
return {
|
|
63
|
+
toolName: safeToolName,
|
|
64
|
+
timestamp: Date.now(),
|
|
65
|
+
executionTimeMs: safeTime,
|
|
66
|
+
success: safeSuccess,
|
|
67
|
+
inputSummary: compressToolValue(safeInput),
|
|
68
|
+
outputSummary: compressToolValue(toolOutput),
|
|
69
|
+
error: error ? String(error).slice(0, 200) : undefined,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Statistics Update (pure function, no side effects)
|
|
74
|
+
// ============================================================================
|
|
75
|
+
/**
|
|
76
|
+
* Update tool statistics with a new record
|
|
77
|
+
* Safe: never mutates input, returns new object
|
|
78
|
+
*/
|
|
79
|
+
export function updateToolStats(existingStats, executionTimeMs) {
|
|
80
|
+
const count = (existingStats?.count ?? 0) + 1;
|
|
81
|
+
const totalMs = (existingStats?.totalMs ?? 0) + executionTimeMs;
|
|
82
|
+
const avgMs = count > 0 ? Math.round((totalMs / count) * 100) / 100 : 0;
|
|
83
|
+
return {
|
|
84
|
+
count,
|
|
85
|
+
totalMs,
|
|
86
|
+
avgMs,
|
|
87
|
+
lastUsed: Date.now(),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// History Management (pure functions)
|
|
92
|
+
// ============================================================================
|
|
93
|
+
/**
|
|
94
|
+
* Add record to history, respecting max limit
|
|
95
|
+
* Safe: never exceeds max records, returns new array
|
|
96
|
+
*/
|
|
97
|
+
export function addToHistory(history, record, maxRecords = TOOL_USAGE_MAX_RECORDS) {
|
|
98
|
+
const newHistory = [...history, record];
|
|
99
|
+
// Keep only last N records
|
|
100
|
+
if (newHistory.length > maxRecords) {
|
|
101
|
+
return newHistory.slice(-maxRecords);
|
|
102
|
+
}
|
|
103
|
+
return newHistory;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Get recent tool usage (last N records)
|
|
107
|
+
* Safe: never mutates input, returns slice
|
|
108
|
+
*/
|
|
109
|
+
export function getRecentToolUsage(history, count = 10) {
|
|
110
|
+
return history.slice(-count);
|
|
111
|
+
}
|
|
112
|
+
// ============================================================================
|
|
113
|
+
// Query Functions (for future use)
|
|
114
|
+
// ============================================================================
|
|
115
|
+
/**
|
|
116
|
+
* Get statistics for a specific tool
|
|
117
|
+
* Safe: returns undefined if tool not found
|
|
118
|
+
*/
|
|
119
|
+
export function getToolStats(usage, toolName) {
|
|
120
|
+
return usage?.tools[toolName];
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Get all tool names sorted by usage frequency
|
|
124
|
+
* Safe: returns empty array if no data
|
|
125
|
+
*/
|
|
126
|
+
export function getToolsByFrequency(usage) {
|
|
127
|
+
if (!usage?.tools)
|
|
128
|
+
return [];
|
|
129
|
+
return Object.entries(usage.tools)
|
|
130
|
+
.map(([toolName, stats]) => ({ toolName, count: stats.count }))
|
|
131
|
+
.sort((a, b) => b.count - a.count);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Get slowest tools (by average execution time)
|
|
135
|
+
* Safe: returns empty array if no data
|
|
136
|
+
*/
|
|
137
|
+
export function getSlowestTools(usage, limit = 5) {
|
|
138
|
+
if (!usage?.tools)
|
|
139
|
+
return [];
|
|
140
|
+
return Object.entries(usage.tools)
|
|
141
|
+
.map(([toolName, stats]) => ({ toolName, avgMs: stats.avgMs }))
|
|
142
|
+
.sort((a, b) => b.avgMs - a.avgMs)
|
|
143
|
+
.slice(0, limit);
|
|
144
|
+
}
|
|
@@ -5,6 +5,8 @@ import { respondInvalidParams, respondUnavailableOnThrow, safeParseJson, uniqueS
|
|
|
5
5
|
import { loadConfig } from "../../config/config.js";
|
|
6
6
|
import { isNodeCommandAllowed, resolveNodeCommandAllowlist } from "../node-command-policy.js";
|
|
7
7
|
function isNodeEntry(entry) {
|
|
8
|
+
if (entry.clientMode === "node")
|
|
9
|
+
return true;
|
|
8
10
|
if (entry.role === "node")
|
|
9
11
|
return true;
|
|
10
12
|
if (Array.isArray(entry.roles) && entry.roles.includes("node"))
|
|
@@ -27,6 +27,7 @@ import { createExecApprovalHandlers } from "./server-methods/exec-approval.js";
|
|
|
27
27
|
import { createExecApprovalForwarder } from "../infra/exec-approval-forwarder.js";
|
|
28
28
|
import { createChannelManager } from "./server-channels.js";
|
|
29
29
|
import { createAgentEventHandler } from "./server-chat.js";
|
|
30
|
+
import { startLifecycleHooksIntegration } from "./hooks/lifecycle-hooks-integration.js";
|
|
30
31
|
import { createGatewayCloseHandler } from "./server-close.js";
|
|
31
32
|
import { buildGatewayCronService } from "./server-cron.js";
|
|
32
33
|
import { applyGatewayLaneConcurrency } from "./server-lanes.js";
|
|
@@ -263,6 +264,9 @@ export async function startGatewayServer(port = 18789, opts = {}) {
|
|
|
263
264
|
resolveSessionKeyForRun,
|
|
264
265
|
clearAgentRunContext,
|
|
265
266
|
}));
|
|
267
|
+
// Lifecycle Hooks Integration (POC Day 1.5)
|
|
268
|
+
const lifecycleHooksUnsub = startLifecycleHooksIntegration();
|
|
269
|
+
logHooks.info("Lifecycle hooks integration started");
|
|
266
270
|
const heartbeatUnsub = onHeartbeatEvent((evt) => {
|
|
267
271
|
broadcast("heartbeat", evt, { dropIfSlow: true });
|
|
268
272
|
});
|
|
@@ -108,9 +108,18 @@ export async function monitorIMessageProvider(opts = {}) {
|
|
|
108
108
|
const inboundDebouncer = createInboundDebouncer({
|
|
109
109
|
debounceMs: inboundDebounceMs,
|
|
110
110
|
buildKey: (entry) => {
|
|
111
|
+
// Use message-specific key to preserve attachments
|
|
112
|
+
// This prevents different messages from being merged incorrectly
|
|
111
113
|
const sender = entry.message.sender?.trim();
|
|
112
114
|
if (!sender)
|
|
113
115
|
return null;
|
|
116
|
+
// Prefer message id for unique identification (prevents cross-message contamination)
|
|
117
|
+
// If id is not available, fall back to conversation-based key
|
|
118
|
+
const messageId = entry.message.id;
|
|
119
|
+
if (messageId != null) {
|
|
120
|
+
return `imessage:${accountInfo.accountId}:msg:${messageId}`;
|
|
121
|
+
}
|
|
122
|
+
// Fallback to conversation-based key only when id is unavailable
|
|
114
123
|
const conversationId = entry.message.chat_id != null
|
|
115
124
|
? `chat:${entry.message.chat_id}`
|
|
116
125
|
: (entry.message.chat_guid ?? entry.message.chat_identifier ?? "unknown");
|
|
@@ -120,6 +129,8 @@ export async function monitorIMessageProvider(opts = {}) {
|
|
|
120
129
|
const text = entry.message.text?.trim() ?? "";
|
|
121
130
|
if (!text)
|
|
122
131
|
return false;
|
|
132
|
+
// Messages with attachments should not be debounced across message boundaries
|
|
133
|
+
// to preserve attachment context
|
|
123
134
|
if (entry.message.attachments && entry.message.attachments.length > 0)
|
|
124
135
|
return false;
|
|
125
136
|
return !hasControlCommand(text, cfg);
|
|
@@ -139,7 +150,9 @@ export async function monitorIMessageProvider(opts = {}) {
|
|
|
139
150
|
const syntheticMessage = {
|
|
140
151
|
...last.message,
|
|
141
152
|
text: combinedText,
|
|
142
|
-
attachments
|
|
153
|
+
// Preserve attachments from the last message in the batch
|
|
154
|
+
// (should always be null here due to shouldDebounce guard, but keep for safety)
|
|
155
|
+
attachments: last.message.attachments ?? null,
|
|
143
156
|
};
|
|
144
157
|
await handleMessageNow(syntheticMessage);
|
|
145
158
|
},
|
package/dist/media/store.js
CHANGED
|
@@ -12,6 +12,40 @@ const resolveMediaDir = () => path.join(resolveConfigDir(), "media");
|
|
|
12
12
|
export const MEDIA_MAX_BYTES = 5 * 1024 * 1024; // 5MB default
|
|
13
13
|
const MAX_BYTES = MEDIA_MAX_BYTES;
|
|
14
14
|
const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes
|
|
15
|
+
/**
|
|
16
|
+
* SECURITY: Safe paths for local file extraction
|
|
17
|
+
*
|
|
18
|
+
* Prevents LFI (Local File Inclusion) attacks by restricting
|
|
19
|
+
* local file extraction to known-safe directories.
|
|
20
|
+
*
|
|
21
|
+
* Backported from: openclaw/openclaw@2026.1.30 (PR #4880)
|
|
22
|
+
* Security: CRITICAL
|
|
23
|
+
*/
|
|
24
|
+
const SAFE_PATHS = [
|
|
25
|
+
"/tmp",
|
|
26
|
+
"/var/tmp",
|
|
27
|
+
process.cwd(), // Current working directory
|
|
28
|
+
resolveConfigDir(), // PoolBot config directory
|
|
29
|
+
];
|
|
30
|
+
/**
|
|
31
|
+
* Validate that a local path is safe for file extraction.
|
|
32
|
+
*
|
|
33
|
+
* Resolves the path and checks if it's within one of the SAFE_PATHS.
|
|
34
|
+
* Throws an error if the path is outside safe directories.
|
|
35
|
+
*
|
|
36
|
+
* @param filePath - The path to validate
|
|
37
|
+
* @throws Error if path is outside safe directories
|
|
38
|
+
*/
|
|
39
|
+
function validateLocalPath(filePath) {
|
|
40
|
+
const resolved = path.resolve(filePath);
|
|
41
|
+
for (const safePath of SAFE_PATHS) {
|
|
42
|
+
if (resolved.startsWith(safePath)) {
|
|
43
|
+
return; // Path is safe
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
throw new Error(`Unsafe local path: ${filePath} (resolved: ${resolved}). ` +
|
|
47
|
+
`Path must be within one of: ${SAFE_PATHS.join(", ")}`);
|
|
48
|
+
}
|
|
15
49
|
/**
|
|
16
50
|
* Sanitize a filename for cross-platform safety.
|
|
17
51
|
* Removes chars unsafe on Windows/SharePoint/all platforms.
|
|
@@ -158,6 +192,8 @@ export async function saveMediaSource(source, headers, subdir = "") {
|
|
|
158
192
|
return { id, path: finalDest, size, contentType: mime };
|
|
159
193
|
}
|
|
160
194
|
// local path
|
|
195
|
+
// SECURITY: Validate local path before reading
|
|
196
|
+
validateLocalPath(source); // LFI prevention
|
|
161
197
|
const stat = await fs.stat(source);
|
|
162
198
|
if (!stat.isFile()) {
|
|
163
199
|
throw new Error("Media path is not a file");
|
|
@@ -175,7 +211,7 @@ export async function saveMediaSource(source, headers, subdir = "") {
|
|
|
175
211
|
}
|
|
176
212
|
export async function saveMediaBuffer(buffer, contentType, subdir = "inbound", maxBytes = MAX_BYTES, originalFilename) {
|
|
177
213
|
if (buffer.byteLength > maxBytes) {
|
|
178
|
-
throw new Error(`Media exceeds ${(maxBytes /
|
|
214
|
+
throw new Error(`Media exceeds ${(maxBytes / 1024 / 1024).toFixed(0)}MB limit`);
|
|
179
215
|
}
|
|
180
216
|
const dir = path.join(resolveMediaDir(), subdir);
|
|
181
217
|
await fs.mkdir(dir, { recursive: true, mode: 0o700 });
|
package/dist/memory/index.js
CHANGED
package/dist/memory/manager.js
CHANGED
|
@@ -157,6 +157,7 @@ export class MemoryIndexManager {
|
|
|
157
157
|
this.sessionWarm.add(key);
|
|
158
158
|
}
|
|
159
159
|
async search(query, opts) {
|
|
160
|
+
const searchStart = Date.now();
|
|
160
161
|
void this.warmSession(opts?.sessionKey);
|
|
161
162
|
if (this.settings.sync.onSearch && (this.dirty || this.sessionsDirty)) {
|
|
162
163
|
void this.sync({ reason: "search" }).catch((err) => {
|
|
@@ -179,7 +180,17 @@ export class MemoryIndexManager {
|
|
|
179
180
|
? await this.searchVector(queryVec, candidates).catch(() => [])
|
|
180
181
|
: [];
|
|
181
182
|
if (!hybrid.enabled) {
|
|
182
|
-
|
|
183
|
+
const results = vectorResults.filter((entry) => entry.score >= minScore).slice(0, maxResults);
|
|
184
|
+
const searchDuration = Date.now() - searchStart;
|
|
185
|
+
log.debug("memory search performance", {
|
|
186
|
+
query: cleaned.substring(0, 50),
|
|
187
|
+
durationMs: searchDuration,
|
|
188
|
+
resultCount: results.length,
|
|
189
|
+
provider: this.provider.id,
|
|
190
|
+
model: this.provider.model,
|
|
191
|
+
hybrid: false,
|
|
192
|
+
});
|
|
193
|
+
return results;
|
|
183
194
|
}
|
|
184
195
|
const merged = this.mergeHybridResults({
|
|
185
196
|
vector: vectorResults,
|
|
@@ -187,7 +198,19 @@ export class MemoryIndexManager {
|
|
|
187
198
|
vectorWeight: hybrid.vectorWeight,
|
|
188
199
|
textWeight: hybrid.textWeight,
|
|
189
200
|
});
|
|
190
|
-
|
|
201
|
+
const results = merged.filter((entry) => entry.score >= minScore).slice(0, maxResults);
|
|
202
|
+
const searchDuration = Date.now() - searchStart;
|
|
203
|
+
log.debug("memory search performance", {
|
|
204
|
+
query: cleaned.substring(0, 50),
|
|
205
|
+
durationMs: searchDuration,
|
|
206
|
+
resultCount: results.length,
|
|
207
|
+
provider: this.provider.id,
|
|
208
|
+
model: this.provider.model,
|
|
209
|
+
hybrid: true,
|
|
210
|
+
keywordResults: keywordResults.length,
|
|
211
|
+
vectorResults: vectorResults.length,
|
|
212
|
+
});
|
|
213
|
+
return results;
|
|
191
214
|
}
|
|
192
215
|
async searchVector(queryVec, limit) {
|
|
193
216
|
const results = await searchVector({
|