@xfxstudio/claworld 2026.4.21-testing.2 → 2026.4.22-testing.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -25,6 +25,20 @@ export {
25
25
  LOCAL_AGENT_BOOTSTRAP_SCHEMA,
26
26
  LOCAL_AGENT_BOOTSTRAP_REQUIRED,
27
27
  } from './src/openclaw/index.js';
28
+ export {
29
+ CLAWORLD_WORKING_MEMORY_DIR,
30
+ CLAWORLD_WORKING_MEMORY_FILES,
31
+ CLAWORLD_MAINTENANCE_RUN_TYPES,
32
+ appendClaworldJournalEvent,
33
+ buildClaworldContextPointer,
34
+ buildClaworldMaintenanceEvent,
35
+ buildClaworldRuntimeMaintenanceEvent,
36
+ buildClaworldToolMaintenanceEvent,
37
+ ensureClaworldWorkingMemory,
38
+ readClaworldWorkingMemory,
39
+ runClaworldMemoryMaintenance,
40
+ validateClaworldMaintenanceOutput,
41
+ } from './src/openclaw/index.js';
28
42
  export { createClaworldLifecycleManager } from './src/openclaw/plugin/lifecycle.js';
29
43
  export { ClaworldRelayClient, createClaworldRelayClient } from './src/openclaw/plugin/relay-client.js';
30
44
 
@@ -8,7 +8,7 @@
8
8
  ],
9
9
  "name": "Claworld Persona Relay",
10
10
  "description": "Claworld relay world channel plugin for OpenClaw.",
