@adhdev/daemon-core 1.0.18-rc.10 → 1.0.18-rc.12
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/index.js +691 -35
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +691 -35
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-ledger.d.ts +1 -1
- package/dist/mesh/mesh-node-identity.d.ts +23 -0
- package/dist/mesh/mesh-refine-gates.d.ts +89 -0
- package/dist/mesh/mesh-work-queue.d.ts +24 -1
- package/dist/providers/cli-provider-instance-types.d.ts +2 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +14 -0
- package/dist/providers/types/interactive-prompt.d.ts +18 -0
- package/dist/shared-types.d.ts +1 -1
- package/package.json +3 -3
- package/src/commands/high-family/mesh-events.ts +13 -2
- package/src/commands/med-family/mesh-queue.ts +74 -1
- package/src/commands/router-refine.ts +71 -4
- package/src/commands/router.ts +8 -0
- package/src/mesh/coordinator-prompt.ts +2 -1
- package/src/mesh/mesh-active-work.ts +11 -1
- package/src/mesh/mesh-event-classify.ts +19 -0
- package/src/mesh/mesh-event-forwarding.ts +46 -4
- package/src/mesh/mesh-events-utils.ts +42 -0
- package/src/mesh/mesh-ledger.ts +5 -0
- package/src/mesh/mesh-node-identity.ts +49 -0
- package/src/mesh/mesh-queue-assignment.ts +18 -1
- package/src/mesh/mesh-reconcile-loop.ts +6 -1
- package/src/mesh/mesh-refine-gates.ts +300 -0
- package/src/mesh/mesh-work-queue.ts +85 -2
- package/src/providers/cli-provider-instance-types.ts +28 -0
- package/src/providers/cli-provider-instance.ts +90 -15
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +23 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +20 -0
- package/src/providers/types/interactive-prompt.ts +77 -0
- package/src/shared-types.ts +6 -0
- package/src/status/reporter.ts +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { MeshLedgerEntry } from './mesh-ledger.js';
|
|
2
2
|
import type { MeshWorkQueueEntry, DirectDispatchRecord } from './mesh-work-queue.js';
|
|
3
3
|
export type MeshActiveWorkSource = 'queue' | 'direct';
|
|
4
|
-
export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval';
|
|
4
|
+
export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval' | 'awaiting_choice';
|
|
5
5
|
export interface MeshActiveWorkRecord {
|
|
6
6
|
taskId: string;
|
|
7
7
|
source: MeshActiveWorkSource;
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
import { EventEmitter } from 'events';
|
|
16
16
|
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
17
17
|
import { type MeshLedgerOriginatingCoordinatorV2 } from './contracts.js';
|
|
18
|
-
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'event_held_requeued' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis' | 'key_injection' | 'worktree_cleanup_candidate';
|
|
18
|
+
export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'task_question_pending' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'event_held_requeued' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis' | 'key_injection' | 'worktree_cleanup_candidate';
|
|
19
19
|
export interface MeshLedgerEntry {
|
|
20
20
|
id: string;
|
|
21
21
|
meshId: string;
|
|
@@ -107,6 +107,29 @@ export declare function resolveEffectiveMeshNodeHealth(node: any): string;
|
|
|
107
107
|
* replica that can never run.
|
|
108
108
|
*/
|
|
109
109
|
export declare function isMeshNodeHealthLaunchable(node: any): boolean;
|
|
110
|
+
/**
|
|
111
|
+
* Launch FRESHNESS gate — distinct from the health gate above.
|
|
112
|
+
*
|
|
113
|
+
* `deriveMeshNodeHealthFromGit` reports a clean-tree node with `behind > 0` as
|
|
114
|
+
* 'online', so the health gate (isMeshNodeHealthLaunchable) happily lets a STALE
|
|
115
|
+
* node — one whose branch is N commits behind its upstream — win auto-launch fitness
|
|
116
|
+
* routing and run a fresh worker against out-of-date code. `behind` is deliberately
|
|
117
|
+
* NOT folded into deriveMeshNodeHealthFromGit because "behind" is not universally
|
|
118
|
+
* unhealthy (the MAGI planner and other callers share that resolver); it is only
|
|
119
|
+
* unhealthy for *spawning new work*. This gate encodes exactly that launch-time axis.
|
|
120
|
+
*
|
|
121
|
+
* Returns false (NOT fresh → skip / de-rank) only when git telemetry is PRESENT and
|
|
122
|
+
* proves staleness:
|
|
123
|
+
* - behind count exceeds `maxBehind` (default 0 — any behind blocks), OR
|
|
124
|
+
* - a submodule is out of sync (gitlink points off upstream — cannot be caught up
|
|
125
|
+
* by a simple worktree ff and would launch against a mismatched submodule).
|
|
126
|
+
*
|
|
127
|
+
* When telemetry is absent it returns true (fresh), preserving the online/unknown-pass
|
|
128
|
+
* philosophy: we never block on missing data, only on data that proves the node stale.
|
|
129
|
+
*/
|
|
130
|
+
export declare function isMeshNodeFreshEnoughToLaunch(node: any, opts?: {
|
|
131
|
+
maxBehind?: number;
|
|
132
|
+
}): boolean;
|
|
110
133
|
export declare function applyInlineMeshBranchConvergence(mesh: any, node: any, status: Record<string, unknown>): void;
|
|
111
134
|
export declare function summarizeInlineMeshBranchConvergence(nodes: Array<Record<string, unknown>>): Record<string, unknown>;
|
|
112
135
|
export declare function readCachedInlineMeshActiveSessions(node: any): string[];
|
|
@@ -504,6 +504,95 @@ type GitlinkTrivialFastForwardEvaluation = {
|
|
|
504
504
|
* gate.
|
|
505
505
|
*/
|
|
506
506
|
export declare function collectFastForwardGitlinkPaths(repoRoot: string, baseHead: string, branchHead: string): string[];
|
|
507
|
+
export type SubmoduleGitlinkConvergeResult = {
|
|
508
|
+
/** True when at least one diverged gitlink submodule was rebased onto its base-side commit. */
|
|
509
|
+
converged: boolean;
|
|
510
|
+
/**
|
|
511
|
+
* Set when convergence was declined/aborted (fail-safe → caller keeps the
|
|
512
|
+
* original defer→blocked_review path). One of:
|
|
513
|
+
* no_changed_gitlinks — no gitlink differs base↔branch
|
|
514
|
+
* not_diverged — the gitlink is ff/behind/equal (gate handles it)
|
|
515
|
+
* rebase_conflict — replaying branch-side onto base-side hit a real
|
|
516
|
+
* content conflict inside the submodule (aborted)
|
|
517
|
+
*/
|
|
518
|
+
reason?: string;
|
|
519
|
+
/**
|
|
520
|
+
* Converged gitlinks: path → the rebased submodule commit (SUBNEW) that the
|
|
521
|
+
* root rebase must resolve the gitlink conflict to. Only populated for paths
|
|
522
|
+
* whose submodule was successfully rebased (base-side is now a strict ancestor).
|
|
523
|
+
*/
|
|
524
|
+
resolutions: Array<{
|
|
525
|
+
path: string;
|
|
526
|
+
baseCommit: string;
|
|
527
|
+
branchCommit: string;
|
|
528
|
+
rebasedCommit: string;
|
|
529
|
+
}>;
|
|
530
|
+
/** Per-path outcome for observability/logging. */
|
|
531
|
+
gitlinks: Array<{
|
|
532
|
+
path: string;
|
|
533
|
+
baseCommit?: string;
|
|
534
|
+
branchCommit?: string;
|
|
535
|
+
rebasedCommit?: string;
|
|
536
|
+
action: 'rebased' | 'skipped_not_diverged' | 'rebase_conflict';
|
|
537
|
+
}>;
|
|
538
|
+
};
|
|
539
|
+
/**
|
|
540
|
+
* STEP 1 of auto-converging diverged oss-style submodule gitlinks: rebase the
|
|
541
|
+
* branch-side submodule commit(s) onto the base-side submodule commit INSIDE the
|
|
542
|
+
* worktree's submodule checkout (detached HEAD), so the base-side commit becomes a
|
|
543
|
+
* strict ancestor of the rebased tip. Returns the rebased commit per path so the
|
|
544
|
+
* caller's root rebase can resolve the gitlink conflict to it (STEP 2, see
|
|
545
|
+
* {@link rootRebaseResolvingGitlinks}). This automates the documented manual
|
|
546
|
+
* strict-fast-forward bypass and keeps the landed submodule history linear rather
|
|
547
|
+
* than masking a divergence.
|
|
548
|
+
*
|
|
549
|
+
* Fail-safe by construction: it only touches gitlinks that are a genuine sibling
|
|
550
|
+
* divergence (both commits local, neither an ancestor of the other, shared merge
|
|
551
|
+
* base). A submodule rebase content conflict aborts, restores the branch-side
|
|
552
|
+
* checkout, and returns `converged:false` (caller keeps defer→blocked_review). It
|
|
553
|
+
* never commits the root and NEVER pushes — remote publish is the merge/
|
|
554
|
+
* reachability stage's job; this stage is local reconciliation only.
|
|
555
|
+
*
|
|
556
|
+
* @param worktreeRoot the branch worktree root (its `<path>` submodule checkout is rebased)
|
|
557
|
+
* @param baseRepoRoot the source/base repo root (reads the base-side gitlink commit)
|
|
558
|
+
* @param baseHead the fetched base head (root ref) — source of base-side gitlink commits
|
|
559
|
+
* @param branchHead the worktree branch head (root ref) — source of branch-side gitlink commits
|
|
560
|
+
*/
|
|
561
|
+
export declare function convergeDivergedSubmoduleGitlinks(worktreeRoot: string, baseRepoRoot: string, baseHead: string, branchHead: string): SubmoduleGitlinkConvergeResult;
|
|
562
|
+
export type RootRebaseGitlinkResolveResult = {
|
|
563
|
+
/** True when the root rebase completed (with gitlink conflicts resolved to the converged commits). */
|
|
564
|
+
ok: boolean;
|
|
565
|
+
/** New root HEAD after the rebase (only meaningful when ok). */
|
|
566
|
+
branchHead?: string;
|
|
567
|
+
/**
|
|
568
|
+
* Set when the rebase was aborted (fail-safe). One of:
|
|
569
|
+
* non_gitlink_conflict — a conflict on a non-submodule path (genuine content conflict)
|
|
570
|
+
* unexpected_gitlink — a gitlink conflicted that we have no converged commit for
|
|
571
|
+
* rebase_error — the rebase failed for a non-conflict reason
|
|
572
|
+
*/
|
|
573
|
+
reason?: string;
|
|
574
|
+
/** The paths that conflicted at the point of abort (for diagnostics). */
|
|
575
|
+
conflictPaths?: string[];
|
|
576
|
+
};
|
|
577
|
+
/**
|
|
578
|
+
* STEP 2 of auto-converging diverged submodule gitlinks: rebase the worktree root
|
|
579
|
+
* branch onto `baseHead`, resolving each submodule-gitlink conflict to the
|
|
580
|
+
* pre-converged commit from {@link convergeDivergedSubmoduleGitlinks}. git's
|
|
581
|
+
* recursive merge refuses to auto-merge a diverged gitlink ("Recursive merging
|
|
582
|
+
* with submodules currently only supports trivial cases"), so we drive the rebase
|
|
583
|
+
* ourselves: on each stop, if the ONLY unmerged paths are gitlinks we have a
|
|
584
|
+
* converged commit for, we stage those to the converged commit and `--continue`.
|
|
585
|
+
* Any non-gitlink conflict (or a gitlink with no converged commit) aborts the
|
|
586
|
+
* rebase and returns `ok:false` → caller keeps the defer→blocked_review path.
|
|
587
|
+
*
|
|
588
|
+
* On success the base-side gitlink is a strict ancestor of the resolved branch-side
|
|
589
|
+
* commit, so the downstream patch-equivalence gate treats it as a trivial
|
|
590
|
+
* fast-forward and passes.
|
|
591
|
+
*/
|
|
592
|
+
export declare function rootRebaseResolvingGitlinks(worktreeRoot: string, baseHead: string, resolutions: Array<{
|
|
593
|
+
path: string;
|
|
594
|
+
rebasedCommit: string;
|
|
595
|
+
}>): RootRebaseGitlinkResolveResult;
|
|
507
596
|
/**
|
|
508
597
|
* Decide whether a merge-tree submodule conflict between base and branch is a
|
|
509
598
|
* trivial gitlink fast-forward (and nothing else).
|
|
@@ -337,7 +337,15 @@ export type DependencyFailurePolicy = 'block' | 'cancel';
|
|
|
337
337
|
* Update the status of a specific task.
|
|
338
338
|
* Used when a session completes, fails, or stalls.
|
|
339
339
|
*/
|
|
340
|
-
export declare function updateTaskStatus(meshId: string, taskId: string, status: MeshTaskStatus, opts?:
|
|
340
|
+
export declare function updateTaskStatus(meshId: string, taskId: string, status: MeshTaskStatus, opts?: {
|
|
341
|
+
/**
|
|
342
|
+
* CANCEL-STICKY-TERMINAL: operator/system override to permit a terminal→non-terminal
|
|
343
|
+
* transition (e.g. an explicit operator reopen). Without this, a write that would flip
|
|
344
|
+
* a `completed`/`failed`/`cancelled` row back to `pending`/`assigned` is refused as a
|
|
345
|
+
* no-op. Terminal→terminal and any transition FROM a non-terminal state are unaffected.
|
|
346
|
+
*/
|
|
347
|
+
force?: boolean;
|
|
348
|
+
} & MeshQueueMutationOptions): MeshWorkQueueEntry | null;
|
|
341
349
|
export declare function recordTaskAutoLaunch(meshId: string, taskId: string, autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>): MeshWorkQueueEntry | null;
|
|
342
350
|
/**
|
|
343
351
|
* Mark a queue task as manually cancelled without deleting audit history.
|
|
@@ -345,6 +353,21 @@ export declare function recordTaskAutoLaunch(meshId: string, taskId: string, aut
|
|
|
345
353
|
export declare function cancelTask(meshId: string, taskId: string, opts?: {
|
|
346
354
|
reason?: string;
|
|
347
355
|
} & MeshQueueMutationOptions): MeshWorkQueueEntry | null;
|
|
356
|
+
/**
|
|
357
|
+
* CANCEL-STICKY-TERMINAL: the assignment a task carried at cancel time, handed to the cancel
|
|
358
|
+
* command handler so it can stop the bound worker. cancelTask runs in the pure queue-store
|
|
359
|
+
* module (no DaemonComponents), so it records the binding here and the handler drains it.
|
|
360
|
+
*/
|
|
361
|
+
export interface CancelledTaskAssignment {
|
|
362
|
+
sessionId: string;
|
|
363
|
+
nodeId?: string;
|
|
364
|
+
providerType?: string;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Read-and-clear the assignment a just-cancelled task was bound to. Returns undefined when the
|
|
368
|
+
* cancelled task had no live assignment (nothing to stop). One-shot: the entry is deleted on read.
|
|
369
|
+
*/
|
|
370
|
+
export declare function takeCancelledTaskAssignment(meshId: string, taskId: string): CancelledTaskAssignment | undefined;
|
|
348
371
|
/**
|
|
349
372
|
* Return a queue task to pending for retry. By default, dead session targeting
|
|
350
373
|
* and assigned ownership are cleared so stale assignments do not strand again.
|
|
@@ -19,6 +19,7 @@ export type CompletedFinalizationBlock = {
|
|
|
19
19
|
terminal?: boolean;
|
|
20
20
|
allowTimeout?: boolean;
|
|
21
21
|
holdForTranscript?: boolean;
|
|
22
|
+
noExternalTranscriptSource?: boolean;
|
|
22
23
|
};
|
|
23
24
|
export type CompletionFinalAssistantEvidence = {
|
|
24
25
|
present: boolean;
|
|
@@ -37,6 +38,7 @@ export type ExternalTranscriptProbe = {
|
|
|
37
38
|
};
|
|
38
39
|
export declare const COMPLETED_FINALIZATION_RETRY_MS = 1000;
|
|
39
40
|
export declare const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30000;
|
|
41
|
+
export declare const CANON_C_MISSING_ASSISTANT_MIN_ELAPSED_MS = 20000;
|
|
40
42
|
export declare const NATIVE_HISTORY_MESH_IDLE_SETTLE_MS = 4000;
|
|
41
43
|
export declare const PTY_PARSED_FINAL_ASSISTANT_QUIET_DWELL_MS = 1200;
|
|
42
44
|
export declare const ANTIGRAVITY_HOLD_QUIET_DWELL_MS = 3000;
|
|
@@ -75,6 +75,20 @@ export interface DetectStatusTuiSpec {
|
|
|
75
75
|
declare function scopeText(input: CliStatusInput, scope: SpinnerSpec['scope'] | SettledPromptSpec['scope'], windowLines: number | undefined): string;
|
|
76
76
|
declare function spinnerMatches(spec: SpinnerSpec, input: CliStatusInput): boolean;
|
|
77
77
|
declare function settledPromptMatches(spec: SettledPromptSpec, input: CliStatusInput): boolean;
|
|
78
|
+
/**
|
|
79
|
+
* APPROVAL-PICKER-MISROUTE (mission f1d25e11) defense-in-depth: an
|
|
80
|
+
* AskUserQuestion multi-choice picker is NOT an approval modal. Its option rows
|
|
81
|
+
* ("❯ 1. label") can otherwise satisfy the approval button cue and get
|
|
82
|
+
* mis-classified as `waiting_approval`, so the worker's question is surfaced to
|
|
83
|
+
* the coordinator as a task_approval_needed (→ mesh_approve, which cannot answer
|
|
84
|
+
* it). The picker carries a distinctive signature the approval FSM never does:
|
|
85
|
+
* the claude TUI select footer ("Enter to select … Esc to cancel") together with
|
|
86
|
+
* the freeform escape hatch ("Type something" / "Chat about this"). When both are
|
|
87
|
+
* present the screen is a question picker — surfaced separately as
|
|
88
|
+
* waiting_choice — so the approval matchers must yield. Mirrors the legacy
|
|
89
|
+
* looksLikeSelectionPicker guard (cli-provider-instance.ts) ported to SDK-v1.
|
|
90
|
+
*/
|
|
91
|
+
export declare function isAskUserQuestionPickerSignature(text: string): boolean;
|
|
78
92
|
declare function modalMatches(spec: ModalSpec, input: CliStatusInput): boolean;
|
|
79
93
|
export declare function buildDetectStatusFromTui(spec: DetectStatusTuiSpec): CliDetectStatusFn;
|
|
80
94
|
export declare const __internal: {
|
|
@@ -28,6 +28,24 @@ export interface InteractiveAnswer {
|
|
|
28
28
|
}
|
|
29
29
|
export declare function normalizeInteractivePrompt(raw: unknown): InteractivePrompt | null;
|
|
30
30
|
export declare function normalizeInteractivePromptResponse(raw: unknown): InteractivePromptResponse;
|
|
31
|
+
/**
|
|
32
|
+
* Resolve a coordinator-friendly answer form into the strict, questionId-keyed
|
|
33
|
+
* InteractivePromptResponse the TUI/answer machinery consumes (mission f1d25e11).
|
|
34
|
+
*
|
|
35
|
+
* mesh_answer_question lets the coordinator answer against the option LABELS or
|
|
36
|
+
* 1-based INDEXES it saw in the agent:waiting_choice event, without having to
|
|
37
|
+
* reconstruct the exact questionId → selectedLabels map. This resolves that
|
|
38
|
+
* ergonomic form against the AUTHORITATIVE active prompt (the daemon holds it),
|
|
39
|
+
* so index/label resolution and question ordering are correct by construction.
|
|
40
|
+
*
|
|
41
|
+
* Accepted `raw` shapes:
|
|
42
|
+
* - The strict form ({ promptId, answers: { [questionId]: { selectedLabels } } })
|
|
43
|
+
* — passed straight to normalizeInteractivePromptResponse (back-compat).
|
|
44
|
+
* - The friendly form ({ promptId, answers: [ { questionId?, select?, freeform? } ] })
|
|
45
|
+
* — entries map to questions by questionId, else by array position. `select`
|
|
46
|
+
* is a label (string) / 1-based index (number) / array of either.
|
|
47
|
+
*/
|
|
48
|
+
export declare function resolveInteractivePromptResponse(prompt: InteractivePrompt, raw: unknown): InteractivePromptResponse;
|
|
31
49
|
export declare function buildClaudeInteractiveToolResult(response: InteractivePromptResponse): string;
|
|
32
50
|
export interface ClaudeInteractiveTuiPage {
|
|
33
51
|
screenText: string;
|
package/dist/shared-types.d.ts
CHANGED
|
@@ -624,7 +624,7 @@ export interface DashboardBootstrapDaemonEntry extends Partial<CloudDaemonSummar
|
|
|
624
624
|
p2p?: StatusReportPayload['p2p'];
|
|
625
625
|
timestamp?: number;
|
|
626
626
|
}
|
|
627
|
-
export type DaemonStatusEventName = 'agent:generating_started' | 'agent:waiting_approval' | 'agent:generating_completed' | 'agent:stopped' | 'monitor:no_progress' | 'monitor:long_generating';
|
|
627
|
+
export type DaemonStatusEventName = 'agent:generating_started' | 'agent:waiting_approval' | 'agent:waiting_choice' | 'agent:generating_completed' | 'agent:stopped' | 'monitor:no_progress' | 'monitor:long_generating';
|
|
628
628
|
/** Minimal daemon-originated event payload relayed through the server. */
|
|
629
629
|
export interface DaemonStatusEventPayload {
|
|
630
630
|
event: DaemonStatusEventName;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "1.0.18-rc.
|
|
3
|
+
"version": "1.0.18-rc.12",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -47,8 +47,8 @@
|
|
|
47
47
|
"author": "vilmire",
|
|
48
48
|
"license": "AGPL-3.0-or-later",
|
|
49
49
|
"dependencies": {
|
|
50
|
-
"@adhdev/mesh-shared": "1.0.18-rc.
|
|
51
|
-
"@adhdev/session-host-core": "1.0.18-rc.
|
|
50
|
+
"@adhdev/mesh-shared": "1.0.18-rc.12",
|
|
51
|
+
"@adhdev/session-host-core": "1.0.18-rc.12",
|
|
52
52
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
53
53
|
"ajv": "^8.20.0",
|
|
54
54
|
"ajv-formats": "^3.0.1",
|
|
@@ -93,10 +93,21 @@ export const meshEventsHandlers: Record<string, HighFamilyHandler> = {
|
|
|
93
93
|
? args.sessionId.trim()
|
|
94
94
|
: '';
|
|
95
95
|
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
96
|
-
const
|
|
96
|
+
const rawResponse = args?.response ?? args;
|
|
97
97
|
const instance = ctx.deps.instanceManager.getInstance(sessionId);
|
|
98
98
|
if (!instance) return { success: false, error: `No running instance for session ${sessionId}` };
|
|
99
|
-
|
|
99
|
+
// mesh_answer_question (mission f1d25e11) sends a coordinator-friendly answer array
|
|
100
|
+
// that is resolved against the AUTHORITATIVE active prompt inside the instance
|
|
101
|
+
// (resolveInteractivePromptResponse). Forward that shape RAW. The legacy strict
|
|
102
|
+
// (questionId-keyed) form is still validated here so a malformed dashboard-local
|
|
103
|
+
// answer is rejected before it reaches the instance.
|
|
104
|
+
const isFriendlyArrayForm = rawResponse
|
|
105
|
+
&& typeof rawResponse === 'object'
|
|
106
|
+
&& Array.isArray((rawResponse as { answers?: unknown }).answers);
|
|
107
|
+
const payload = isFriendlyArrayForm
|
|
108
|
+
? rawResponse
|
|
109
|
+
: normalizeInteractivePromptResponse(rawResponse);
|
|
110
|
+
ctx.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', payload);
|
|
100
111
|
return { success: true };
|
|
101
112
|
},
|
|
102
113
|
};
|
|
@@ -9,6 +9,67 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { readStringValue } from '../router.js';
|
|
11
11
|
import type { MedFamilyContext, MedFamilyHandler } from './types.js';
|
|
12
|
+
import { LOG } from '../../logging/logger.js';
|
|
13
|
+
import type { CancelledTaskAssignment } from '../../mesh/mesh-work-queue.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* CANCEL-STICKY-TERMINAL (authoritative cancel): stop the worker a just-cancelled task was
|
|
17
|
+
* bound to. Mirrors the transport-aware stop mesh-event-forwarding's stopStaleMeshWorker
|
|
18
|
+
* performs for reclaimed workers, but runs from the command layer using ctx.deps (which the
|
|
19
|
+
* pure queue-store module lacks): local adapter → stop_cli directly; otherwise resolve the
|
|
20
|
+
* worker node's daemon id from mesh config and dispatch stop_cli over P2P/relay. Fully
|
|
21
|
+
* best-effort — a failed stop only loses the belt-and-suspenders; the cancelled row is already
|
|
22
|
+
* terminal and cannot be resurrected (updateTaskStatus terminal guard).
|
|
23
|
+
*/
|
|
24
|
+
async function stopCancelledTaskWorker(
|
|
25
|
+
ctx: MedFamilyContext,
|
|
26
|
+
meshId: string,
|
|
27
|
+
prior: CancelledTaskAssignment,
|
|
28
|
+
): Promise<void> {
|
|
29
|
+
const { sessionId, nodeId, providerType } = prior;
|
|
30
|
+
const stopArgs: Record<string, unknown> = {
|
|
31
|
+
targetSessionId: sessionId,
|
|
32
|
+
...(providerType ? { cliType: providerType } : {}),
|
|
33
|
+
mode: 'hard',
|
|
34
|
+
reason: 'mesh_task_cancelled',
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
const cliManager = ctx.deps.cliManager as any;
|
|
38
|
+
const isLocal = cliManager?.adapters?.has?.(sessionId) === true;
|
|
39
|
+
if (isLocal) {
|
|
40
|
+
if (!stopArgs.cliType) {
|
|
41
|
+
const localType = cliManager?.adapters?.get?.(sessionId)?.cliType;
|
|
42
|
+
if (localType) stopArgs.cliType = localType;
|
|
43
|
+
}
|
|
44
|
+
await Promise.resolve(cliManager?.handleCliCommand?.('stop_cli', stopArgs))
|
|
45
|
+
.catch((e: any) => LOG.warn('MeshQueue', `Local stop of cancelled worker ${sessionId} failed: ${e?.message || e}`));
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Remote: resolve the worker node's daemon id from mesh config, then dispatch stop_cli.
|
|
49
|
+
const dispatch = ctx.deps.dispatchMeshCommand;
|
|
50
|
+
let daemonId: string | undefined;
|
|
51
|
+
if (nodeId) {
|
|
52
|
+
try {
|
|
53
|
+
const [{ getMesh }, { meshNodeIdMatches }, { readMeshNodeDaemonId }] = await Promise.all([
|
|
54
|
+
import('../../config/mesh-config.js'),
|
|
55
|
+
import('@adhdev/mesh-shared'),
|
|
56
|
+
import('../../mesh/mesh-node-identity.js'),
|
|
57
|
+
]);
|
|
58
|
+
const mesh = getMesh(meshId) as { nodes?: any[] } | undefined;
|
|
59
|
+
const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId));
|
|
60
|
+
daemonId = node ? readMeshNodeDaemonId(node) || undefined : undefined;
|
|
61
|
+
} catch { /* best-effort resolution */ }
|
|
62
|
+
}
|
|
63
|
+
if (daemonId && dispatch) {
|
|
64
|
+
await Promise.resolve(dispatch(daemonId, 'stop_cli', stopArgs))
|
|
65
|
+
.catch((e: any) => LOG.warn('MeshQueue', `Remote stop of cancelled worker ${sessionId} on daemon ${daemonId} failed: ${e?.message || e}`));
|
|
66
|
+
} else {
|
|
67
|
+
LOG.warn('MeshQueue', `Cannot stop cancelled worker ${sessionId}: no local adapter and no resolvable remote daemon id (node ${nodeId ?? '?'}). Row is already terminal; if the worker completes it strand-fails harmlessly.`);
|
|
68
|
+
}
|
|
69
|
+
} catch (e: any) {
|
|
70
|
+
LOG.warn('MeshQueue', `stopCancelledTaskWorker error for ${sessionId}: ${e?.message || e}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
12
73
|
|
|
13
74
|
export const meshQueueHandlers: Record<string, MedFamilyHandler> = {
|
|
14
75
|
get_mesh_queue: async (_ctx: MedFamilyContext, args: any) => {
|
|
@@ -50,10 +111,22 @@ export const meshQueueHandlers: Record<string, MedFamilyHandler> = {
|
|
|
50
111
|
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
51
112
|
if (ownerFailure) return ownerFailure;
|
|
52
113
|
try {
|
|
53
|
-
const { cancelTask } = await import('../../mesh/mesh-work-queue.js');
|
|
114
|
+
const { cancelTask, takeCancelledTaskAssignment } = await import('../../mesh/mesh-work-queue.js');
|
|
54
115
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
55
116
|
const task = cancelTask(meshId, taskId, { reason });
|
|
56
117
|
if (!task) return { success: false, error: `Queue task '${taskId}' not found` };
|
|
118
|
+
// CANCEL-STICKY-TERMINAL (authoritative cancel): stop the worker that was bound to the
|
|
119
|
+
// now-cancelled task. cancelTask already cleared the queue's assignment fields (so the
|
|
120
|
+
// reclaim watchdog no longer sees it as a live assignment), but a still-running worker
|
|
121
|
+
// keeps emitting delivery/turn signals that re-ignite reclaim. cancelTask runs in the
|
|
122
|
+
// pure queue-store module (no DaemonComponents), so the transport-aware stop is done
|
|
123
|
+
// here where ctx.deps holds cliManager / dispatchMeshCommand. Best-effort: a failed
|
|
124
|
+
// stop only loses the belt-and-suspenders — the row is already terminal and un-revivable
|
|
125
|
+
// thanks to the updateTaskStatus terminal guard.
|
|
126
|
+
const prior = takeCancelledTaskAssignment(meshId, taskId);
|
|
127
|
+
if (prior?.sessionId) {
|
|
128
|
+
void stopCancelledTaskWorker(ctx, meshId, prior);
|
|
129
|
+
}
|
|
57
130
|
return { success: true, task };
|
|
58
131
|
} catch (e: any) {
|
|
59
132
|
return { success: false, error: e.message };
|
|
@@ -36,8 +36,10 @@ import {
|
|
|
36
36
|
RefineExecFileAsync,
|
|
37
37
|
RefineStageOutcome,
|
|
38
38
|
classifyPatchEquivalenceFailure,
|
|
39
|
+
convergeDivergedSubmoduleGitlinks,
|
|
39
40
|
recordMeshRefineStage,
|
|
40
41
|
resolveRefineryAutoPublishSubmoduleMainCommits,
|
|
42
|
+
rootRebaseResolvingGitlinks,
|
|
41
43
|
runMeshRefineEffectiveDiffGate,
|
|
42
44
|
runMeshRefinePatchEquivalenceGate,
|
|
43
45
|
runMeshRefineSubmoduleReachabilityGate,
|
|
@@ -539,6 +541,9 @@ async function computeBranchBaseDivergence(
|
|
|
539
541
|
export async function refineSyncBaseStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
540
542
|
const { repoRoot, baseHead, node, branch, baseBranch, refineStages, execFileAsync } = ctx;
|
|
541
543
|
let branchHead = ctx.branchHead;
|
|
544
|
+
// Converged submodule gitlink resolutions (path → rebased commit) from STEP 1;
|
|
545
|
+
// consumed by the gitlink-aware root rebase (STEP 2) to resolve gitlink conflicts.
|
|
546
|
+
let gitlinkResolutions: Array<{ path: string; rebasedCommit: string }> = [];
|
|
542
547
|
const syncStarted = Date.now();
|
|
543
548
|
const divergence = await computeBranchBaseDivergence(execFileAsync, node.workspace, baseHead, branchHead);
|
|
544
549
|
|
|
@@ -571,25 +576,87 @@ export async function refineSyncBaseStage(self: DaemonCommandRouter, ctx: Refine
|
|
|
571
576
|
const preRebasePe = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
572
577
|
const alreadyMerged = !preRebasePe.actualPatchId && !!preRebasePe.expectedPatchId;
|
|
573
578
|
const submoduleConflict = preRebasePe.actionableHint?.kind === 'submodule_conflict';
|
|
574
|
-
if (alreadyMerged
|
|
579
|
+
if (alreadyMerged) {
|
|
575
580
|
recordMeshRefineStage(refineStages, 'sync_base', 'passed', syncStarted, {
|
|
576
581
|
ahead: divergence.ahead,
|
|
577
582
|
behind: divergence.behind,
|
|
578
583
|
rebased: false,
|
|
579
|
-
reason:
|
|
584
|
+
reason: 'already_merged_via_other_path_skip_rebase',
|
|
580
585
|
});
|
|
581
586
|
return { kind: 'continue', ctx };
|
|
582
587
|
}
|
|
588
|
+
if (submoduleConflict) {
|
|
589
|
+
// DS3: a "submodule conflict" here means base and branch advanced the
|
|
590
|
+
// SAME submodule to DIVERGED sibling commits (neither an ancestor of the
|
|
591
|
+
// other), so the gitlink stays in the diff and patch-equivalence fails.
|
|
592
|
+
// Attempt to auto-converge it (STEP 1): rebase the branch-side submodule
|
|
593
|
+
// commit onto the base-side commit INSIDE the worktree submodule, so the
|
|
594
|
+
// base-side commit becomes a strict ancestor of the rebased tip. The root
|
|
595
|
+
// rebase below (STEP 2) then resolves the gitlink conflict to that rebased
|
|
596
|
+
// commit. Together this automates the documented manual strict-ff bypass
|
|
597
|
+
// and keeps the landed oss history linear. On any real submodule content
|
|
598
|
+
// conflict it backs out cleanly → we FALL BACK to the historical
|
|
599
|
+
// defer→patch_equivalence path below.
|
|
600
|
+
const converge = convergeDivergedSubmoduleGitlinks(node.workspace, repoRoot, baseHead, branchHead);
|
|
601
|
+
if (converge.converged) {
|
|
602
|
+
gitlinkResolutions = converge.resolutions;
|
|
603
|
+
recordMeshRefineStage(refineStages, 'submodule_gitlink_converge', 'passed', syncStarted, {
|
|
604
|
+
reason: 'submodule_diverged_auto_rebased',
|
|
605
|
+
gitlinks: converge.gitlinks,
|
|
606
|
+
});
|
|
607
|
+
LOG.info('Mesh', `[Refinery] Auto-converged diverged submodule gitlink(s) onto base for node ${node.id}: `
|
|
608
|
+
+ converge.resolutions.map(r => `${r.path}→${r.rebasedCommit.slice(0, 12)}`).join(', '));
|
|
609
|
+
// Fall through (do NOT return) → the gitlink-aware root rebase below runs.
|
|
610
|
+
} else {
|
|
611
|
+
// Fail-safe: convergence declined (conflict / unreachable / not a real
|
|
612
|
+
// divergence) → preserve the historical defer→blocked_review behavior.
|
|
613
|
+
recordMeshRefineStage(refineStages, 'submodule_gitlink_converge', 'skipped', syncStarted, {
|
|
614
|
+
reason: 'submodule_conflict_defer_to_patch_equivalence',
|
|
615
|
+
convergeReason: converge.reason,
|
|
616
|
+
gitlinks: converge.gitlinks,
|
|
617
|
+
});
|
|
618
|
+
recordMeshRefineStage(refineStages, 'sync_base', 'passed', syncStarted, {
|
|
619
|
+
ahead: divergence.ahead,
|
|
620
|
+
behind: divergence.behind,
|
|
621
|
+
rebased: false,
|
|
622
|
+
reason: 'submodule_conflict_defer_to_patch_equivalence',
|
|
623
|
+
});
|
|
624
|
+
return { kind: 'continue', ctx };
|
|
625
|
+
}
|
|
626
|
+
}
|
|
583
627
|
} catch { /* fail-open: on gate error, fall through to the rebase */ }
|
|
584
628
|
|
|
585
629
|
// behind>0: strictly-behind OR diverged. Rebase the branch onto the pinned
|
|
586
630
|
// baseHead. A conflict aborts and terminates blocked_review (retryable=false —
|
|
587
631
|
// a real content conflict needs human resolution, not a base-movement retry).
|
|
632
|
+
//
|
|
633
|
+
// When STEP 1 converged a diverged submodule gitlink, use the gitlink-aware
|
|
634
|
+
// root rebase (STEP 2): git's recursive merge refuses to auto-merge the still-
|
|
635
|
+
// diverged intermediate gitlink, so we drive the rebase and resolve each
|
|
636
|
+
// submodule-gitlink conflict to the converged commit. A non-gitlink conflict
|
|
637
|
+
// aborts and falls through to the same blocked_review handling as a plain
|
|
638
|
+
// rebase conflict below (via the thrown gitlinkRebaseError).
|
|
588
639
|
const rebaseStarted = Date.now();
|
|
589
640
|
try {
|
|
590
|
-
|
|
641
|
+
if (gitlinkResolutions.length > 0) {
|
|
642
|
+
const gitlinkRebase = rootRebaseResolvingGitlinks(node.workspace, baseHead, gitlinkResolutions);
|
|
643
|
+
if (!gitlinkRebase.ok) {
|
|
644
|
+
// Surface as a rebase failure so the shared blocked_review handling
|
|
645
|
+
// (submodule-hint recovery included) runs — nothing was left mid-rebase
|
|
646
|
+
// (rootRebaseResolvingGitlinks aborts on failure).
|
|
647
|
+
const err: any = new Error(`gitlink-aware rebase aborted: ${gitlinkRebase.reason || 'unknown'}`);
|
|
648
|
+
err.gitlinkRebaseReason = gitlinkRebase.reason;
|
|
649
|
+
err.gitlinkRebaseConflicts = gitlinkRebase.conflictPaths;
|
|
650
|
+
err.alreadyAborted = true;
|
|
651
|
+
throw err;
|
|
652
|
+
}
|
|
653
|
+
} else {
|
|
654
|
+
execFileSync('git', ['rebase', baseHead], { cwd: node.workspace, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
655
|
+
}
|
|
591
656
|
} catch (rebaseErr: any) {
|
|
592
|
-
|
|
657
|
+
if (!rebaseErr?.alreadyAborted) {
|
|
658
|
+
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
659
|
+
}
|
|
593
660
|
// A rebase conflict on a submodule/gitlink divergence is a SPECIAL case the
|
|
594
661
|
// patch-equivalence gate describes with a rich actionable hint (which
|
|
595
662
|
// submodule, base vs branch commit, how to resolve). Run that gate against
|
package/src/commands/router.ts
CHANGED
|
@@ -227,6 +227,14 @@ const MESH_FORWARDABLE_SESSION_COMMANDS = new Set([
|
|
|
227
227
|
// MUTATES the worker PTY, so forwarding to the real owner (not a wrong local session) is
|
|
228
228
|
// doubly important. The daemon re-enforces the destructive-key confirm gate after the forward.
|
|
229
229
|
'send_keys',
|
|
230
|
+
// interactive_prompt_response (mesh_answer_question, mission f1d25e11): the coordinator
|
|
231
|
+
// answers a REMOTE worker's AskUserQuestion (waiting_choice). The answer must reach the
|
|
232
|
+
// OWNING worker session's live instance — its activeInteractivePrompt (the authoritative
|
|
233
|
+
// prompt the labels/indexes resolve against) and its adapter.setInteractivePromptResponse
|
|
234
|
+
// live only there. Without forwarding, the coordinator's local high-family handler returns
|
|
235
|
+
// 'No running instance for session …' and the question is never answered — the exact
|
|
236
|
+
// remote-worker forwarding gap of mission 6938892f, now closed for questions too.
|
|
237
|
+
'interactive_prompt_response',
|
|
230
238
|
]);
|
|
231
239
|
|
|
232
240
|
function normalizeCommandSource(source: string): CommandLogEntry['source'] {
|
|
@@ -1007,7 +1007,8 @@ const TOOLS_SECTION = `## Available Tools
|
|
|
1007
1007
|
| \`mesh_fast_forward_node\` | Safely dry-run or explicitly execute an obvious clean fast-forward without launching an agent session |
|
|
1008
1008
|
| \`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
1009
|
| \`mesh_checkpoint\` | Create a git checkpoint on a node |
|
|
1010
|
-
| \`mesh_approve\` | Approve/reject a pending agent action |
|
|
1010
|
+
| \`mesh_approve\` | Approve/reject a pending agent action (a yes/no tool-consent modal) |
|
|
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 |
|
|
1011
1012
|
| \`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 |
|
|
1012
1013
|
| \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
|
|
1013
1014
|
| \`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' | 'awaiting_choice';
|
|
9
9
|
|
|
10
10
|
export interface MeshActiveWorkRecord {
|
|
11
11
|
taskId: string;
|
|
@@ -135,6 +135,10 @@ 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' };
|
|
138
142
|
if (raw.includes('approval')) return { status: 'awaiting_approval' };
|
|
139
143
|
if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return { status: 'generating' };
|
|
140
144
|
if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return { status: 'failed' };
|
|
@@ -166,6 +170,10 @@ function terminalMatchesDispatch(terminal: MeshLedgerEntry, dispatch: MeshLedger
|
|
|
166
170
|
|
|
167
171
|
function statusFromTerminal(entry: MeshLedgerEntry): MeshActiveWorkStatus {
|
|
168
172
|
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';
|
|
169
177
|
if (entry.kind === 'task_completed') return 'idle';
|
|
170
178
|
return 'failed';
|
|
171
179
|
}
|
|
@@ -331,6 +339,7 @@ export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]):
|
|
|
331
339
|
idle: 0,
|
|
332
340
|
failed: 0,
|
|
333
341
|
awaiting_approval: 0,
|
|
342
|
+
awaiting_choice: 0,
|
|
334
343
|
};
|
|
335
344
|
const sourceCounts: Record<MeshActiveWorkSource, number> = { queue: 0, direct: 0 };
|
|
336
345
|
for (const item of activeWork) {
|
|
@@ -380,6 +389,7 @@ export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeW
|
|
|
380
389
|
? sessionStatusFromNodes(opts.nodes, queueNodeId ?? undefined, queueSessionId ?? undefined)
|
|
381
390
|
: {};
|
|
382
391
|
const queueStatus: MeshActiveWorkStatus = queueLive.status === 'awaiting_approval'
|
|
392
|
+
|| queueLive.status === 'awaiting_choice'
|
|
383
393
|
|| queueLive.status === 'generating'
|
|
384
394
|
? queueLive.status
|
|
385
395
|
: task.status;
|
|
@@ -8,6 +8,11 @@ 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',
|
|
11
16
|
'agent:stopped',
|
|
12
17
|
'agent:ready',
|
|
13
18
|
'monitor:no_progress',
|
|
@@ -26,6 +31,7 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
26
31
|
export const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
27
32
|
'agent:generating_completed': 'task_completed',
|
|
28
33
|
'agent:waiting_approval': 'task_approval_needed',
|
|
34
|
+
'agent:waiting_choice': 'task_question_pending',
|
|
29
35
|
'agent:stopped': 'task_failed',
|
|
30
36
|
'monitor:no_progress': 'task_stalled',
|
|
31
37
|
};
|
|
@@ -45,6 +51,11 @@ export const MESH_FORCE_INJECT_EVENTS: ReadonlySet<string> = new Set([
|
|
|
45
51
|
'agent:generating_completed',
|
|
46
52
|
'agent:stopped',
|
|
47
53
|
'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',
|
|
48
59
|
'refine:completed',
|
|
49
60
|
'refine:failed',
|
|
50
61
|
'worktree_bootstrap_complete',
|
|
@@ -71,6 +82,14 @@ export function shouldForceInjectMeshEvent(eventName: unknown): boolean {
|
|
|
71
82
|
// require, and why a stale/resolved approval nudge can simply be dropped.
|
|
72
83
|
export const MESH_APPROVAL_EVENTS: ReadonlySet<string> = new Set([
|
|
73
84
|
'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',
|
|
74
93
|
]);
|
|
75
94
|
|
|
76
95
|
export function isMeshApprovalEvent(eventName: unknown): boolean {
|