akemon 0.3.3 → 0.3.5

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/server.js CHANGED
@@ -2,6 +2,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
2
2
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
3
3
  import { exec } from "child_process";
4
4
  import { scanAndKillOrphans } from "./orphan-scan.js";
5
+ import { timingSafeEqual } from "node:crypto";
5
6
  import { createServer } from "http";
6
7
  import { createInterface } from "readline";
7
8
  import { mkdir } from "fs/promises";
@@ -86,8 +87,35 @@ function bearerToken(req) {
86
87
  }
87
88
  function isOwnerRequest(req, options) {
88
89
  const token = bearerToken(req);
89
- const validTokens = [options.secretKey, options.key].filter(Boolean);
90
- return !!token && validTokens.includes(token);
90
+ const validTokens = [options.secretKey, options.key]
91
+ .filter((validToken) => typeof validToken === "string" && validToken.length > 0);
92
+ return !!token && validTokens.some((validToken) => constantTimeTokenEqual(token, validToken));
93
+ }
94
+ function constantTimeTokenEqual(left, right) {
95
+ const leftBuffer = Buffer.from(left);
96
+ const rightBuffer = Buffer.from(right);
97
+ const length = Math.max(leftBuffer.length, rightBuffer.length, 1);
98
+ const leftPadded = Buffer.alloc(length);
99
+ const rightPadded = Buffer.alloc(length);
100
+ leftBuffer.copy(leftPadded);
101
+ rightBuffer.copy(rightPadded);
102
+ return timingSafeEqual(leftPadded, rightPadded) && leftBuffer.length === rightBuffer.length;
103
+ }
104
+ function writeJsonResponse(res, statusCode, body, pretty = false) {
105
+ res.writeHead(statusCode, { "Content-Type": "application/json" })
106
+ .end(JSON.stringify(body, null, pretty ? 2 : 0));
107
+ }
108
+ function requireOwnerRequest(req, res, options) {
109
+ if (isOwnerRequest(req, options))
110
+ return true;
111
+ writeJsonResponse(res, 401, { error: "Owner token required" });
112
+ return false;
113
+ }
114
+ function requireSoftwareAgent(res, softwareAgent) {
115
+ if (softwareAgent)
116
+ return softwareAgent;
117
+ writeJsonResponse(res, 503, { error: "Software agent peripheral not ready" });
118
+ return null;
91
119
  }
