@adhdev/daemon-core 1.0.18-rc.20 → 1.0.18-rc.4
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/provider-cli-shared.d.ts +0 -33
- package/dist/commands/cli-manager.d.ts +0 -9
- package/dist/commands/upgrade-helper.d.ts +0 -1
- package/dist/commands/windows-atomic-upgrade.d.ts +0 -5
- package/dist/config/mesh-json-config.d.ts +0 -62
- package/dist/index.d.ts +2 -4
- package/dist/index.js +3998 -6112
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +3855 -5966
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +0 -76
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-ledger.d.ts +1 -28
- package/dist/mesh/mesh-node-identity.d.ts +0 -23
- package/dist/mesh/mesh-refine-gates.d.ts +0 -89
- package/dist/mesh/mesh-remote-event-pull.d.ts +0 -3
- package/dist/mesh/mesh-runtime-store.d.ts +0 -11
- package/dist/mesh/mesh-work-queue.d.ts +1 -24
- package/dist/providers/chat-message-normalization.d.ts +0 -22
- package/dist/providers/cli-provider-instance-types.d.ts +0 -4
- package/dist/providers/cli-provider-instance.d.ts +0 -78
- package/dist/providers/contracts.d.ts +0 -17
- package/dist/providers/provider-schema.d.ts +0 -1
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +0 -14
- package/dist/providers/types/interactive-prompt.d.ts +0 -18
- package/dist/repo-mesh-types.d.ts +6 -38
- package/dist/shared-types.d.ts +2 -4
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +0 -10
- package/src/cli-adapters/cli-state-engine.ts +1 -17
- package/src/cli-adapters/provider-cli-adapter.ts +6 -2
- package/src/cli-adapters/provider-cli-shared.ts +0 -48
- package/src/commands/cli-manager.ts +8 -72
- package/src/commands/high-family/mesh-coordinator-launch.ts +2 -17
- package/src/commands/high-family/mesh-events.ts +2 -13
- package/src/commands/med-family/mesh-crud.ts +0 -155
- package/src/commands/med-family/mesh-queue.ts +1 -74
- package/src/commands/router-refine.ts +4 -71
- package/src/commands/router.ts +0 -8
- package/src/commands/upgrade-helper.ts +15 -78
- package/src/commands/windows-atomic-upgrade.ts +40 -194
- package/src/config/mesh-json-config.ts +0 -111
- package/src/index.ts +1 -21
- package/src/mesh/coordinator-prompt.ts +11 -258
- package/src/mesh/mesh-active-work.ts +1 -11
- package/src/mesh/mesh-completion-synthesis.ts +1 -12
- package/src/mesh/mesh-event-classify.ts +0 -19
- package/src/mesh/mesh-event-forwarding.ts +6 -64
- package/src/mesh/mesh-events-utils.ts +0 -42
- package/src/mesh/mesh-ledger.ts +0 -77
- package/src/mesh/mesh-node-identity.ts +0 -49
- package/src/mesh/mesh-queue-assignment.ts +11 -210
- package/src/mesh/mesh-reconcile-loop.ts +3 -306
- package/src/mesh/mesh-refine-gates.ts +0 -300
- package/src/mesh/mesh-remote-event-pull.ts +47 -68
- package/src/mesh/mesh-runtime-store.ts +0 -21
- package/src/mesh/mesh-work-queue.ts +2 -85
- package/src/providers/chat-message-normalization.ts +0 -53
- package/src/providers/cli-provider-instance-types.ts +0 -53
- package/src/providers/cli-provider-instance.ts +15 -497
- package/src/providers/contracts.ts +1 -25
- package/src/providers/provider-schema.ts +0 -83
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +0 -23
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +0 -20
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +0 -44
- package/src/providers/types/interactive-prompt.ts +0 -77
- package/src/repo-mesh-types.ts +12 -112
- package/src/shared-types.ts +1 -9
- package/src/status/reporter.ts +0 -1
- package/src/status/snapshot.ts +0 -2
- package/dist/mesh/mesh-disk-retention.d.ts +0 -105
- package/dist/providers/auto-approve-modes.d.ts +0 -14
- package/src/mesh/mesh-disk-retention.ts +0 -370
- package/src/providers/auto-approve-modes.ts +0 -103
|
@@ -34,7 +34,6 @@ import type {
|
|
|
34
34
|
import { mergeAndNormalizePolicy, resolveProviderMaxParallel } from '../repo-mesh-types.js';
|
|
35
35
|
import { getDifficultyBrains } from '../config/mesh-config.js';
|
|
36
36
|
import { resolveNodeCapabilitySlots } from './mesh-node-slots.js';
|
|
37
|
-
import { isNoteExpired, OPERATING_NOTE_CATEGORY_TTL_DAYS } from './mesh-ledger.js';
|
|
38
37
|
import { MESH_TASK_DIFFICULTIES } from '@adhdev/mesh-shared';
|
|
39
38
|
import type { MagiKindPanelMap, MagiSlot, MagiTaskKind } from '@adhdev/mesh-shared';
|
|
40
39
|
|
|
@@ -85,41 +84,6 @@ export interface CoordinatorOperatingNote {
|
|
|
85
84
|
category?: 'provider_quirk' | 'pattern_to_avoid' | 'recovery_lesson';
|
|
86
85
|
createdAt?: string;
|
|
87
86
|
sourceCoordinator?: string;
|
|
88
|
-
/**
|
|
89
|
-
* Operating-notes lifecycle (minimal first cut). When true the note ALWAYS
|
|
90
|
-
* rides into the coordinator prompt — it is never dropped by TTL expiry and
|
|
91
|
-
* survives the injection cap ahead of unpinned notes. Legacy notes without
|
|
92
|
-
* this field default to false.
|
|
93
|
-
*/
|
|
94
|
-
pinned?: boolean;
|
|
95
|
-
/**
|
|
96
|
-
* Optional explicit expiry ISO timestamp. When set and in the past, an
|
|
97
|
-
* UNPINNED note is dropped from the injected prompt (read-side only — the
|
|
98
|
-
* ledger entry is never pruned by age). When absent, the category→TTL map
|
|
99
|
-
* (see isNoteExpired) governs expiry; pinned notes never expire regardless.
|
|
100
|
-
*/
|
|
101
|
-
expiresAt?: string;
|
|
102
|
-
/**
|
|
103
|
-
* Phase 2 — the ledger id of this note, when known. Threaded from the ledger
|
|
104
|
-
* entry id at launch so version-supersede can target a specific note and
|
|
105
|
-
* same-class folding can list the subsumed ids. Repo-declared notes may lack
|
|
106
|
-
* one; absent is fine (folding/supersede fall back to the subject key).
|
|
107
|
-
*/
|
|
108
|
-
noteId?: string;
|
|
109
|
-
/**
|
|
110
|
-
* Phase 2 (b) version-supersede — an optional note-id or stable subject-key
|
|
111
|
-
* this note replaces. At injection, any earlier LIVE note whose `noteId` or
|
|
112
|
-
* `subjectKey` matches this value is hidden from the prompt (the ledger entry
|
|
113
|
-
* is retained for audit). Optional and lossless: absent = supersedes nothing.
|
|
114
|
-
*/
|
|
115
|
-
supersedes?: string;
|
|
116
|
-
/**
|
|
117
|
-
* Phase 2 (b)/(c) — an optional stable subject-key grouping notes about the
|
|
118
|
-
* same subject. Drives version-supersede targeting and same-class folding.
|
|
119
|
-
* When absent, folding derives a key from a leading `[tag]` bracket instead,
|
|
120
|
-
* so legacy notes still collapse by their conventional tag prefix.
|
|
121
|
-
*/
|
|
122
|
-
subjectKey?: string;
|
|
123
87
|
}
|
|
124
88
|
|
|
125
89
|
// ─── Prompt Builder ─────────────────────────────
|
|
@@ -618,243 +582,33 @@ function buildRecentActivitySection(activity?: CoordinatorRecentActivity): strin
|
|
|
618
582
|
const OPERATING_NOTES_PROMPT_CAP = 20;
|
|
619
583
|
const OPERATING_NOTE_MAX_CHARS = 300;
|
|
620
584
|
|
|
621
|
-
|
|
622
|
-
* Phase 2 (d) — byte budget for the injected Operating Notes list. The count cap
|
|
623
|
-
* above (20) is still a ceiling, but selection now fills up to this UTF-8 byte
|
|
624
|
-
* budget on the RENDERED note lines, ranked pinned > durable > recency, so a few
|
|
625
|
-
* long notes don't crowd out many short ones and vice-versa. Pinned notes are
|
|
626
|
-
* ALWAYS kept even if they alone exceed the budget (pinned is author-opt-in and
|
|
627
|
-
* bounded by the author); the budget only bounds the unpinned tail.
|
|
628
|
-
*
|
|
629
|
-
* ~6KB ≈ the 20-note cap at typical note length, so on ordinary meshes this
|
|
630
|
-
* changes nothing — it only kicks in when notes run long. It sits well under the
|
|
631
|
-
* whole-prompt PROMPT_SOFT_CAP_BYTES (60KB) so the two caps don't fight.
|
|
632
|
-
*/
|
|
633
|
-
const OPERATING_NOTE_INJECTION_BYTE_BUDGET = 6 * 1024;
|
|
634
|
-
|
|
635
|
-
/**
|
|
636
|
-
* A category is "durable" (survives the cap ahead of recency) when it has no
|
|
637
|
-
* TTL entry — provider_quirk, uncategorized, or any unknown category. This
|
|
638
|
-
* mirrors isNoteExpired's durability rule so ranking and expiry agree.
|
|
639
|
-
*/
|
|
640
|
-
function isDurableCategory(category?: string): boolean {
|
|
641
|
-
if (!category) return true;
|
|
642
|
-
return !(category in OPERATING_NOTE_CATEGORY_TTL_DAYS);
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Phase 2 (b)/(c) — derive a note's subject key for supersede targeting and
|
|
647
|
-
* same-class folding. Precedence:
|
|
648
|
-
* 1. an explicit `subjectKey` (trimmed, lowercased) when present, else
|
|
649
|
-
* 2. a leading `[tag]` bracket, so legacy notes that follow the conventional
|
|
650
|
-
* `[some-tag] …` prefix still group/supersede by that tag, else
|
|
651
|
-
* 3. undefined — the note has no derivable subject and never folds/supersedes
|
|
652
|
-
* by key (it can still be targeted by exact noteId).
|
|
653
|
-
* Pure. Lowercased so `[Foo]` and `[foo]` collapse together.
|
|
654
|
-
*/
|
|
655
|
-
function deriveSubjectKey(note: CoordinatorOperatingNote): string | undefined {
|
|
656
|
-
const explicit = typeof note.subjectKey === 'string' ? note.subjectKey.trim().toLowerCase() : '';
|
|
657
|
-
if (explicit) return explicit;
|
|
658
|
-
const text = typeof note.text === 'string' ? note.text.trimStart() : '';
|
|
659
|
-
const m = /^\[([^\]]{1,80})\]/.exec(text);
|
|
660
|
-
const tag = m ? m[1].trim().toLowerCase() : '';
|
|
661
|
-
return tag || undefined;
|
|
662
|
-
}
|
|
663
|
-
|
|
664
|
-
/**
|
|
665
|
-
* Phase 2 (c) — deterministic same-class fold. Given ranked notes (already
|
|
666
|
-
* highest-priority first), collapse runs of UNPINNED notes that share the same
|
|
667
|
-
* category AND subject key into a single injected entry: keep the highest-ranked
|
|
668
|
-
* (first-seen) note and record the note-ids/count it subsumes so the rendered
|
|
669
|
-
* line can say "(+N earlier)". Pinned notes never fold — each pinned note is
|
|
670
|
-
* author-opted-in and shown verbatim. The store is untouched; this is a pure,
|
|
671
|
-
* model-free text fold at injection time.
|
|
672
|
-
*
|
|
673
|
-
* Returns the folded note list (order preserved) with per-entry subsumed info.
|
|
674
|
-
*/
|
|
675
|
-
interface FoldedNote {
|
|
676
|
-
note: CoordinatorOperatingNote;
|
|
677
|
-
/** note-ids of same-class notes this entry subsumes (may be empty). */
|
|
678
|
-
subsumedIds: string[];
|
|
679
|
-
/** count of subsumed notes (== subsumedIds.length, but counts id-less ones too). */
|
|
680
|
-
subsumedCount: number;
|
|
681
|
-
}
|
|
682
|
-
|
|
683
|
-
function foldSameClassNotes(ranked: CoordinatorOperatingNote[]): FoldedNote[] {
|
|
684
|
-
const out: FoldedNote[] = [];
|
|
685
|
-
// group key → index into `out` of the surviving (highest-ranked) entry.
|
|
686
|
-
const survivorByKey = new Map<string, number>();
|
|
687
|
-
for (const note of ranked) {
|
|
688
|
-
const subject = deriveSubjectKey(note);
|
|
689
|
-
// Only unpinned notes with a category AND a subject key are foldable —
|
|
690
|
-
// without both there is no reliable "same class/subject" signal, so we
|
|
691
|
-
// keep the note standalone (lossless for legacy/uncategorized notes).
|
|
692
|
-
const foldable = !note.pinned && !!note.category && !!subject;
|
|
693
|
-
if (foldable) {
|
|
694
|
-
const key = `${note.category}${subject}`;
|
|
695
|
-
const existing = survivorByKey.get(key);
|
|
696
|
-
if (existing !== undefined) {
|
|
697
|
-
const survivor = out[existing];
|
|
698
|
-
survivor.subsumedCount += 1;
|
|
699
|
-
if (typeof note.noteId === 'string' && note.noteId) survivor.subsumedIds.push(note.noteId);
|
|
700
|
-
continue;
|
|
701
|
-
}
|
|
702
|
-
survivorByKey.set(key, out.length);
|
|
703
|
-
}
|
|
704
|
-
out.push({ note, subsumedIds: [], subsumedCount: 0 });
|
|
705
|
-
}
|
|
706
|
-
return out;
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
/** Estimate the UTF-8 byte cost of one rendered operating-note line (Phase 2 d). */
|
|
710
|
-
function renderedNoteBytes(folded: FoldedNote): number {
|
|
711
|
-
return byteLength(renderOperatingNoteLine(folded));
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
/**
|
|
715
|
-
* Operating-notes lifecycle selection (read-side). Given the effective notes
|
|
716
|
-
* (oldest-first, ledger order) and the current time, produce the ordered list
|
|
717
|
-
* that rides into the prompt:
|
|
718
|
-
* (i) ALWAYS include pinned notes.
|
|
719
|
-
* (ii) drop expired UNPINNED notes (per category TTL / explicit expiresAt).
|
|
720
|
-
* (iii) Phase 2 (b) drop any note SUPERSEDED by a later live note (by noteId
|
|
721
|
-
* or subjectKey); pinned notes are never dropped by supersede.
|
|
722
|
-
* (iv) rank pinned-first, then durable-category, then recency (newest first).
|
|
723
|
-
* (v) Phase 2 (c) fold same-category/same-subject unpinned runs into one.
|
|
724
|
-
* (vi) Phase 2 (d) fill up to a byte budget AND a count cap; pinned always
|
|
725
|
-
* kept even if they alone exceed the byte budget.
|
|
726
|
-
* Pure + deterministic given `now`. Returns { shown, omittedCount } where `shown`
|
|
727
|
-
* carries per-entry fold info for the renderer.
|
|
728
|
-
*
|
|
729
|
-
* Ranking is stable on the original ledger index so, within a tier, newest notes
|
|
730
|
-
* lead. The caps keep the leading (highest-priority) entries.
|
|
731
|
-
*/
|
|
732
|
-
export function selectOperatingNotesForPrompt(
|
|
733
|
-
notes: CoordinatorOperatingNote[],
|
|
734
|
-
now: number,
|
|
735
|
-
cap: number = OPERATING_NOTES_PROMPT_CAP,
|
|
736
|
-
byteBudget: number = OPERATING_NOTE_INJECTION_BYTE_BUDGET,
|
|
737
|
-
): { shown: FoldedNote[]; omittedCount: number } {
|
|
585
|
+
function buildOperatingNotesSection(notes?: CoordinatorOperatingNote[]): string {
|
|
738
586
|
const valid = Array.isArray(notes)
|
|
739
587
|
? notes.filter(n => n && typeof n.text === 'string' && n.text.trim())
|
|
740
588
|
: [];
|
|
589
|
+
if (valid.length === 0) return '';
|
|
741
590
|
|
|
742
|
-
// (i)+(ii): keep pinned always; drop expired unpinned. Retain original index
|
|
743
|
-
// for a stable recency tiebreak (later index == newer, ledger is oldest-first).
|
|
744
|
-
let kept = valid
|
|
745
|
-
.map((note, index) => ({ note, index }))
|
|
746
|
-
.filter(({ note }) => note.pinned || !isNoteExpired(note as any, now));
|
|
747
|
-
|
|
748
|
-
// (iii) Phase 2 (b) version-supersede: a LATER live note may name (via
|
|
749
|
-
// `supersedes`) an earlier note's noteId or subjectKey; the earlier one is
|
|
750
|
-
// then hidden. "Later" == higher ledger index. Collect every supersede target
|
|
751
|
-
// paired with the max index that supersedes it, then drop any UNPINNED note
|
|
752
|
-
// whose id/subjectKey is superseded by a strictly-later note. Pinned notes are
|
|
753
|
-
// never dropped by supersede (pinned always wins, mirroring TTL).
|
|
754
|
-
const supersededAtIndex = new Map<string, number>();
|
|
755
|
-
for (const { note, index } of kept) {
|
|
756
|
-
const target = typeof note.supersedes === 'string' ? note.supersedes.trim().toLowerCase() : '';
|
|
757
|
-
if (!target) continue;
|
|
758
|
-
const prev = supersededAtIndex.get(target);
|
|
759
|
-
if (prev === undefined || index > prev) supersededAtIndex.set(target, index);
|
|
760
|
-
}
|
|
761
|
-
if (supersededAtIndex.size > 0) {
|
|
762
|
-
kept = kept.filter(({ note, index }) => {
|
|
763
|
-
if (note.pinned) return true;
|
|
764
|
-
const byId = typeof note.noteId === 'string' ? note.noteId.trim().toLowerCase() : '';
|
|
765
|
-
const bySubject = deriveSubjectKey(note);
|
|
766
|
-
const supIdx = Math.max(
|
|
767
|
-
byId ? (supersededAtIndex.get(byId) ?? -1) : -1,
|
|
768
|
-
bySubject ? (supersededAtIndex.get(bySubject) ?? -1) : -1,
|
|
769
|
-
);
|
|
770
|
-
return !(supIdx > index); // superseded by a strictly-later note → drop
|
|
771
|
-
});
|
|
772
|
-
}
|
|
773
|
-
|
|
774
|
-
// (iv): rank pinned > durable > recency (newest first within a tier).
|
|
775
|
-
const rank = (n: CoordinatorOperatingNote): number =>
|
|
776
|
-
n.pinned ? 0 : isDurableCategory(n.category) ? 1 : 2;
|
|
777
|
-
kept.sort((a, b) => {
|
|
778
|
-
const r = rank(a.note) - rank(b.note);
|
|
779
|
-
if (r !== 0) return r;
|
|
780
|
-
return b.index - a.index; // newer (higher index) first
|
|
781
|
-
});
|
|
782
|
-
|
|
783
|
-
// (v) Phase 2 (c): fold same-class/same-subject unpinned runs into one entry.
|
|
784
|
-
const folded = foldSameClassNotes(kept.map(k => k.note));
|
|
785
|
-
|
|
786
|
-
// (vi) Phase 2 (d): fill up to the byte budget AND the count cap. Pinned
|
|
787
|
-
// entries are always kept (never dropped to fit the budget); the budget/cap
|
|
788
|
-
// bound the unpinned tail. Entries are already highest-priority first, so we
|
|
789
|
-
// walk in order and stop once an UNPINNED entry would break a bound.
|
|
790
|
-
const shown: FoldedNote[] = [];
|
|
791
|
-
let usedBytes = 0;
|
|
792
|
-
let usedCount = 0;
|
|
793
|
-
for (const entry of folded) {
|
|
794
|
-
if (entry.note.pinned) {
|
|
795
|
-
shown.push(entry);
|
|
796
|
-
usedBytes += renderedNoteBytes(entry);
|
|
797
|
-
usedCount += 1;
|
|
798
|
-
continue;
|
|
799
|
-
}
|
|
800
|
-
if (usedCount >= cap) break;
|
|
801
|
-
const bytes = renderedNoteBytes(entry);
|
|
802
|
-
if (usedCount > 0 && usedBytes + bytes > byteBudget) break;
|
|
803
|
-
shown.push(entry);
|
|
804
|
-
usedBytes += bytes;
|
|
805
|
-
usedCount += 1;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
// omittedCount counts eligible (non-expired, non-superseded, post-fold) entries
|
|
809
|
-
// that did not make the cut. Folded-away duplicates are not "omitted" — they are
|
|
810
|
-
// represented by their survivor's "(+N earlier)" marker, so exclude them here.
|
|
811
|
-
const omittedCount = Math.max(0, folded.length - shown.length);
|
|
812
|
-
return { shown, omittedCount };
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
/**
|
|
816
|
-
* Render one operating-note bullet line (Phase 2 c fold-aware). Shared by the
|
|
817
|
-
* byte-budget estimator and the section renderer so the budget is measured on
|
|
818
|
-
* the exact bytes that ship.
|
|
819
|
-
*/
|
|
820
|
-
function renderOperatingNoteLine(folded: FoldedNote): string {
|
|
821
|
-
const n = folded.note;
|
|
822
591
|
const categoryLabel: Record<string, string> = {
|
|
823
592
|
provider_quirk: 'provider quirk',
|
|
824
593
|
pattern_to_avoid: 'pattern to avoid',
|
|
825
594
|
recovery_lesson: 'recovery lesson',
|
|
826
595
|
};
|
|
827
|
-
const pin = n.pinned ? '📌 ' : '';
|
|
828
|
-
const cat = n.category && categoryLabel[n.category] ? `[${categoryLabel[n.category]}] ` : '';
|
|
829
|
-
const fold = folded.subsumedCount > 0
|
|
830
|
-
? ` _(+${folded.subsumedCount} earlier same-subject note${folded.subsumedCount === 1 ? '' : 's'} folded${folded.subsumedIds.length ? `: ${folded.subsumedIds.join(', ')}` : ''})_`
|
|
831
|
-
: '';
|
|
832
|
-
return `- ${pin}${cat}${truncateNote(n.text.trim())}${fold}`;
|
|
833
|
-
}
|
|
834
|
-
|
|
835
|
-
function buildOperatingNotesSection(notes?: CoordinatorOperatingNote[], now: number = Date.now()): string {
|
|
836
|
-
const hasAny = Array.isArray(notes)
|
|
837
|
-
? notes.some(n => n && typeof n.text === 'string' && n.text.trim())
|
|
838
|
-
: false;
|
|
839
|
-
if (!hasAny) return '';
|
|
840
596
|
|
|
841
|
-
//
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
// budget was measured against.
|
|
846
|
-
const { shown, omittedCount } = selectOperatingNotesForPrompt(notes ?? [], now);
|
|
847
|
-
if (shown.length === 0) return '';
|
|
597
|
+
// Keep only the most recent OPERATING_NOTES_PROMPT_CAP notes in the prompt.
|
|
598
|
+
// `valid` is oldest-first (ledger order), so the newest are at the tail.
|
|
599
|
+
const omittedCount = Math.max(0, valid.length - OPERATING_NOTES_PROMPT_CAP);
|
|
600
|
+
const shown = omittedCount > 0 ? valid.slice(-OPERATING_NOTES_PROMPT_CAP) : valid;
|
|
848
601
|
|
|
849
602
|
const lines: string[] = ['## Operating Notes', ''];
|
|
850
603
|
lines.push('Lessons earlier coordinators on this mesh recorded via `mesh_record_note`. Treat them as accumulated operating knowledge — apply them. When you learn a durable lesson (a provider quirk, a pattern to avoid, a recovery lesson), record it with `mesh_record_note` so future coordinators inherit it.');
|
|
851
604
|
lines.push('');
|
|
852
|
-
for (const
|
|
853
|
-
|
|
605
|
+
for (const n of shown) {
|
|
606
|
+
const cat = n.category && categoryLabel[n.category] ? `[${categoryLabel[n.category]}] ` : '';
|
|
607
|
+
lines.push(`- ${cat}${truncateNote(n.text.trim())}`);
|
|
854
608
|
}
|
|
855
609
|
if (omittedCount > 0) {
|
|
856
610
|
lines.push('');
|
|
857
|
-
lines.push(`_${omittedCount}
|
|
611
|
+
lines.push(`_${omittedCount} older note${omittedCount === 1 ? '' : 's'} omitted (kept in ledger; prune with \`mesh_forget_note\`)._`);
|
|
858
612
|
}
|
|
859
613
|
return lines.join('\n');
|
|
860
614
|
}
|
|
@@ -1007,8 +761,7 @@ const TOOLS_SECTION = `## Available Tools
|
|
|
1007
761
|
| \`mesh_fast_forward_node\` | Safely dry-run or explicitly execute an obvious clean fast-forward without launching an agent session |
|
|
1008
762
|
| \`mesh_restart_daemon\` | Update a node's daemon to the latest published version on its channel and restart it (the dashboard "preview update" path, as a mesh command) |
|
|
1009
763
|
| \`mesh_checkpoint\` | Create a git checkpoint on a node |
|
|
1010
|
-
| \`mesh_approve\` | Approve/reject a pending agent action
|
|
1011
|
-
| \`mesh_answer_question\` | Answer a delegated session's multi-choice QUESTION (AskUserQuestion / status awaiting_choice). NOT an approval — it offers labelled options (possibly multi-select or freeform). Pass the promptId from the agent:waiting_choice event + one answer per question (select by option label or 1-based index). Use this, never mesh_approve, for a question |
|
|
764
|
+
| \`mesh_approve\` | Approve/reject a pending agent action |
|
|
1012
765
|
| \`mesh_list_pending_approvals\` | List every session across the mesh awaiting an approval decision (the approval inbox) — read-only; enumerate all blocked sessions at once, then drive a mesh_approve for each |
|
|
1013
766
|
| \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
|
|
1014
767
|
| \`mesh_refine_node\` | Validate and merge a completed worktree node back into its base branch |
|
|
@@ -5,7 +5,7 @@ import { deleteDirectDispatchesByTaskId } from './mesh-work-queue.js';
|
|
|
5
5
|
import { meshNodeIdMatches, daemonIdsEquivalent, sessionIdsEquivalent } from '@adhdev/mesh-shared';
|
|
6
6
|
|
|
7
7
|
export type MeshActiveWorkSource = 'queue' | 'direct';
|
|
8
|
-
export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval'
|
|
8
|
+
export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval';
|
|
9
9
|
|
|
10
10
|
export interface MeshActiveWorkRecord {
|
|
11
11
|
taskId: string;
|
|
@@ -135,10 +135,6 @@ export function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string
|
|
|
135
135
|
if (!session) return { staleReason: 'direct task session is not present in live session records' };
|
|
136
136
|
if (typeof session === 'string') return {};
|
|
137
137
|
const raw = `${readString(session.status) || ''} ${readString(session.lifecycle) || ''} ${readString(session.state) || ''} ${readString(session.activeChat?.status) || ''}`.toLowerCase();
|
|
138
|
-
// A question picker surfaces as waiting_choice — distinct from an approval modal.
|
|
139
|
-
// Check it first so a question worker is not mislabeled awaiting_approval and
|
|
140
|
-
// pulled into the approval inbox (mission f1d25e11).
|
|
141
|
-
if (raw.includes('waiting_choice') || raw.includes('choice')) return { status: 'awaiting_choice' };
|
|
142
138
|
if (raw.includes('approval')) return { status: 'awaiting_approval' };
|
|
143
139
|
if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return { status: 'generating' };
|
|
144
140
|
if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return { status: 'failed' };
|
|
@@ -170,10 +166,6 @@ function terminalMatchesDispatch(terminal: MeshLedgerEntry, dispatch: MeshLedger
|
|
|
170
166
|
|
|
171
167
|
function statusFromTerminal(entry: MeshLedgerEntry): MeshActiveWorkStatus {
|
|
172
168
|
if (entry.kind === 'task_approval_needed') return 'awaiting_approval';
|
|
173
|
-
// A question (waiting_choice) is a distinct blocked state — kept OUT of
|
|
174
|
-
// awaiting_approval so it is not surfaced in the approval inbox / mesh_approve
|
|
175
|
-
// flow (mission f1d25e11). Answered via mesh_answer_question.
|
|
176
|
-
if (entry.kind === 'task_question_pending') return 'awaiting_choice';
|
|
177
169
|
if (entry.kind === 'task_completed') return 'idle';
|
|
178
170
|
return 'failed';
|
|
179
171
|
}
|
|
@@ -339,7 +331,6 @@ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]):
|
|
|
339
331
|
idle: 0,
|
|
340
332
|
failed: 0,
|
|
341
333
|
awaiting_approval: 0,
|
|
342
|
-
awaiting_choice: 0,
|
|
343
334
|
};
|
|
344
335
|
const sourceCounts: Record<MeshActiveWorkSource, number> = { queue: 0, direct: 0 };
|
|
345
336
|
for (const item of activeWork) {
|
|
@@ -389,7 +380,6 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
389
380
|
? sessionStatusFromNodes(opts.nodes, queueNodeId ?? undefined, queueSessionId ?? undefined)
|
|
390
381
|
: {};
|
|
391
382
|
const queueStatus: MeshActiveWorkStatus = queueLive.status === 'awaiting_approval'
|
|
392
|
-
|| queueLive.status === 'awaiting_choice'
|
|
393
383
|
|| queueLive.status === 'generating'
|
|
394
384
|
? queueLive.status
|
|
395
385
|
: task.status;
|
|
@@ -26,7 +26,7 @@ import { getActiveDirectDispatches, getQueue } from './mesh-work-queue.js';
|
|
|
26
26
|
import { readLedgerEntries } from './mesh-ledger.js';
|
|
27
27
|
import { pruneStaleDirectDispatches } from './mesh-active-work.js';
|
|
28
28
|
import { reconcileDirectDispatchCompletionFromTranscript } from './mesh-events-stale.js';
|
|
29
|
-
import { extractFinalAssistantSummaryEvidence
|
|
29
|
+
import { extractFinalAssistantSummaryEvidence } from '../providers/chat-message-normalization.js';
|
|
30
30
|
import type { ChatMessage } from '../types.js';
|
|
31
31
|
import {
|
|
32
32
|
getMeshV2BackstopCounters,
|
|
@@ -483,17 +483,6 @@ export async function pollAssignedTaskTerminalEvidence(
|
|
|
483
483
|
const evidence = extractFinalAssistantSummaryEvidence(messages);
|
|
484
484
|
if (!evidence.finalSummary) return null; // idle but no assistant result yet → not a turn-end
|
|
485
485
|
|
|
486
|
-
// EARLY-IDLE-COMPLETION-FALSE-POSITIVE (poll defense-in-depth): a momentary idle read
|
|
487
|
-
// (startup-grace, or the sliver between an assistant preamble and the tool it fires) can
|
|
488
|
-
// show an assistant "Let me explore…" bubble FOLLOWED by trailing Read/Grep tool_use — a
|
|
489
|
-
// turn that is still executing, not a turn-end. selectFinalAssistantTurnEndMessage skips
|
|
490
|
-
// those trailing tool bubbles and would promote the preamble, so guard here: if a
|
|
491
|
-
// tool/terminal activity bubble trails the final assistant message, the worker is mid-turn
|
|
492
|
-
// — refuse the completion (fall through to the caller's reclaim/grace path). A genuinely
|
|
493
|
-
// finished pure-PTY worker ends on its final assistant with no trailing tool activity, so
|
|
494
|
-
// the kimi rescue is preserved.
|
|
495
|
-
if (hasTrailingToolActivityAfterFinalAssistant(messages)) return null;
|
|
496
|
-
|
|
497
486
|
// Stale-summary guard (same bar as PHASE 4): a reused session's transcript tail may hold a
|
|
498
487
|
// PRIOR task's summary. Require the final assistant message to be dated at/after THIS task's
|
|
499
488
|
// dispatch. When either timestamp is unusable, do NOT short-circuit — fall through so we never
|
|
@@ -8,11 +8,6 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
8
8
|
'agent:generating_started',
|
|
9
9
|
'agent:generating_completed',
|
|
10
10
|
'agent:waiting_approval',
|
|
11
|
-
// A worker parked on an AskUserQuestion multi-choice prompt. DISTINCT from
|
|
12
|
-
// agent:waiting_approval (a yes/no tool-consent modal): a question is answered
|
|
13
|
-
// with mesh_answer_question, never mesh_approve (mission f1d25e11). Carries the
|
|
14
|
-
// full InteractivePrompt payload so the coordinator can render + answer.
|
|
15
|
-
'agent:waiting_choice',
|
|
16
11
|
'agent:stopped',
|
|
17
12
|
'agent:ready',
|
|
18
13
|
'monitor:no_progress',
|
|
@@ -31,7 +26,6 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
31
26
|
export const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
32
27
|
'agent:generating_completed': 'task_completed',
|
|
33
28
|
'agent:waiting_approval': 'task_approval_needed',
|
|
34
|
-
'agent:waiting_choice': 'task_question_pending',
|
|
35
29
|
'agent:stopped': 'task_failed',
|
|
36
30
|
'monitor:no_progress': 'task_stalled',
|
|
37
31
|
};
|
|
@@ -51,11 +45,6 @@ export const MESH_FORCE_INJECT_EVENTS: ReadonlySet<string> = new Set([
|
|
|
51
45
|
'agent:generating_completed',
|
|
52
46
|
'agent:stopped',
|
|
53
47
|
'agent:waiting_approval',
|
|
54
|
-
// A worker's question (waiting_choice) is real-time and the coordinator may be
|
|
55
|
-
// generating when it arrives; force-inject it like approval so the busy coordinator
|
|
56
|
-
// learns it must answer (mesh_answer_question) rather than the nudge sitting in the
|
|
57
|
-
// adapter's outbound queue until the coordinator idles on its own (mission f1d25e11).
|
|
58
|
-
'agent:waiting_choice',
|
|
59
48
|
'refine:completed',
|
|
60
49
|
'refine:failed',
|
|
61
50
|
'worktree_bootstrap_complete',
|
|
@@ -82,14 +71,6 @@ export function shouldForceInjectMeshEvent(eventName: unknown): boolean {
|
|
|
82
71
|
// require, and why a stale/resolved approval nudge can simply be dropped.
|
|
83
72
|
export const MESH_APPROVAL_EVENTS: ReadonlySet<string> = new Set([
|
|
84
73
|
'agent:waiting_approval',
|
|
85
|
-
// agent:waiting_choice (a worker's AskUserQuestion) shares the approval class's
|
|
86
|
-
// real-time-nudge / level-backed contract: its authoritative state is recorded at
|
|
87
|
-
// LEVEL the moment it is processed (task_question_pending → mesh_status
|
|
88
|
-
// awaiting_choice), so the pending event is only a NUDGE and can be delivered to a
|
|
89
|
-
// busy coordinator's inbox (and dropped) without data loss — the level state
|
|
90
|
-
// re-derives it. It is therefore exempt from the idle-edge hold completions require,
|
|
91
|
-
// exactly like an approval nudge (mission f1d25e11).
|
|
92
|
-
'agent:waiting_choice',
|
|
93
74
|
]);
|
|
94
75
|
|
|
95
76
|
export function isMeshApprovalEvent(eventName: unknown): boolean {
|
|
@@ -16,8 +16,7 @@ import { resolveMeshHostStatus } from './mesh-host-ownership.js';
|
|
|
16
16
|
import { enqueueUnresolvedDelegateForward, nudgeUnresolvedForwardRetry } from './mesh-unresolved-forward-outbox.js';
|
|
17
17
|
import { traceMeshEventStage, traceMeshEventDrop } from './mesh-event-trace.js';
|
|
18
18
|
import { getLastDisplayMessage } from '../status/snapshot.js';
|
|
19
|
-
import {
|
|
20
|
-
import { loadRepoMeshJsonConfig } from '../config/mesh-json-config.js';
|
|
19
|
+
import { resolveDelegatedWorkerAutoApprove } from '../repo-mesh-types.js';
|
|
21
20
|
import { meshNodeIdMatches, daemonIdsEquivalent, expandDaemonIdForms, sessionIdsEquivalent, withStatusProbeMarker, type MeshNodeIdentified } from '@adhdev/mesh-shared';
|
|
22
21
|
import {
|
|
23
22
|
findRecentTerminalLedgerEvidence,
|
|
@@ -1072,44 +1071,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1072
1071
|
// C2: prefer an exact taskId match when the completion event carries one —
|
|
1073
1072
|
// it's immune to coordinator↔worker clock skew that can hide the assigned row.
|
|
1074
1073
|
const eventTaskId = readNonEmptyString(args.metadataEvent.taskId) || undefined;
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
// terminal. Until this net, only DIRECT dispatches were kept tentative on weak evidence;
|
|
1080
|
-
// a queue-claimed task's session flipped its row to 'completed' unconditionally, so a
|
|
1081
|
-
// worker that dropped to idle ~13s in with no final assistant closed the task early even
|
|
1082
|
-
// though it never produced an answer. Leaving the row 'assigned' hands it to the reconcile
|
|
1083
|
-
// loop's proven net (PHASE 4 records the GENUINE completion once the transcript lands via
|
|
1084
|
-
// findTerminalLedgerEvidenceForTask; PHASE 2.5 reclaims a truly-stranded row back to
|
|
1085
|
-
// 'pending' for re-dispatch, bounded by MAX_STRANDED_RECLAIMS) — never a permanent wedge.
|
|
1086
|
-
//
|
|
1087
|
-
// CRUCIAL SCOPE: this covers only the PREMATURE decoupled-immediate emit
|
|
1088
|
-
// (emittedAfterFinalizationTimeout !== true). A weak completion that already waited out the
|
|
1089
|
-
// full 30s COMPLETED_FINALIZATION_MAX_WAIT_MS window (emittedAfterFinalizationTimeout=true)
|
|
1090
|
-
// is a GENUINE — if answerless — terminal: the worker exhausted its finalization wait and
|
|
1091
|
-
// is done, so it flips 'completed' as before (a tool-only turn that never produced an
|
|
1092
|
-
// assistant bubble must still close its task). A `failed` outcome or a completion with
|
|
1093
|
-
// genuine evidence flips terminal as before too.
|
|
1094
|
-
const completionDiagnostic = args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
|
|
1095
|
-
? args.metadataEvent.completionDiagnostic as Record<string, unknown>
|
|
1096
|
-
: undefined;
|
|
1097
|
-
const emittedAfterFinalizationTimeout = completionDiagnostic?.emittedAfterFinalizationTimeout === true;
|
|
1098
|
-
const weakCompleted = outcome === 'completed'
|
|
1099
|
-
&& isWeakCompletionEvidence(args.metadataEvent)
|
|
1100
|
-
&& !emittedAfterFinalizationTimeout;
|
|
1101
|
-
const task = weakCompleted
|
|
1102
|
-
? updateSessionTaskStatus(args.meshId, sessionId, 'assigned', {
|
|
1103
|
-
occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
|
|
1104
|
-
taskId: eventTaskId,
|
|
1105
|
-
})
|
|
1106
|
-
: updateSessionTaskStatus(args.meshId, sessionId, outcome, {
|
|
1107
|
-
occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
|
|
1108
|
-
taskId: eventTaskId,
|
|
1109
|
-
});
|
|
1110
|
-
if (weakCompleted && task) {
|
|
1111
|
-
LOG.info('MeshQueue', `Weak completion (${readNonEmptyString(args.metadataEvent.evidenceLevel) || 'missing_final_assistant'}) kept queue task ${task.id} tentative (session ${sessionId}); reconcile owns the genuine terminal`);
|
|
1112
|
-
}
|
|
1074
|
+
const task = updateSessionTaskStatus(args.meshId, sessionId, outcome, {
|
|
1075
|
+
occurredAt: occurredAtMs != null ? new Date(occurredAtMs).toISOString() : undefined,
|
|
1076
|
+
taskId: eventTaskId,
|
|
1077
|
+
});
|
|
1113
1078
|
// Fix A (early-terminal prevention): a false-idle completion (no confirmed final
|
|
1114
1079
|
// assistant) for a DIRECT dispatch — i.e. no work-queue row matched — must not flip the
|
|
1115
1080
|
// dispatch row terminal. Leaving it active lets the reconcile loop (PHASE 4) re-read the
|
|
@@ -1615,22 +1580,7 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1615
1580
|
spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
|
|
1616
1581
|
// Coordinator-dispatched recovery relaunch: same auto-approve
|
|
1617
1582
|
// policy as the primary worker launch path.
|
|
1618
|
-
|
|
1619
|
-
mesh?.policy,
|
|
1620
|
-
node?.policy,
|
|
1621
|
-
components.providerLoader?.getMeta(recoveryContext.failedProviderType),
|
|
1622
|
-
// Recovery relaunch: same repo-declared requested mode as the
|
|
1623
|
-
// primary path. node.workspace missing → null → provider default.
|
|
1624
|
-
(() => {
|
|
1625
|
-
const ws = typeof node?.workspace === 'string' && node.workspace.trim() ? node.workspace.trim() : '';
|
|
1626
|
-
if (!ws) return null;
|
|
1627
|
-
try {
|
|
1628
|
-
const r = loadRepoMeshJsonConfig(ws);
|
|
1629
|
-
return r.sourceType === 'repo_file' && r.config ? r.config : null;
|
|
1630
|
-
} catch { return null; }
|
|
1631
|
-
})(),
|
|
1632
|
-
recoveryContext.failedProviderType,
|
|
1633
|
-
),
|
|
1583
|
+
autoApprove: resolveDelegatedWorkerAutoApprove(mesh?.policy, node?.policy),
|
|
1634
1584
|
launchedByCoordinator: true,
|
|
1635
1585
|
}
|
|
1636
1586
|
}).catch((e: any) => LOG.error('MeshRecovery', `Failed to auto-relaunch session for ${node.id}: ${e?.message}`));
|
|
@@ -1804,14 +1754,6 @@ export function buildRelayMetadataEvent(payload: Record<string, unknown>): Recor
|
|
|
1804
1754
|
retryOfJobId: readNonEmptyString(payload.retryOfJobId),
|
|
1805
1755
|
...(relayModalMessage ? { modalMessage: relayModalMessage } : {}),
|
|
1806
1756
|
...(relayModalButtons && relayModalButtons.length > 0 ? { modalButtons: relayModalButtons } : {}),
|
|
1807
|
-
// agent:waiting_choice (mission f1d25e11): carry the FULL structured question
|
|
1808
|
-
// payload across the machine boundary so a REMOTE worker's AskUserQuestion reaches
|
|
1809
|
-
// the coordinator with every question + option intact — the coordinator renders
|
|
1810
|
-
// these and answers with mesh_answer_question. The local in-process forward path
|
|
1811
|
-
// preserves the whole event for free; this mirrors the fields for the remote relay.
|
|
1812
|
-
...(payload.interactivePrompt && typeof payload.interactivePrompt === 'object' && !Array.isArray(payload.interactivePrompt) ? { interactivePrompt: payload.interactivePrompt } : {}),
|
|
1813
|
-
...(readNonEmptyString(payload.promptId) ? { promptId: readNonEmptyString(payload.promptId) } : {}),
|
|
1814
|
-
...(payload.multiSelect === true ? { multiSelect: true } : {}),
|
|
1815
1757
|
...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
|
|
1816
1758
|
...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
|
|
1817
1759
|
...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
|
|
@@ -330,48 +330,6 @@ export function buildMeshSystemMessage(args: {
|
|
|
330
330
|
if (args.event === 'agent:waiting_approval') {
|
|
331
331
|
return `[System] ${args.nodeLabel} is waiting for approval to proceed${metadata}. You may use mesh_read_chat and mesh_approve to handle it.`;
|
|
332
332
|
}
|
|
333
|
-
if (args.event === 'agent:waiting_choice') {
|
|
334
|
-
// A multi-choice QUESTION (AskUserQuestion) — NOT an approval. It is answered with
|
|
335
|
-
// mesh_answer_question (never mesh_approve). Surface the question text + choices
|
|
336
|
-
// inline so the coordinator can decide without a mesh_read_chat round-trip, and
|
|
337
|
-
// name the correct tool + promptId (mission f1d25e11).
|
|
338
|
-
const prompt = args.metadataEvent.interactivePrompt as
|
|
339
|
-
| { promptId?: unknown; questions?: Array<Record<string, unknown>> }
|
|
340
|
-
| undefined;
|
|
341
|
-
const promptId = readNonEmptyString(args.metadataEvent.promptId)
|
|
342
|
-
|| (prompt && readNonEmptyString(prompt.promptId));
|
|
343
|
-
const lines: string[] = [
|
|
344
|
-
`[System] ${args.nodeLabel} is asking a question and is waiting for your answer${metadata}.`,
|
|
345
|
-
];
|
|
346
|
-
const questions = Array.isArray(prompt?.questions) ? prompt!.questions! : [];
|
|
347
|
-
if (questions.length > 0) {
|
|
348
|
-
for (const q of questions) {
|
|
349
|
-
const header = readNonEmptyString(q.header);
|
|
350
|
-
const question = readNonEmptyString(q.question);
|
|
351
|
-
const multiSelect = q.multiSelect === true;
|
|
352
|
-
if (question) {
|
|
353
|
-
lines.push(`\n**${header ? `${header}: ` : ''}${question}**${multiSelect ? ' (select one or more)' : ''}`);
|
|
354
|
-
}
|
|
355
|
-
const options = Array.isArray(q.options) ? q.options : [];
|
|
356
|
-
options.forEach((opt, i) => {
|
|
357
|
-
const record = (opt && typeof opt === 'object') ? opt as Record<string, unknown> : {};
|
|
358
|
-
const label = readNonEmptyString(record.label);
|
|
359
|
-
if (!label) return;
|
|
360
|
-
const description = readNonEmptyString(record.description);
|
|
361
|
-
lines.push(` ${i + 1}. ${label}${description ? ` — ${description}` : ''}`);
|
|
362
|
-
});
|
|
363
|
-
}
|
|
364
|
-
} else {
|
|
365
|
-
const modalMessage = readNonEmptyString(args.metadataEvent.modalMessage);
|
|
366
|
-
if (modalMessage) lines.push(`\n${modalMessage}`);
|
|
367
|
-
}
|
|
368
|
-
lines.push(
|
|
369
|
-
`\nAnswer with mesh_answer_question(node_id, session_id${promptId ? `, promptId: "${promptId}"` : ''}, answers). ` +
|
|
370
|
-
`Do NOT use mesh_approve — that only resolves yes/no consent modals, not a question. ` +
|
|
371
|
-
`Use mesh_read_chat once if you need the full context first.`,
|
|
372
|
-
);
|
|
373
|
-
return lines.join('\n');
|
|
374
|
-
}
|
|
375
333
|
if (args.event === 'agent:stopped') {
|
|
376
334
|
const rc = args.recoveryContext;
|
|
377
335
|
if (rc && rc.consecutiveNodeFailures > 0) {
|