@xfxstudio/claworld 2026.4.30-testing.3 → 2026.5.3-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.
@@ -7,6 +7,7 @@ export const CLAWORLD_WORKING_MEMORY_DIR = '.claworld';
7
7
  export const CLAWORLD_CONTEXT_DIR = 'context';
8
8
  export const CLAWORLD_JOURNAL_DIR = 'journal';
9
9
  export const CLAWORLD_REPORTS_DIR = 'reports';
10
+ export const CLAWORLD_SESSIONS_DIR = 'sessions';
10
11
 
11
12
  export const CLAWORLD_WORKING_MEMORY_FILES = Object.freeze({
12
13
  index: 'INDEX.md',
@@ -20,6 +21,7 @@ export const CLAWORLD_WORKING_MEMORY_DIRECTORIES = Object.freeze([
20
21
  `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_CONTEXT_DIR}`,
21
22
  `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_JOURNAL_DIR}`,
22
23
  `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_REPORTS_DIR}`,
24
+ `${CLAWORLD_WORKING_MEMORY_DIR}/${CLAWORLD_SESSIONS_DIR}`,
23
25
  ]);
24
26
 
25
27
  export const CLAWORLD_BOOTSTRAP_TARGETS = Object.freeze({
@@ -49,7 +51,10 @@ const L2_ALLOWED_TARGETS = new Set([
49
51
  const MAX_EVENT_EXCERPT_CHARS = 600;
50
52
  const MAX_MEMORY_SLICE_CHARS = 4000;
51
53
  const MAX_BOOTSTRAP_FILE_CHARS = 2200;
52
- const MAX_BOOTSTRAP_TOTAL_CHARS = 6000;
54
+ const MAX_BOOTSTRAP_TOTAL_CHARS = 30000;
55
+ const CLAWORLD_JOURNAL_SCHEMA = 'claworld.journal.v2';
56
+ const CLAWORLD_SESSION_DIRECTORY_SCHEMA = 'claworld.sessions.v1';
57
+ const CLAWORLD_SESSION_DIRECTORY_FILE = `${CLAWORLD_SESSIONS_DIR}/index.json`;
53
58
 
54
59
  const MAIN_BOOTSTRAP_FILES = Object.freeze([
55
60
  CLAWORLD_WORKING_MEMORY_FILES.memory,
@@ -67,14 +72,155 @@ const CONVERSATION_BOOTSTRAP_FILES = Object.freeze([
67
72
  CLAWORLD_WORKING_MEMORY_FILES.profile,
68
73
  ]);
69
74
 
70
- export function buildClaworldContextPointer() {
75
+ const CLAWORLD_CONTEXT_FILE_SCHEMAS = Object.freeze({
76
+ [CLAWORLD_WORKING_MEMORY_FILES.now]: Object.freeze({
77
+ title: '# Claworld Now',
78
+ headings: Object.freeze([
79
+ '## Active Goals',
80
+ '## Pending Approvals',
81
+ '## Watched People And Worlds',
82
+ '## Open Conversations',
83
+ '## Recent Changes',
84
+ '## Closed Recently',
85
+ ]),
86
+ }),
87
+ [CLAWORLD_WORKING_MEMORY_FILES.profile]: Object.freeze({
88
+ title: '# Claworld Profile',
89
+ headings: Object.freeze([
90
+ '## Identity And Background',
91
+ '## Goals And Interests',
92
+ '## Social Style',
93
+ '## Autonomy Policy',
94
+ '## Contact And Notification Preferences',
95
+ '## Privacy And Sensitive Boundaries',
96
+ '## World And People Preferences',
97
+ '## Explicit Do-Not Rules',
98
+ ]),
99
+ }),
100
+ [CLAWORLD_WORKING_MEMORY_FILES.memory]: Object.freeze({
101
+ title: '# Claworld Memory',
102
+ headings: Object.freeze([
103
+ '## Memories',
104
+ ]),
105
+ maxBulletLength: 280,
106
+ }),
107
+ });
108
+
109
+ function buildClaworldArtifactPaths(workspaceRoot = null) {
110
+ const basePath = workspaceRoot
111
+ ? path.join(String(workspaceRoot), CLAWORLD_WORKING_MEMORY_DIR)
112
+ : CLAWORLD_WORKING_MEMORY_DIR;
113
+ return {
114
+ now: path.join(basePath, CLAWORLD_WORKING_MEMORY_FILES.now),
115
+ memory: path.join(basePath, CLAWORLD_WORKING_MEMORY_FILES.memory),
116
+ profile: path.join(basePath, CLAWORLD_WORKING_MEMORY_FILES.profile),
117
+ journal: path.join(basePath, CLAWORLD_JOURNAL_DIR),
118
+ reports: path.join(basePath, CLAWORLD_REPORTS_DIR),
119
+ sessionsIndex: path.join(basePath, CLAWORLD_SESSION_DIRECTORY_FILE),
120
+ };
121
+ }
122
+
123
+ function normalizePromptOptions(options = {}) {
124
+ if (typeof options === 'string') {
125
+ return { workspaceRoot: options };
126
+ }
127
+ return options && typeof options === 'object' && !Array.isArray(options)
128
+ ? options
129
+ : {};
130
+ }
131
+
132
+ export function buildClaworldContextPointer(options = {}) {
133
+ const { workspaceRoot = null } = normalizePromptOptions(options);
134
+ const artifacts = buildClaworldArtifactPaths(workspaceRoot);
71
135
  return [
72
- '# Claworld Context Pointer',
136
+ '# Claworld Working Context',
73
137
  '',
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.',
138
+ 'Claworld private working files are available directly:',
139
+ `- Current focus and open loops: \`${artifacts.now}\`.`,
140
+ `- Durable Claworld facts and decisions: \`${artifacts.memory}\`.`,
141
+ `- Stable user preferences and social boundaries: \`${artifacts.profile}\`.`,
142
+ `- Daily structured event journal: \`${artifacts.journal}/\`.`,
143
+ `- Generated local reports: \`${artifacts.reports}/\`.`,
144
+ `- Session directory for local session keys and file hints: \`${artifacts.sessionsIndex}\`.`,
76
145
  '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`.',
146
+ 'Use the session directory before searching raw local session files.',
147
+ 'Do not treat open Claworld loops as ordinary main-session todos before checking these files.',
148
+ ].join('\n');
149
+ }
150
+
151
+ function buildClaworldManagementReportingInstruction(mainSessionKey = null) {
152
+ const normalizedMainSessionKey = typeof mainSessionKey === 'string' && mainSessionKey.trim()
153
+ ? mainSessionKey.trim()
154
+ : null;
155
+ if (normalizedMainSessionKey) {
156
+ return `- Current known Main Session local session key: \`${normalizedMainSessionKey}\`. Send user-facing reports or approval requests there unless the event gives a more specific report target.`;
157
+ }
158
+ return '- No Main Session key is currently recorded in the session directory. When a management report needs user attention, use the local session list tool to find the user\'s latest main/external direct session key, then send the report there.';
159
+ }
160
+
161
+ function buildClaworldManagementStartupPrompt(options = {}) {
162
+ const { workspaceRoot = null, mainSessionKey = null } = normalizePromptOptions(options);
163
+ const artifacts = buildClaworldArtifactPaths(workspaceRoot);
164
+ return [
165
+ '# Claworld Management Session Policy',
166
+ '',
167
+ 'You are the user\'s private Claworld operator. This is not a user-facing chat window or a peer conversation. Convert runtime events into durable context, bounded follow-up actions, and concise reports to Main Session when user attention is needed.',
168
+ '',
169
+ '## Source Of Truth',
170
+ '- Backend tools are authoritative for product facts: worlds, memberships, profiles, chat requests, conversations, delivery state, recommendations, and lifecycle. Verify before acting.',
171
+ '- Local md files are the user agent\'s Claworld cognitive state. Session transcript is process context, not durable truth.',
172
+ '- Use injected startup memory first. Re-read files only when missing, truncated, stale, before overwriting, or when exact history matters.',
173
+ '',
174
+ '## Working Memory Files',
175
+ `- \`${artifacts.profile}\` (PROFILE.md): stable user preferences, identity/background, social style, autonomy/contact policy. Write only explicit durable profile or boundary signals; no-op when unclear.`,
176
+ `- \`${artifacts.memory}\` (MEMORY.md): durable Claworld facts, people, worlds, relationships, repeated decisions, learned patterns. Include brief source/date context.`,
177
+ `- \`${artifacts.now}\` (NOW.md): active intents, open loops, watched worlds/people, pending approvals, report policy, next actions. Close resolved loops.`,
178
+ `- \`${artifacts.journal}/\` (journal/YYYY-MM-DD.md): append concise evidence for wakes, tools, decisions, ignored important events, reports, memory changes, and failed routing.`,
179
+ `- \`${artifacts.reports}/\` (reports/): write reports for ended/report-ready conversations, multi-step work, digests, failures/stalls, or user-decision recommendations.`,
180
+ `- \`${artifacts.sessionsIndex}\` (sessions/index.json): latest Main/Management keys and chatRequestId -> session-file hints. Read before raw transcripts.`,
181
+ '',
182
+ '## File Schemas',
183
+ '- Preserve required headings exactly. NOW.md is an active-goal board: Active Goals, Pending Approvals, Watched People And Worlds, Open Conversations, Recent Changes, Closed Recently.',
184
+ '- MEMORY.md is a concise bullet list under Memories. Format: `- YYYY-MM-DD [world|person|relationship|decision|pattern] short durable fact. Source: event/report/tool id.`',
185
+ '- PROFILE.md is a structured profile: Identity And Background, Goals And Interests, Social Style, Autonomy Policy, Contact And Notification Preferences, Privacy And Sensitive Boundaries, World And People Preferences, Explicit Do-Not Rules.',
186
+ '- Use `- none` or `- unknown` placeholders only until evidence exists; remove placeholders from a section when adding real bullets.',
187
+ '',
188
+ '## Wake Decision Protocol',
189
+ '1. Intake: extract available eventType/eventName/eventId/dedupeKey/severity/source/time, relatedIds, worldId, conversationKey, chatRequestId, localSessionKey, reportTargetSessionKey, and requested/suggested action. Missing fields are normal; do not invent them.',
190
+ '2. Dedupe: check sessions/index.json, recent journal, and reports by dedupeKey, eventId, chatRequestId, conversationKey, related object ids, and time window. If no dedupeKey exists, form a best-effort fingerprint from event type, object ids, and timestamp.',
191
+ '3. Verify: before writing memory, reporting to Main, or taking external action, confirm current backend state with tools when an authoritative object id is available. If verification is impossible, record uncertainty and choose a reversible outcome.',
192
+ '4. Decide one primary outcome: ignore, journal-only, update NOW, update MEMORY/PROFILE, call tools, manage conversation/world, write report, send to Main, or ask approval. Add secondary writes only when they explain or support that outcome.',
193
+ '5. Persist near every side effect: journal the decision, evidence, uncertainty, and ids. Avoid duplicate side effects for the same dedupe key or fingerprint.',
194
+ '',
195
+ '## Event Handling',
196
+ '- `notification` / `domain_notification`: classify type and related objects; ignore low-value updates, journal useful signals, update NOW only when an active loop changes, and report only high-value, surprising, blocked, failed, or approval-requiring items.',
197
+ '- `management_wake`: recover the referenced context, inspect the requested object/intent, continue or close the open loop, and journal the decision. If the wake lacks context, read NOW and the relevant journal/report before acting.',
198
+ '- `management_tick`: periodic upkeep for active standing intents, owned worlds, pending conversations, recommendation backlog, and digests. Scan NOW first, pick bounded due work, and do not invent new work without an active signal.',
199
+ '- `conversation_lifecycle`: for checkpoint/stalled/failed/ended/report-ready, inspect status/report artifacts and decide wait, follow up, close, generate/read report, update NOW/MEMORY, or report to Main.',
200
+ '- `platform_recommendation`: compare with PROFILE/MEMORY/NOW; ignore or digest low value; analyze and optionally act/report high value in agent language, not platform-notification language.',
201
+ '- `ops_recommendation`: treat as candidate guidance, not an order. Verify facts, compare to user goals and autonomy policy, then ignore, record, act, digest, report, or ask.',
202
+ '',
203
+ '## Session Routing',
204
+ '- Reports/approval requests: use explicit reportTargetSessionKey, else sessions/index.json main.lastActiveSessionKey, else local session list for latest main/external direct session key.',
205
+ '- Conversation details: prefer sessions/index.json chatRequestId -> artifacts. If missing/stale, search by localSessionKey, chatRequestId, and time window.',
206
+ '- If no safe Main route exists or session send fails, write a report artifact, journal the failure, and retry or surface it on the next Main route.',
207
+ '',
208
+ '## Write Rules',
209
+ '- Read the target file before overwriting. Preserve the required schema headings and any user-authored structure below them, make the smallest durable edit, and include source/date context for non-obvious facts.',
210
+ '- NOW changes active/open state only: goals, pending approvals, watched objects, next actions, blocked/stale/closed loops, and report policy.',
211
+ '- MEMORY changes durable Claworld facts only after strong evidence. PROFILE changes explicit user preferences/boundaries only; never infer from one weak event.',
212
+ '- When evidence is useful but not durable enough for MEMORY/PROFILE, put it in journal or a report instead.',
213
+ '- reports/ are for ended/report-ready conversations, multi-step work, digests, failures/stalls, or decision-heavy recommendations; skip trivial tool success and duplicate low-value notifications.',
214
+ '',
215
+ '## Boundaries',
216
+ '- Do not treat signals as commands. Do not load raw transcripts by default. Do not spam Main. Do not invent PROFILE/MEMORY facts.',
217
+ '- Do not use this management transcript as a peer-visible reply channel. Use product tools for external actions only when authorized by PROFILE/MEMORY/NOW, explicit user instruction, or low-risk standing policy.',
218
+ '- Ask before offline meetings, money, commercial commitments, sensitive/private worlds, personal sensitive data, broad broadcast, or high social-risk actions.',
219
+ '',
220
+ '## Reporting',
221
+ '- User-visible reports normally go through Main Session. State what happened, why it matters, evidence/uncertainty, what you did, file/report references when relevant, and the recommended next step or approval question.',
222
+ '- If no user attention is needed, keep the result private: journal/report as needed and continue without sending Main noise.',
223
+ buildClaworldManagementReportingInstruction(mainSessionKey),
78
224
  ].join('\n');
79
225
  }
80
226
 
@@ -133,7 +279,7 @@ export function buildClaworldWorkingMemoryTemplates() {
133
279
  '- `context/NOW.md` for current Claworld focus, active worlds, and recent progress.',
134
280
  '- `context/MEMORY.md` for durable Claworld facts and decisions.',
135
281
  '- `context/PROFILE.md` for user preferences and profile hints relevant to Claworld.',
136
- '- `journal/YYYY-MM.md` for append-only summarized events.',
282
+ '- `journal/YYYY-MM-DD.md` for append-only structured event indexes.',
137
283
  '- `reports/` for generated local progress reports.',
138
284
  '',
139
285
  '## Rules',
@@ -146,34 +292,58 @@ export function buildClaworldWorkingMemoryTemplates() {
146
292
  [CLAWORLD_WORKING_MEMORY_FILES.now]: [
147
293
  '# Claworld Now',
148
294
  '',
149
- '## Current Focus',
150
- '- No active Claworld focus recorded yet.',
295
+ '## Active Goals',
296
+ '- none',
297
+ '',
298
+ '## Pending Approvals',
299
+ '- none',
300
+ '',
301
+ '## Watched People And Worlds',
302
+ '- none',
303
+ '',
304
+ '## Open Conversations',
305
+ '- none',
151
306
  '',
152
- '## Recent Activity',
153
- '- No recent Claworld activity recorded yet.',
307
+ '## Recent Changes',
308
+ '- none',
154
309
  '',
155
- '## Open Questions',
310
+ '## Closed Recently',
156
311
  '- none',
157
312
  '',
158
313
  ].join('\n'),
159
314
  [CLAWORLD_WORKING_MEMORY_FILES.profile]: [
160
315
  '# Claworld Profile',
161
316
  '',
162
- '## Stable Preferences',
163
- '- No Claworld-specific preferences recorded yet.',
317
+ '## Identity And Background',
318
+ '- unknown',
164
319
  '',
165
- '## People And Context',
166
- '- No Claworld people context recorded yet.',
320
+ '## Goals And Interests',
321
+ '- unknown',
322
+ '',
323
+ '## Social Style',
324
+ '- unknown',
325
+ '',
326
+ '## Autonomy Policy',
327
+ '- unknown',
328
+ '',
329
+ '## Contact And Notification Preferences',
330
+ '- unknown',
331
+ '',
332
+ '## Privacy And Sensitive Boundaries',
333
+ '- unknown',
334
+ '',
335
+ '## World And People Preferences',
336
+ '- unknown',
337
+ '',
338
+ '## Explicit Do-Not Rules',
339
+ '- unknown',
167
340
  '',
168
341
  ].join('\n'),
169
342
  [CLAWORLD_WORKING_MEMORY_FILES.memory]: [
170
343
  '# Claworld Memory',
171
344
  '',
172
- '## Durable Facts',
173
- '- No durable Claworld facts recorded yet.',
174
- '',
175
- '## Decisions',
176
- '- No durable Claworld decisions recorded yet.',
345
+ '## Memories',
346
+ '- none',
177
347
  '',
178
348
  ].join('\n'),
179
349
  };
@@ -270,12 +440,47 @@ function isMainBootstrapContext({ sessionKey = null, sessionType = null } = {})
270
440
  return /^agent:[^:]+:main(?:$|:)/i.test(normalizeText(sessionKey, ''));
271
441
  }
272
442
 
443
+ function isExternalMainBootstrapContext({
444
+ channel = null,
445
+ sessionKey = null,
446
+ sessionType = null,
447
+ } = {}) {
448
+ const normalizedChannel = normalizeText(channel, null)?.toLowerCase() || null;
449
+ const normalizedSessionType = normalizeSessionType(sessionType);
450
+ const normalizedSessionKey = normalizeText(sessionKey, '');
451
+ if (normalizedChannel === 'claworld') return false;
452
+ if (
453
+ isManagementBootstrapContext({ sessionKey: normalizedSessionKey, sessionType: normalizedSessionType })
454
+ || isClaworldConversationBootstrapContext({ channel: normalizedChannel, sessionKey: normalizedSessionKey, sessionType: normalizedSessionType })
455
+ ) {
456
+ return false;
457
+ }
458
+ if (
459
+ normalizedSessionType === 'direct'
460
+ || normalizedSessionType === 'dm'
461
+ || normalizedSessionType === 'direct_message'
462
+ ) {
463
+ return true;
464
+ }
465
+ return /^agent:[^:]+:[^:]+:direct:/i.test(normalizedSessionKey);
466
+ }
467
+
273
468
  function isManagementBootstrapContext({ sessionKey = null, sessionType = null } = {}) {
274
469
  const normalizedSessionType = normalizeSessionType(sessionType);
275
- if (normalizedSessionType === 'management' || normalizedSessionType === 'management_session') {
470
+ if (
471
+ normalizedSessionType === 'management'
472
+ || normalizedSessionType === 'management_session'
473
+ || normalizedSessionType === 'orchestration'
474
+ || normalizedSessionType === 'orchestration_session'
475
+ || normalizedSessionType === 'operator'
476
+ || normalizedSessionType === 'operator_session'
477
+ ) {
276
478
  return true;
277
479
  }
278
- return /^management:[^:]+/i.test(normalizeText(sessionKey, ''));
480
+ const normalizedSessionKey = normalizeText(sessionKey, '');
481
+ return /^management:[^:]+/i.test(normalizedSessionKey)
482
+ || /^agent:[^:]+:management:[^:]+/i.test(normalizedSessionKey)
483
+ || /^agent:[^:]+:claworld:(orchestration|operator|management)(?::|$)/i.test(normalizedSessionKey);
279
484
  }
280
485
 
281
486
  function isClaworldConversationBootstrapContext({
@@ -286,8 +491,13 @@ function isClaworldConversationBootstrapContext({
286
491
  const normalizedChannel = normalizeText(channel, null)?.toLowerCase() || null;
287
492
  const normalizedSessionType = normalizeSessionType(sessionType);
288
493
  const normalizedSessionKey = normalizeText(sessionKey, null);
494
+ const hasClaworldConversationSessionKey = (
495
+ /^agent:[^:]+:conversation:.*:(direct|world)(:|$)/i.test(normalizedSessionKey || '')
496
+ || /^conversation:.*:(direct|world)(:|$)/i.test(normalizedSessionKey || '')
497
+ );
289
498
  const hasClaworldChannel = normalizedChannel === 'claworld'
290
- || /:claworld:/i.test(normalizedSessionKey || '');
499
+ || /:claworld:/i.test(normalizedSessionKey || '')
500
+ || hasClaworldConversationSessionKey;
291
501
  if (!hasClaworldChannel) return false;
292
502
  if (
293
503
  normalizedSessionType === 'conversation'
@@ -300,6 +510,7 @@ function isClaworldConversationBootstrapContext({
300
510
  }
301
511
  return (
302
512
  /^agent:[^:]+:claworld:(direct|world):/i.test(normalizedSessionKey || '')
513
+ || /^agent:[^:]+:conversation:.*:(direct|world)(:|$)/i.test(normalizedSessionKey || '')
303
514
  || /^conversation:.*:(direct|world)(:|$)/i.test(normalizedSessionKey || '')
304
515
  );
305
516
  }
@@ -407,9 +618,9 @@ export function resolveClaworldBootstrapContext(...sources) {
407
618
  collectBootstrapRecords(source, records);
408
619
  }
409
620
  return {
410
- channel: firstBootstrapField(records, ['channel', 'channelId']),
411
- sessionKey: firstBootstrapField(records, ['sessionKey', 'localSessionKey']),
412
- sessionType: firstBootstrapField(records, ['sessionType', 'sessionKind', 'sessionMode', 'mode']),
621
+ channel: firstBootstrapField(records, ['channel', 'channelId', 'OriginatingChannel', 'Provider', 'Surface']),
622
+ sessionKey: firstBootstrapField(records, ['sessionKey', 'localSessionKey', 'SessionKey', 'RelaySessionKey']),
623
+ sessionType: firstBootstrapField(records, ['sessionType', 'sessionKind', 'sessionMode', 'mode', 'SessionType', 'ChatType']),
413
624
  };
414
625
  }
415
626
 
@@ -424,9 +635,43 @@ export function resolveClaworldBootstrapTarget(context = {}) {
424
635
  if (isClaworldConversationBootstrapContext(normalizedContext)) {
425
636
  return CLAWORLD_BOOTSTRAP_TARGETS.CLAWORLD_CONVERSATION;
426
637
  }
638
+ if (isExternalMainBootstrapContext(normalizedContext)) {
639
+ return CLAWORLD_BOOTSTRAP_TARGETS.MAIN;
640
+ }
427
641
  return CLAWORLD_BOOTSTRAP_TARGETS.NONE;
428
642
  }
429
643
 
644
+ function resolveClaworldSessionDirectoryKind(input = {}, relations = {}) {
645
+ const explicitScope = normalizeText(input.scope, null);
646
+ if (explicitScope === 'main') return 'main';
647
+ if (explicitScope === 'management') return 'management';
648
+ if (explicitScope === 'conversation') return 'conversation';
649
+ const context = isPlainObject(input.context) ? input.context : {};
650
+ const target = resolveClaworldBootstrapTarget({
651
+ sessionKey: firstText(
652
+ relations.localSessionKey,
653
+ relations.sessionKey,
654
+ input.localSessionKey,
655
+ input.sessionKey,
656
+ context.SessionKey,
657
+ context.sessionKey,
658
+ ),
659
+ sessionType: firstText(
660
+ input.sessionType,
661
+ input.sessionKind,
662
+ context.SessionType,
663
+ context.ChatType,
664
+ context.sessionType,
665
+ context.sessionKind,
666
+ ),
667
+ channel: firstText(context.OriginatingChannel, context.channel),
668
+ });
669
+ if (target === CLAWORLD_BOOTSTRAP_TARGETS.MAIN) return 'main';
670
+ if (target === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT) return 'management';
671
+ if (target === CLAWORLD_BOOTSTRAP_TARGETS.CLAWORLD_CONVERSATION) return 'conversation';
672
+ return null;
673
+ }
674
+
430
675
  async function readTextIfPresent(filePath) {
431
676
  try {
432
677
  return await fs.readFile(filePath, 'utf8');
@@ -436,6 +681,17 @@ async function readTextIfPresent(filePath) {
436
681
  }
437
682
  }
438
683
 
684
+ async function readJsonIfPresent(filePath) {
685
+ const text = await readTextIfPresent(filePath);
686
+ if (text == null) return null;
687
+ try {
688
+ const parsed = JSON.parse(text);
689
+ return isPlainObject(parsed) ? parsed : null;
690
+ } catch {
691
+ return null;
692
+ }
693
+ }
694
+
439
695
  async function atomicWriteText(filePath, content, {
440
696
  backup = true,
441
697
  rejectEmptyOverwrite = true,
@@ -530,8 +786,8 @@ function toIsoTimestamp(value = null) {
530
786
  return date.toISOString();
531
787
  }
532
788
 
533
- function toMonthKey(timestamp) {
534
- return toIsoTimestamp(timestamp).slice(0, 7);
789
+ function toDayKey(timestamp) {
790
+ return toIsoTimestamp(timestamp).slice(0, 10);
535
791
  }
536
792
 
537
793
  function truncateText(value, maxChars = MAX_EVENT_EXCERPT_CHARS) {
@@ -540,10 +796,436 @@ function truncateText(value, maxChars = MAX_EVENT_EXCERPT_CHARS) {
540
796
  return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
541
797
  }
542
798
 
799
+ function resolveClaworldSessionDirectoryPath(workspaceRoot) {
800
+ return path.join(
801
+ workspaceRoot,
802
+ CLAWORLD_WORKING_MEMORY_DIR,
803
+ CLAWORLD_SESSION_DIRECTORY_FILE,
804
+ );
805
+ }
806
+
807
+ function createEmptyClaworldSessionDirectory(timestamp = null) {
808
+ return {
809
+ schema: CLAWORLD_SESSION_DIRECTORY_SCHEMA,
810
+ version: 1,
811
+ updatedAt: toIsoTimestamp(timestamp),
812
+ main: {},
813
+ management: {},
814
+ conversationSessions: {},
815
+ };
816
+ }
817
+
818
+ function normalizeClaworldSessionDirectory(value = null) {
819
+ const source = isPlainObject(value) ? value : {};
820
+ const directory = {
821
+ ...source,
822
+ schema: CLAWORLD_SESSION_DIRECTORY_SCHEMA,
823
+ version: 1,
824
+ updatedAt: normalizeText(source.updatedAt, toIsoTimestamp()),
825
+ main: isPlainObject(source.main) ? { ...source.main } : {},
826
+ management: isPlainObject(source.management) ? { ...source.management } : {},
827
+ conversationSessions: isPlainObject(source.conversationSessions)
828
+ ? { ...source.conversationSessions }
829
+ : {},
830
+ };
831
+ return directory;
832
+ }
833
+
834
+ function normalizeChatRequestId(input = {}, relations = {}) {
835
+ return firstText(
836
+ relations.chatRequestId,
837
+ relations.requestId,
838
+ input.chatRequestId,
839
+ input.requestId,
840
+ input.refs?.chatRequestId,
841
+ input.refs?.requestId,
842
+ );
843
+ }
844
+
845
+ function compactDirectoryObject(value = {}) {
846
+ return cleanJournalObject(value);
847
+ }
848
+
849
+ function compactSessionArtifact(artifact = {}) {
850
+ const sessionFile = firstText(artifact.sessionFile, artifact.transcriptPath);
851
+ const transcriptPath = firstText(
852
+ artifact.transcriptPath && artifact.transcriptPath !== sessionFile ? artifact.transcriptPath : null,
853
+ );
854
+ const deliveryId = firstText(artifact.deliveryId);
855
+ return compactDirectoryObject({
856
+ sessionId: firstText(artifact.sessionId),
857
+ sessionFile,
858
+ transcriptPath,
859
+ deliveryId,
860
+ sourceEventId: deliveryId ? null : firstText(artifact.sourceEventId, artifact.eventId),
861
+ seenAt: firstText(artifact.seenAt, artifact.lastSeenAt, artifact.firstSeenAt),
862
+ });
863
+ }
864
+
865
+ function normalizeSessionArtifacts(artifacts = []) {
866
+ if (!Array.isArray(artifacts)) return [];
867
+ return artifacts.reduce((nextArtifacts, artifact) => (
868
+ upsertSessionArtifact(nextArtifacts, artifact)
869
+ ), []);
870
+ }
871
+
872
+ function upsertSessionArtifact(artifacts = [], artifact = {}) {
873
+ const normalizedArtifact = compactSessionArtifact(artifact);
874
+ if (
875
+ !normalizedArtifact.sessionId
876
+ && !normalizedArtifact.sessionFile
877
+ && !normalizedArtifact.transcriptPath
878
+ ) {
879
+ return artifacts;
880
+ }
881
+ const key = [
882
+ normalizedArtifact.sessionId || '',
883
+ normalizedArtifact.sessionFile || '',
884
+ normalizedArtifact.transcriptPath || '',
885
+ ].join('\u0000');
886
+ const nextArtifacts = Array.isArray(artifacts) ? [...artifacts] : [];
887
+ const index = nextArtifacts.findIndex((entry) => [
888
+ entry?.sessionId || '',
889
+ entry?.sessionFile || '',
890
+ entry?.transcriptPath || '',
891
+ ].join('\u0000') === key);
892
+ if (index >= 0) {
893
+ nextArtifacts[index] = compactDirectoryObject({
894
+ ...nextArtifacts[index],
895
+ ...normalizedArtifact,
896
+ seenAt: normalizedArtifact.seenAt || nextArtifacts[index].seenAt,
897
+ });
898
+ return nextArtifacts;
899
+ }
900
+ nextArtifacts.push(normalizedArtifact);
901
+ return nextArtifacts;
902
+ }
903
+
904
+ function buildLatestSessionHint(source = {}, timestamp = null) {
905
+ const artifact = compactSessionArtifact({
906
+ sessionId: source.sessionId || source.latestSessionId,
907
+ sessionFile: source.sessionFile || source.latestSessionFile,
908
+ transcriptPath: source.transcriptPath || source.latestTranscriptPath,
909
+ seenAt: timestamp || source.seenAt || source.lastSeenAt || source.firstSeenAt,
910
+ });
911
+ if (!artifact.sessionId && !artifact.sessionFile && !artifact.transcriptPath) return null;
912
+ const latest = { ...artifact };
913
+ delete latest.deliveryId;
914
+ delete latest.sourceEventId;
915
+ return compactDirectoryObject(latest);
916
+ }
917
+
918
+ function normalizeChatRequestDirectoryEntry(entry = {}) {
919
+ const current = isPlainObject(entry) ? entry : {};
920
+ const artifacts = normalizeSessionArtifacts(
921
+ Array.isArray(current.artifacts) ? current.artifacts : current.sessionArtifacts,
922
+ );
923
+ return compactDirectoryObject({
924
+ firstSeenAt: current.firstSeenAt,
925
+ lastSeenAt: current.lastSeenAt,
926
+ artifacts,
927
+ });
928
+ }
929
+
930
+ function normalizeChatRequestsDirectory(chatRequests = {}) {
931
+ if (!isPlainObject(chatRequests)) return {};
932
+ const normalized = {};
933
+ for (const [chatRequestId, entry] of Object.entries(chatRequests)) {
934
+ const normalizedEntry = normalizeChatRequestDirectoryEntry(entry);
935
+ if (Object.keys(normalizedEntry).length > 0) {
936
+ normalized[chatRequestId] = normalizedEntry;
937
+ }
938
+ }
939
+ return normalized;
940
+ }
941
+
942
+ function buildSessionArtifact({ relations = {}, timestamp = null } = {}) {
943
+ return compactSessionArtifact({
944
+ sessionId: relations.sessionId,
945
+ sessionFile: relations.sessionFile,
946
+ transcriptPath: relations.transcriptPath,
947
+ deliveryId: relations.deliveryId,
948
+ eventId: relations.eventId,
949
+ seenAt: timestamp,
950
+ });
951
+ }
952
+
953
+ function applyClaworldSessionDirectoryUpdate(directory = {}, input = {}) {
954
+ const refs = isPlainObject(input.refs) ? input.refs : {};
955
+ const relations = resolveJournalRelations(input, refs);
956
+ const kind = resolveClaworldSessionDirectoryKind(input, relations);
957
+ const timestamp = toIsoTimestamp(input.timestamp || Date.now());
958
+ const localSessionKey = firstText(relations.localSessionKey, relations.sessionKey, input.localSessionKey, input.sessionKey);
959
+ const relaySessionKey = firstText(relations.relaySessionKey, input.relaySessionKey);
960
+ if (!kind || !localSessionKey) {
961
+ return { updated: false, reason: 'missing_session_reference', directory };
962
+ }
963
+
964
+ const nextDirectory = normalizeClaworldSessionDirectory(directory);
965
+ nextDirectory.updatedAt = timestamp;
966
+
967
+ if (kind === 'main') {
968
+ nextDirectory.main = compactDirectoryObject({
969
+ ...nextDirectory.main,
970
+ lastActiveSessionKey: localSessionKey,
971
+ lastUpdatedAt: timestamp,
972
+ localAgentId: relations.localAgentId,
973
+ source: input.source,
974
+ });
975
+ return { updated: true, kind, directory: nextDirectory };
976
+ }
977
+
978
+ if (kind === 'management') {
979
+ nextDirectory.management = compactDirectoryObject({
980
+ ...nextDirectory.management,
981
+ lastActiveLocalSessionKey: localSessionKey,
982
+ relaySessionKey,
983
+ localAgentId: relations.localAgentId,
984
+ targetAgentId: relations.targetAgentId,
985
+ lastUpdatedAt: timestamp,
986
+ });
987
+ return { updated: true, kind, directory: nextDirectory };
988
+ }
989
+
990
+ const conversationSessions = isPlainObject(nextDirectory.conversationSessions)
991
+ ? { ...nextDirectory.conversationSessions }
992
+ : {};
993
+ const currentSession = isPlainObject(conversationSessions[localSessionKey])
994
+ ? conversationSessions[localSessionKey]
995
+ : {};
996
+ const currentLatest = isPlainObject(currentSession.latest)
997
+ ? buildLatestSessionHint(currentSession.latest)
998
+ : buildLatestSessionHint(currentSession);
999
+ const nextLatest = buildLatestSessionHint(relations, timestamp) || currentLatest;
1000
+ let nextSession = compactDirectoryObject({
1001
+ relaySessionKey: firstText(relaySessionKey, currentSession.relaySessionKey),
1002
+ conversationKey: firstText(currentSession.conversationKey, relations.conversationKey),
1003
+ worldId: firstText(currentSession.worldId, relations.worldId),
1004
+ localAgentId: firstText(currentSession.localAgentId, relations.localAgentId),
1005
+ firstSeenAt: currentSession.firstSeenAt || timestamp,
1006
+ lastSeenAt: timestamp,
1007
+ latest: nextLatest,
1008
+ chatRequests: normalizeChatRequestsDirectory(currentSession.chatRequests),
1009
+ });
1010
+
1011
+ const chatRequestId = normalizeChatRequestId(input, relations);
1012
+ if (chatRequestId) {
1013
+ const chatRequests = isPlainObject(nextSession.chatRequests)
1014
+ ? { ...nextSession.chatRequests }
1015
+ : {};
1016
+ const currentRequest = isPlainObject(chatRequests[chatRequestId])
1017
+ ? chatRequests[chatRequestId]
1018
+ : {};
1019
+ const artifact = buildSessionArtifact({ relations, timestamp });
1020
+ const currentArtifacts = Array.isArray(currentRequest.artifacts)
1021
+ ? currentRequest.artifacts
1022
+ : currentRequest.sessionArtifacts;
1023
+ const nextRequest = compactDirectoryObject({
1024
+ firstSeenAt: currentRequest.firstSeenAt || timestamp,
1025
+ lastSeenAt: timestamp,
1026
+ artifacts: upsertSessionArtifact(currentArtifacts, artifact),
1027
+ });
1028
+ chatRequests[chatRequestId] = nextRequest;
1029
+ nextSession = compactDirectoryObject({
1030
+ ...nextSession,
1031
+ chatRequests,
1032
+ });
1033
+ }
1034
+
1035
+ conversationSessions[localSessionKey] = nextSession;
1036
+ nextDirectory.conversationSessions = conversationSessions;
1037
+ return { updated: true, kind, directory: nextDirectory };
1038
+ }
1039
+
1040
+ export async function readClaworldSessionDirectory(options = {}, readOptions = {}) {
1041
+ const workspaceRoot = resolveClaworldWorkspaceRoot(options, readOptions.homeDir || os.homedir());
1042
+ const sessionDirectoryPath = resolveClaworldSessionDirectoryPath(workspaceRoot);
1043
+ const current = await readJsonIfPresent(sessionDirectoryPath);
1044
+ return {
1045
+ workspaceRoot,
1046
+ sessionDirectoryPath,
1047
+ directory: normalizeClaworldSessionDirectory(current || createEmptyClaworldSessionDirectory()),
1048
+ exists: current != null,
1049
+ };
1050
+ }
1051
+
1052
+ export async function updateClaworldSessionDirectory(options = {}, input = {}, updateOptions = {}) {
1053
+ const workspaceRoot = resolveClaworldWorkspaceRoot(options, updateOptions.homeDir || os.homedir());
1054
+ await ensureClaworldWorkingMemory(workspaceRoot, updateOptions);
1055
+ const sessionDirectoryPath = resolveClaworldSessionDirectoryPath(workspaceRoot);
1056
+ const current = await readJsonIfPresent(sessionDirectoryPath);
1057
+ const baseDirectory = current || createEmptyClaworldSessionDirectory(input.timestamp || updateOptions.timestamp);
1058
+ const result = applyClaworldSessionDirectoryUpdate(baseDirectory, input);
1059
+ if (!result.updated) {
1060
+ return {
1061
+ ok: true,
1062
+ updated: false,
1063
+ reason: result.reason,
1064
+ workspaceRoot,
1065
+ sessionDirectoryPath,
1066
+ directory: normalizeClaworldSessionDirectory(baseDirectory),
1067
+ };
1068
+ }
1069
+ await atomicWriteText(
1070
+ sessionDirectoryPath,
1071
+ `${JSON.stringify(result.directory, null, 2)}\n`,
1072
+ {
1073
+ backup: false,
1074
+ rejectEmptyOverwrite: false,
1075
+ },
1076
+ );
1077
+ return {
1078
+ ok: true,
1079
+ updated: true,
1080
+ kind: result.kind,
1081
+ workspaceRoot,
1082
+ sessionDirectoryPath,
1083
+ directory: result.directory,
1084
+ };
1085
+ }
1086
+
543
1087
  function flattenInline(value) {
544
1088
  return truncateText(String(value ?? '').replace(/\s+/g, ' ').trim());
545
1089
  }
546
1090
 
1091
+ function firstText(...values) {
1092
+ for (const value of values) {
1093
+ const normalized = normalizeText(value, null);
1094
+ if (normalized) return normalized;
1095
+ }
1096
+ return null;
1097
+ }
1098
+
1099
+ function hasStructuredValue(value) {
1100
+ if (value == null) return false;
1101
+ if (typeof value === 'string') return normalizeText(value, null) != null;
1102
+ if (Array.isArray(value)) return value.length > 0;
1103
+ if (isPlainObject(value)) return Object.keys(value).length > 0;
1104
+ return true;
1105
+ }
1106
+
1107
+ function cleanJournalValue(value) {
1108
+ if (value == null) return null;
1109
+ if (value instanceof Date) return toIsoTimestamp(value);
1110
+ if (typeof value === 'string') return normalizeText(value, null);
1111
+ if (typeof value === 'number' || typeof value === 'boolean') return value;
1112
+ if (Array.isArray(value)) {
1113
+ const cleanedArray = value
1114
+ .map((entry) => cleanJournalValue(entry))
1115
+ .filter(hasStructuredValue);
1116
+ return cleanedArray.length > 0 ? cleanedArray : null;
1117
+ }
1118
+ if (isPlainObject(value)) {
1119
+ const cleanedObject = {};
1120
+ for (const [key, entry] of Object.entries(value)) {
1121
+ const cleaned = cleanJournalValue(entry);
1122
+ if (hasStructuredValue(cleaned)) {
1123
+ cleanedObject[key] = cleaned;
1124
+ }
1125
+ }
1126
+ return Object.keys(cleanedObject).length > 0 ? cleanedObject : null;
1127
+ }
1128
+ return normalizeText(value, null);
1129
+ }
1130
+
1131
+ function cleanJournalObject(value = {}) {
1132
+ return cleanJournalValue(value) || {};
1133
+ }
1134
+
1135
+ function resolveJournalScope(input = {}, relations = {}) {
1136
+ const directScope = normalizeText(input.scope, null);
1137
+ if (directScope) return directScope;
1138
+ const context = isPlainObject(input.context) ? input.context : {};
1139
+ const normalizedTarget = resolveClaworldBootstrapTarget({
1140
+ sessionKey: firstText(
1141
+ input.localSessionKey,
1142
+ input.sessionKey,
1143
+ relations.localSessionKey,
1144
+ relations.sessionKey,
1145
+ context.SessionKey,
1146
+ context.sessionKey,
1147
+ ),
1148
+ sessionType: firstText(
1149
+ input.sessionType,
1150
+ input.sessionKind,
1151
+ context.SessionType,
1152
+ context.ChatType,
1153
+ context.sessionType,
1154
+ context.sessionKind,
1155
+ ),
1156
+ channel: firstText(context.OriginatingChannel, context.channel),
1157
+ });
1158
+ if (normalizedTarget === CLAWORLD_BOOTSTRAP_TARGETS.CLAWORLD_CONVERSATION) return 'conversation';
1159
+ if (normalizedTarget === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT) return 'management';
1160
+ if (normalizedTarget === CLAWORLD_BOOTSTRAP_TARGETS.MAIN) return 'main';
1161
+ return firstText(input.sessionKind, input.sessionType, context.sessionKind, context.SessionType, 'runtime');
1162
+ }
1163
+
1164
+ function resolveJournalRelations(input = {}, refs = {}) {
1165
+ const relations = isPlainObject(input.relations)
1166
+ ? input.relations
1167
+ : isPlainObject(input.correlation)
1168
+ ? input.correlation
1169
+ : {};
1170
+ const context = isPlainObject(input.context) ? input.context : {};
1171
+ const artifacts = isPlainObject(input.artifacts) ? input.artifacts : {};
1172
+ const requestId = firstText(
1173
+ relations.requestId,
1174
+ relations.chatRequestId,
1175
+ input.requestId,
1176
+ input.chatRequestId,
1177
+ refs.requestId,
1178
+ refs.chatRequestId,
1179
+ refs.friendRequestId,
1180
+ );
1181
+ return cleanJournalObject({
1182
+ requestId,
1183
+ chatRequestId: firstText(relations.chatRequestId, input.chatRequestId, refs.chatRequestId, requestId),
1184
+ deliveryId: firstText(relations.deliveryId, input.deliveryId, refs.deliveryId, context.RelayDeliveryId),
1185
+ eventId: firstText(relations.eventId, input.eventId, input.id, refs.eventId, context.RelayEventId),
1186
+ notificationId: firstText(relations.notificationId, refs.notificationId),
1187
+ inboxItemId: firstText(relations.inboxItemId, refs.inboxItemId),
1188
+ conversationKey: firstText(relations.conversationKey, input.conversationKey, refs.conversationKey),
1189
+ worldId: firstText(relations.worldId, input.worldId, refs.worldId),
1190
+ accountId: firstText(relations.accountId, input.accountId, refs.accountId, context.AccountId),
1191
+ agentCode: firstText(relations.agentCode, refs.agentCode),
1192
+ localAgentId: firstText(relations.localAgentId, input.localAgentId, context.AgentId, context.agentId),
1193
+ targetAgentId: firstText(relations.targetAgentId, input.targetAgentId, refs.targetAgentId, context.RelayTargetAgentId),
1194
+ fromAgentId: firstText(relations.fromAgentId, input.fromAgentId, refs.fromAgentId, context.RelayFromAgentId),
1195
+ sessionKey: firstText(relations.sessionKey, input.sessionKey, context.SessionKey, context.sessionKey),
1196
+ localSessionKey: firstText(relations.localSessionKey, input.localSessionKey, context.LocalSessionKey, context.SessionKey),
1197
+ relaySessionKey: firstText(relations.relaySessionKey, input.relaySessionKey, context.RelaySessionKey),
1198
+ sessionId: firstText(relations.sessionId, input.sessionId, artifacts.sessionId, context.SessionId),
1199
+ sessionFile: firstText(relations.sessionFile, input.sessionFile, artifacts.sessionFile, artifacts.sessionPath, context.SessionFile),
1200
+ sessionStorePath: firstText(relations.sessionStorePath, input.sessionStorePath, artifacts.sessionStorePath),
1201
+ transcriptPath: firstText(relations.transcriptPath, input.transcriptPath, artifacts.transcriptPath),
1202
+ reportPath: firstText(relations.reportPath, input.reportPath, artifacts.reportPath),
1203
+ });
1204
+ }
1205
+
1206
+ function buildJournalKey(relations = {}, fallbackEventId = null) {
1207
+ const event = firstText(relations.eventId, fallbackEventId);
1208
+ const request = firstText(relations.requestId, relations.chatRequestId);
1209
+ const session = firstText(relations.localSessionKey, relations.relaySessionKey, relations.sessionKey);
1210
+ const continuity = request
1211
+ ? `request:${request}`
1212
+ : firstText(
1213
+ relations.conversationKey ? `conversation:${relations.conversationKey}` : null,
1214
+ session ? `session:${session}` : null,
1215
+ relations.worldId ? `world:${relations.worldId}` : null,
1216
+ event ? `event:${event}` : null,
1217
+ );
1218
+ return cleanJournalObject({
1219
+ event,
1220
+ continuity,
1221
+ request,
1222
+ session,
1223
+ sessionId: relations.sessionId,
1224
+ world: relations.worldId,
1225
+ conversation: relations.conversationKey,
1226
+ });
1227
+ }
1228
+
547
1229
  export function buildClaworldMaintenanceEvent(input = {}) {
548
1230
  const toolName = normalizeText(input.toolName, null);
549
1231
  const source = normalizeText(input.source, toolName ? 'claworld_tool' : 'claworld_runtime');
@@ -557,14 +1239,26 @@ export function buildClaworldMaintenanceEvent(input = {}) {
557
1239
  .filter(([, value]) => value != null),
558
1240
  )
559
1241
  : {};
1242
+ const id = normalizeText(input.id, `${source}:${kind}:${timestamp}`);
1243
+ const relations = resolveJournalRelations({ ...input, eventId: input.eventId || id }, refs);
1244
+ const scope = resolveJournalScope(input, relations);
560
1245
  return {
561
- id: normalizeText(input.id, `${source}:${kind}:${timestamp}`),
1246
+ schema: CLAWORLD_JOURNAL_SCHEMA,
1247
+ id,
562
1248
  timestamp,
563
1249
  source,
564
1250
  kind,
1251
+ eventType: normalizeText(input.eventType, kind),
1252
+ scope,
565
1253
  summary,
566
1254
  excerpt: truncateText(input.excerpt || ''),
567
1255
  refs,
1256
+ key: buildJournalKey(relations, id),
1257
+ relations,
1258
+ actor: cleanJournalObject(input.actor || {}),
1259
+ tool: cleanJournalObject(input.tool || {}),
1260
+ artifacts: cleanJournalObject(input.artifacts || {}),
1261
+ maintenance: cleanJournalObject(input.maintenance || {}),
568
1262
  };
569
1263
  }
570
1264
 
@@ -597,9 +1291,11 @@ function compactResultPayload(payload = {}) {
597
1291
  const keys = [
598
1292
  'status',
599
1293
  'tool',
1294
+ 'action',
600
1295
  'accountId',
601
1296
  'worldId',
602
1297
  'displayName',
1298
+ 'requestId',
603
1299
  'chatRequestId',
604
1300
  'conversationKey',
605
1301
  'feedbackId',
@@ -616,29 +1312,96 @@ function compactResultPayload(payload = {}) {
616
1312
  return compact;
617
1313
  }
618
1314
 
1315
+ function readNestedObject(value, key) {
1316
+ const nested = value?.[key];
1317
+ return nested && typeof nested === 'object' && !Array.isArray(nested) ? nested : {};
1318
+ }
1319
+
619
1320
  export function buildClaworldToolMaintenanceEvent({
620
1321
  toolName,
621
1322
  params = {},
622
1323
  result = null,
623
1324
  timestamp = null,
1325
+ context = {},
624
1326
  } = {}) {
625
1327
  const normalizedToolName = normalizeText(toolName, null);
626
1328
  if (!normalizedToolName || !normalizedToolName.startsWith('claworld_')) return null;
627
1329
  const payload = parseToolResultPayload(result) || {};
1330
+ const chatRequest = readNestedObject(payload, 'chatRequest');
1331
+ const request = readNestedObject(payload, 'request');
1332
+ const kickoff = readNestedObject(payload, 'kickoff');
1333
+ const conversation = readNestedObject(payload, 'conversation');
628
1334
  const compactPayload = compactResultPayload(payload);
1335
+ const requestId = firstText(
1336
+ params.requestId,
1337
+ params.chatRequestId,
1338
+ payload.requestId,
1339
+ payload.chatRequestId,
1340
+ chatRequest.chatRequestId,
1341
+ chatRequest.requestId,
1342
+ request.requestId,
1343
+ );
1344
+ const localSessionKey = firstText(
1345
+ context.sessionKey,
1346
+ context.SessionKey,
1347
+ context.localSessionKey,
1348
+ payload.localSessionKey,
1349
+ kickoff.localSessionKey,
1350
+ );
1351
+ const relaySessionKey = firstText(
1352
+ context.RelaySessionKey,
1353
+ payload.sessionKey,
1354
+ kickoff.sessionKey,
1355
+ conversation.sessionKey,
1356
+ );
1357
+ const sessionKind = resolveJournalScope({
1358
+ sessionKey: localSessionKey || relaySessionKey,
1359
+ sessionType: firstText(context.sessionType, context.SessionType, context.ChatType, context.sessionKind),
1360
+ context,
1361
+ });
629
1362
  const refs = {
630
1363
  accountId: params.accountId || payload.accountId,
631
1364
  worldId: params.worldId || payload.worldId,
632
- chatRequestId: params.chatRequestId || payload.chatRequestId,
633
- conversationKey: params.conversationKey || payload.conversationKey,
1365
+ requestId,
1366
+ chatRequestId: params.chatRequestId || payload.chatRequestId || chatRequest.chatRequestId,
1367
+ conversationKey: params.conversationKey || payload.conversationKey || conversation.conversationKey,
634
1368
  agentCode: params.agentCode || payload.agentCode,
1369
+ sessionKey: localSessionKey || relaySessionKey,
1370
+ relaySessionKey,
635
1371
  };
636
1372
  return buildClaworldMaintenanceEvent({
637
1373
  source: 'claworld_tool',
638
1374
  kind: normalizedToolName,
1375
+ eventType: 'tool_call',
1376
+ scope: sessionKind,
639
1377
  toolName: normalizedToolName,
640
1378
  timestamp,
641
1379
  refs,
1380
+ relations: {
1381
+ requestId,
1382
+ chatRequestId: refs.chatRequestId,
1383
+ conversationKey: refs.conversationKey,
1384
+ worldId: refs.worldId,
1385
+ accountId: refs.accountId,
1386
+ agentCode: refs.agentCode,
1387
+ localSessionKey,
1388
+ relaySessionKey,
1389
+ sessionKey: localSessionKey || relaySessionKey,
1390
+ localAgentId: firstText(context.agentId, context.AgentId),
1391
+ sessionId: firstText(context.sessionId, context.SessionId),
1392
+ sessionFile: firstText(context.sessionFile, context.SessionFile, context.sessionPath),
1393
+ transcriptPath: firstText(context.transcriptPath),
1394
+ },
1395
+ actor: {
1396
+ sessionKind,
1397
+ agentId: firstText(context.agentId, context.AgentId),
1398
+ sessionKey: localSessionKey || relaySessionKey,
1399
+ },
1400
+ tool: {
1401
+ name: normalizedToolName,
1402
+ action: firstText(params.action, payload.action),
1403
+ status: firstText(payload.status, 'succeeded'),
1404
+ },
642
1405
  summary: `${normalizedToolName} succeeded.`,
643
1406
  excerpt: Object.keys(compactPayload).length > 0
644
1407
  ? JSON.stringify(compactPayload)
@@ -646,45 +1409,84 @@ export function buildClaworldToolMaintenanceEvent({
646
1409
  });
647
1410
  }
648
1411
 
649
- function formatRefs(refs = {}) {
650
- return Object.entries(refs)
651
- .filter(([, value]) => value != null)
652
- .map(([key, value]) => `${key}=${flattenInline(value)}`)
653
- .join(', ');
1412
+ function buildJournalEntryPayload(event = {}) {
1413
+ return cleanJournalObject({
1414
+ schema: CLAWORLD_JOURNAL_SCHEMA,
1415
+ id: event.id,
1416
+ key: event.key,
1417
+ timestamp: event.timestamp,
1418
+ source: event.source,
1419
+ scope: event.scope,
1420
+ eventType: event.eventType,
1421
+ kind: event.kind,
1422
+ summary: event.summary,
1423
+ refs: event.refs,
1424
+ relations: event.relations,
1425
+ actor: event.actor,
1426
+ tool: event.tool,
1427
+ artifacts: event.artifacts,
1428
+ maintenance: event.maintenance,
1429
+ excerpt: event.excerpt || null,
1430
+ });
1431
+ }
1432
+
1433
+ function buildJournalEntry(event = {}) {
1434
+ const title = flattenInline(event.summary || event.kind || 'Claworld event recorded.');
1435
+ const heading = [
1436
+ event.timestamp,
1437
+ event.scope || event.source,
1438
+ event.kind,
1439
+ title,
1440
+ ].filter(Boolean).join(' · ');
1441
+ return [
1442
+ `## ${heading}`,
1443
+ '',
1444
+ '```json',
1445
+ JSON.stringify(buildJournalEntryPayload(event), null, 2),
1446
+ '```',
1447
+ '',
1448
+ ].join('\n');
654
1449
  }
655
1450
 
656
1451
  export async function appendClaworldJournalEvent(options = {}, event = {}, appendOptions = {}) {
657
1452
  const workspaceRoot = resolveClaworldWorkspaceRoot(options, appendOptions.homeDir || os.homedir());
658
1453
  await ensureClaworldWorkingMemory(workspaceRoot, appendOptions);
659
1454
  const normalizedEvent = buildClaworldMaintenanceEvent(event);
660
- const monthKey = toMonthKey(normalizedEvent.timestamp);
1455
+ const dayKey = toDayKey(normalizedEvent.timestamp);
661
1456
  const journalPath = path.join(
662
1457
  workspaceRoot,
663
1458
  CLAWORLD_WORKING_MEMORY_DIR,
664
1459
  CLAWORLD_JOURNAL_DIR,
665
- `${monthKey}.md`,
1460
+ `${dayKey}.md`,
666
1461
  );
667
1462
  const currentContent = await readTextIfPresent(journalPath);
668
- const refsLine = formatRefs(normalizedEvent.refs);
669
- const entry = [
670
- `## ${normalizedEvent.timestamp} - ${normalizedEvent.kind}`,
671
- `- Source: ${normalizedEvent.source}`,
672
- `- Summary: ${flattenInline(normalizedEvent.summary)}`,
673
- normalizedEvent.excerpt ? `- Excerpt: ${flattenInline(normalizedEvent.excerpt)}` : null,
674
- refsLine ? `- Refs: ${refsLine}` : null,
675
- '',
676
- ].filter((line) => line != null).join('\n');
1463
+ const entry = buildJournalEntry(normalizedEvent);
677
1464
  if (currentContent == null) {
678
- await atomicWriteText(journalPath, `# Claworld Journal ${monthKey}\n\n${entry}`, {
1465
+ await atomicWriteText(journalPath, `# Claworld Journal ${dayKey}\n\n${entry}`, {
679
1466
  backup: false,
680
1467
  rejectEmptyOverwrite: false,
681
1468
  });
682
1469
  } else {
683
1470
  await fs.appendFile(journalPath, `${currentContent.endsWith('\n') ? '' : '\n'}${entry}`, 'utf8');
684
1471
  }
1472
+ let sessionDirectory = null;
1473
+ if (appendOptions.updateSessionDirectory !== false) {
1474
+ try {
1475
+ sessionDirectory = await updateClaworldSessionDirectory(workspaceRoot, normalizedEvent, {
1476
+ ...appendOptions,
1477
+ timestamp: normalizedEvent.timestamp,
1478
+ });
1479
+ } catch (error) {
1480
+ sessionDirectory = {
1481
+ ok: false,
1482
+ error: error?.message || String(error),
1483
+ };
1484
+ }
1485
+ }
685
1486
  return {
686
1487
  ok: true,
687
1488
  journalPath,
1489
+ sessionDirectory,
688
1490
  event: normalizedEvent,
689
1491
  };
690
1492
  }
@@ -731,11 +1533,27 @@ export async function buildClaworldBootstrapPromptContext(context = {}, options
731
1533
  })
732
1534
  : { slices: {} };
733
1535
  const pointerInjected = target === CLAWORLD_BOOTSTRAP_TARGETS.MAIN;
1536
+ const managementPolicyInjected = target === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT;
1537
+ let managementMainSessionKey = null;
1538
+ if (managementPolicyInjected && resolvedWorkspaceRoot) {
1539
+ try {
1540
+ const sessionDirectory = await readClaworldSessionDirectory(resolvedWorkspaceRoot);
1541
+ managementMainSessionKey = normalizeText(
1542
+ sessionDirectory.directory?.main?.lastActiveSessionKey,
1543
+ null,
1544
+ );
1545
+ } catch {
1546
+ managementMainSessionKey = null;
1547
+ }
1548
+ }
734
1549
  const parts = [];
735
1550
  let truncated = false;
736
1551
  if (pointerInjected) {
737
1552
  const pointerBudget = maxTotalChars - measureBootstrapParts(parts) - (parts.length > 0 ? 2 : 0);
738
- const fittedPointer = truncateBootstrapText(buildClaworldContextPointer(), pointerBudget);
1553
+ const fittedPointer = truncateBootstrapText(
1554
+ buildClaworldContextPointer({ workspaceRoot: resolvedWorkspaceRoot }),
1555
+ pointerBudget,
1556
+ );
739
1557
  if (fittedPointer.text) {
740
1558
  parts.push(fittedPointer.text);
741
1559
  }
@@ -743,6 +1561,22 @@ export async function buildClaworldBootstrapPromptContext(context = {}, options
743
1561
  truncated = true;
744
1562
  }
745
1563
  }
1564
+ if (managementPolicyInjected) {
1565
+ const policyBudget = maxTotalChars - measureBootstrapParts(parts) - (parts.length > 0 ? 2 : 0);
1566
+ const fittedPolicy = truncateBootstrapText(
1567
+ buildClaworldManagementStartupPrompt({
1568
+ workspaceRoot: resolvedWorkspaceRoot,
1569
+ mainSessionKey: managementMainSessionKey,
1570
+ }),
1571
+ policyBudget,
1572
+ );
1573
+ if (fittedPolicy.text) {
1574
+ parts.push(fittedPolicy.text);
1575
+ }
1576
+ if (fittedPolicy.truncated) {
1577
+ truncated = true;
1578
+ }
1579
+ }
746
1580
  const sectionTitle = target === CLAWORLD_BOOTSTRAP_TARGETS.MAIN
747
1581
  ? '# Claworld Startup Memory'
748
1582
  : target === CLAWORLD_BOOTSTRAP_TARGETS.MANAGEMENT
@@ -773,6 +1607,7 @@ export async function buildClaworldBootstrapPromptContext(context = {}, options
773
1607
  workspaceRoot: resolvedWorkspaceRoot,
774
1608
  files: selectedFiles.map((relativePath) => buildBootstrapFileLabel(relativePath)),
775
1609
  pointerInjected,
1610
+ managementPolicyInjected,
776
1611
  fallbackFiles: fileSections.fallbackFiles,
777
1612
  omittedFiles: fileSections.omittedFiles,
778
1613
  truncated,
@@ -834,6 +1669,40 @@ function assertAllowedTarget(runType, target) {
834
1669
  }
835
1670
  }
836
1671
 
1672
+ function normalizeMarkdownNewlines(content) {
1673
+ return String(content ?? '').replace(/\r\n/g, '\n');
1674
+ }
1675
+
1676
+ function assertContextMarkdownUsesBullets(target, normalizedContent, schema) {
1677
+ const maxBulletLength = schema.maxBulletLength || null;
1678
+ const lines = normalizedContent.split('\n');
1679
+ for (const line of lines) {
1680
+ const trimmed = line.trim();
1681
+ if (!trimmed || trimmed.startsWith('#')) continue;
1682
+ if (!trimmed.startsWith('- ')) {
1683
+ throw new Error(`Claworld maintenance patch for ${target} must use bullet lines under schema sections.`);
1684
+ }
1685
+ if (maxBulletLength && trimmed.length > maxBulletLength) {
1686
+ throw new Error(`Claworld maintenance patch for ${target} has a bullet longer than ${maxBulletLength} characters.`);
1687
+ }
1688
+ }
1689
+ }
1690
+
1691
+ function assertContextFileSchema(target, content) {
1692
+ const schema = CLAWORLD_CONTEXT_FILE_SCHEMAS[target];
1693
+ if (!schema) return;
1694
+ const normalizedContent = normalizeMarkdownNewlines(content);
1695
+ if (!normalizedContent.startsWith(`${schema.title}\n`)) {
1696
+ throw new Error(`Claworld maintenance patch for ${target} must start with ${schema.title}.`);
1697
+ }
1698
+ for (const heading of schema.headings) {
1699
+ if (!normalizedContent.includes(`\n${heading}\n`)) {
1700
+ throw new Error(`Claworld maintenance patch for ${target} is missing required section ${heading}.`);
1701
+ }
1702
+ }
1703
+ assertContextMarkdownUsesBullets(target, normalizedContent, schema);
1704
+ }
1705
+
837
1706
  function normalizeReportPatch(report, index) {
838
1707
  const filename = normalizeText(report?.filename ?? report?.name, `report-${index + 1}.md`);
839
1708
  const normalizedFilename = path.posix.basename(filename.endsWith('.md') ? filename : `${filename}.md`);
@@ -890,10 +1759,10 @@ export function normalizeClaworldMaintenanceOutput(runType, output = {}, options
890
1759
  patches.push(normalizeFilePatchValue(output.memoryMd, CLAWORLD_WORKING_MEMORY_FILES.memory, 'memoryMd'));
891
1760
  }
892
1761
  if (Object.prototype.hasOwnProperty.call(output, 'journalAppendMd')) {
893
- const monthKey = toMonthKey(options.timestamp || output.timestamp || Date.now());
1762
+ const dayKey = toDayKey(options.timestamp || output.timestamp || Date.now());
894
1763
  patches.push({
895
1764
  operation: 'append_section',
896
- target: `${CLAWORLD_JOURNAL_DIR}/${monthKey}.md`,
1765
+ target: `${CLAWORLD_JOURNAL_DIR}/${dayKey}.md`,
897
1766
  content: String(output.journalAppendMd ?? ''),
898
1767
  });
899
1768
  }
@@ -926,6 +1795,9 @@ export function normalizeClaworldMaintenanceOutput(runType, output = {}, options
926
1795
  target,
927
1796
  content: String(patch.content ?? ''),
928
1797
  };
1798
+ if (operation === 'replace') {
1799
+ assertContextFileSchema(target, normalizedPatch.content);
1800
+ }
929
1801
  const rationale = normalizeText(patch.rationale, null);
930
1802
  if (rationale) normalizedPatch.rationale = rationale;
931
1803
  return normalizedPatch;
@@ -955,8 +1827,8 @@ async function applyMaintenancePatch(workspaceRoot, patch) {
955
1827
  await fs.mkdir(path.dirname(absolutePath), { recursive: true });
956
1828
  const currentContent = await readTextIfPresent(absolutePath);
957
1829
  if (currentContent == null) {
958
- const monthKey = path.basename(patch.target, '.md');
959
- await atomicWriteText(absolutePath, `# Claworld Journal ${monthKey}\n\n${patch.content}`, {
1830
+ const dayKey = path.basename(patch.target, '.md');
1831
+ await atomicWriteText(absolutePath, `# Claworld Journal ${dayKey}\n\n${patch.content}`, {
960
1832
  backup: false,
961
1833
  rejectEmptyOverwrite: false,
962
1834
  });