@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.
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +2 -0
- package/dist/commands/router.d.ts +6 -0
- package/dist/git/git-commands.d.ts +2 -0
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +5699 -3486
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +5679 -3483
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +6 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +5 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +151 -0
- package/dist/mesh/mesh-events-pending.d.ts +33 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +14 -0
- package/dist/mesh/mesh-events.d.ts +5 -198
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
- package/dist/mesh/mesh-ledger.d.ts +19 -0
- package/dist/mesh/mesh-missions.d.ts +58 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-runtime-store.d.ts +175 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +82 -0
- package/dist/mesh/refine-config.d.ts +24 -2
- package/dist/mesh/worktree-bootstrap-config.d.ts +22 -0
- package/dist/providers/acp-provider-instance.d.ts +2 -0
- package/dist/providers/spec/driver.d.ts +8 -0
- package/dist/providers/spec/evaluator.d.ts +4 -5
- package/dist/providers/spec/loader.d.ts +1 -0
- package/dist/providers/spec/schema.gen.d.ts +1409 -6
- package/dist/providers/spec/types.d.ts +188 -175
- package/dist/repo-mesh-types.d.ts +1 -0
- package/package.json +1 -1
- package/src/boot/daemon-lifecycle.ts +3 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +20 -7
- package/src/commands/router.ts +594 -66
- package/src/git/git-commands.ts +5 -5
- package/src/git/git-diff.ts +53 -0
- package/src/index.ts +11 -5
- package/src/mesh/coordinator-prompt.ts +14 -1
- package/src/mesh/mesh-delivery-policy.ts +17 -0
- package/src/mesh/mesh-events-coordinator.ts +1404 -0
- package/src/mesh/mesh-events-pending.ts +371 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +161 -0
- package/src/mesh/mesh-events.ts +27 -2143
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +134 -2
- package/src/mesh/mesh-missions.ts +151 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-runtime-store.ts +539 -3
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +233 -17
- package/src/mesh/refine-config.ts +42 -5
- package/src/mesh/worktree-bootstrap-config.ts +79 -0
- package/src/providers/acp-provider-instance.ts +15 -1
- package/src/providers/cli-provider-instance.ts +34 -13
- package/src/providers/spec/driver.ts +57 -29
- package/src/providers/spec/evaluator.ts +302 -112
- package/src/providers/spec/loader.ts +226 -37
- package/src/providers/spec/schema.gen.ts +450 -334
- package/src/providers/spec/schema.json +162 -75
- package/src/providers/spec/types.ts +234 -183
- 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: '
|
|
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?:
|
|
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?:
|
|
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: '
|
|
101
|
+
kind: 'coordinator_local_sqlite',
|
|
95
102
|
p2pOnly: true,
|
|
96
103
|
cloudD1LedgerSync: false,
|
|
97
|
-
notes: 'Coordinator reconciles bounded slices from daemon-local
|
|
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: {
|
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
+
}
|