@adhdev/daemon-core 0.9.82-rc.209 → 0.9.82-rc.210

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.
Files changed (64) hide show
  1. package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +2 -0
  2. package/dist/commands/router.d.ts +6 -0
  3. package/dist/git/git-commands.d.ts +2 -0
  4. package/dist/git/git-diff.d.ts +6 -0
  5. package/dist/index.d.ts +11 -5
  6. package/dist/index.js +5699 -3486
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +5679 -3483
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/mesh/coordinator-prompt.d.ts +6 -0
  11. package/dist/mesh/mesh-delivery-policy.d.ts +5 -0
  12. package/dist/mesh/mesh-events-coordinator.d.ts +151 -0
  13. package/dist/mesh/mesh-events-pending.d.ts +33 -0
  14. package/dist/mesh/mesh-events-stale.d.ts +40 -0
  15. package/dist/mesh/mesh-events-utils.d.ts +14 -0
  16. package/dist/mesh/mesh-events.d.ts +5 -198
  17. package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
  18. package/dist/mesh/mesh-ledger.d.ts +19 -0
  19. package/dist/mesh/mesh-missions.d.ts +58 -0
  20. package/dist/mesh/mesh-review-inbox.d.ts +90 -0
  21. package/dist/mesh/mesh-runtime-store.d.ts +175 -0
  22. package/dist/mesh/mesh-task-stats.d.ts +49 -0
  23. package/dist/mesh/mesh-work-queue.d.ts +82 -0
  24. package/dist/mesh/refine-config.d.ts +24 -2
  25. package/dist/mesh/worktree-bootstrap-config.d.ts +22 -0
  26. package/dist/providers/acp-provider-instance.d.ts +2 -0
  27. package/dist/providers/spec/driver.d.ts +8 -0
  28. package/dist/providers/spec/evaluator.d.ts +4 -5
  29. package/dist/providers/spec/loader.d.ts +1 -0
  30. package/dist/providers/spec/schema.gen.d.ts +1409 -6
  31. package/dist/providers/spec/types.d.ts +188 -175
  32. package/dist/repo-mesh-types.d.ts +1 -0
  33. package/package.json +1 -1
  34. package/src/boot/daemon-lifecycle.ts +3 -0
  35. package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +20 -7
  36. package/src/commands/router.ts +594 -66
  37. package/src/git/git-commands.ts +5 -5
  38. package/src/git/git-diff.ts +53 -0
  39. package/src/index.ts +11 -5
  40. package/src/mesh/coordinator-prompt.ts +14 -1
  41. package/src/mesh/mesh-delivery-policy.ts +17 -0
  42. package/src/mesh/mesh-events-coordinator.ts +1404 -0
  43. package/src/mesh/mesh-events-pending.ts +371 -0
  44. package/src/mesh/mesh-events-stale.ts +283 -0
  45. package/src/mesh/mesh-events-utils.ts +161 -0
  46. package/src/mesh/mesh-events.ts +27 -2143
  47. package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
  48. package/src/mesh/mesh-ledger.ts +134 -2
  49. package/src/mesh/mesh-missions.ts +151 -0
  50. package/src/mesh/mesh-review-inbox.ts +307 -0
  51. package/src/mesh/mesh-runtime-store.ts +539 -3
  52. package/src/mesh/mesh-task-stats.ts +154 -0
  53. package/src/mesh/mesh-work-queue.ts +233 -17
  54. package/src/mesh/refine-config.ts +42 -5
  55. package/src/mesh/worktree-bootstrap-config.ts +79 -0
  56. package/src/providers/acp-provider-instance.ts +15 -1
  57. package/src/providers/cli-provider-instance.ts +34 -13
  58. package/src/providers/spec/driver.ts +57 -29
  59. package/src/providers/spec/evaluator.ts +302 -112
  60. package/src/providers/spec/loader.ts +226 -37
  61. package/src/providers/spec/schema.gen.ts +450 -334
  62. package/src/providers/spec/schema.json +162 -75
  63. package/src/providers/spec/types.ts +234 -183
  64. package/src/repo-mesh-types.ts +1 -0
@@ -1,5 +1,12 @@
1
1
  import type { AppendRemoteLedgerResult, MeshLedgerSlice, MeshLedgerSummary } from './mesh-ledger.js';
