ticlawk 0.1.16-dev.1 → 0.1.16-dev.10

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.
@@ -145,6 +145,10 @@ export async function handleMessageSend(req, body, ctx) {
145
145
  return { status: 400, body: { error: 'target or conversation_id is required' } };
146
146
  }
147
147
 
148
+ const mediaAssetIds = Array.isArray(body?.media_asset_ids)
149
+ ? body.media_asset_ids.map((v) => String(v).trim()).filter(Boolean)
150
+ : [];
151
+
148
152
  try {
149
153
  const data = await api.sendAgentMessage({
150
154
  actingAgentId,
@@ -153,6 +157,7 @@ export async function handleMessageSend(req, body, ctx) {
153
157
  seenUpToSeq: body?.seen_up_to_seq,
154
158
  replyToMessageId: body?.reply_to_message_id || threadRootMsgId || null,
155
159
  runtimeHostId: getRuntimeHostId(req, body),
160
+ mediaAssetIds,
156
161
  });
157
162
  debugLog('agent-cli', 'send.ok', {
158
163
  actingAgentId,
@@ -200,22 +205,86 @@ export async function handleMessageRead(req, query, ctx) {
200
205
  }
201
206
  }
202
207
 
208
+ export async function handleTaskCreate(req, body, ctx) {
209
+ const actingAgentId = getActingAgentId(req, body);
210
+ const v = validateActingAgent(actingAgentId, ctx);
211
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
212
+
213
+ const text = String(body?.text || '').trim();
214
+ if (!text) return { status: 400, body: { error: 'text is required' } };
215
+
216
+ let conversationId = body?.conversation_id || null;
217
+ if (!conversationId && body?.target) {
218
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
219
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
220
+ conversationId = resolved.conversationId;
221
+ }
222
+ if (!conversationId) {
223
+ return { status: 400, body: { error: 'target or conversation_id is required' } };
224
+ }
225
+
226
+ try {
227
+ const data = await api.createAgentTask({
228
+ actingAgentId,
229
+ conversationId,
230
+ text,
231
+ title: body?.title ?? null,
232
+ });
233
+ debugLog('agent-cli', 'task.create', {
234
+ actingAgentId,
235
+ conversationId,
236
+ messageId: data?.message?.id,
237
+ taskNumber: data?.task?.number,
238
+ });
239
+ return { status: 200, body: data };
240
+ } catch (err) {
241
+ debugError('agent-cli', 'task.create.failed', {
242
+ actingAgentId,
243
+ conversationId,
244
+ error: err?.message || String(err),
245
+ });
246
+ return { status: err?.status || 500, body: { error: err?.message || 'task create failed' } };
247
+ }
248
+ }
249
+
203
250
  export async function handleTaskClaim(req, body, ctx) {
204
251
  const actingAgentId = getActingAgentId(req, body);
205
252
  const v = validateActingAgent(actingAgentId, ctx);
206
253
  if (!v.ok) return { status: v.status, body: { error: v.error } };
207
- const sourceMessageId = body?.source_message_id || body?.message_id;
208
- if (!sourceMessageId) return { status: 400, body: { error: 'source_message_id (or message_id) is required' } };
254
+ const sourceMessageId = body?.source_message_id || body?.message_id || null;
255
+ const number = body?.number != null ? Number(body.number) : null;
256
+
257
+ // Number-based claim needs the conversation_id to disambiguate. Allow
258
+ // the caller to pass either conversation_id directly or a target.
259
+ let conversationId = body?.conversation_id || null;
260
+ if (number != null && !conversationId && body?.target) {
261
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
262
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
263
+ conversationId = resolved.conversationId;
264
+ }
265
+
266
+ if (!sourceMessageId && number == null) {
267
+ return { status: 400, body: { error: '--message-id or --number is required' } };
268
+ }
269
+ if (number != null && !conversationId) {
270
+ return { status: 400, body: { error: '--target or --conversation-id is required when claiming by number' } };
271
+ }
272
+
209
273
  try {
210
274
  const data = await api.claimAgentTask({
211
275
  actingAgentId,
212
276
  sourceMessageId,
277
+ conversationId,
278
+ number,
213
279
  leaseSeconds: body?.lease_seconds,
214
280
  });
215
281
  debugLog('agent-cli', 'task.claim', {
216
282
  actingAgentId,
217
- sourceMessageId,
283
+ sourceMessageId: sourceMessageId || null,
284
+ number: number ?? null,
285
+ conversationId: conversationId || null,
218
286
  claimed: data?.claimed,
287
+ taskNumber: data?.task?.number || null,
219
288
  });
220
289
  return { status: data?.claimed === false ? 409 : 200, body: data };
221
290
  } catch (err) {
@@ -223,6 +292,27 @@ export async function handleTaskClaim(req, body, ctx) {
223
292
  }
224
293
  }
225
294
 
295
+ export async function handleTaskUnclaim(req, body, ctx) {
296
+ const actingAgentId = getActingAgentId(req, body);
297
+ const v = validateActingAgent(actingAgentId, ctx);
298
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
299
+ if (!body?.task_id) return { status: 400, body: { error: 'task_id is required' } };
300
+ try {
301
+ const data = await api.unclaimAgentTask({
302
+ actingAgentId,
303
+ taskId: body.task_id,
304
+ });
305
+ debugLog('agent-cli', 'task.unclaim', {
306
+ actingAgentId,
307
+ taskId: body.task_id,
308
+ ok: data?.ok,
309
+ });
310
+ return { status: data?.ok ? 200 : 409, body: data };
311
+ } catch (err) {
312
+ return { status: err?.status || 500, body: { error: err?.message || 'unclaim failed' } };
313
+ }
314
+ }
315
+
226
316
  export async function handleTaskUpdate(req, body, ctx) {
227
317
  const actingAgentId = getActingAgentId(req, body);
228
318
  const v = validateActingAgent(actingAgentId, ctx);
@@ -276,6 +366,356 @@ export async function handleGroupMembers(req, query, ctx) {
276
366
  }
277
367
  }
278
368
 
369
+ export async function handleMessageReact(req, body, ctx) {
370
+ const actingAgentId = getActingAgentId(req, body);
371
+ const v = validateActingAgent(actingAgentId, ctx);
372
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
373
+ const messageId = body?.message_id;
374
+ const emoji = body?.emoji;
375
+ const remove = !!body?.remove;
376
+ if (!messageId) return { status: 400, body: { error: 'message_id is required' } };
377
+ if (!emoji) return { status: 400, body: { error: 'emoji is required' } };
378
+ try {
379
+ const data = await api.reactToMessage({
380
+ actingAgentId, messageId, emoji, remove,
381
+ });
382
+ debugLog('agent-cli', remove ? 'message.unreact' : 'message.react', {
383
+ actingAgentId, messageId, emoji,
384
+ });
385
+ return { status: 200, body: data };
386
+ } catch (err) {
387
+ return { status: err?.status || 500, body: { error: err?.message || 'react failed' } };
388
+ }
389
+ }
390
+
391
+ export async function handleReminderSchedule(req, body, ctx) {
392
+ const actingAgentId = getActingAgentId(req, body);
393
+ const v = validateActingAgent(actingAgentId, ctx);
394
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
395
+ if (!body?.title) return { status: 400, body: { error: 'title is required' } };
396
+ if (!body?.fire_at) return { status: 400, body: { error: 'fire_at (ISO timestamp) is required' } };
397
+
398
+ let anchorConversationId = body?.anchor_conversation_id || null;
399
+ if (!anchorConversationId && body?.target) {
400
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
401
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
402
+ anchorConversationId = resolved.conversationId;
403
+ }
404
+ if (!anchorConversationId) {
405
+ return { status: 400, body: { error: '--target or --anchor-conversation-id required' } };
406
+ }
407
+
408
+ try {
409
+ const data = await api.scheduleAgentReminder({
410
+ actingAgentId,
411
+ title: body.title,
412
+ fireAt: body.fire_at,
413
+ anchorConversationId,
414
+ anchorMessageId: body?.anchor_message_id || null,
415
+ });
416
+ debugLog('agent-cli', 'reminder.schedule', {
417
+ actingAgentId,
418
+ reminderId: data?.data?.id,
419
+ fireAt: body.fire_at,
420
+ });
421
+ return { status: 200, body: data };
422
+ } catch (err) {
423
+ return { status: err?.status || 500, body: { error: err?.message || 'schedule failed' } };
424
+ }
425
+ }
426
+
427
+ export async function handleReminderList(req, query, ctx) {
428
+ const actingAgentId = getActingAgentId(req, query);
429
+ const v = validateActingAgent(actingAgentId, ctx);
430
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
431
+ try {
432
+ const data = await api.listAgentReminders({
433
+ actingAgentId,
434
+ status: query?.status || null,
435
+ });
436
+ return { status: 200, body: { data } };
437
+ } catch (err) {
438
+ return { status: err?.status || 500, body: { error: err?.message || 'list failed' } };
439
+ }
440
+ }
441
+
442
+ export async function handleReminderSnooze(req, body, ctx) {
443
+ const actingAgentId = getActingAgentId(req, body);
444
+ const v = validateActingAgent(actingAgentId, ctx);
445
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
446
+ if (!body?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
447
+ if (!body?.fire_at) return { status: 400, body: { error: 'fire_at is required' } };
448
+ try {
449
+ const data = await api.snoozeAgentReminder({
450
+ actingAgentId,
451
+ reminderId: body.reminder_id,
452
+ fireAt: body.fire_at,
453
+ });
454
+ return { status: 200, body: data };
455
+ } catch (err) {
456
+ return { status: err?.status || 500, body: { error: err?.message || 'snooze failed' } };
457
+ }
458
+ }
459
+
460
+ export async function handleReminderUpdate(req, body, ctx) {
461
+ const actingAgentId = getActingAgentId(req, body);
462
+ const v = validateActingAgent(actingAgentId, ctx);
463
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
464
+ if (!body?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
465
+ try {
466
+ const data = await api.updateAgentReminder({
467
+ actingAgentId,
468
+ reminderId: body.reminder_id,
469
+ title: body?.title || null,
470
+ fireAt: body?.fire_at || null,
471
+ });
472
+ return { status: 200, body: data };
473
+ } catch (err) {
474
+ return { status: err?.status || 500, body: { error: err?.message || 'update failed' } };
475
+ }
476
+ }
477
+
478
+ export async function handleReminderCancel(req, body, ctx) {
479
+ const actingAgentId = getActingAgentId(req, body);
480
+ const v = validateActingAgent(actingAgentId, ctx);
481
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
482
+ if (!body?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
483
+ try {
484
+ const data = await api.cancelAgentReminder({
485
+ actingAgentId,
486
+ reminderId: body.reminder_id,
487
+ });
488
+ return { status: 200, body: data };
489
+ } catch (err) {
490
+ return { status: err?.status || 500, body: { error: err?.message || 'cancel failed' } };
491
+ }
492
+ }
493
+
494
+ export async function handleReminderLog(req, query, ctx) {
495
+ const actingAgentId = getActingAgentId(req, query);
496
+ const v = validateActingAgent(actingAgentId, ctx);
497
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
498
+ if (!query?.reminder_id) return { status: 400, body: { error: 'reminder_id is required' } };
499
+ try {
500
+ const data = await api.getAgentReminderLog({
501
+ actingAgentId,
502
+ reminderId: query.reminder_id,
503
+ });
504
+ return { status: 200, body: { data } };
505
+ } catch (err) {
506
+ return { status: err?.status || 500, body: { error: err?.message || 'log failed' } };
507
+ }
508
+ }
509
+
510
+ export async function handleMessageCheck(req, query, ctx) {
511
+ const actingAgentId = getActingAgentId(req, query);
512
+ const v = validateActingAgent(actingAgentId, ctx);
513
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
514
+ let conversationId = query?.conversation_id || null;
515
+ if (!conversationId && query?.target) {
516
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
517
+ if (!resolved.error) conversationId = resolved.conversationId;
518
+ }
519
+ try {
520
+ const data = await api.checkAgentMessages({ actingAgentId, conversationId });
521
+ return { status: 200, body: data };
522
+ } catch (err) {
523
+ return { status: err?.status || 500, body: { error: err?.message || 'check failed' } };
524
+ }
525
+ }
526
+
527
+ export async function handleMessageSearch(req, query, ctx) {
528
+ const actingAgentId = getActingAgentId(req, query);
529
+ const v = validateActingAgent(actingAgentId, ctx);
530
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
531
+ if (!query?.q) return { status: 400, body: { error: 'q is required' } };
532
+ let conversationId = query?.conversation_id || null;
533
+ if (!conversationId && query?.target) {
534
+ const resolved = await resolveTarget(actingAgentId, String(query.target));
535
+ if (!resolved.error) conversationId = resolved.conversationId;
536
+ }
537
+ try {
538
+ const data = await api.searchAgentMessages({
539
+ actingAgentId,
540
+ query: query.q,
541
+ conversationId,
542
+ limit: query?.limit != null ? Number(query.limit) : null,
543
+ });
544
+ return { status: 200, body: { data } };
545
+ } catch (err) {
546
+ return { status: err?.status || 500, body: { error: err?.message || 'search failed' } };
547
+ }
548
+ }
549
+
550
+ export async function handleProfileShow(req, query, ctx) {
551
+ const actingAgentId = getActingAgentId(req, query);
552
+ const v = validateActingAgent(actingAgentId, ctx);
553
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
554
+ const idOrHandle = query?.id || query?.handle || actingAgentId;
555
+ try {
556
+ const data = await api.getAgentProfile({ actingAgentId, idOrHandle });
557
+ return { status: 200, body: data };
558
+ } catch (err) {
559
+ return { status: err?.status || 500, body: { error: err?.message || 'profile show failed' } };
560
+ }
561
+ }
562
+
563
+ export async function handleProfileUpdate(req, body, ctx) {
564
+ const actingAgentId = getActingAgentId(req, body);
565
+ const v = validateActingAgent(actingAgentId, ctx);
566
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
567
+ try {
568
+ const data = await api.patchAgentProfile({
569
+ actingAgentId,
570
+ displayName: body?.display_name ?? null,
571
+ description: body?.description ?? null,
572
+ avatarUrl: body?.avatar_url ?? null,
573
+ });
574
+ debugLog('agent-cli', 'profile.update', { actingAgentId, keys: Object.keys(body || {}) });
575
+ return { status: 200, body: data };
576
+ } catch (err) {
577
+ return { status: err?.status || 500, body: { error: err?.message || 'profile update failed' } };
578
+ }
579
+ }
580
+
581
+ export async function handleAttachmentUpload(req, body, ctx) {
582
+ const actingAgentId = getActingAgentId(req, body);
583
+ const v = validateActingAgent(actingAgentId, ctx);
584
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
585
+ if (!body?.data_base64) return { status: 400, body: { error: 'data_base64 is required' } };
586
+ try {
587
+ const data = await api.uploadAgentAttachment({
588
+ actingAgentId,
589
+ filename: body?.filename || 'attachment.bin',
590
+ contentType: body?.content_type || 'application/octet-stream',
591
+ dataBase64: body.data_base64,
592
+ });
593
+ debugLog('agent-cli', 'attachment.upload', {
594
+ actingAgentId,
595
+ asset_id: data?.data?.asset_id,
596
+ bytes: data?.data?.size_bytes,
597
+ });
598
+ return { status: 200, body: data };
599
+ } catch (err) {
600
+ return { status: err?.status || 500, body: { error: err?.message || 'attachment upload failed' } };
601
+ }
602
+ }
603
+
604
+ export async function handleProfileAvatarUpload(req, body, ctx) {
605
+ const actingAgentId = getActingAgentId(req, body);
606
+ const v = validateActingAgent(actingAgentId, ctx);
607
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
608
+ if (!body?.data_base64) return { status: 400, body: { error: 'data_base64 is required' } };
609
+ if (!body?.content_type || !String(body.content_type).startsWith('image/')) {
610
+ return { status: 400, body: { error: 'content_type must be image/*' } };
611
+ }
612
+ try {
613
+ const data = await api.uploadAgentAvatar({
614
+ actingAgentId,
615
+ filename: body?.filename || 'avatar.bin',
616
+ contentType: body.content_type,
617
+ dataBase64: body.data_base64,
618
+ });
619
+ debugLog('agent-cli', 'avatar.upload', { actingAgentId, url: data?.url });
620
+ return { status: 200, body: data };
621
+ } catch (err) {
622
+ return { status: err?.status || 500, body: { error: err?.message || 'avatar upload failed' } };
623
+ }
624
+ }
625
+
626
+ export async function handleAttachmentView(req, query, ctx) {
627
+ const actingAgentId = getActingAgentId(req, query);
628
+ const v = validateActingAgent(actingAgentId, ctx);
629
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
630
+ const assetId = query?.asset_id;
631
+ if (!assetId) return { status: 400, body: { error: 'asset_id is required' } };
632
+ try {
633
+ const data = await api.viewAgentAttachment({ actingAgentId, assetId });
634
+ return { status: 200, body: data };
635
+ } catch (err) {
636
+ return { status: err?.status || 500, body: { error: err?.message || 'attachment view failed' } };
637
+ }
638
+ }
639
+
640
+ export async function handleGroupCreate(req, body, ctx) {
641
+ const actingAgentId = getActingAgentId(req, body);
642
+ const v = validateActingAgent(actingAgentId, ctx);
643
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
644
+ const name = String(body?.name || '').trim();
645
+ if (!name) return { status: 400, body: { error: 'name is required' } };
646
+ try {
647
+ const data = await api.createAgentGroup({
648
+ actingAgentId,
649
+ name,
650
+ description: body?.description || null,
651
+ memberAgentIds: Array.isArray(body?.member_agent_ids) ? body.member_agent_ids : [],
652
+ });
653
+ invalidateServerInfoCache(actingAgentId);
654
+ debugLog('agent-cli', 'group.create', {
655
+ actingAgentId,
656
+ conversationId: data?.conversation?.id,
657
+ members: data?.member_agent_ids,
658
+ });
659
+ return { status: 200, body: data };
660
+ } catch (err) {
661
+ return { status: err?.status || 500, body: { error: err?.message || 'group create failed' } };
662
+ }
663
+ }
664
+
665
+ export async function handleGroupMembersAdd(req, body, ctx) {
666
+ const actingAgentId = getActingAgentId(req, body);
667
+ const v = validateActingAgent(actingAgentId, ctx);
668
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
669
+ let conversationId = body?.conversation_id || null;
670
+ if (!conversationId && body?.target) {
671
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
672
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
673
+ conversationId = resolved.conversationId;
674
+ }
675
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
676
+ const agentIds = Array.isArray(body?.agent_ids) ? body.agent_ids : [];
677
+ if (agentIds.length === 0) return { status: 400, body: { error: 'agent_ids must be non-empty' } };
678
+ try {
679
+ const data = await api.addAgentGroupMembers({
680
+ actingAgentId, conversationId, agentIds,
681
+ });
682
+ invalidateServerInfoCache(actingAgentId);
683
+ debugLog('agent-cli', 'group.members.add', {
684
+ actingAgentId, conversationId, agentIds,
685
+ });
686
+ return { status: 200, body: data };
687
+ } catch (err) {
688
+ return { status: err?.status || 500, body: { error: err?.message || 'add failed' } };
689
+ }
690
+ }
691
+
692
+ export async function handleGroupMembersRemove(req, body, ctx) {
693
+ const actingAgentId = getActingAgentId(req, body);
694
+ const v = validateActingAgent(actingAgentId, ctx);
695
+ if (!v.ok) return { status: v.status, body: { error: v.error } };
696
+ let conversationId = body?.conversation_id || null;
697
+ if (!conversationId && body?.target) {
698
+ const resolved = await resolveTarget(actingAgentId, String(body.target));
699
+ if (resolved.error) return { status: 404, body: { error: resolved.error } };
700
+ conversationId = resolved.conversationId;
701
+ }
702
+ if (!conversationId) return { status: 400, body: { error: 'target or conversation_id is required' } };
703
+ const agentId = body?.agent_id;
704
+ if (!agentId) return { status: 400, body: { error: 'agent_id is required' } };
705
+ try {
706
+ const data = await api.removeAgentGroupMember({
707
+ actingAgentId, conversationId, agentId,
708
+ });
709
+ invalidateServerInfoCache(actingAgentId);
710
+ debugLog('agent-cli', 'group.members.remove', {
711
+ actingAgentId, conversationId, agentId,
712
+ });
713
+ return { status: 200, body: data };
714
+ } catch (err) {
715
+ return { status: err?.status || 500, body: { error: err?.message || 'remove failed' } };
716
+ }
717
+ }
718
+
279
719
  export async function handleServerInfo(req, query, ctx) {
280
720
  const actingAgentId = getActingAgentId(req, query);
281
721
  const v = validateActingAgent(actingAgentId, ctx);
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Per-agent home directory: ~/.ticlawk/agents/<agent_id>/
3
+ *
4
+ * The agent's authoritative workspace. The daemon spawns every runtime
5
+ * with this as cwd; the agent's MEMORY.md, notes/, and any artifacts
6
+ * it produces live here. No project binding — one MEMORY.md per agent,
7
+ * lives in cwd, agent reads it via `cat MEMORY.md`.
8
+ */
9
+
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
+ import { join } from 'node:path';
12
+ import { AF_HOME } from './config.mjs';
13
+
14
+ export const AF_AGENTS_DIR = join(AF_HOME, 'agents');
15
+
16
+ export function getAgentHome(agentId) {
17
+ if (!agentId) throw new Error('getAgentHome: agentId is required');
18
+ return join(AF_AGENTS_DIR, String(agentId));
19
+ }
20
+
21
+ export function getAgentMemoryPath(agentId) {
22
+ return join(getAgentHome(agentId), 'MEMORY.md');
23
+ }
24
+
25
+ /**
26
+ * Make sure the agent home dir exists + has a starter MEMORY.md. Idempotent;
27
+ * safe to call on every spawn. The daemon does this before driver.spawn()
28
+ * so cwd is always a real, writable directory.
29
+ *
30
+ * Pass `displayName` only when seeding for the first time — existing
31
+ * MEMORY.md is never overwritten (the agent owns it).
32
+ */
33
+ export function ensureAgentHome(agentId, { displayName } = {}) {
34
+ const home = getAgentHome(agentId);
35
+ mkdirSync(home, { recursive: true });
36
+ const memoryPath = getAgentMemoryPath(agentId);
37
+ if (!existsSync(memoryPath)) {
38
+ writeFileSync(memoryPath, buildInitialMemoryMd({ displayName, home }), 'utf8');
39
+ }
40
+ return home;
41
+ }
42
+
43
+ function buildInitialMemoryMd({ displayName, home }) {
44
+ const lines = [
45
+ `# ${displayName || 'Agent'}`,
46
+ '',
47
+ '## Role',
48
+ '<your role definition, evolved over time>',
49
+ '',
50
+ '## Workspace',
51
+ home,
52
+ '',
53
+ ];
54
+ lines.push(
55
+ '## Key Knowledge',
56
+ '- (none yet — populate as you learn the user, project, and domain)',
57
+ '',
58
+ '## Active Context',
59
+ '- (none)',
60
+ '',
61
+ '## How to update this file',
62
+ 'MEMORY.md is your entry point. Read it at the top of every turn. Add a',
63
+ '`notes/<topic>.md` for each long-lived knowledge area and link it here.',
64
+ '',
65
+ );
66
+ return lines.join('\n');
67
+ }
68
+
69
+ /**
70
+ * Best-effort read of the "## Workspace" line from a MEMORY.md. Useful
71
+ * if some code wants to know where the agent currently believes it
72
+ * works (e.g. for UI display). The daemon does NOT use this for spawn
73
+ * cwd — spawn cwd is always getAgentHome(id).
74
+ */
75
+ export function readWorkspaceFromMemory(agentId) {
76
+ const memoryPath = getAgentMemoryPath(agentId);
77
+ if (!existsSync(memoryPath)) return null;
78
+ try {
79
+ const text = readFileSync(memoryPath, 'utf8');
80
+ const m = text.match(/^##\s+Workspace\s*\n([^\n]+)/m);
81
+ return m ? m[1].trim() : null;
82
+ } catch {
83
+ return null;
84
+ }
85
+ }