@xfxstudio/claworld 2026.4.22-testing.5 → 2026.4.22-testing.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1086 @@
1
+ import fs from 'fs/promises';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { resolveOpenClawWorkspaceRoot } from './workspace-resolver.js';
5
+
6
+ export const CLAWORLD_WORKING_MEMORY_DIR = '.claworld';
7
+ export const CLAWORLD_CONTEXT_DIR = 'context';
8
+ export const CLAWORLD_JOURNAL_DIR = 'journal';
9
+ export const CLAWORLD_REPORTS_DIR = 'reports';
10
+
11
+ export const CLAWORLD_WORKING_MEMORY_FILES = Object.freeze({
12
+ index: 'INDEX.md',
13
+ now: 'context/NOW.md',
14
+ profile: 'context/PROFILE.md',
15
+ memory: 'context/MEMORY.md',
16
+ });
17
+
18
+ export const CLAWORLD_WORKING_MEMORY_DIRECTORIES = Object.freeze([
19
+ CLAWORLD_WORKING_MEMORY_DIR,
20
+ `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_CONTEXT_DIR}`,
21
+ `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_JOURNAL_DIR}`,
22
+ `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_REPORTS_DIR}`,
23
+ ]);
24
+
25
+ export const CLAWORLD_BOOTSTRAP_TARGETS = Object.freeze({
26
+ MAIN: 'main',
27
+ MANAGEMENT: 'management',
28
+ CLAWORLD_CONVERSATION: 'claworld_conversation',
29
+ NONE: 'none',
30
+ });
31
+
32
+ export const CLAWORLD_MAINTENANCE_RUN_TYPES = Object.freeze({
33
+ L1_NOW_REFRESH: 'L1_NOW_REFRESH',
34
+ L2_MEMORY_PROFILE_REVIEW: 'L2_MEMORY_PROFILE_REVIEW',
35
+ });
36
+
37
+ const MAINTENANCE_RUN_TYPE_VALUES = new Set(Object.values(CLAWORLD_MAINTENANCE_RUN_TYPES));
38
+
39
+ const L1_ALLOWED_TARGETS = new Set([
40
+ CLAWORLD_WORKING_MEMORY_FILES.now,
41
+ ]);
42
+
43
+ const L2_ALLOWED_TARGETS = new Set([
44
+ CLAWORLD_WORKING_MEMORY_FILES.now,
45
+ CLAWORLD_WORKING_MEMORY_FILES.profile,
46
+ CLAWORLD_WORKING_MEMORY_FILES.memory,
47
+ ]);
48
+
49
+ const MAX_EVENT_EXCERPT_CHARS = 600;
50
+ const MAX_MEMORY_SLICE_CHARS = 4000;
51
+ const MAX_BOOTSTRAP_FILE_CHARS = 2200;
52
+ const MAX_BOOTSTRAP_TOTAL_CHARS = 6000;
53
+
54
+ const MAIN_BOOTSTRAP_FILES = Object.freeze([
55
+ CLAWORLD_WORKING_MEMORY_FILES.memory,
56
+ ]);
57
+
58
+ const MANAGEMENT_BOOTSTRAP_FILES = Object.freeze([
59
+ CLAWORLD_WORKING_MEMORY_FILES.profile,
60
+ CLAWORLD_WORKING_MEMORY_FILES.memory,
61
+ CLAWORLD_WORKING_MEMORY_FILES.now,
62
+ ]);
63
+
64
+ const CONVERSATION_BOOTSTRAP_FILES = Object.freeze([
65
+ CLAWORLD_WORKING_MEMORY_FILES.now,
66
+ CLAWORLD_WORKING_MEMORY_FILES.memory,
67
+ CLAWORLD_WORKING_MEMORY_FILES.profile,
68
+ ]);
69
+
70
+ export function buildClaworldContextPointer() {
71
+ return [
72
+ '# Claworld Context Pointer',
73
+ '',
74
+ 'Claworld working memory is available at `.claworld/INDEX.md`.',
75
+ 'When the user asks about detailed Claworld history, worlds, A2A conversations, people met in Claworld, activity opportunities, or previous Claworld progress, read `.claworld/INDEX.md` first.',
76
+ 'Do not load raw Claworld transcripts by default.',
77
+ 'Do not treat open Claworld loops as ordinary main-session todos before checking `.claworld/INDEX.md`.',
78
+ ].join('\n');
79
+ }
80
+
81
+ function normalizeBootstrapTotalChars(rawValue, fallback) {
82
+ return Number.isInteger(rawValue) && rawValue >= 0
83
+ ? rawValue
84
+ : fallback;
85
+ }
86
+
87
+ function truncateBootstrapText(text, maxChars, note = '\n\n_(Truncated to fit the total Claworld bootstrap budget.)_') {
88
+ const normalizedText = String(text || '');
89
+ if (normalizedText.length <= maxChars) {
90
+ return {
91
+ text: normalizedText,
92
+ truncated: false,
93
+ };
94
+ }
95
+ if (maxChars <= 0) {
96
+ return {
97
+ text: '',
98
+ truncated: normalizedText.length > 0,
99
+ };
100
+ }
101
+ if (note.length >= maxChars) {
102
+ return {
103
+ text: normalizedText.slice(0, maxChars),
104
+ truncated: true,
105
+ };
106
+ }
107
+ const excerpt = normalizedText.slice(0, maxChars - note.length).trimEnd();
108
+ if (!excerpt) {
109
+ return {
110
+ text: normalizedText.slice(0, maxChars),
111
+ truncated: true,
112
+ };
113
+ }
114
+ return {
115
+ text: `${excerpt}${note}`,
116
+ truncated: true,
117
+ };
118
+ }
119
+
120
+ function measureBootstrapParts(parts) {
121
+ return parts.filter(Boolean).join('\n\n').length;
122
+ }
123
+
124
+ export function buildClaworldWorkingMemoryTemplates() {
125
+ return {
126
+ [CLAWORLD_WORKING_MEMORY_FILES.index]: [
127
+ '# Claworld Working Memory',
128
+ '',
129
+ 'This directory is the workspace-local private working memory for Claworld.',
130
+ 'Read this file first when the user asks about Claworld, worlds, A2A conversations, people met in Claworld, activity opportunities, or previous Claworld progress.',
131
+ '',
132
+ '## Read Order',
133
+ '- `context/NOW.md` for current Claworld focus, active worlds, and recent progress.',
134
+ '- `context/MEMORY.md` for durable Claworld facts and decisions.',
135
+ '- `context/PROFILE.md` for user preferences and profile hints relevant to Claworld.',
136
+ '- `journal/YYYY-MM.md` for append-only summarized events.',
137
+ '- `reports/` for generated local progress reports.',
138
+ '',
139
+ '## Rules',
140
+ '- Do not load raw Claworld transcripts by default.',
141
+ '- Do not write this content into global `MEMORY.md` automatically.',
142
+ '- Prefer short summaries and references over raw chat history.',
143
+ '- `context/PROFILE.md` and `context/MEMORY.md` are updated only by L2 maintenance review.',
144
+ '',
145
+ ].join('\n'),
146
+ [CLAWORLD_WORKING_MEMORY_FILES.now]: [
147
+ '# Claworld Now',
148
+ '',
149
+ '## Current Focus',
150
+ '- No active Claworld focus recorded yet.',
151
+ '',
152
+ '## Recent Activity',
153
+ '- No recent Claworld activity recorded yet.',
154
+ '',
155
+ '## Open Questions',
156
+ '- none',
157
+ '',
158
+ ].join('\n'),
159
+ [CLAWORLD_WORKING_MEMORY_FILES.profile]: [
160
+ '# Claworld Profile',
161
+ '',
162
+ '## Stable Preferences',
163
+ '- No Claworld-specific preferences recorded yet.',
164
+ '',
165
+ '## People And Context',
166
+ '- No Claworld people context recorded yet.',
167
+ '',
168
+ ].join('\n'),
169
+ [CLAWORLD_WORKING_MEMORY_FILES.memory]: [
170
+ '# Claworld Memory',
171
+ '',
172
+ '## Durable Facts',
173
+ '- No durable Claworld facts recorded yet.',
174
+ '',
175
+ '## Decisions',
176
+ '- No durable Claworld decisions recorded yet.',
177
+ '',
178
+ ].join('\n'),
179
+ };
180
+ }
181
+
182
+ export function buildClaworldWorkingMemoryFileSpecs() {
183
+ const templates = buildClaworldWorkingMemoryTemplates();
184
+ return Object.entries(templates).map(([relativePath, content]) => ({
185
+ relativePath: `${CLAWORLD_WORKING_MEMORY_DIR}/${relativePath}`,
186
+ workingMemoryRelativePath: relativePath,
187
+ policy: 'durable',
188
+ content,
189
+ }));
190
+ }
191
+
192
+ function normalizeText(value, fallback = null) {
193
+ if (value == null) return fallback;
194
+ const normalized = String(value).trim();
195
+ return normalized || fallback;
196
+ }
197
+
198
+ function isPlainObject(value) {
199
+ return value && typeof value === 'object' && !Array.isArray(value);
200
+ }
201
+
202
+ function normalizeSessionType(value) {
203
+ const normalized = normalizeText(value, null);
204
+ return normalized ? normalized.toLowerCase().replace(/[\s-]+/g, '_') : null;
205
+ }
206
+
207
+ function expandUserPath(inputPath, homeDir = os.homedir()) {
208
+ const text = normalizeText(inputPath, null);
209
+ if (!text) return null;
210
+ if (text === '~') return homeDir;
211
+ if (text.startsWith('~/') || text.startsWith('~\\')) {
212
+ return path.join(homeDir, text.slice(2));
213
+ }
214
+ return text;
215
+ }
216
+
217
+ export function resolveClaworldWorkspaceRoot(options = {}, homeDir = os.homedir()) {
218
+ const source = typeof options === 'string'
219
+ ? options
220
+ : options?.workspaceRoot
221
+ ?? options?.workspacePath
222
+ ?? options?.workspaceDir
223
+ ?? options?.workspace
224
+ ?? options?.cwd
225
+ ?? process.cwd();
226
+ return path.resolve(expandUserPath(source, homeDir) || process.cwd());
227
+ }
228
+
229
+ export function resolveClaworldMemoryRoot(options = {}, homeDir = os.homedir()) {
230
+ return path.join(resolveClaworldWorkspaceRoot(options, homeDir), CLAWORLD_WORKING_MEMORY_DIR);
231
+ }
232
+
233
+ function collectBootstrapRecords(source, records) {
234
+ if (!isPlainObject(source)) return;
235
+ records.push(source);
236
+ for (const key of ['session', 'context', 'metadata', 'runtimeContext', 'delivery']) {
237
+ if (isPlainObject(source[key])) {
238
+ records.push(source[key]);
239
+ }
240
+ }
241
+ }
242
+
243
+ function firstBootstrapField(records, keys) {
244
+ for (const record of records) {
245
+ for (const key of keys) {
246
+ const value = normalizeText(record[key], null);
247
+ if (value) return value;
248
+ }
249
+ }
250
+ return null;
251
+ }
252
+
253
+ function buildBootstrapFileLabel(relativePath) {
254
+ return `${CLAWORLD_WORKING_MEMORY_DIR}/${relativePath}`;
255
+ }
256
+
257
+ function buildBootstrapSection(displayPath, content, note = null) {
258
+ return [
259
+ `## \`${displayPath}\``,
260
+ String(content || '').trimEnd(),
261
+ note ? `_${note}_` : null,
262
+ ].filter((line) => line != null && line !== '').join('\n');
263
+ }
264
+
265
+ function isMainBootstrapContext({ sessionKey = null, sessionType = null } = {}) {
266
+ const normalizedSessionType = normalizeSessionType(sessionType);
267
+ if (normalizedSessionType === 'main' || normalizedSessionType === 'main_session') {
268
+ return true;
269
+ }
270
+ return /^agent:[^:]+:main(?:$|:)/i.test(normalizeText(sessionKey, ''));
271
+ }
272
+
273
+ function isManagementBootstrapContext({ sessionKey = null, sessionType = null } = {}) {
274
+ const normalizedSessionType = normalizeSessionType(sessionType);
275
+ if (normalizedSessionType === 'management' || normalizedSessionType === 'management_session') {
276
+ return true;
277
+ }
278
+ return /^management:[^:]+/i.test(normalizeText(sessionKey, ''));
279
+ }
280
+
281
+ function isClaworldConversationBootstrapContext({
282
+ channel = null,
283
+ sessionKey = null,
284
+ sessionType = null,
285
+ } = {}) {
286
+ const normalizedChannel = normalizeText(channel, null)?.toLowerCase() || null;
287
+ const normalizedSessionType = normalizeSessionType(sessionType);
288
+ const normalizedSessionKey = normalizeText(sessionKey, null);
289
+ const hasClaworldChannel = normalizedChannel === 'claworld'
290
+ || /:claworld:/i.test(normalizedSessionKey || '');
291
+ if (!hasClaworldChannel) return false;
292
+ if (
293
+ normalizedSessionType === 'conversation'
294
+ || normalizedSessionType === 'conversation_session'
295
+ || normalizedSessionType === 'chat'
296
+ || normalizedSessionType === 'direct'
297
+ || normalizedSessionType === 'world'
298
+ ) {
299
+ return true;
300
+ }
301
+ return (
302
+ /^agent:[^:]+:claworld:(direct|world):/i.test(normalizedSessionKey || '')
303
+ || /^conversation:.*:(direct|world)(:|$)/i.test(normalizedSessionKey || '')
304
+ );
305
+ }
306
+
307
+ function bootstrapFilesForTarget(target) {
308
+ if (target === CLAWORLD_BOOTSTRAP_TARGETS.MAIN) {
309
+ return MAIN_BOOTSTRAP_FILES;
310
+ }
311
+ if (target === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT) {
312
+ return MANAGEMENT_BOOTSTRAP_FILES;
313
+ }
314
+ if (target === CLAWORLD_BOOTSTRAP_TARGETS.CLAWORLD_CONVERSATION) {
315
+ return CONVERSATION_BOOTSTRAP_FILES;
316
+ }
317
+ return [];
318
+ }
319
+
320
+ function buildClaworldBootstrapFileSections(selectedFiles, slices = {}, options = {}) {
321
+ const maxTotalChars = normalizeBootstrapTotalChars(options.maxTotalChars, MAX_BOOTSTRAP_TOTAL_CHARS);
322
+ const summaryPrefix = '## Claworld Bootstrap Budget\nOmitted to fit the prompt budget: ';
323
+ const summarySuffix = '.';
324
+ const buildBudgetSummary = (files) => `${summaryPrefix}${files.map((filePath) => `\`${filePath}\``).join(', ')}${summarySuffix}`;
325
+ const sections = [];
326
+ const fallbackFiles = [];
327
+ const omittedFiles = [];
328
+ let totalChars = 0;
329
+ let truncated = false;
330
+
331
+ for (let index = 0; index < selectedFiles.length; index += 1) {
332
+ const relativePath = selectedFiles[index];
333
+ const displayPath = buildBootstrapFileLabel(relativePath);
334
+ const slice = slices[relativePath] || null;
335
+ const content = slice?.content || `No local content was available for \`${displayPath}\` at startup. Continue without this file.`;
336
+ if (slice == null) {
337
+ fallbackFiles.push(displayPath);
338
+ }
339
+ if (slice?.truncated) {
340
+ truncated = true;
341
+ }
342
+ const note = slice?.truncated
343
+ ? 'Truncated to the per-file Claworld bootstrap budget.'
344
+ : slice == null
345
+ ? 'Missing-file fallback.'
346
+ : null;
347
+ const section = buildBootstrapSection(displayPath, content, note);
348
+ const joinCost = sections.length > 0 ? 2 : 0;
349
+ if (totalChars + joinCost + section.length <= maxTotalChars) {
350
+ sections.push(section);
351
+ totalChars += joinCost + section.length;
352
+ continue;
353
+ }
354
+
355
+ truncated = true;
356
+ const remainingLabels = selectedFiles
357
+ .slice(index + 1)
358
+ .map((filePath) => buildBootstrapFileLabel(filePath));
359
+ const possibleSummary = remainingLabels.length > 0 ? buildBudgetSummary(remainingLabels) : '';
360
+ const summaryReserve = possibleSummary
361
+ ? (sections.length > 0 ? 2 : 0) + possibleSummary.length
362
+ : 0;
363
+ const remainingChars = maxTotalChars - totalChars - joinCost;
364
+ const header = `## \`${displayPath}\`\n`;
365
+ const suffix = '\n_(Truncated to fit the total Claworld bootstrap budget.)_';
366
+ const availableChars = Math.max(
367
+ 0,
368
+ remainingChars - summaryReserve - header.length - suffix.length,
369
+ );
370
+ if (availableChars > 0) {
371
+ const excerpt = String(content).slice(0, availableChars).trimEnd();
372
+ if (excerpt) {
373
+ const truncatedSection = `${header}${excerpt}${suffix}`;
374
+ sections.push(truncatedSection);
375
+ totalChars += joinCost + truncatedSection.length;
376
+ } else {
377
+ omittedFiles.push(displayPath);
378
+ }
379
+ } else {
380
+ omittedFiles.push(displayPath);
381
+ }
382
+ omittedFiles.push(...remainingLabels);
383
+ break;
384
+ }
385
+
386
+ if (omittedFiles.length > 0) {
387
+ const summary = buildBudgetSummary(omittedFiles);
388
+ const joinCost = sections.length > 0 ? 2 : 0;
389
+ if (totalChars + joinCost + summary.length <= maxTotalChars) {
390
+ sections.push(summary);
391
+ totalChars += joinCost + summary.length;
392
+ }
393
+ }
394
+
395
+ return {
396
+ text: sections.join('\n\n'),
397
+ fallbackFiles,
398
+ omittedFiles,
399
+ truncated,
400
+ };
401
+ }
402
+
403
+ export function resolveClaworldBootstrapContext(...sources) {
404
+ const records = [];
405
+ const flattenedSources = sources.flat ? sources.flat() : sources;
406
+ for (const source of flattenedSources) {
407
+ collectBootstrapRecords(source, records);
408
+ }
409
+ return {
410
+ channel: firstBootstrapField(records, ['channel', 'channelId']),
411
+ sessionKey: firstBootstrapField(records, ['sessionKey', 'localSessionKey']),
412
+ sessionType: firstBootstrapField(records, ['sessionType', 'sessionKind', 'sessionMode', 'mode']),
413
+ };
414
+ }
415
+
416
+ export function resolveClaworldBootstrapTarget(context = {}) {
417
+ const normalizedContext = resolveClaworldBootstrapContext(context);
418
+ if (isMainBootstrapContext(normalizedContext)) {
419
+ return CLAWORLD_BOOTSTRAP_TARGETS.MAIN;
420
+ }
421
+ if (isManagementBootstrapContext(normalizedContext)) {
422
+ return CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT;
423
+ }
424
+ if (isClaworldConversationBootstrapContext(normalizedContext)) {
425
+ return CLAWORLD_BOOTSTRAP_TARGETS.CLAWORLD_CONVERSATION;
426
+ }
427
+ return CLAWORLD_BOOTSTRAP_TARGETS.NONE;
428
+ }
429
+
430
+ async function readTextIfPresent(filePath) {
431
+ try {
432
+ return await fs.readFile(filePath, 'utf8');
433
+ } catch (error) {
434
+ if (error && error.code === 'ENOENT') return null;
435
+ throw error;
436
+ }
437
+ }
438
+
439
+ async function atomicWriteText(filePath, content, {
440
+ backup = true,
441
+ rejectEmptyOverwrite = true,
442
+ } = {}) {
443
+ const nextContent = String(content ?? '');
444
+ const currentContent = await readTextIfPresent(filePath);
445
+ if (
446
+ rejectEmptyOverwrite
447
+ && currentContent != null
448
+ && normalizeText(currentContent, null)
449
+ && !normalizeText(nextContent, null)
450
+ ) {
451
+ throw new Error(`Refusing to overwrite non-empty Claworld memory file with empty content: ${filePath}`);
452
+ }
453
+
454
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
455
+ if (backup && currentContent != null && currentContent !== nextContent) {
456
+ await fs.writeFile(`${filePath}.bak`, currentContent, 'utf8');
457
+ }
458
+
459
+ const tempPath = path.join(
460
+ path.dirname(filePath),
461
+ `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`,
462
+ );
463
+ await fs.writeFile(tempPath, nextContent, 'utf8');
464
+ await fs.rename(tempPath, filePath);
465
+ }
466
+
467
+ export async function ensureClaworldWorkingMemory(options = {}, ensureOptions = {}) {
468
+ const workspaceRoot = resolveClaworldWorkspaceRoot(options, ensureOptions.homeDir || os.homedir());
469
+ const memoryRoot = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR);
470
+ const directories = CLAWORLD_WORKING_MEMORY_DIRECTORIES.map((relativePath) => ({
471
+ relativePath,
472
+ absolutePath: path.join(workspaceRoot, relativePath),
473
+ }));
474
+ const files = buildClaworldWorkingMemoryFileSpecs().map((file) => ({
475
+ ...file,
476
+ absolutePath: path.join(workspaceRoot, file.relativePath),
477
+ }));
478
+ const actions = [];
479
+
480
+ if (ensureOptions.dryRun === true) {
481
+ for (const directory of directories) {
482
+ actions.push(`mkdir -p ${directory.absolutePath}`);
483
+ }
484
+ for (const file of files) {
485
+ actions.push(`seed ${file.absolutePath} if missing`);
486
+ }
487
+ return {
488
+ ok: true,
489
+ dryRun: true,
490
+ workspaceRoot,
491
+ memoryRoot,
492
+ directories,
493
+ files,
494
+ actions,
495
+ };
496
+ }
497
+
498
+ for (const directory of directories) {
499
+ await fs.mkdir(directory.absolutePath, { recursive: true });
500
+ actions.push(`ensured ${directory.absolutePath}`);
501
+ }
502
+
503
+ for (const file of files) {
504
+ const currentContent = await readTextIfPresent(file.absolutePath);
505
+ if (currentContent == null) {
506
+ await atomicWriteText(file.absolutePath, file.content, {
507
+ backup: false,
508
+ rejectEmptyOverwrite: false,
509
+ });
510
+ actions.push(`created ${file.absolutePath}`);
511
+ } else {
512
+ actions.push(`preserved ${file.absolutePath}`);
513
+ }
514
+ }
515
+
516
+ return {
517
+ ok: true,
518
+ dryRun: false,
519
+ workspaceRoot,
520
+ memoryRoot,
521
+ directories,
522
+ files,
523
+ actions,
524
+ };
525
+ }
526
+
527
+ function toIsoTimestamp(value = null) {
528
+ const date = value instanceof Date ? value : new Date(value || Date.now());
529
+ if (Number.isNaN(date.getTime())) return new Date().toISOString();
530
+ return date.toISOString();
531
+ }
532
+
533
+ function toMonthKey(timestamp) {
534
+ return toIsoTimestamp(timestamp).slice(0, 7);
535
+ }
536
+
537
+ function truncateText(value, maxChars = MAX_EVENT_EXCERPT_CHARS) {
538
+ const text = normalizeText(value, '');
539
+ if (text.length <= maxChars) return text;
540
+ return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
541
+ }
542
+
543
+ function flattenInline(value) {
544
+ return truncateText(String(value ?? '').replace(/\s+/g, ' ').trim());
545
+ }
546
+
547
+ export function buildClaworldMaintenanceEvent(input = {}) {
548
+ const toolName = normalizeText(input.toolName, null);
549
+ const source = normalizeText(input.source, toolName ? 'claworld_tool' : 'claworld_runtime');
550
+ const kind = normalizeText(input.kind, toolName || 'milestone');
551
+ const timestamp = toIsoTimestamp(input.timestamp);
552
+ const summary = normalizeText(input.summary, toolName ? `${toolName} succeeded.` : 'Claworld milestone recorded.');
553
+ const refs = input.refs && typeof input.refs === 'object' && !Array.isArray(input.refs)
554
+ ? Object.fromEntries(
555
+ Object.entries(input.refs)
556
+ .map(([key, value]) => [key, normalizeText(value, null)])
557
+ .filter(([, value]) => value != null),
558
+ )
559
+ : {};
560
+ return {
561
+ id: normalizeText(input.id, `${source}:${kind}:${timestamp}`),
562
+ timestamp,
563
+ source,
564
+ kind,
565
+ summary,
566
+ excerpt: truncateText(input.excerpt || ''),
567
+ refs,
568
+ };
569
+ }
570
+
571
+ export function buildClaworldRuntimeMaintenanceEvent(input = {}) {
572
+ return buildClaworldMaintenanceEvent({
573
+ ...input,
574
+ source: normalizeText(input.source, 'claworld_runtime'),
575
+ kind: normalizeText(input.kind, 'milestone'),
576
+ });
577
+ }
578
+
579
+ function parseToolResultPayload(result) {
580
+ if (!result || typeof result !== 'object') return null;
581
+ if (result.payload && typeof result.payload === 'object' && !Array.isArray(result.payload)) {
582
+ return result.payload;
583
+ }
584
+ const textContent = Array.isArray(result.content)
585
+ ? result.content.find((entry) => entry?.type === 'text' && typeof entry.text === 'string')?.text
586
+ : null;
587
+ if (!textContent) return null;
588
+ try {
589
+ const parsed = JSON.parse(textContent);
590
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
591
+ } catch {
592
+ return null;
593
+ }
594
+ }
595
+
596
+ function compactResultPayload(payload = {}) {
597
+ const keys = [
598
+ 'status',
599
+ 'tool',
600
+ 'accountId',
601
+ 'worldId',
602
+ 'displayName',
603
+ 'chatRequestId',
604
+ 'conversationKey',
605
+ 'candidateId',
606
+ 'feedbackId',
607
+ 'nextAction',
608
+ 'requiredAction',
609
+ 'summary',
610
+ 'message',
611
+ ];
612
+ const compact = {};
613
+ for (const key of keys) {
614
+ const value = normalizeText(payload[key], null);
615
+ if (value != null) compact[key] = value;
616
+ }
617
+ return compact;
618
+ }
619
+
620
+ export function buildClaworldToolMaintenanceEvent({
621
+ toolName,
622
+ params = {},
623
+ result = null,
624
+ timestamp = null,
625
+ } = {}) {
626
+ const normalizedToolName = normalizeText(toolName, null);
627
+ if (!normalizedToolName || !normalizedToolName.startsWith('claworld_')) return null;
628
+ const payload = parseToolResultPayload(result) || {};
629
+ const compactPayload = compactResultPayload(payload);
630
+ const refs = {
631
+ accountId: params.accountId || payload.accountId,
632
+ worldId: params.worldId || payload.worldId,
633
+ chatRequestId: params.chatRequestId || payload.chatRequestId,
634
+ conversationKey: params.conversationKey || payload.conversationKey,
635
+ candidateId: params.candidateId || payload.candidateId,
636
+ agentCode: params.agentCode || payload.agentCode,
637
+ };
638
+ return buildClaworldMaintenanceEvent({
639
+ source: 'claworld_tool',
640
+ kind: normalizedToolName,
641
+ toolName: normalizedToolName,
642
+ timestamp,
643
+ refs,
644
+ summary: `${normalizedToolName} succeeded.`,
645
+ excerpt: Object.keys(compactPayload).length > 0
646
+ ? JSON.stringify(compactPayload)
647
+ : null,
648
+ });
649
+ }
650
+
651
+ function formatRefs(refs = {}) {
652
+ return Object.entries(refs)
653
+ .filter(([, value]) => value != null)
654
+ .map(([key, value]) => `${key}=${flattenInline(value)}`)
655
+ .join(', ');
656
+ }
657
+
658
+ export async function appendClaworldJournalEvent(options = {}, event = {}, appendOptions = {}) {
659
+ const workspaceRoot = resolveClaworldWorkspaceRoot(options, appendOptions.homeDir || os.homedir());
660
+ await ensureClaworldWorkingMemory(workspaceRoot, appendOptions);
661
+ const normalizedEvent = buildClaworldMaintenanceEvent(event);
662
+ const monthKey = toMonthKey(normalizedEvent.timestamp);
663
+ const journalPath = path.join(
664
+ workspaceRoot,
665
+ CLAWORLD_WORKING_MEMORY_DIR,
666
+ CLAWORLD_JOURNAL_DIR,
667
+ `${monthKey}.md`,
668
+ );
669
+ const currentContent = await readTextIfPresent(journalPath);
670
+ const refsLine = formatRefs(normalizedEvent.refs);
671
+ const entry = [
672
+ `## ${normalizedEvent.timestamp} - ${normalizedEvent.kind}`,
673
+ `- Source: ${normalizedEvent.source}`,
674
+ `- Summary: ${flattenInline(normalizedEvent.summary)}`,
675
+ normalizedEvent.excerpt ? `- Excerpt: ${flattenInline(normalizedEvent.excerpt)}` : null,
676
+ refsLine ? `- Refs: ${refsLine}` : null,
677
+ '',
678
+ ].filter((line) => line != null).join('\n');
679
+ if (currentContent == null) {
680
+ await atomicWriteText(journalPath, `# Claworld Journal ${monthKey}\n\n${entry}`, {
681
+ backup: false,
682
+ rejectEmptyOverwrite: false,
683
+ });
684
+ } else {
685
+ await fs.appendFile(journalPath, `${currentContent.endsWith('\n') ? '' : '\n'}${entry}`, 'utf8');
686
+ }
687
+ return {
688
+ ok: true,
689
+ journalPath,
690
+ event: normalizedEvent,
691
+ };
692
+ }
693
+
694
+ export async function readClaworldWorkingMemory(options = {}, readOptions = {}) {
695
+ const workspaceRoot = resolveClaworldWorkspaceRoot(options, readOptions.homeDir || os.homedir());
696
+ const maxCharsPerFile = Number.isInteger(readOptions.maxCharsPerFile) && readOptions.maxCharsPerFile > 0
697
+ ? readOptions.maxCharsPerFile
698
+ : MAX_MEMORY_SLICE_CHARS;
699
+ const slices = {};
700
+ for (const relativePath of Object.values(CLAWORLD_WORKING_MEMORY_FILES)) {
701
+ const absolutePath = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR, relativePath);
702
+ const content = await readTextIfPresent(absolutePath);
703
+ slices[relativePath] = content == null
704
+ ? null
705
+ : {
706
+ content: content.length > maxCharsPerFile ? content.slice(0, maxCharsPerFile) : content,
707
+ truncated: content.length > maxCharsPerFile,
708
+ };
709
+ }
710
+ return {
711
+ workspaceRoot,
712
+ memoryRoot: path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR),
713
+ slices,
714
+ };
715
+ }
716
+
717
+ export async function buildClaworldBootstrapPromptContext(context = {}, options = {}) {
718
+ const normalizedContext = resolveClaworldBootstrapContext(context, options.context);
719
+ const target = resolveClaworldBootstrapTarget(normalizedContext);
720
+ const selectedFiles = bootstrapFilesForTarget(target);
721
+ const maxTotalChars = normalizeBootstrapTotalChars(options.maxTotalChars, MAX_BOOTSTRAP_TOTAL_CHARS);
722
+ const workspaceRoot = normalizeText(
723
+ options.workspaceRoot ?? context.workspaceRoot ?? context.workspaceDir ?? context.workspacePath ?? context.workspace,
724
+ null,
725
+ );
726
+ const resolvedWorkspaceRoot = workspaceRoot
727
+ ? resolveClaworldWorkspaceRoot(workspaceRoot, options.homeDir || os.homedir())
728
+ : null;
729
+ const workingMemory = target !== CLAWORLD_BOOTSTRAP_TARGETS.NONE && resolvedWorkspaceRoot
730
+ ? await readClaworldWorkingMemory(resolvedWorkspaceRoot, {
731
+ ...options,
732
+ maxCharsPerFile: options.maxCharsPerFile || MAX_BOOTSTRAP_FILE_CHARS,
733
+ })
734
+ : { slices: {} };
735
+ const pointerInjected = target === CLAWORLD_BOOTSTRAP_TARGETS.MAIN;
736
+ const parts = [];
737
+ let truncated = false;
738
+ if (pointerInjected) {
739
+ const pointerBudget = maxTotalChars - measureBootstrapParts(parts) - (parts.length > 0 ? 2 : 0);
740
+ const fittedPointer = truncateBootstrapText(buildClaworldContextPointer(), pointerBudget);
741
+ if (fittedPointer.text) {
742
+ parts.push(fittedPointer.text);
743
+ }
744
+ if (fittedPointer.truncated) {
745
+ truncated = true;
746
+ }
747
+ }
748
+ const sectionTitle = target === CLAWORLD_BOOTSTRAP_TARGETS.MAIN
749
+ ? '# Claworld Startup Memory'
750
+ : target === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT
751
+ ? '# Claworld Management Startup Memory'
752
+ : '# Claworld Conversation Startup Context';
753
+ const sectionPrefix = `${sectionTitle}\n\n`;
754
+ const remainingBudget = maxTotalChars - measureBootstrapParts(parts) - (parts.length > 0 ? 2 : 0);
755
+ const fileSections = buildClaworldBootstrapFileSections(selectedFiles, workingMemory.slices, {
756
+ ...options,
757
+ maxTotalChars: Math.max(0, remainingBudget - sectionPrefix.length),
758
+ });
759
+ if (fileSections.truncated) {
760
+ truncated = true;
761
+ }
762
+ if (fileSections.text) {
763
+ parts.push(`${sectionPrefix}${fileSections.text}`);
764
+ }
765
+ const appendSystemContext = parts.filter(Boolean).join('\n\n');
766
+ const finalContext = appendSystemContext.length > maxTotalChars
767
+ ? truncateBootstrapText(appendSystemContext, maxTotalChars)
768
+ : { text: appendSystemContext, truncated: false };
769
+ if (finalContext.truncated) {
770
+ truncated = true;
771
+ }
772
+ return {
773
+ target,
774
+ context: normalizedContext,
775
+ workspaceRoot: resolvedWorkspaceRoot,
776
+ files: selectedFiles.map((relativePath) => buildBootstrapFileLabel(relativePath)),
777
+ pointerInjected,
778
+ fallbackFiles: fileSections.fallbackFiles,
779
+ omittedFiles: fileSections.omittedFiles,
780
+ truncated,
781
+ appendSystemContext: finalContext.text,
782
+ };
783
+ }
784
+
785
+ function normalizePatchOperation(operation) {
786
+ const normalized = normalizeText(operation, 'replace');
787
+ if (normalized === 'replace' || normalized === 'append_section' || normalized === 'no_op') {
788
+ return normalized;
789
+ }
790
+ throw new Error(`Unsupported Claworld maintenance patch operation: ${normalized}`);
791
+ }
792
+
793
+ function normalizeMaintenanceRunType(runType) {
794
+ const normalized = normalizeText(runType, null);
795
+ if (MAINTENANCE_RUN_TYPE_VALUES.has(normalized)) return normalized;
796
+ throw new Error(`Unsupported Claworld maintenance run type: ${runType}`);
797
+ }
798
+
799
+ function stripClaworldPrefix(rawTarget) {
800
+ let target = String(rawTarget || '').replace(/\\/g, '/').trim();
801
+ target = target.replace(/^\/+/, '');
802
+ if (target.startsWith(`${CLAWORLD_WORKING_MEMORY_DIR}/`)) {
803
+ target = target.slice(CLAWORLD_WORKING_MEMORY_DIR.length + 1);
804
+ }
805
+ return target;
806
+ }
807
+
808
+ function normalizePatchTarget(rawTarget) {
809
+ const stripped = stripClaworldPrefix(rawTarget);
810
+ if (['NOW.md', 'PROFILE.md', 'MEMORY.md'].includes(stripped)) {
811
+ throw new Error(`Global ${stripped} is not a valid Claworld working-memory target; use context/${stripped}.`);
812
+ }
813
+ const normalized = path.posix.normalize(stripped);
814
+ if (!normalized || normalized === '.' || normalized.startsWith('../') || normalized === '..' || path.posix.isAbsolute(normalized)) {
815
+ throw new Error(`Invalid Claworld maintenance patch target: ${rawTarget}`);
816
+ }
817
+ return normalized;
818
+ }
819
+
820
+ function isAllowedTarget(runType, target) {
821
+ if (target.startsWith(`${CLAWORLD_JOURNAL_DIR}/`) || target.startsWith(`${CLAWORLD_REPORTS_DIR}/`)) {
822
+ return true;
823
+ }
824
+ if (runType === CLAWORLD_MAINTENANCE_RUN_TYPES.L1_NOW_REFRESH) {
825
+ return L1_ALLOWED_TARGETS.has(target);
826
+ }
827
+ if (runType === CLAWORLD_MAINTENANCE_RUN_TYPES.L2_MEMORY_PROFILE_REVIEW) {
828
+ return L2_ALLOWED_TARGETS.has(target);
829
+ }
830
+ return false;
831
+ }
832
+
833
+ function assertAllowedTarget(runType, target) {
834
+ if (!isAllowedTarget(runType, target)) {
835
+ throw new Error(`Claworld maintenance run ${runType} cannot write target ${target}`);
836
+ }
837
+ }
838
+
839
+ function normalizeReportPatch(report, index) {
840
+ const filename = normalizeText(report?.filename ?? report?.name, `report-${index + 1}.md`);
841
+ const normalizedFilename = path.posix.basename(filename.endsWith('.md') ? filename : `${filename}.md`);
842
+ return {
843
+ operation: 'replace',
844
+ target: `${CLAWORLD_REPORTS_DIR}/${normalizedFilename}`,
845
+ content: String(report?.md ?? report?.content ?? report?.text ?? ''),
846
+ };
847
+ }
848
+
849
+ function hasOwn(value, key) {
850
+ return Object.prototype.hasOwnProperty.call(value, key);
851
+ }
852
+
853
+ function readPatchContent(patch, fieldName) {
854
+ if (hasOwn(patch, 'content')) return patch.content;
855
+ if (hasOwn(patch, 'md')) return patch.md;
856
+ if (hasOwn(patch, 'text')) return patch.text;
857
+ throw new Error(`Claworld maintenance FilePatch ${fieldName} requires content for replace or append_section.`);
858
+ }
859
+
860
+ function normalizeFilePatchValue(value, target, fieldName) {
861
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
862
+ const operation = normalizePatchOperation(value.operation);
863
+ const normalized = {
864
+ operation,
865
+ target,
866
+ content: operation === 'no_op' ? '' : String(readPatchContent(value, fieldName) ?? ''),
867
+ };
868
+ const rationale = normalizeText(value.rationale, null);
869
+ if (rationale) normalized.rationale = rationale;
870
+ return normalized;
871
+ }
872
+ return {
873
+ operation: 'replace',
874
+ target,
875
+ content: String(value ?? ''),
876
+ };
877
+ }
878
+
879
+ export function normalizeClaworldMaintenanceOutput(runType, output = {}, options = {}) {
880
+ if (!output || typeof output !== 'object' || Array.isArray(output)) {
881
+ throw new Error('Claworld maintenance output must be an object.');
882
+ }
883
+ const normalizedRunType = normalizeMaintenanceRunType(runType);
884
+ const patches = [];
885
+ if (Object.prototype.hasOwnProperty.call(output, 'nowMd')) {
886
+ patches.push(normalizeFilePatchValue(output.nowMd, CLAWORLD_WORKING_MEMORY_FILES.now, 'nowMd'));
887
+ }
888
+ if (Object.prototype.hasOwnProperty.call(output, 'profileMd')) {
889
+ patches.push(normalizeFilePatchValue(output.profileMd, CLAWORLD_WORKING_MEMORY_FILES.profile, 'profileMd'));
890
+ }
891
+ if (Object.prototype.hasOwnProperty.call(output, 'memoryMd')) {
892
+ patches.push(normalizeFilePatchValue(output.memoryMd, CLAWORLD_WORKING_MEMORY_FILES.memory, 'memoryMd'));
893
+ }
894
+ if (Object.prototype.hasOwnProperty.call(output, 'journalAppendMd')) {
895
+ const monthKey = toMonthKey(options.timestamp || output.timestamp || Date.now());
896
+ patches.push({
897
+ operation: 'append_section',
898
+ target: `${CLAWORLD_JOURNAL_DIR}/${monthKey}.md`,
899
+ content: String(output.journalAppendMd ?? ''),
900
+ });
901
+ }
902
+ if (Array.isArray(output.reports)) {
903
+ output.reports.forEach((report, index) => {
904
+ patches.push(normalizeReportPatch(report, index));
905
+ });
906
+ }
907
+ if (Array.isArray(output.patches)) {
908
+ for (const patch of output.patches) {
909
+ if (!patch || typeof patch !== 'object' || Array.isArray(patch)) {
910
+ throw new Error('Claworld maintenance patches entries must be FilePatch objects.');
911
+ }
912
+ const operation = normalizePatchOperation(patch.operation);
913
+ patches.push({
914
+ operation,
915
+ target: normalizePatchTarget(patch.target ?? patch.path ?? patch.relativePath),
916
+ content: operation === 'no_op' ? '' : String(readPatchContent(patch, 'patches[]') ?? ''),
917
+ rationale: normalizeText(patch.rationale, null),
918
+ });
919
+ }
920
+ }
921
+
922
+ const normalizedPatches = patches.map((patch) => {
923
+ const target = normalizePatchTarget(patch.target);
924
+ const operation = normalizePatchOperation(patch.operation);
925
+ assertAllowedTarget(normalizedRunType, target);
926
+ const normalizedPatch = {
927
+ operation,
928
+ target,
929
+ content: String(patch.content ?? ''),
930
+ };
931
+ const rationale = normalizeText(patch.rationale, null);
932
+ if (rationale) normalizedPatch.rationale = rationale;
933
+ return normalizedPatch;
934
+ });
935
+
936
+ return {
937
+ runType: normalizedRunType,
938
+ noOpReason: normalizeText(output.noOpReason, null),
939
+ patches: normalizedPatches,
940
+ };
941
+ }
942
+
943
+ export function validateClaworldMaintenanceOutput(runType, output = {}, options = {}) {
944
+ return normalizeClaworldMaintenanceOutput(runType, output, options);
945
+ }
946
+
947
+ async function applyMaintenancePatch(workspaceRoot, patch) {
948
+ if (patch.operation === 'no_op') {
949
+ return {
950
+ operation: patch.operation,
951
+ target: patch.target,
952
+ applied: false,
953
+ };
954
+ }
955
+ const absolutePath = path.join(workspaceRoot, CLAWORLD_WORKING_MEMORY_DIR, patch.target);
956
+ if (patch.operation === 'append_section') {
957
+ await fs.mkdir(path.dirname(absolutePath), { recursive: true });
958
+ const currentContent = await readTextIfPresent(absolutePath);
959
+ if (currentContent == null) {
960
+ const monthKey = path.basename(patch.target, '.md');
961
+ await atomicWriteText(absolutePath, `# Claworld Journal ${monthKey}\n\n${patch.content}`, {
962
+ backup: false,
963
+ rejectEmptyOverwrite: false,
964
+ });
965
+ } else {
966
+ await fs.appendFile(
967
+ absolutePath,
968
+ `${currentContent.endsWith('\n') ? '' : '\n'}${patch.content}`,
969
+ 'utf8',
970
+ );
971
+ }
972
+ return {
973
+ operation: patch.operation,
974
+ target: patch.target,
975
+ applied: true,
976
+ };
977
+ }
978
+
979
+ await atomicWriteText(absolutePath, patch.content, {
980
+ backup: true,
981
+ rejectEmptyOverwrite: true,
982
+ });
983
+ return {
984
+ operation: patch.operation,
985
+ target: patch.target,
986
+ applied: true,
987
+ };
988
+ }
989
+
990
+ export async function runClaworldMemoryMaintenance(runType, requestBundle = {}, options = {}) {
991
+ const normalizedRunType = normalizeMaintenanceRunType(runType);
992
+ const workspaceRoot = resolveClaworldWorkspaceRoot(
993
+ options.workspaceRoot || requestBundle.workspaceRoot || requestBundle.workspaceDir || process.cwd(),
994
+ options.homeDir || os.homedir(),
995
+ );
996
+ await ensureClaworldWorkingMemory(workspaceRoot, options);
997
+ const workingMemory = await readClaworldWorkingMemory(workspaceRoot, options);
998
+ const request = {
999
+ ...requestBundle,
1000
+ runType: normalizedRunType,
1001
+ workspaceRoot,
1002
+ workingMemory,
1003
+ };
1004
+ const output = options.output
1005
+ ?? (typeof options.maintenanceRunner === 'function'
1006
+ ? await options.maintenanceRunner(request)
1007
+ : null);
1008
+ if (!output) {
1009
+ return {
1010
+ ok: true,
1011
+ runType: normalizedRunType,
1012
+ noOpReason: 'no_maintenance_runner',
1013
+ request,
1014
+ applied: [],
1015
+ };
1016
+ }
1017
+ const normalized = validateClaworldMaintenanceOutput(normalizedRunType, output, options);
1018
+ const applied = [];
1019
+ for (const patch of normalized.patches) {
1020
+ applied.push(await applyMaintenancePatch(workspaceRoot, patch));
1021
+ }
1022
+ return {
1023
+ ok: true,
1024
+ runType: normalizedRunType,
1025
+ noOpReason: normalized.noOpReason,
1026
+ request,
1027
+ applied,
1028
+ };
1029
+ }
1030
+
1031
+ function isOpenClawConfigCandidate(value) {
1032
+ return isPlainObject(value);
1033
+ }
1034
+
1035
+ function selectOpenClawConfig(...candidates) {
1036
+ const withAgents = candidates.find((candidate) => (
1037
+ isOpenClawConfigCandidate(candidate)
1038
+ && candidate.agents
1039
+ && typeof candidate.agents === 'object'
1040
+ && !Array.isArray(candidate.agents)
1041
+ ));
1042
+ if (withAgents) return withAgents;
1043
+ return candidates.find(isOpenClawConfigCandidate) || {};
1044
+ }
1045
+
1046
+ export function resolveClaworldMaintenanceWorkspaceRoot(requestBundle = {}, options = {}) {
1047
+ const config = selectOpenClawConfig(
1048
+ options.openClawConfig,
1049
+ options.config,
1050
+ requestBundle.openClawConfig,
1051
+ requestBundle.config,
1052
+ requestBundle.cfg,
1053
+ );
1054
+ const agentId = options.agentId
1055
+ ?? options.localAgentId
1056
+ ?? requestBundle.agentId
1057
+ ?? requestBundle.localAgentId
1058
+ ?? options.agent?.id
1059
+ ?? options.agent?.agentId
1060
+ ?? requestBundle.agent?.id
1061
+ ?? requestBundle.agent?.agentId
1062
+ ?? null;
1063
+ return resolveOpenClawWorkspaceRoot({
1064
+ sources: [options, requestBundle],
1065
+ config,
1066
+ agentId,
1067
+ }, options.homeDir || os.homedir());
1068
+ }
1069
+
1070
+ export async function runClaworldMemoryMaintenanceForOpenClaw(runType, requestBundle = {}, options = {}) {
1071
+ const workspaceRoot = resolveClaworldMaintenanceWorkspaceRoot(requestBundle, options);
1072
+ if (!workspaceRoot) {
1073
+ throw new Error('Unable to resolve Claworld maintenance workspace root from OpenClaw context.');
1074
+ }
1075
+ return runClaworldMemoryMaintenance(
1076
+ runType,
1077
+ {
1078
+ ...requestBundle,
1079
+ workspaceRoot,
1080
+ },
1081
+ {
1082
+ ...options,
1083
+ workspaceRoot,
1084
+ },
1085
+ );
1086
+ }