2
2
 
3
+ /** Minimal ledger slice shape accepted by buildMeshLedgerReplicaEvidence. Covers both JSONL and SQLite slices. */
4
+ export interface AnyLedgerSlice {
5
+ entries: Array<{ id: string; meshId: string; timestamp: string; kind: string; nodeId?: string | null; sessionId?: string | null; providerType?: string | null; payload: unknown }>;
6
+ cursor: { afterId: string | null; nextAfterId: string | null; limit: number; hasMore: boolean };
7
+ summary?: MeshLedgerSummary;
8
+ }
9
+
3
10
  export type MeshLedgerReplicaStatus = 'local' | 'queried' | 'imported' | 'failed';
4
11
 
5
12
  export interface MeshLedgerReplicaEvidence {
@@ -25,7 +32,7 @@ export interface MeshLedgerReconciliationEvidence {
25
32
  meshId: string;
26
33
  generatedAt: string;
27
34
  sourceOfTruth: {
28
- kind: 'coordinator_local_jsonl';
35
+ kind: 'coordinator_local_sqlite';
29
36
  p2pOnly: true;
30
37
  cloudD1LedgerSync: false;
31
38
  notes: string;
@@ -47,7 +54,7 @@ export interface MeshLedgerReconciliationEvidence {
47
54
  };
48
55
  }
49
56
 
50
- function lastTimestamp(slice?: MeshLedgerSlice): string | null {
57
+ function lastTimestamp(slice?: AnyLedgerSlice): string | null {
51
58
  const entries = Array.isArray(slice?.entries) ? slice!.entries : [];
52
59
  return entries.length ? entries[entries.length - 1].timestamp : null;
53
60
  }
@@ -56,7 +63,7 @@ export function buildMeshLedgerReplicaEvidence(args: {
56
63
  nodeId: string;
57
64
  daemonId?: string;
58
65
  transport: 'local' | 'p2p_datachannel';
59
- slice?: MeshLedgerSlice;
66
+ slice?: AnyLedgerSlice;
60
67
  importResult?: AppendRemoteLedgerResult;
61
68
  status?: MeshLedgerReplicaStatus;
62
69
  error?: string;
@@ -91,10 +98,10 @@ export function buildMeshLedgerReconciliationEvidence(meshId: string, replicas:
91
98
  meshId,
92
99
  generatedAt: new Date().toISOString(),
93
100
  sourceOfTruth: {
94
- kind: 'coordinator_local_jsonl',
101
+ kind: 'coordinator_local_sqlite',
95
102
  p2pOnly: true,
96
103
  cloudD1LedgerSync: false,
97
- notes: 'Coordinator reconciles bounded slices from daemon-local JSONL ledgers over P2P DataChannel; Cloud/D1 is not a ledger source of truth.',
104
+ notes: 'Coordinator reconciles bounded slices from daemon-local SQLite event ledgers (mesh_event_ledger table) over P2P DataChannel; Cloud/D1 is not a ledger source of truth.',
98
105
  },
99
106
  replicas,
100
107
  totals: {
@@ -18,6 +18,7 @@ import { join } from 'path';
18
18
  import { randomUUID } from 'crypto';
19
19
  import { getConfigDir } from '../config/config.js';
20
20
  import { EventEmitter } from 'events';
21
+ import { MeshRuntimeStore } from './mesh-runtime-store.js';
21
22
  // ─── Types ──────────────────────────────────────
22
23
 
23
24
  export type MeshLedgerKind =
@@ -377,6 +378,13 @@ export function compactLedger(meshId: string): { archivedCount: number; retained
377
378
  return { archivedCount: archive.length, retainedCount: keep.length };
378
379
  }
379
380
 
381
+ // G2: mirror the compaction in SQLite so the runtime store matches the active
382
+ // ledger set. Archived entries live on in the JSONL archive files (export/debug).
383
+ try {
384
+ MeshRuntimeStore.getInstance().deleteLedgerEntries(meshId, archive.map(e => e.id));
385
+ invalidateLedgerCache(meshId);
386
+ } catch { /* best-effort; summary counts absorb the difference via archived counts */ }
387
+
380
388
  return { archivedCount: archive.length, retainedCount: keep.length };
381
389
  }
382
390
 
@@ -551,6 +559,26 @@ export function appendLedgerEntry(
551
559
  }
552
560
  }
553
561
 
562
+ // Write to SQLite (G2: primary runtime read/write path)
563
+ try {
564
+ MeshRuntimeStore.getInstance().appendLedgerEntry({
565
+ id: entry.id,
566
+ meshId: entry.meshId,
567
+ timestamp: entry.timestamp,
568
+ kind: entry.kind,
569
+ nodeId: entry.nodeId ?? null,
570
+ sessionId: entry.sessionId ?? null,
571
+ providerType: entry.providerType ?? null,
572
+ payload: entry.payload,
573
+ });
574
+ } catch {
575
+ // SQLite write failed but the JSONL append below still records the entry.
576
+ // Reset the one-time import flag so the next read re-imports from JSONL
577
+ // and the store self-heals instead of silently missing this entry.
578
+ ledgerImportDone.delete(meshId);
579
+ }
580
+
581
+ // Also write to JSONL (retained as export/import/debug/legacy artifact)
554
582
  try {
555
583
  const line = JSON.stringify(entry) + '\n';
556
584
  appendFileSync(filePath, line, { encoding: 'utf-8', mode: 0o600 });
@@ -609,6 +637,20 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
609
637
  return { accepted: 0, skippedDuplicate, rejectedInvalid, entries: [] };
610
638
  }
611
639
 
640
+ // G2: write to SQLite (primary runtime store); INSERT OR IGNORE dedups by id.
641
+ try {
642
+ MeshRuntimeStore.getInstance().importLedgerEntries(validEntries.map(e => ({
643
+ id: e.id,
644
+ meshId: e.meshId,
645
+ timestamp: e.timestamp,
646
+ kind: e.kind,
647
+ nodeId: e.nodeId ?? null,
648
+ sessionId: e.sessionId ?? null,
649
+ providerType: e.providerType ?? null,
650
+ payload: e.payload ?? {},
651
+ })));
652
+ } catch { /* best-effort; JSONL append below still records the entries */ }
653
+
612
654
  try {
613
655
  const lines = validEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
614
656
  appendFileSync(ledgerPath, lines, { encoding: 'utf-8', mode: 0o600 });
@@ -625,7 +667,7 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
625
667
  // ─── Ledger Read Cache ─────────────────────────
626
668
  // Absorbs repeated reads within a single event-processing burst (e.g. agent:stopped
627
669
  // triggers shouldSuppressIntentionalCleanupStop, findRecentTerminalLedgerEvidence,
628
- // hasDispatchAfterTerminal, and getSessionRecoveryContext — all reading the same file).
670
+ // hasDispatchAfterTerminal, and getSessionRecoveryContext — all reading the same store).
629
671
  // TTL is 100ms: short enough to stay current, long enough to cover one event cycle.
630
672
  // Cache is invalidated on every write (append, remote import, compaction).
631
673
 
@@ -648,11 +690,66 @@ function readLedgerFile(meshId: string): MeshLedgerEntry[] {
648
690
  return entries;
649
691
  }
650
692
 
693
+ // ─── G2: One-Time JSONL → SQLite Import ─────────
694
+ // On the first SQLite read for a mesh (per store instance), import any legacy
695
+ // JSONL entries into mesh_event_ledger. INSERT OR IGNORE makes this idempotent:
696
+ // dual-written entries are skipped, only pre-cutover legacy entries are added.
697
+ // Keyed by the store instance so MeshRuntimeStore.resetForTests() (fresh DB)
698
+ // naturally re-imports.
699
+
700
+ let ledgerImportStoreRef: MeshRuntimeStore | undefined;
701
+ const ledgerImportDone = new Set<string>();
702
+
703
+ function ensureLedgerImported(store: MeshRuntimeStore, meshId: string): void {
704
+ if (ledgerImportStoreRef !== store) {
705
+ ledgerImportDone.clear();
706
+ ledgerImportStoreRef = store;
707
+ }
708
+ if (ledgerImportDone.has(meshId)) return;
709
+ ledgerImportDone.add(meshId);
710
+ const fileEntries = readLedgerFile(meshId);
711
+ if (fileEntries.length === 0) return;
712
+ try {
713
+ store.importLedgerEntries(fileEntries.map(e => ({
714
+ id: e.id,
715
+ meshId: e.meshId,
716
+ timestamp: e.timestamp,
717
+ kind: e.kind,
718
+ nodeId: e.nodeId ?? null,
719
+ sessionId: e.sessionId ?? null,
720
+ providerType: e.providerType ?? null,
721
+ payload: e.payload ?? {},
722
+ })));
723
+ } catch { /* import is best-effort; reads fall back to JSONL on store failure */ }
724
+ }
725
+
726
+ function readLedgerFromStore(meshId: string): MeshLedgerEntry[] {
727
+ const store = MeshRuntimeStore.getInstance();
728
+ ensureLedgerImported(store, meshId);
729
+ return store.readLedgerEntriesOrdered(meshId).map(r => ({
730
+ id: r.id,
731
+ meshId: r.meshId,
732
+ timestamp: r.timestamp,
733
+ kind: r.kind as MeshLedgerKind,
734
+ ...(r.nodeId ? { nodeId: r.nodeId } : {}),
735
+ ...(r.sessionId ? { sessionId: r.sessionId } : {}),
736
+ ...(r.providerType ? { providerType: r.providerType } : {}),
737
+ payload: (r.payload && typeof r.payload === 'object' ? r.payload : {}) as Record<string, unknown>,
738
+ }));
739
+ }
740
+
651
741
  function getCachedRawEntries(meshId: string): MeshLedgerEntry[] {
652
742
  const now = Date.now();
653
743
  const cached = ledgerReadCache.get(meshId);
654
744
  if (cached && now - cached.cachedAt < LEDGER_CACHE_TTL_MS) return cached.entries;
655
- const entries = readLedgerFile(meshId);
745
+ let entries: MeshLedgerEntry[];
746
+ try {
747
+ // G2: SQLite mesh_event_ledger is the primary runtime read path.
748
+ entries = readLedgerFromStore(meshId);
749
+ } catch {
750
+ // Store unavailable — fall back to the JSONL export artifact.
751
+ entries = readLedgerFile(meshId);
752
+ }
656
753
  ledgerReadCache.set(meshId, { entries, cachedAt: now });
657
754
  return entries;
658
755
  }
@@ -661,8 +758,23 @@ function invalidateLedgerCache(meshId: string): void {
661
758
  ledgerReadCache.delete(meshId);
662
759
  }
663
760
 
761
+ /**
762
+ * Test helper: clear all runtime ledger state for a mesh — SQLite rows, read
763
+ * cache, and the one-time import flag. JSONL files are the caller's concern.
764
+ */
765
+ export function __clearMeshLedgerForTests(meshId: string): void {
766
+ try {
767
+ MeshRuntimeStore.getInstance().clearLedgerForMesh(meshId);
768
+ } catch { /* store unavailable — nothing to clear */ }
769
+ ledgerReadCache.delete(meshId);
770
+ ledgerImportDone.delete(meshId);
771
+ }
772
+
664
773
  /**
665
774
  * Read ledger entries with optional filtering.
775
+ * G2: SQLite (mesh_event_ledger) is the primary read path; legacy JSONL is
776
+ * imported once per store instance and otherwise retained as an
777
+ * export/import/debug artifact only.
666
778
  */
667
779
  export function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): MeshLedgerEntry[] {
668
780
  let entries = getCachedRawEntries(meshId);
@@ -777,6 +889,26 @@ export function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions):
777
889
  };
778
890
  }
779
891
 
892
+ /**
893
+ * G4: Read a bounded ledger slice from the SQLite mesh_event_ledger table.
894
+ * This is the preferred P2P reconcile read path; JSONL files are retained as
895
+ * export/import/debug/legacy artifacts only.
896
+ *
897
+ * Returns a shape structurally compatible with MeshLedgerSlice (minus the
898
+ * JSONL-specific `summary` and `sourceOfTruth.path` fields) so callers can
899
+ * pass it to buildMeshLedgerReplicaEvidence without modification.
900
+ */
901
+ export function readLedgerSliceFromStore(meshId: string, opts?: ReadLedgerSliceOptions): ReturnType<typeof MeshRuntimeStore.prototype.readLedgerSlice> {
902
+ return MeshRuntimeStore.getInstance().readLedgerSlice(meshId, {
903
+ afterId: opts?.afterId,
904
+ since: opts?.since,
905
+ // ReadLedgerSliceOptions allows kind as array; SQLite path takes a single kind string.
906
+ // Pass first kind value if provided; callers needing multi-kind filtering should use readLedgerSlice (JSONL).
907
+ kind: opts?.kind?.length ? opts.kind[0] : undefined,
908
+ limit: opts?.limit,
909
+ });
910
+ }
911
+
780
912
  /**
781
913
  * Get a summary of mesh activity from the ledger.
782
914
  */
@@ -0,0 +1,151 @@
1
+ /**
2
+ * M3: Mission persistence (minimal form).
3
+ *
4
+ * A mission is a persistent record of a multi-task goal so the plan lives in
5
+ * the system rather than in the coordinator LLM's context. Coordinator
6
+ * sessions can die or compact; a new coordinator reads the mission back at
7
+ * launch and continues.
8
+ *
9
+ * Explicit non-goals (see docs/mesh-product-plan-v2.md Phase M3): this is not
10
+ * a workflow engine and there is no automatic takeover daemon. Progress is
11
+ * never stored — it is derived from queue task statuses (mission_id) at
12
+ * query time.
13
+ */
14
+
15
+ import { randomUUID } from 'crypto';
16
+ import { MeshRuntimeStore } from './mesh-runtime-store.js';
17
+ import { getQueue } from './mesh-work-queue.js';
18
+
19
+ export type MeshMissionStatus = 'active' | 'paused' | 'completed' | 'abandoned';
20
+
21
+ export const MESH_MISSION_STATUSES: MeshMissionStatus[] = ['active', 'paused', 'completed', 'abandoned'];
22
+
23
+ export interface MeshMissionRecord {
24
+ id: string;
25
+ meshId: string;
26
+ title: string;
27
+ goal: string;
28
+ status: MeshMissionStatus;
29
+ createdAt: string;
30
+ updatedAt: string;
31
+ }
32
+
33
+ export interface MeshMissionTaskAggregate {
34
+ total: number;
35
+ pending: number;
36
+ assigned: number;
37
+ completed: number;
38
+ failed: number;
39
+ cancelled: number;
40
+ /** Pending tasks held back by a dependency failure (blockedReason set). */
41
+ blocked: number;
42
+ /** Latest updatedAt across the mission's tasks, or null with no tasks. */
43
+ lastActivityAt: string | null;
44
+ }
45
+
46
+ export interface MeshMissionSummary extends MeshMissionRecord {
47
+ tasks: MeshMissionTaskAggregate;
48
+ }
49
+
50
+ function normalizeMissionStatus(value: unknown): MeshMissionStatus {
51
+ return MESH_MISSION_STATUSES.includes(value as MeshMissionStatus)
52
+ ? value as MeshMissionStatus
53
+ : 'active';
54
+ }
55
+
56
+ export function upsertMeshMission(meshId: string, input: {
57
+ id?: string;
58
+ title: string;
59
+ goal?: string;
60
+ status?: string;
61
+ }): MeshMissionRecord {
62
+ const title = typeof input.title === 'string' ? input.title.trim() : '';
63
+ if (!title) throw new Error('mission_title_required: a mission needs a non-empty title');
64
+ if (input.status !== undefined && !MESH_MISSION_STATUSES.includes(input.status as MeshMissionStatus)) {
65
+ throw new Error(`invalid_mission_status: '${input.status}' (valid: ${MESH_MISSION_STATUSES.join(', ')})`);
66
+ }
67
+ const id = typeof input.id === 'string' && input.id.trim() ? input.id.trim() : randomUUID();
68
+ const store = MeshRuntimeStore.getInstance();
69
+ const existing = store.getMission(meshId, id);
70
+ const record = {
71
+ id,
72
+ meshId,
73
+ title,
74
+ goal: typeof input.goal === 'string' ? input.goal : existing?.goal ?? '',
75
+ status: normalizeMissionStatus(input.status ?? existing?.status),
76
+ };
77
+ store.upsertMission(record);
78
+ const saved = store.getMission(meshId, id)!;
79
+ return { ...saved, status: normalizeMissionStatus(saved.status) };
80
+ }
81
+
82
+ export function getMeshMissions(meshId: string, statuses?: MeshMissionStatus[]): MeshMissionRecord[] {
83
+ return MeshRuntimeStore.getInstance().getMissions(meshId, statuses)
84
+ .map(m => ({ ...m, status: normalizeMissionStatus(m.status) }));
85
+ }
86
+
87
+ export function getMeshMission(meshId: string, missionId: string): MeshMissionRecord | null {
88
+ const record = MeshRuntimeStore.getInstance().getMission(meshId, missionId);
89
+ return record ? { ...record, status: normalizeMissionStatus(record.status) } : null;
90
+ }
91
+
92
+ /** Aggregate task statuses for a mission at query time (no stored progress). */
93
+ export function summarizeMissionTasks(meshId: string, missionId: string): MeshMissionTaskAggregate {
94
+ const tasks = getQueue(meshId).filter(task => task.missionId === missionId);
95
+ const aggregate: MeshMissionTaskAggregate = {
96
+ total: tasks.length,
97
+ pending: 0,
98
+ assigned: 0,
99
+ completed: 0,
100
+ failed: 0,
101
+ cancelled: 0,
102
+ blocked: 0,
103
+ lastActivityAt: null,
104
+ };
105
+ for (const task of tasks) {
106
+ if (task.status === 'pending') aggregate.pending += 1;
107
+ else if (task.status === 'assigned') aggregate.assigned += 1;
108
+ else if (task.status === 'completed') aggregate.completed += 1;
109
+ else if (task.status === 'failed') aggregate.failed += 1;
110
+ else if (task.status === 'cancelled') aggregate.cancelled += 1;
111
+ if (task.status === 'pending' && task.blockedReason) aggregate.blocked += 1;
112
+ if (task.updatedAt && (!aggregate.lastActivityAt || task.updatedAt > aggregate.lastActivityAt)) {
113
+ aggregate.lastActivityAt = task.updatedAt;
114
+ }
115
+ }
116
+ return aggregate;
117
+ }
118
+
119
+ export function summarizeMeshMission(meshId: string, mission: MeshMissionRecord): MeshMissionSummary {
120
+ return { ...mission, tasks: summarizeMissionTasks(meshId, mission.id) };
121
+ }
122
+
123
+ /** Active mission summaries for mesh_status / coordinator prompt injection. */
124
+ export function getActiveMeshMissionSummaries(meshId: string): MeshMissionSummary[] {
125
+ return getMeshMissions(meshId, ['active']).map(mission => summarizeMeshMission(meshId, mission));
126
+ }
127
+
128
+ /**
129
+ * M3-3: render active missions as a prompt section for {{mission}}.
130
+ * Empty string when no active mission — the prompt stays byte-identical to
131
+ * the pre-M3 output in that case (regression guarantee).
132
+ */
133
+ export function buildMissionPromptSection(meshId: string): string {
134
+ const summaries = getActiveMeshMissionSummaries(meshId);
135
+ if (summaries.length === 0) return '';
136
+ const lines: string[] = ['## Active Mission' + (summaries.length > 1 ? 's' : '')];
137
+ for (const mission of summaries) {
138
+ const t = mission.tasks;
139
+ lines.push(
140
+ `- **${mission.title}** (id: \`${mission.id}\`)`
141
+ + (mission.goal ? `\n Goal: ${mission.goal}` : '')
142
+ + `\n Tasks: ${t.total} total — ${t.pending} pending (${t.blocked} blocked), ${t.assigned} assigned, ${t.completed} completed, ${t.failed} failed, ${t.cancelled} cancelled`
143
+ + (t.lastActivityAt ? `\n Last activity: ${t.lastActivityAt}` : ''),
144
+ );
145
+ }
146
+ lines.push(
147
+ 'Continue this mission from its current task state. Do not re-enqueue tasks that already exist — check mesh_view_queue first. '
148
+ + 'Update the mission with mesh_mission_upsert when its goal changes or it reaches a terminal state (completed/abandoned).',
149
+ );
150
+ return lines.join('\n');
151
+ }