11
- "version": "2026.4.21-testing.2",
11
+ "version": "2026.4.22-testing.1",
12
12
  "configSchema": {
13
13
  "type": "object",
14
14
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfxstudio/claworld",
3
- "version": "2026.4.21-testing.2",
3
+ "version": "2026.4.22-testing.1",
4
4
  "description": "Claworld channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -30,6 +30,12 @@ description: |
30
30
  message.
31
31
  - That document is internal. It is for you only. Do not quote it, paraphrase
32
32
  it, summarize it to the peer, or mention its section names.
33
+ - If workspace-local Claworld working memory is available and the current
34
+ intent needs prior Claworld progress, read `.claworld/INDEX.md` first and
35
+ follow its read order. Prefer summarized `NOW`, `MEMORY`, `PROFILE`, journal,
36
+ and report content over raw transcripts.
37
+ - Do not write `.claworld/` content into global `MEMORY.md`, and do not load
38
+ raw Claworld transcripts by default.
33
39
  - A single local session transcript may contain multiple accepted-chat intents
34
40
  over time. When a new full baseline appears, treat it as a fresh intent even
35
41
  if the same local session file is reused.
@@ -26,6 +26,8 @@ description: |
26
26
 
27
27
  先诊断,再修复;先走 canonical tool,再走 CLI fallback。
28
28
 
29
+ 如果用户问的是 Claworld 历史进展、已加入的 worlds、A2A 对话、Claworld 里遇到的人、机会/活动线索,先读工作区本地 `.claworld/INDEX.md`,再按它的 read order 读取 `context/NOW.md`、`context/MEMORY.md`、`context/PROFILE.md` 或 `reports/`。不要默认加载 raw transcripts,也不要把 `.claworld/` 内容自动 promotion 到全局 `MEMORY.md`。
30
+
29
31
  默认第一步:
30
32
 
31
33
  ```json
@@ -29,6 +29,14 @@ description: |
29
29
  - 如果必须引用技术信息,先翻译成人话,再附上最少量必要原文;不要整段转储工具返回。
30
30
  - 汇报重点放在:现在发生了什么、这对用户意味着什么、下一步该怎么做。
31
31
 
32
+ ## 本地 Claworld 工作记忆
33
+
34
+ 如果用户问的是 Claworld 历史进展、worlds、A2A 对话、Claworld 里遇到的人、机会/活动线索、之前推进到哪一步,先读工作区本地 `.claworld/INDEX.md`,再按里面的 read order 读取 `context/NOW.md`、`context/MEMORY.md`、`context/PROFILE.md` 或 `reports/`。
35
+
36
+ - `.claworld/` 是本地私有工作记忆,不是 backend public state。
37
+ - 默认只读摘要和索引,不加载 raw transcripts。
38
+ - 不要把 `.claworld/` 内容自动写入全局 `MEMORY.md`。
39
+
32
40
  ## claworld channel inter-session 汇报:announce / ANNOUNCE_SKIP 规则
33
41
 
34
42
  这份 skill 虽然不负责 claworld channel 内 live chat runtime 的对话推进,但 main session 仍可能收到来自 claworld channel session 的 inter-session 汇报、总结、完成事件或阶段性判断。
@@ -22,6 +22,20 @@ export {
22
22
  } from './plugin/relay-client.js';
23
23
  export { createRelayEventProtocol } from './protocol/relay-event-protocol.js';
24
24
  export { OPENCLAW_RUNTIME_PATH, createRuntimePathTrace } from './runtime/runtime-path.js';
25
+ export {
26
+ CLAWORLD_WORKING_MEMORY_DIR,
27
+ CLAWORLD_WORKING_MEMORY_FILES,
28
+ CLAWORLD_MAINTENANCE_RUN_TYPES,
29
+ appendClaworldJournalEvent,
30
+ buildClaworldContextPointer,
31
+ buildClaworldMaintenanceEvent,
32
+ buildClaworldRuntimeMaintenanceEvent,
33
+ buildClaworldToolMaintenanceEvent,
34
+ ensureClaworldWorkingMemory,
35
+ readClaworldWorkingMemory,
36
+ runClaworldMemoryMaintenance,
37
+ validateClaworldMaintenanceOutput,
38
+ } from './runtime/working-memory.js';
25
39
  export { createInboundSessionRouter } from './runtime/inbound-session-router.js';
26
40
  export { createOutboundSessionBridge } from './runtime/outbound-session-bridge.js';
27
41
  export { createSystemMessageOrchestrator } from './runtime/system-message-orchestrator.js';
@@ -11,6 +11,12 @@ import {
11
11
  projectToolWorldSearchResponse,
12
12
  } from '../runtime/tool-contracts.js';
13
13
  import { CLAWORLD_TOOL_CONTRACT_VERSION } from '../runtime/tool-inventory.js';
14
+ import {
15
+ appendClaworldJournalEvent,
16
+ buildClaworldContextPointer,
17
+ buildClaworldToolMaintenanceEvent,
18
+ ensureClaworldWorkingMemory,
19
+ } from '../runtime/working-memory.js';
14
20
  import { setClaworldRuntime } from './runtime.js';
15
21
  import {
16
22
  CHAT_REQUEST_APPROVAL_POLICY_MODES,
@@ -72,6 +78,74 @@ function buildClaworldStatusRoute(plugin) {
72
78
  };
73
79
  }
74
80
 
81
+ function firstWorkspaceCandidate(...sources) {
82
+ for (const source of sources) {
83
+ if (!source || typeof source !== 'object') continue;
84
+ const candidates = [
85
+ source.workspaceRoot,
86
+ source.workspaceDir,
87
+ source.workspacePath,
88
+ source.workspace,
89
+ source.cwd,
90
+ source.agent?.workspaceRoot,
91
+ source.agent?.workspaceDir,
92
+ source.agent?.workspace,
93
+ source.context?.workspaceRoot,
94
+ source.context?.workspaceDir,
95
+ source.context?.workspace,
96
+ source.session?.workspaceRoot,
97
+ source.session?.workspaceDir,
98
+ source.session?.workspace,
99
+ ];
100
+ for (const candidate of candidates) {
101
+ const normalized = normalizeText(candidate, null);
102
+ if (normalized) return normalized;
103
+ }
104
+ }
105
+ return null;
106
+ }
107
+
108
+ async function resolveHookWorkspaceRoot(api, event = {}, ctx = {}) {
109
+ const directCandidate = firstWorkspaceCandidate(event, ctx);
110
+ if (directCandidate) return directCandidate;
111
+ const agentId = normalizeText(ctx?.agentId ?? event?.agentId, null);
112
+ if (!agentId) return null;
113
+ const cfg = await loadCurrentConfig(api);
114
+ const agentEntry = cfg?.agents?.list?.[agentId] || cfg?.agents?.[agentId] || null;
115
+ return firstWorkspaceCandidate(agentEntry);
116
+ }
117
+
118
+ function getHookLogger(api) {
119
+ return api?.logger || api?.runtime?.logger || console;
120
+ }
121
+
122
+ function parseHookToolPayload(result) {
123
+ if (!result || typeof result !== 'object') return null;
124
+ const text = Array.isArray(result.content)
125
+ ? result.content.find((entry) => entry?.type === 'text' && typeof entry.text === 'string')?.text
126
+ : null;
127
+ if (!text) return null;
128
+ try {
129
+ const parsed = JSON.parse(text);
130
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+
136
+ function isSuccessfulHookToolCall(event = {}) {
137
+ if (event?.error || event?.isError === true) return false;
138
+ const result = event?.result ?? event?.output ?? event?.response ?? null;
139
+ if (result?.isError === true) return false;
140
+ const payload = parseHookToolPayload(result);
141
+ if (normalizeText(payload?.status, null) === 'error') return false;
142
+ return true;
143
+ }
144
+
145
+ function hookToolResult(event = {}) {
146
+ return event?.result ?? event?.output ?? event?.response ?? null;
147
+ }
148
+
75
149
  const CHAT_INBOX_FILTER_DIRECTIONS = Object.freeze([
76
150
  'inbound',
77
151
  'outbound',
@@ -1573,6 +1647,21 @@ export function registerClaworldPluginFull(api, plugin) {
1573
1647
  throw new Error('registerClaworldPluginFull requires a plugin instance');
1574
1648
  }
1575
1649
  if (typeof api.on === 'function') {
1650
+ api.on('before_prompt_build', async (event = {}, ctx = {}) => {
1651
+ const logger = getHookLogger(api);
1652
+ try {
1653
+ const workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
1654
+ if (workspaceRoot) {
1655
+ await ensureClaworldWorkingMemory(workspaceRoot);
1656
+ }
1657
+ } catch (error) {
1658
+ logger?.warn?.('[claworld:working-memory] unable to ensure workspace memory', error);
1659
+ }
1660
+ return {
1661
+ appendSystemContext: buildClaworldContextPointer(),
1662
+ };
1663
+ });
1664
+
1576
1665
  api.on('before_tool_call', async (event, ctx) => {
1577
1666
  if (event?.toolName !== 'claworld_request_chat') return;
1578
1667
  const requesterSessionKey = normalizeText(ctx?.sessionKey, null);
@@ -1584,6 +1673,27 @@ export function registerClaworldPluginFull(api, plugin) {
1584
1673
  },
1585
1674
  };
1586
1675
  });
1676
+
1677
+ api.on('after_tool_call', async (event = {}, ctx = {}) => {
1678
+ if (!isSuccessfulHookToolCall(event)) return;
1679
+ const toolName = normalizeText(event?.toolName ?? ctx?.toolName, null);
1680
+ if (!toolName || !toolName.startsWith('claworld_')) return;
1681
+ const logger = getHookLogger(api);
1682
+ try {
1683
+ const workspaceRoot = await resolveHookWorkspaceRoot(api, event, ctx);
1684
+ if (!workspaceRoot) return;
1685
+ const maintenanceEvent = buildClaworldToolMaintenanceEvent({
1686
+ toolName,
1687
+ params: event?.params || {},
1688
+ result: hookToolResult(event),
1689
+ timestamp: event?.timestamp || ctx?.timestamp || null,
1690
+ });
1691
+ if (!maintenanceEvent) return;
1692
+ await appendClaworldJournalEvent(workspaceRoot, maintenanceEvent);
1693
+ } catch (error) {
1694
+ logger?.warn?.('[claworld:working-memory] unable to append tool event', error);
1695
+ }
1696
+ });
1587
1697
  }
1588
1698
  if (typeof api.registerHttpRoute === 'function') {
1589
1699
  api.registerHttpRoute(buildClaworldStatusRoute(plugin));
@@ -0,0 +1,702 @@
1
+ import fs from 'fs/promises';
2
+ import os from 'os';
3
+ import path from 'path';
4
+
5
+ export const CLAWORLD_WORKING_MEMORY_DIR = '.claworld';
6
+ export const CLAWORLD_CONTEXT_DIR = 'context';
7
+ export const CLAWORLD_JOURNAL_DIR = 'journal';
8
+ export const CLAWORLD_REPORTS_DIR = 'reports';
9
+
10
+ export const CLAWORLD_WORKING_MEMORY_FILES = Object.freeze({
11
+ index: 'INDEX.md',
12
+ now: 'context/NOW.md',
13
+ profile: 'context/PROFILE.md',
14
+ memory: 'context/MEMORY.md',
15
+ });
16
+
17
+ export const CLAWORLD_WORKING_MEMORY_DIRECTORIES = Object.freeze([
18
+ CLAWORLD_WORKING_MEMORY_DIR,
19
+ `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_CONTEXT_DIR}`,
20
+ `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_JOURNAL_DIR}`,
21
+ `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_REPORTS_DIR}`,
22
+ ]);
23
+
24
+ export const CLAWORLD_MAINTENANCE_RUN_TYPES = Object.freeze({
25
+ L1_NOW_REFRESH: 'L1_NOW_REFRESH',
26
+ L2_MEMORY_PROFILE_REVIEW: 'L2_MEMORY_PROFILE_REVIEW',
27
+ L2_PROFILE_MEMORY_REVIEW: 'L2_MEMORY_PROFILE_REVIEW',
28
+ });
29
+
30
+ const MAINTENANCE_RUN_TYPE_ALIASES = Object.freeze({
31
+ L1_NOW_REFRESH: CLAWORLD_MAINTENANCE_RUN_TYPES.L1_NOW_REFRESH,
32
+ L2_MEMORY_PROFILE_REVIEW: CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW,
33
+ L2_PROFILE_MEMORY_REVIEW: CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW,
34
+ });
35
+
36
+ const FILE_TARGET_ALIASES = Object.freeze({
37
+ INDEX: CLAWORLD_WORKING_MEMORY_FILES.index,
38
+ 'INDEX.md': CLAWORLD_WORKING_MEMORY_FILES.index,
39
+ NOW: CLAWORLD_WORKING_MEMORY_FILES.now,
40
+ 'NOW.md': CLAWORLD_WORKING_MEMORY_FILES.now,
41
+ PROFILE: CLAWORLD_WORKING_MEMORY_FILES.profile,
42
+ 'PROFILE.md': CLAWORLD_WORKING_MEMORY_FILES.profile,
43
+ });
44
+
45
+ const L1_ALLOWED_TARGETS = new Set([
46
+ CLAWORLD_WORKING_MEMORY_FILES.now,
47
+ ]);
48
+
49
+ const L2_ALLOWED_TARGETS = new Set([
50
+ CLAWORLD_WORKING_MEMORY_FILES.now,
51
+ CLAWORLD_WORKING_MEMORY_FILES.profile,
52
+ CLAWORLD_WORKING_MEMORY_FILES.memory,
53
+ ]);
54
+
55
+ const MAX_EVENT_EXCERPT_CHARS = 600;
56
+ const MAX_MEMORY_SLICE_CHARS = 4000;
57
+
58
+ export function buildClaworldContextPointer() {
59
+ return [
60
+ '# Claworld Context Pointer',
61
+ '',
62
+ 'Claworld working memory is available at `.claworld/INDEX.md`.',
63
+ 'When the user asks about Claworld, worlds, A2A conversations, people met in Claworld, activity opportunities, or previous Claworld progress, read `.claworld/INDEX.md` first.',
64
+ 'Do not load raw Claworld transcripts by default.',
65
+ ].join('\n');
66
+ }
67
+
68
+ export function buildClaworldWorkingMemoryTemplates() {
69
+ return {
70
+ [CLAWORLD_WORKING_MEMORY_FILES.index]: [
71
+ '# Claworld Working Memory',
72
+ '',
73
+ 'This directory is the workspace-local private working memory for Claworld.',
74
+ 'Read this file first when the user asks about Claworld, worlds, A2A conversations, people met in Claworld, activity opportunities, or previous Claworld progress.',
75
+ '',
76
+ '## Read Order',
77
+ '- `context/NOW.md` for current Claworld focus, active worlds, and recent progress.',
78
+ '- `context/MEMORY.md` for durable Claworld facts and decisions.',
79
+ '- `context/PROFILE.md` for user preferences and profile hints relevant to Claworld.',
80
+ '- `journal/YYYY-MM.md` for append-only summarized events.',
81
+ '- `reports/` for generated local progress reports.',
82
+ '',
83
+ '## Rules',
84
+ '- Do not load raw Claworld transcripts by default.',
85
+ '- Do not write this content into global `MEMORY.md` automatically.',
86
+ '- Prefer short summaries and references over raw chat history.',
87
+ '- `context/PROFILE.md` and `context/MEMORY.md` are updated only by L2 maintenance review.',
88
+ '',
89
+ ].join('\n'),
90
+ [CLAWORLD_WORKING_MEMORY_FILES.now]: [
91
+ '# Claworld Now',
92
+ '',
93
+ '## Current Focus',
94
+ '- No active Claworld focus recorded yet.',
95
+ '',
96
+ '## Recent Activity',
97
+ '- No recent Claworld activity recorded yet.',
98
+ '',
99
+ '## Open Questions',
100
+ '- none',
101
+ '',
102
+ ].join('\n'),
103
+ [CLAWORLD_WORKING_MEMORY_FILES.profile]: [
104
+ '# Claworld Profile',
105
+ '',
106
+ '## Stable Preferences',
107
+ '- No Claworld-specific preferences recorded yet.',
108
+ '',
109
+ '## People And Context',
110
+ '- No Claworld people context recorded yet.',
111
+ '',
112
+ ].join('\n'),
113
+ [CLAWORLD_WORKING_MEMORY_FILES.memory]: [
114
+ '# Claworld Memory',
115
+ '',
116
+ '## Durable Facts',
117
+ '- No durable Claworld facts recorded yet.',
118
+ '',
119
+ '## Decisions',
120
+ '- No durable Claworld decisions recorded yet.',
121
+ '',
122
+ ].join('\n'),
123
+ };
124
+ }
125
+
126
+ export function buildClaworldWorkingMemoryFileSpecs() {
127
+ const templates = buildClaworldWorkingMemoryTemplates();
128
+ return Object.entries(templates).map(([relativePath, content]) => ({
129
+ relativePath: `${CLAWORLD_WORKING_MEMORY_DIR}/${relativePath}`,
130
+ workingMemoryRelativePath: relativePath,
131
+ policy: 'durable',
132
+ content,
133
+ }));
134
+ }
135
+
136
+ function normalizeText(value, fallback = null) {
137
+ if (value == null) return fallback;
138
+ const normalized = String(value).trim();
139
+ return normalized || fallback;
140
+ }
141
+
142
+ function expandUserPath(inputPath, homeDir = os.homedir()) {
143
+ const text = normalizeText(inputPath, null);
144
+ if (!text) return null;
145
+ if (text === '~') return homeDir;
146
+ if (text.startsWith('~/') || text.startsWith('~\\')) {
147
+ return path.join(homeDir, text.slice(2));
148
+ }
149
+ return text;
150
+ }
151
+
152
+ export function resolveClaworldWorkspaceRoot(options = {}, homeDir = os.homedir()) {
153
+ const source = typeof options === 'string'
154
+ ? options
155
+ : options?.workspaceRoot
156
+ ?? options?.workspacePath
157
+ ?? options?.workspaceDir
158
+ ?? options?.workspace
159
+ ?? options?.cwd
160
+ ?? process.cwd();
161
+ return path.resolve(expandUserPath(source, homeDir) || process.cwd());
162
+ }
163
+
164
+ export function resolveClaworldMemoryRoot(options = {}, homeDir = os.homedir()) {
165
+ return path.join(resolveClaworldWorkspaceRoot(options, homeDir), CLAWORLD_WORKING_MEMORY_DIR);
166
+ }
167
+
168
+ async function readTextIfPresent(filePath) {
169
+ try {
170
+ return await fs.readFile(filePath, 'utf8');
171
+ } catch (error) {
172
+ if (error && error.code === 'ENOENT') return null;
173
+ throw error;
174
+ }
175
+ }
176
+
177
+ async function atomicWriteText(filePath, content, {
178
+ backup = true,
179
+ rejectEmptyOverwrite = true,
180
+ } = {}) {
181
+ const nextContent = String(content ?? '');
182
+ const currentContent = await readTextIfPresent(filePath);
183
+ if (
184
+ rejectEmptyOverwrite
185
+ && currentContent != null
186
+ && normalizeText(currentContent, null)
187
+ && !normalizeText(nextContent, null)
188
+ ) {
189
+ throw new Error(`Refusing to overwrite non-empty Claworld memory file with empty content: ${filePath}`);
190
+ }
191
+
192
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
193
+ if (backup && currentContent != null && currentContent !== nextContent) {
194
+ await fs.writeFile(`${filePath}.bak`, currentContent, 'utf8');
195
+ }
196
+
197
+ const tempPath = path.join(
198
+ path.dirname(filePath),
199
+ `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
200
+ );
201
+ await fs.writeFile(tempPath, nextContent, 'utf8');
202
+ await fs.rename(tempPath, filePath);
203
+ }
204
+
205
+ export async function ensureClaworldWorkingMemory(options = {}, ensureOptions = {}) {
206
+ const workspaceRoot = resolveClaworldWorkspaceRoot(options, ensureOptions.homeDir || os.homedir());
207
+ const memoryRoot = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR);
208
+ const directories = CLAWORLD_WORKING_MEMORY_DIRECTORIES.map((relativePath) => ({
209
+ relativePath,
210
+ absolutePath: path.join(workspaceRoot, relativePath),
211
+ }));
212
+ const files = buildClaworldWorkingMemoryFileSpecs().map((file) => ({
213
+ ...file,
214
+ absolutePath: path.join(workspaceRoot, file.relativePath),
215
+ }));
216
+ const actions = [];
217
+
218
+ if (ensureOptions.dryRun === true) {
219
+ for (const directory of directories) {
220
+ actions.push(`mkdir -p ${directory.absolutePath}`);
221
+ }
222
+ for (const file of files) {
223
+ actions.push(`seed ${file.absolutePath} if missing`);
224
+ }
225
+ return {
226
+ ok: true,
227
+ dryRun: true,
228
+ workspaceRoot,
229
+ memoryRoot,
230
+ directories,
231
+ files,
232
+ actions,
233
+ };
234
+ }
235
+
236
+ for (const directory of directories) {
237
+ await fs.mkdir(directory.absolutePath, { recursive: true });
238
+ actions.push(`ensured ${directory.absolutePath}`);
239
+ }
240
+
241
+ for (const file of files) {
242
+ const currentContent = await readTextIfPresent(file.absolutePath);
243
+ if (currentContent == null) {
244
+ await atomicWriteText(file.absolutePath, file.content, {
245
+ backup: false,
246
+ rejectEmptyOverwrite: false,
247
+ });
248
+ actions.push(`created ${file.absolutePath}`);
249
+ } else {
250
+ actions.push(`preserved ${file.absolutePath}`);
251
+ }
252
+ }
253
+
254
+ return {
255
+ ok: true,
256
+ dryRun: false,
257
+ workspaceRoot,
258
+ memoryRoot,
259
+ directories,
260
+ files,
261
+ actions,
262
+ };
263
+ }
264
+
265
+ function toIsoTimestamp(value = null) {
266
+ const date = value instanceof Date ? value : new Date(value || Date.now());
267
+ if (Number.isNaN(date.getTime())) return new Date().toISOString();
268
+ return date.toISOString();
269
+ }
270
+
271
+ function toMonthKey(timestamp) {
272
+ return toIsoTimestamp(timestamp).slice(0, 7);
273
+ }
274
+
275
+ function truncateText(value, maxChars = MAX_EVENT_EXCERPT_CHARS) {
276
+ const text = normalizeText(value, '');
277
+ if (text.length <= maxChars) return text;
278
+ return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
279
+ }
280
+
281
+ function flattenInline(value) {
282
+ return truncateText(String(value ?? '').replace(/\s+/g, ' ').trim());
283
+ }
284
+
285
+ export function buildClaworldMaintenanceEvent(input = {}) {
286
+ const toolName = normalizeText(input.toolName, null);
287
+ const source = normalizeText(input.source, toolName ? 'claworld_tool' : 'claworld_runtime');
288
+ const kind = normalizeText(input.kind, toolName || 'milestone');
289
+ const timestamp = toIsoTimestamp(input.timestamp);
290
+ const summary = normalizeText(input.summary, toolName ? `${toolName} succeeded.` : 'Claworld milestone recorded.');
291
+ const refs = input.refs && typeof input.refs === 'object' && !Array.isArray(input.refs)
292
+ ? Object.fromEntries(
293
+ Object.entries(input.refs)
294
+ .map(([key, value]) => [key, normalizeText(value, null)])
295
+ .filter(([, value]) => value != null),
296
+ )
297
+ : {};
298
+ return {
299
+ id: normalizeText(input.id, `${source}:${kind}:${timestamp}`),
300
+ timestamp,
301
+ source,
302
+ kind,
303
+ summary,
304
+ excerpt: truncateText(input.excerpt || ''),
305
+ refs,
306
+ };
307
+ }
308
+
309
+ export function buildClaworldRuntimeMaintenanceEvent(input = {}) {
310
+ return buildClaworldMaintenanceEvent({
311
+ ...input,
312
+ source: normalizeText(input.source, 'claworld_runtime'),
313
+ kind: normalizeText(input.kind, 'milestone'),
314
+ });
315
+ }
316
+
317
+ function parseToolResultPayload(result) {
318
+ if (!result || typeof result !== 'object') return null;
319
+ if (result.payload && typeof result.payload === 'object' && !Array.isArray(result.payload)) {
320
+ return result.payload;
321
+ }
322
+ const textContent = Array.isArray(result.content)
323
+ ? result.content.find((entry) => entry?.type === 'text' && typeof entry.text === 'string')?.text
324
+ : null;
325
+ if (!textContent) return null;
326
+ try {
327
+ const parsed = JSON.parse(textContent);
328
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
329
+ } catch {
330
+ return null;
331
+ }
332
+ }
333
+
334
+ function compactResultPayload(payload = {}) {
335
+ const keys = [
336
+ 'status',
337
+ 'tool',
338
+ 'accountId',
339
+ 'worldId',
340
+ 'displayName',
341
+ 'chatRequestId',
342
+ 'conversationKey',
343
+ 'candidateId',
344
+ 'feedbackId',
345
+ 'nextAction',
346
+ 'requiredAction',
347
+ 'summary',
348
+ 'message',
349
+ ];
350
+ const compact = {};
351
+ for (const key of keys) {
352
+ const value = normalizeText(payload[key], null);
353
+ if (value != null) compact[key] = value;
354
+ }
355
+ return compact;
356
+ }
357
+
358
+ export function buildClaworldToolMaintenanceEvent({
359
+ toolName,
360
+ params = {},
361
+ result = null,
362
+ timestamp = null,
363
+ } = {}) {
364
+ const normalizedToolName = normalizeText(toolName, null);
365
+ if (!normalizedToolName || !normalizedToolName.startsWith('claworld_')) return null;
366
+ const payload = parseToolResultPayload(result) || {};
367
+ const compactPayload = compactResultPayload(payload);
368
+ const refs = {
369
+ accountId: params.accountId || payload.accountId,
370
+ worldId: params.worldId || payload.worldId,
371
+ chatRequestId: params.chatRequestId || payload.chatRequestId,
372
+ conversationKey: params.conversationKey || payload.conversationKey,
373
+ candidateId: params.candidateId || payload.candidateId,
374
+ agentCode: params.agentCode || payload.agentCode,
375
+ };
376
+ return buildClaworldMaintenanceEvent({
377
+ source: 'claworld_tool',
378
+ kind: normalizedToolName,
379
+ toolName: normalizedToolName,
380
+ timestamp,
381
+ refs,
382
+ summary: `${normalizedToolName} succeeded.`,
383
+ excerpt: Object.keys(compactPayload).length > 0
384
+ ? JSON.stringify(compactPayload)
385
+ : null,
386
+ });
387
+ }
388
+
389
+ function formatRefs(refs = {}) {
390
+ return Object.entries(refs)
391
+ .filter(([, value]) => value != null)
392
+ .map(([key, value]) => `${key}=${flattenInline(value)}`)
393
+ .join(', ');
394
+ }
395
+
396
+ export async function appendClaworldJournalEvent(options = {}, event = {}, appendOptions = {}) {
397
+ const workspaceRoot = resolveClaworldWorkspaceRoot(options, appendOptions.homeDir || os.homedir());
398
+ await ensureClaworldWorkingMemory(workspaceRoot, appendOptions);
399
+ const normalizedEvent = buildClaworldMaintenanceEvent(event);
400
+ const monthKey = toMonthKey(normalizedEvent.timestamp);
401
+ const journalPath = path.join(
402
+ workspaceRoot,
403
+ CLAWORLD_WORKING_MEMORY_DIR,
404
+ CLAWORLD_JOURNAL_DIR,
405
+ `${monthKey}.md`,
406
+ );
407
+ const currentContent = await readTextIfPresent(journalPath);
408
+ const refsLine = formatRefs(normalizedEvent.refs);
409
+ const entry = [
410
+ `## ${normalizedEvent.timestamp} - ${normalizedEvent.kind}`,
411
+ `- Source: ${normalizedEvent.source}`,
412
+ `- Summary: ${flattenInline(normalizedEvent.summary)}`,
413
+ normalizedEvent.excerpt ? `- Excerpt: ${flattenInline(normalizedEvent.excerpt)}` : null,
414
+ refsLine ? `- Refs: ${refsLine}` : null,
415
+ '',
416
+ ].filter((line) => line != null).join('\n');
417
+ if (currentContent == null) {
418
+ await atomicWriteText(journalPath, `# Claworld Journal ${monthKey}\n\n${entry}`, {
419
+ backup: false,
420
+ rejectEmptyOverwrite: false,
421
+ });
422
+ } else {
423
+ await fs.appendFile(journalPath, `${currentContent.endsWith('\n') ? '' : '\n'}${entry}`, 'utf8');
424
+ }
425
+ return {
426
+ ok: true,
427
+ journalPath,
428
+ event: normalizedEvent,
429
+ };
430
+ }
431
+
432
+ export async function readClaworldWorkingMemory(options = {}, readOptions = {}) {
433
+ const workspaceRoot = resolveClaworldWorkspaceRoot(options, readOptions.homeDir || os.homedir());
434
+ const maxCharsPerFile = Number.isInteger(readOptions.maxCharsPerFile) && readOptions.maxCharsPerFile > 0
435
+ ? readOptions.maxCharsPerFile
436
+ : MAX_MEMORY_SLICE_CHARS;
437
+ const slices = {};
438
+ for (const relativePath of Object.values(CLAWORLD_WORKING_MEMORY_FILES)) {
439
+ const absolutePath = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR, relativePath);
440
+ const content = await readTextIfPresent(absolutePath);
441
+ slices[relativePath] = content == null
442
+ ? null
443
+ : {
444
+ content: content.length > maxCharsPerFile ? content.slice(0, maxCharsPerFile) : content,
445
+ truncated: content.length > maxCharsPerFile,
446
+ };
447
+ }
448
+ return {
449
+ workspaceRoot,
450
+ memoryRoot: path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR),
451
+ slices,
452
+ };
453
+ }
454
+
455
+ function normalizePatchOperation(operation) {
456
+ const normalized = normalizeText(operation, 'replace');
457
+ if (normalized === 'replace' || normalized === 'append_section' || normalized === 'no_op') {
458
+ return normalized;
459
+ }
460
+ throw new Error(`Unsupported Claworld maintenance patch operation: ${normalized}`);
461
+ }
462
+
463
+ function normalizeMaintenanceRunType(runType) {
464
+ const normalized = normalizeText(runType, null);
465
+ const alias = MAINTENANCE_RUN_TYPE_ALIASES[normalized];
466
+ if (alias) return alias;
467
+ throw new Error(`Unsupported Claworld maintenance run type: ${runType}`);
468
+ }
469
+
470
+ function stripClaworldPrefix(rawTarget) {
471
+ let target = String(rawTarget || '').replace(/\\/g, '/').trim();
472
+ target = target.replace(/^\/+/, '');
473
+ if (target.startsWith(`${CLAWORLD_WORKING_MEMORY_DIR}/`)) {
474
+ target = target.slice(CLAWORLD_WORKING_MEMORY_DIR.length + 1);
475
+ }
476
+ return target;
477
+ }
478
+
479
+ function normalizePatchTarget(rawTarget) {
480
+ const stripped = stripClaworldPrefix(rawTarget);
481
+ const aliasTarget = FILE_TARGET_ALIASES[stripped] || null;
482
+ if (aliasTarget) return aliasTarget;
483
+ if (stripped === 'MEMORY.md') {
484
+ throw new Error('Global MEMORY.md is not a valid Claworld working-memory target; use context/MEMORY.md.');
485
+ }
486
+ const normalized = path.posix.normalize(stripped);
487
+ if (!normalized || normalized === '.' || normalized.startsWith('../') || normalized === '..' || path.posix.isAbsolute(normalized)) {
488
+ throw new Error(`Invalid Claworld maintenance patch target: ${rawTarget}`);
489
+ }
490
+ return normalized;
491
+ }
492
+
493
+ function isAllowedTarget(runType, target) {
494
+ if (target.startsWith(`${CLAWORLD_JOURNAL_DIR}/`) || target.startsWith(`${CLAWORLD_REPORTS_DIR}/`)) {
495
+ return true;
496
+ }
497
+ if (runType === CLAWORLD_MAINTENANCE_RUN_TYPES.L1_NOW_REFRESH) {
498
+ return L1_ALLOWED_TARGETS.has(target);
499
+ }
500
+ if (runType === CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW) {
501
+ return L2_ALLOWED_TARGETS.has(target);
502
+ }
503
+ return false;
504
+ }
505
+
506
+ function assertAllowedTarget(runType, target) {
507
+ if (!isAllowedTarget(runType, target)) {
508
+ throw new Error(`Claworld maintenance run ${runType} cannot write target ${target}`);
509
+ }
510
+ }
511
+
512
+ function normalizeReportPatch(report, index) {
513
+ const filename = normalizeText(report?.filename ?? report?.name, `report-${index + 1}.md`);
514
+ const normalizedFilename = path.posix.basename(filename.endsWith('.md') ? filename : `${filename}.md`);
515
+ return {
516
+ operation: 'replace',
517
+ target: `${CLAWORLD_REPORTS_DIR}/${normalizedFilename}`,
518
+ content: String(report?.md ?? report?.content ?? report?.text ?? ''),
519
+ };
520
+ }
521
+
522
+ function hasOwn(value, key) {
523
+ return Object.prototype.hasOwnProperty.call(value, key);
524
+ }
525
+
526
+ function readPatchContent(patch, fieldName) {
527
+ if (hasOwn(patch, 'content')) return patch.content;
528
+ if (hasOwn(patch, 'md')) return patch.md;
529
+ if (hasOwn(patch, 'text')) return patch.text;
530
+ throw new Error(`Claworld maintenance FilePatch ${fieldName} requires content for replace or append_section.`);
531
+ }
532
+
533
+ function normalizeFilePatchValue(value, target, fieldName) {
534
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
535
+ const operation = normalizePatchOperation(value.operation);
536
+ const normalized = {
537
+ operation,
538
+ target,
539
+ content: operation === 'no_op' ? '' : String(readPatchContent(value, fieldName) ?? ''),
540
+ };
541
+ const rationale = normalizeText(value.rationale, null);
542
+ if (rationale) normalized.rationale = rationale;
543
+ return normalized;
544
+ }
545
+ return {
546
+ operation: 'replace',
547
+ target,
548
+ content: String(value ?? ''),
549
+ };
550
+ }
551
+
552
+ export function normalizeClaworldMaintenanceOutput(runType, output = {}, options = {}) {
553
+ if (!output || typeof output !== 'object' || Array.isArray(output)) {
554
+ throw new Error('Claworld maintenance output must be an object.');
555
+ }
556
+ const normalizedRunType = normalizeMaintenanceRunType(runType);
557
+ const patches = [];
558
+ if (Object.prototype.hasOwnProperty.call(output, 'nowMd')) {
559
+ patches.push(normalizeFilePatchValue(output.nowMd, CLAWORLD_WORKING_MEMORY_FILES.now, 'nowMd'));
560
+ }
561
+ if (Object.prototype.hasOwnProperty.call(output, 'profileMd')) {
562
+ patches.push(normalizeFilePatchValue(output.profileMd, CLAWORLD_WORKING_MEMORY_FILES.profile, 'profileMd'));
563
+ }
564
+ if (Object.prototype.hasOwnProperty.call(output, 'memoryMd')) {
565
+ patches.push(normalizeFilePatchValue(output.memoryMd, CLAWORLD_WORKING_MEMORY_FILES.memory, 'memoryMd'));
566
+ }
567
+ if (Object.prototype.hasOwnProperty.call(output, 'journalAppendMd')) {
568
+ const monthKey = toMonthKey(options.timestamp || output.timestamp || Date.now());
569
+ patches.push({
570
+ operation: 'append_section',
571
+ target: `${CLAWORLD_JOURNAL_DIR}/${monthKey}.md`,
572
+ content: String(output.journalAppendMd ?? ''),
573
+ });
574
+ }
575
+ if (Array.isArray(output.reports)) {
576
+ output.reports.forEach((report, index) => {
577
+ patches.push(normalizeReportPatch(report, index));
578
+ });
579
+ }
580
+ if (Array.isArray(output.patches)) {
581
+ for (const patch of output.patches) {
582
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
583
+ throw new Error('Claworld maintenance patches entries must be FilePatch objects.');
584
+ }
585
+ const operation = normalizePatchOperation(patch.operation);
586
+ patches.push({
587
+ operation,
588
+ target: normalizePatchTarget(patch.target ?? patch.path ?? patch.relativePath),
589
+ content: operation === 'no_op' ? '' : String(readPatchContent(patch, 'patches[]') ?? ''),
590
+ rationale: normalizeText(patch.rationale, null),
591
+ });
592
+ }
593
+ }
594
+
595
+ const normalizedPatches = patches.map((patch) => {
596
+ const target = normalizePatchTarget(patch.target);
597
+ const operation = normalizePatchOperation(patch.operation);
598
+ assertAllowedTarget(normalizedRunType, target);
599
+ const normalizedPatch = {
600
+ operation,
601
+ target,
602
+ content: String(patch.content ?? ''),
603
+ };
604
+ const rationale = normalizeText(patch.rationale, null);
605
+ if (rationale) normalizedPatch.rationale = rationale;
606
+ return normalizedPatch;
607
+ });
608
+
609
+ return {
610
+ runType: normalizedRunType,
611
+ noOpReason: normalizeText(output.noOpReason, null),
612
+ patches: normalizedPatches,
613
+ };
614
+ }
615
+
616
+ export function validateClaworldMaintenanceOutput(runType, output = {}, options = {}) {
617
+ return normalizeClaworldMaintenanceOutput(runType, output, options);
618
+ }
619
+
620
+ async function applyMaintenancePatch(workspaceRoot, patch) {
621
+ if (patch.operation === 'no_op') {
622
+ return {
623
+ operation: patch.operation,
624
+ target: patch.target,
625
+ applied: false,
626
+ };
627
+ }
628
+ const absolutePath = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR, patch.target);
629
+ if (patch.operation === 'append_section') {
630
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
631
+ const currentContent = await readTextIfPresent(absolutePath);
632
+ if (currentContent == null) {
633
+ const monthKey = path.basename(patch.target, '.md');
634
+ await atomicWriteText(absolutePath, `# Claworld Journal ${monthKey}\n\n${patch.content}`, {
635
+ backup: false,
636
+ rejectEmptyOverwrite: false,
637
+ });
638
+ } else {
639
+ await fs.appendFile(
640
+ absolutePath,
641
+ `${currentContent.endsWith('\n') ? '' : '\n'}${patch.content}`,
642
+ 'utf8',
643
+ );
644
+ }
645
+ return {
646
+ operation: patch.operation,
647
+ target: patch.target,
648
+ applied: true,
649
+ };
650
+ }
651
+
652
+ await atomicWriteText(absolutePath, patch.content, {
653
+ backup: true,
654
+ rejectEmptyOverwrite: true,
655
+ });
656
+ return {
657
+ operation: patch.operation,
658
+ target: patch.target,
659
+ applied: true,
660
+ };
661
+ }
662
+
663
+ export async function runClaworldMemoryMaintenance(runType, requestBundle = {}, options = {}) {
664
+ const normalizedRunType = normalizeMaintenanceRunType(runType);
665
+ const workspaceRoot = resolveClaworldWorkspaceRoot(
666
+ options.workspaceRoot || requestBundle.workspaceRoot || requestBundle.workspaceDir || process.cwd(),
667
+ options.homeDir || os.homedir(),
668
+ );
669
+ await ensureClaworldWorkingMemory(workspaceRoot, options);
670
+ const workingMemory = await readClaworldWorkingMemory(workspaceRoot, options);
671
+ const request = {
672
+ ...requestBundle,
673
+ runType: normalizedRunType,
674
+ workspaceRoot,
675
+ workingMemory,
676
+ };
677
+ const output = options.output
678
+ ?? (typeof options.maintenanceRunner === 'function'
679
+ ? await options.maintenanceRunner(request)
680
+ : null);
681
+ if (!output) {
682
+ return {
683
+ ok: true,
684
+ runType: normalizedRunType,
685
+ noOpReason: 'no_maintenance_runner',
686
+ request,
687
+ applied: [],
688
+ };
689
+ }
690
+ const normalized = validateClaworldMaintenanceOutput(normalizedRunType, output, options);
691
+ const applied = [];
692
+ for (const patch of normalized.patches) {
693
+ applied.push(await applyMaintenancePatch(workspaceRoot, patch));
694
+ }
695
+ return {
696
+ ok: true,
697
+ runType: normalizedRunType,
698
+ noOpReason: normalized.noOpReason,
699
+ request,
700
+ applied,
701
+ };
702
+ }