92
120
  function readJsonBody(req, maxBytes = 256 * 1024) {
93
121
  return new Promise((resolve, reject) => {
@@ -118,79 +146,193 @@ function readJsonBody(req, maxBytes = 256 * 1024) {
118
146
  req.on("error", reject);
119
147
  });
120
148
  }
121
- export async function handleSoftwareAgentRunHttp(req, res, deps) {
122
- if (!isOwnerRequest(req, deps.options)) {
123
- res.writeHead(401, { "Content-Type": "application/json" })
124
- .end(JSON.stringify({ error: "Owner token required" }));
125
- return;
126
- }
127
- if (!deps.softwareAgent) {
128
- res.writeHead(503, { "Content-Type": "application/json" })
129
- .end(JSON.stringify({ error: "Software agent peripheral not ready" }));
130
- return;
131
- }
149
+ async function readOwnerSoftwareAgentEnvelope(req, res, deps) {
132
150
  let body;
133
151
  try {
134
152
  body = await readJsonBody(req);
135
153
  }
136
154
  catch (err) {
137
- res.writeHead(400, { "Content-Type": "application/json" })
138
- .end(JSON.stringify({ error: err.message || "Invalid request body" }));
139
- return;
155
+ writeJsonResponse(res, 400, { error: err.message || "Invalid request body" });
156
+ return null;
140
157
  }
141
- let envelope;
142
158
  try {
143
- envelope = createOwnerTaskEnvelope(body, deps.workdir);
159
+ const envelope = createOwnerTaskEnvelope(body, deps.workdir);
160
+ envelope.memorySummary = await buildSoftwareAgentMemorySummary({
161
+ workdir: deps.workdir,
162
+ agentName: deps.agentName,
163
+ envelope,
164
+ request: body,
165
+ });
166
+ return envelope;
144
167
  }
145
168
  catch (err) {
146
- res.writeHead(400, { "Content-Type": "application/json" })
147
- .end(JSON.stringify({ error: err.message || "Invalid software-agent envelope" }));
148
- return;
169
+ writeJsonResponse(res, 400, { error: err.message || "Invalid software-agent envelope" });
170
+ return null;
149
171
  }
172
+ }
173
+ export async function handleSoftwareAgentRunHttp(req, res, deps) {
174
+ if (!requireOwnerRequest(req, res, deps.options))
175
+ return;
176
+ const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
177
+ if (!softwareAgent)
178
+ return;
179
+ const envelope = await readOwnerSoftwareAgentEnvelope(req, res, deps);
180
+ if (!envelope)
181
+ return;
150
182
  try {
151
- const result = await deps.softwareAgent.sendTask(envelope);
152
- res.writeHead(result.success ? 200 : 500, { "Content-Type": "application/json" })
153
- .end(JSON.stringify(result, null, 2));
183
+ const result = await softwareAgent.sendTask(envelope);
184
+ writeJsonResponse(res, 200, redactSecrets(result), true);
154
185
  }
155
186
  catch (err) {
156
187
  const busy = String(err.message || "").includes("busy");
157
- res.writeHead(busy ? 409 : 500, { "Content-Type": "application/json" })
158
- .end(JSON.stringify({ error: err.message || String(err) }));
188
+ writeJsonResponse(res, busy ? 409 : 500, { error: err.message || String(err) });
189
+ }
190
+ }
191
+ export async function handleSoftwareAgentRunStreamHttp(req, res, deps) {
192
+ if (!requireOwnerRequest(req, res, deps.options))
193
+ return;
194
+ const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
195
+ if (!softwareAgent)
196
+ return;
197
+ const envelope = await readOwnerSoftwareAgentEnvelope(req, res, deps);
198
+ if (!envelope)
199
+ return;
200
+ const abortController = new AbortController();
201
+ let responseFinished = false;
202
+ let streamStarted = false;
203
+ res.on("close", () => {
204
+ if (!responseFinished)
205
+ abortController.abort();
206
+ });
207
+ const ensureStreamStarted = () => {
208
+ if (streamStarted)
209
+ return;
210
+ streamStarted = true;
211
+ res.writeHead(200, {
212
+ "Content-Type": "application/x-ndjson; charset=utf-8",
213
+ "Cache-Control": "no-cache",
214
+ "X-Accel-Buffering": "no",
215
+ });
216
+ res.flushHeaders?.();
217
+ };
218
+ try {
219
+ await softwareAgent.sendTask(envelope, {
220
+ signal: abortController.signal,
221
+ observer: {
222
+ onStart(event) {
223
+ ensureStreamStarted();
224
+ writeSoftwareAgentStreamEvent(res, {
225
+ type: "start",
226
+ taskId: event.taskId,
227
+ commandLine: event.commandLine,
228
+ });
229
+ },
230
+ onStream(event) {
231
+ ensureStreamStarted();
232
+ writeSoftwareAgentStreamEvent(res, {
233
+ type: event.stream,
234
+ taskId: event.taskId,
235
+ chunk: event.chunk,
236
+ });
237
+ },
238
+ onEnd(event) {
239
+ ensureStreamStarted();
240
+ writeSoftwareAgentStreamEvent(res, {
241
+ type: "end",
242
+ taskId: event.taskId,
243
+ exitCode: event.exitCode,
244
+ durationMs: event.durationMs,
245
+ result: event.result,
246
+ });
247
+ },
248
+ },
249
+ });
250
+ }
251
+ catch (err) {
252
+ if (!streamStarted) {
253
+ const busy = String(err.message || "").includes("busy");
254
+ writeJsonResponse(res, busy ? 409 : 500, { error: err.message || String(err) });
255
+ responseFinished = true;
256
+ return;
257
+ }
258
+ writeSoftwareAgentStreamEvent(res, {
259
+ type: "error",
260
+ error: err.message || String(err),
261
+ });
262
+ }
263
+ finally {
264
+ responseFinished = true;
265
+ if (streamStarted && !res.writableEnded)
266
+ res.end();
159
267
  }
160
268
  }
161
269
  export async function handleSoftwareAgentStatusHttp(req, res, deps) {
162
- if (!isOwnerRequest(req, deps.options)) {
163
- res.writeHead(401, { "Content-Type": "application/json" })
164
- .end(JSON.stringify({ error: "Owner token required" }));
270
+ if (!requireOwnerRequest(req, res, deps.options))
271
+ return;
272
+ const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
273
+ if (!softwareAgent)
274
+ return;
275
+ writeJsonResponse(res, 200, softwareAgent.getState(), true);
276
+ }
277
+ export async function handleSoftwareAgentTasksHttp(req, res, deps) {
278
+ if (!requireOwnerRequest(req, res, deps.options))
279
+ return;
280
+ const url = new URL(req.url || "/", "http://127.0.0.1");
281
+ const basePath = "/self/software-agent/tasks";
282
+ const taskLedgerDir = softwareAgentTaskLedgerDir(deps.workdir, deps.agentName);
283
+ if (url.pathname === basePath) {
284
+ const limit = readPositiveIntQuery(url.searchParams.get("limit"), 20, 100);
285
+ const tasks = listSoftwareAgentTaskRecords(taskLedgerDir, limit);
286
+ writeJsonResponse(res, 200, { tasks }, true);
165
287
  return;
166
288
  }
167
- if (!deps.softwareAgent) {
168
- res.writeHead(503, { "Content-Type": "application/json" })
169
- .end(JSON.stringify({ error: "Software agent peripheral not ready" }));
289
+ if (url.pathname.startsWith(`${basePath}/`)) {
290
+ const taskId = decodeURIComponent(url.pathname.slice(basePath.length + 1));
291
+ if (!taskId || taskId.includes("/")) {
292
+ writeJsonResponse(res, 400, { error: "Invalid software-agent task id" });
293
+ return;
294
+ }
295
+ const task = readSoftwareAgentTaskRecord(taskLedgerDir, taskId);
296
+ if (!task) {
297
+ writeJsonResponse(res, 404, { error: "Software-agent task not found" });
298
+ return;
299
+ }
300
+ writeJsonResponse(res, 200, { task }, true);
170
301
  return;
171
302
  }
172
- res.writeHead(200, { "Content-Type": "application/json" })
173
- .end(JSON.stringify(deps.softwareAgent.getState(), null, 2));
303
+ writeJsonResponse(res, 404, { error: "Software-agent task endpoint not found" });
304
+ }
305
+ function softwareAgentTaskLedgerDir(workdir, agentName) {
306
+ return join(workdir, ".akemon", "agents", agentName, "software-agent", "tasks");
307
+ }
308
+ function softwareAgentContextSessionDir(workdir, agentName) {
309
+ return join(workdir, ".akemon", "agents", agentName, "software-agent", "sessions");
310
+ }
311
+ function readPositiveIntQuery(value, fallback, max) {
312
+ if (!value)
313
+ return fallback;
314
+ const parsed = Number(value);
315
+ if (!Number.isInteger(parsed) || parsed <= 0)
316
+ return fallback;
317
+ return Math.min(parsed, max);
318
+ }
319
+ function writeSoftwareAgentStreamEvent(res, event) {
320
+ if (res.destroyed)
321
+ return;
322
+ res.write(`${JSON.stringify(redactSecrets(event))}\n`);
174
323
  }
175
324
  export async function handleSoftwareAgentResetHttp(req, res, deps) {
176
- if (!isOwnerRequest(req, deps.options)) {
177
- res.writeHead(401, { "Content-Type": "application/json" })
178
- .end(JSON.stringify({ error: "Owner token required" }));
325
+ if (!requireOwnerRequest(req, res, deps.options))
179
326
  return;
180
- }
181
- if (!deps.softwareAgent) {
182
- res.writeHead(503, { "Content-Type": "application/json" })
183
- .end(JSON.stringify({ error: "Software agent peripheral not ready" }));
327
+ const softwareAgent = requireSoftwareAgent(res, deps.softwareAgent);
328
+ if (!softwareAgent)
184
329
  return;
185
- }
186
330
  try {
187
- await deps.softwareAgent.resetSession();
188
- res.writeHead(200, { "Content-Type": "application/json" })
189
- .end(JSON.stringify({ ok: true, state: deps.softwareAgent.getState() }, null, 2));
331
+ await softwareAgent.resetSession();
332
+ writeJsonResponse(res, 200, { ok: true, state: softwareAgent.getState() }, true);
190
333
  }
191
334
  catch (err) {
192
- res.writeHead(500, { "Content-Type": "application/json" })
193
- .end(JSON.stringify({ error: err.message || String(err) }));
335
+ writeJsonResponse(res, 500, { error: err.message || String(err) });
194
336
  }
195
337
  }
196
338
  import { RelayPeripheral } from "./relay-peripheral.js";
@@ -205,9 +347,11 @@ import { LongTermModule } from "./longterm-module.js";
205
347
  import { ReflectionModule } from "./reflection-module.js";
206
348
  import { ScriptModule } from "./script-module.js";
207
349
  import { FileEventLog, PersistentEventBus } from "./event-bus.js";
208
- import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope } from "./software-agent-peripheral.js";
350
+ import { CodexSoftwareAgentPeripheral, createOwnerTaskEnvelope, listSoftwareAgentTaskRecords, readSoftwareAgentTaskRecord, } from "./software-agent-peripheral.js";
351
+ import { buildSoftwareAgentMemorySummary } from "./software-agent-memory.js";
209
352
  import { SIG, sig } from "./types.js";
210
353
  import { loadConversation, listConversations, buildLLMContext } from "./context.js";
354
+ import { redactSecrets } from "./redaction.js";
211
355
  import { createMcpServer, initMcpProxy, createMcpProxyServer } from "./mcp-server.js";
212
356
  import { autoRoute, runCollaborativeQuery } from "./agent-utils.js";
213
357
  // createMcpServer, initMcpProxy, createMcpProxyServer → see mcp-server.ts
@@ -216,11 +360,11 @@ const LLM_ENGINES = LLM_ENGINES_SET;
216
360
  // Engine execution — delegates to EnginePeripheral (V2 Step 3)
217
361
  // ---------------------------------------------------------------------------
218
362
  /** Unified engine runner — delegates to EnginePeripheral */
219
- function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, relay, signal, origin, routing, taskId) {
363
+ function runEngine(engine, model, allowAll, task, workdir, extraAllowedTools, relay, signal, origin, routing, taskId, routeRequest) {
220
364
  if (!_engineP) {
221
365
  throw new Error("Engine peripheral not initialized");
222
366
  }
223
- const result = _engineP.runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId);
367
+ const result = _engineP.runEngine(task, allowAll, extraAllowedTools, signal, origin, routing, taskId, routeRequest);
224
368
  // Sync trace back to module-level for reporting
225
369
  result.then(() => { lastEngineTrace = _engineP.lastTrace; }).catch(() => { lastEngineTrace = _engineP.lastTrace; });
226
370
  return result;
@@ -288,10 +432,20 @@ export async function serve(options) {
288
432
  if (!isQuiet)
289
433
  console.log(`[http] ${req.method} ${req.url} session=${req.headers["mcp-session-id"] || "none"}`);
290
434
  try {
435
+ if (req.url === "/self/software-agent/run-stream" && req.method === "POST") {
436
+ await handleSoftwareAgentRunStreamHttp(req, res, {
437
+ options,
438
+ workdir,
439
+ agentName: options.agentName,
440
+ softwareAgent: codexSoftwareAgent,
441
+ });
442
+ return;
443
+ }
291
444
  if (req.url === "/self/software-agent/run" && req.method === "POST") {
292
445
  await handleSoftwareAgentRunHttp(req, res, {
293
446
  options,
294
447
  workdir,
448
+ agentName: options.agentName,
295
449
  softwareAgent: codexSoftwareAgent,
296
450
  });
297
451
  return;
@@ -303,6 +457,16 @@ export async function serve(options) {
303
457
  });
304
458
  return;
305
459
  }
460
+ const requestPath = req.url?.split("?")[0] || "";
461
+ if (req.method === "GET"
462
+ && (requestPath === "/self/software-agent/tasks" || requestPath.startsWith("/self/software-agent/tasks/"))) {
463
+ await handleSoftwareAgentTasksHttp(req, res, {
464
+ options,
465
+ workdir,
466
+ agentName: options.agentName,
467
+ });
468
+ return;
469
+ }
306
470
  if (req.url === "/self/software-agent/reset" && req.method === "POST") {
307
471
  await handleSoftwareAgentResetHttp(req, res, {
308
472
  options,
@@ -498,10 +662,16 @@ export async function serve(options) {
498
662
  workdir,
499
663
  model: process.env.AKEMON_CODEX_MODEL,
500
664
  sandbox: "workspace-write",
665
+ taskLedgerDir: softwareAgentTaskLedgerDir(workdir, options.agentName),
666
+ contextSessionDir: softwareAgentContextSessionDir(workdir, options.agentName),
667
+ envPolicy: options.softwareAgentEnvPolicy,
668
+ envAllowlist: options.softwareAgentEnvAllowlist,
501
669
  });
502
- await codexSoftwareAgent.start(bus);
503
670
  // Peripheral registry — Core routes by capability
504
671
  const peripherals = [relay, engineP, codexSoftwareAgent];
672
+ for (const peripheral of peripherals) {
673
+ await peripheral.start(bus);
674
+ }
505
675
  // requestCompute: acquire the engine slot (priority-aware), execute with a
506
676
  // hard timeout, and release. The slot release and subprocess kill are both
507
677
  // driven by the same AbortController so a stuck engine can't hold the lock.
@@ -533,7 +703,7 @@ export async function serve(options) {
533
703
  const abortController = new AbortController();
534
704
  const timer = setTimeout(() => abortController.abort(), ENGINE_EXEC_TIMEOUT_MS);
535
705
  try {
536
- const response = await runEngine(options.engine || "claude", options.model, options.allowAll, prompt, workdir, req.tools, req.relay, abortController.signal, req.origin, routing, req.taskId);
706
+ const response = await runEngine(options.engine || "claude", options.model, options.allowAll, prompt, workdir, req.tools, req.relay, abortController.signal, req.origin, routing, req.taskId, req.engineHints);
537
707
  emitTokenUsage(prompt.length, response.length);
538
708
  return { success: true, response };
539
709
  }
@@ -0,0 +1,139 @@
1
+ import { buildLLMContext, loadConversation } from "./context.js";
2
+ import { buildRoleContext, loadRoles, resolveRoles } from "./role-module.js";
3
+ const DEFAULT_CONTEXT_BUDGET = 6000;
4
+ const OWNER_MEMORY_EXCLUDE_TERMS = [
5
+ "owner",
6
+ "private",
7
+ "personal",
8
+ "note",
9
+ "diary",
10
+ "bio",
11
+ ];
12
+ export async function buildSoftwareAgentMemorySummary(opts) {
13
+ const budget = opts.contextBudget ?? DEFAULT_CONTEXT_BUDGET;
14
+ const parts = [
15
+ "[Akemon memory boundary]",
16
+ `Role scope: ${opts.envelope.roleScope}`,
17
+ `Memory scope: ${opts.envelope.memoryScope}`,
18
+ boundaryDescription(opts.envelope.roleScope, opts.envelope.memoryScope),
19
+ ];
20
+ if (opts.envelope.memoryScope === "none") {
21
+ parts.push("No Akemon memory/context is included for this task.");
22
+ return parts.join("\n");
23
+ }
24
+ const request = normalizeRequest(opts.request);
25
+ const roleTrigger = readRequestString(request, "roleTrigger") || triggerForRoleScope(opts.envelope.roleScope);
26
+ const productName = readRequestString(request, "productName");
27
+ const productId = readRequestString(request, "productId");
28
+ const rolePolicy = await resolveRoleMemoryPolicy(opts.workdir, opts.agentName, roleTrigger);
29
+ if (rolePolicy.exclude.length) {
30
+ parts.push(`Active role exclusions: ${rolePolicy.exclude.join(", ")}`);
31
+ }
32
+ const roleContext = await buildRoleContext(opts.workdir, opts.agentName, roleTrigger, productName, productId);
33
+ if (roleContext.trim()) {
34
+ parts.push("");
35
+ parts.push("[Role/product context]");
36
+ parts.push(limitText(roleContext.trim(), Math.floor(budget * 0.55)));
37
+ }
38
+ const taskContext = readRequestString(request, "taskContext");
39
+ if (taskContext) {
40
+ parts.push("");
41
+ parts.push("[Task-provided context]");
42
+ parts.push(limitText(taskContext, Math.floor(budget * 0.25)));
43
+ }
44
+ const conversationId = readRequestString(request, "conversationId");
45
+ if (conversationId && canIncludeConversation(opts.envelope.roleScope, opts.envelope.memoryScope)) {
46
+ const conv = await loadConversation(opts.workdir, opts.agentName, conversationId);
47
+ const { text } = buildLLMContext(conv, Math.floor(budget * 0.3));
48
+ if (text.trim()) {
49
+ parts.push("");
50
+ parts.push("[Conversation context]");
51
+ parts.push(text.trim());
52
+ }
53
+ }
54
+ else if (conversationId) {
55
+ parts.push("");
56
+ parts.push("[Excluded conversation context]");
57
+ parts.push("A conversationId was supplied, but conversation memory is only included for owner-scoped software-agent tasks in v1.");
58
+ }
59
+ const ownerMemory = readRequestString(request, "memorySummary");
60
+ if (ownerMemory && canIncludeOwnerMemory(opts.envelope.roleScope, opts.envelope.memoryScope)) {
61
+ if (roleExcludesOwnerMemory(rolePolicy)) {
62
+ parts.push("");
63
+ parts.push("[Role-excluded owner memory]");
64
+ parts.push(`The active role (${rolePolicy.roleName || "unknown"}) excludes ${rolePolicy.exclude.join(", ")}, so owner-provided memory was not included.`);
65
+ }
66
+ else {
67
+ parts.push("");
68
+ parts.push("[Owner-visible memory]");
69
+ parts.push(limitText(ownerMemory, Math.floor(budget * 0.35)));
70
+ }
71
+ }
72
+ else if (ownerMemory) {
73
+ parts.push("");
74
+ parts.push("[Excluded owner memory]");
75
+ parts.push("A memorySummary was supplied, but it was not included because this envelope is not owner/owner scoped.");
76
+ }
77
+ return limitText(parts.join("\n"), budget);
78
+ }
79
+ export function canIncludeOwnerMemory(roleScope, memoryScope) {
80
+ return roleScope === "owner" && memoryScope === "owner";
81
+ }
82
+ function canIncludeConversation(roleScope, memoryScope) {
83
+ return roleScope === "owner" && (memoryScope === "owner" || memoryScope === "task");
84
+ }
85
+ function triggerForRoleScope(roleScope) {
86
+ switch (roleScope) {
87
+ case "owner": return "trigger:chat:owner";
88
+ case "public": return "trigger:chat:public";
89
+ case "order": return "trigger:order";
90
+ case "agent": return "trigger:agent_call";
91
+ case "system": return "trigger:system";
92
+ }
93
+ }
94
+ function boundaryDescription(roleScope, memoryScope) {
95
+ if (roleScope === "owner" && memoryScope === "owner") {
96
+ return "Owner-scoped task: owner-visible memory may be included after Akemon-side selection.";
97
+ }
98
+ if (memoryScope === "none") {
99
+ return "No-memory task: do not use Akemon private memory, conversation history, or subjective state.";
100
+ }
101
+ return "Non-owner task: exclude owner private conversations, personal notes, bio state, diary, subjective impressions, and owner-only memory.";
102
+ }
103
+ async function resolveRoleMemoryPolicy(workdir, agentName, roleTrigger) {
104
+ const roles = await loadRoles(workdir, agentName);
105
+ const { primary } = resolveRoles(roles, roleTrigger);
106
+ return {
107
+ roleName: primary?.name || null,
108
+ exclude: primary?.exclude || [],
109
+ };
110
+ }
111
+ function roleExcludesOwnerMemory(policy) {
112
+ return policy.exclude.some((item) => {
113
+ const normalized = item.toLowerCase();
114
+ return OWNER_MEMORY_EXCLUDE_TERMS.some((term) => normalized.includes(term));
115
+ });
116
+ }
117
+ function normalizeRequest(value) {
118
+ if (value === undefined || value === null)
119
+ return {};
120
+ if (typeof value !== "object" || Array.isArray(value)) {
121
+ throw new Error("Invalid request: expected object");
122
+ }
123
+ return value;
124
+ }
125
+ function readRequestString(request, field) {
126
+ const value = request[field];
127
+ if (value === undefined || value === null)
128
+ return "";
129
+ if (typeof value !== "string")
130
+ throw new Error(`Invalid ${field}: expected string`);
131
+ return value.trim();
132
+ }
133
+ function limitText(text, maxChars) {
134
+ if (text.length <= maxChars)
135
+ return text;
136
+ const head = Math.floor(maxChars * 0.45);
137
+ const tail = Math.max(0, maxChars - head - 40);
138
+ return `${text.slice(0, head)}\n[truncated ${text.length - head - tail} chars]\n${text.slice(-tail)}`;
139
+ }