@adhdev/daemon-core 0.9.82-rc.269 → 0.9.82-rc.270
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.d.ts +1 -1
- package/dist/index.js +109 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +108 -20
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-utils.d.ts +31 -0
- package/dist/providers/spec/fsm-driver.d.ts +1 -0
- package/dist/providers/spec/fsm-types.d.ts +15 -0
- package/dist/providers/spec/types.d.ts +22 -7
- package/dist/repo-mesh-types.d.ts +24 -0
- package/package.json +2 -2
- package/src/commands/router.ts +29 -0
- package/src/index.ts +1 -1
- package/src/mesh/mesh-events-coordinator.ts +26 -1
- package/src/mesh/mesh-events-utils.ts +59 -0
- package/src/providers/spec/evaluator.ts +38 -14
- package/src/providers/spec/fsm-driver.ts +28 -0
- package/src/providers/spec/fsm-types.ts +15 -0
- package/src/providers/spec/types.ts +23 -7
- package/src/repo-mesh-types.ts +37 -0
|
@@ -3,6 +3,37 @@ export declare function readNonEmptyString(value: unknown): string;
|
|
|
3
3
|
export declare function readRecord(value: unknown): Record<string, unknown> | undefined;
|
|
4
4
|
export declare function canonicalDaemonId(value: unknown): string;
|
|
5
5
|
export declare function sameDaemonId(a: unknown, b: unknown): boolean;
|
|
6
|
+
/**
|
|
7
|
+
* The relay-safety metadata a worker session must carry so that its completion /
|
|
8
|
+
* generating events route back to the coordinator proactively (without waiting for
|
|
9
|
+
* a mesh_read_chat reconcile). meshCoordinatorDaemonId is the routing anchor the
|
|
10
|
+
* core forwarder (injectMeshSystemMessage) keys on to pick a remote coordinator
|
|
11
|
+
* target; meshNodeFor/meshNodeId identify the worker, and launchedByCoordinator is
|
|
12
|
+
* the delegation proof. See resolveWorkerDelegateRouting().
|
|
13
|
+
*/
|
|
14
|
+
export interface MeshWorkerRelayStamp {
|
|
15
|
+
meshNodeFor?: string;
|
|
16
|
+
meshNodeId?: string;
|
|
17
|
+
meshCoordinatorDaemonId?: string;
|
|
18
|
+
launchedByCoordinator?: boolean;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Build the relay-safety stamp from a dispatch's meshContext, only including fields
|
|
22
|
+
* the session does not already carry. Returns undefined when there is nothing new to
|
|
23
|
+
* stamp, so callers can skip a no-op updateSettings() write.
|
|
24
|
+
*
|
|
25
|
+
* This closes the remote-session relay gap: a worker session reached by a dispatch
|
|
26
|
+
* that carries coordinatorDaemonId (mesh_send_task / queue assignment over P2P) gets
|
|
27
|
+
* the coordinator anchor persisted onto its settings at dispatch time, even if it was
|
|
28
|
+
* not launched via mesh_launch_session. Without the stamp the forwarder cannot resolve
|
|
29
|
+
* a remote coordinator and the completion event sits in the pending queue until a
|
|
30
|
+
* read_chat-triggered reconcile drains it.
|
|
31
|
+
*/
|
|
32
|
+
export declare function buildMeshWorkerRelayStamp(currentSettings: Record<string, unknown> | undefined, meshContext: {
|
|
33
|
+
meshId?: unknown;
|
|
34
|
+
nodeId?: unknown;
|
|
35
|
+
coordinatorDaemonId?: unknown;
|
|
36
|
+
} | undefined): MeshWorkerRelayStamp | undefined;
|
|
6
37
|
export declare function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string;
|
|
7
38
|
export declare function readRefineJobId(event: {
|
|
8
39
|
metadataEvent?: Record<string, unknown>;
|
|
@@ -186,6 +186,7 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
186
186
|
constructor(opts: SpecDriverOpts);
|
|
187
187
|
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
188
188
|
start(): void;
|
|
189
|
+
private scheduleSpawnPrime;
|
|
189
190
|
dispatch(cmd: DashboardCommand): void;
|
|
190
191
|
snapshot(): string;
|
|
191
192
|
getCursorPosition(): {
|
|
@@ -66,6 +66,21 @@ export interface CliSpecV4 {
|
|
|
66
66
|
spawn_args?: string[];
|
|
67
67
|
env?: Record<string, string>;
|
|
68
68
|
cli_version_range?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Optional raw byte sequences written to the PTY once, shortly after spawn,
|
|
71
|
+
* to prime a TUI that gates its input handling on a terminal event the
|
|
72
|
+
* daemon would otherwise never emit. The canonical case is a focus-event
|
|
73
|
+
* TUI (Ink `useStdin`/`useFocus`, e.g. antigravity's `agy`) that enables
|
|
74
|
+
* focus reporting (`CSI ?1004h`) and treats its input box as unfocused —
|
|
75
|
+
* silently dropping the first programmatic write — until it receives a
|
|
76
|
+
* focus-in event (`ESC [ I`). Declaring `["[I"]` here wakes the input
|
|
77
|
+
* stream on spawn so the first delegated message lands without a manual
|
|
78
|
+
* keystroke. CLIs that do not focus-gate input simply omit this field; the
|
|
79
|
+
* engine writes nothing extra for them, so it stays CLI-agnostic.
|
|
80
|
+
*/
|
|
81
|
+
send_on_spawn?: string[];
|
|
82
|
+
/** Delay (ms) after spawn before writing `send_on_spawn`. Default 250. */
|
|
83
|
+
send_on_spawn_delay_ms?: number;
|
|
69
84
|
send_message: {
|
|
70
85
|
submit_key: string;
|
|
71
86
|
delay_ms_before_submit?: number;
|
|
@@ -75,19 +75,34 @@ export interface NativeHistoryMessageMap {
|
|
|
75
75
|
timestamp_ms?: string;
|
|
76
76
|
kind?: string;
|
|
77
77
|
}
|
|
78
|
+
export interface AnchorContext {
|
|
79
|
+
prev?: string;
|
|
80
|
+
next?: string;
|
|
81
|
+
prev_flags?: string;
|
|
82
|
+
next_flags?: string;
|
|
83
|
+
}
|
|
78
84
|
export interface SectionDef {
|
|
79
85
|
from_top?: Size;
|
|
80
86
|
from_bottom?: Size;
|
|
81
87
|
until?: string;
|
|
82
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Anchor regex(es). A single string anchors on the first/last matching line
|
|
90
|
+
* (per `anchor_last`). An array is an OR-set: every candidate line is one
|
|
91
|
+
* that matches ANY entry; with `anchor_last` the LAST such line across all
|
|
92
|
+
* patterns wins, otherwise the FIRST. This lets one section capture two
|
|
93
|
+
* different shapes — e.g. a box-divider modal AND a divider-less modal whose
|
|
94
|
+
* only stable landmark is the question line above its numbered choices.
|
|
95
|
+
*/
|
|
96
|
+
anchor?: string | string[];
|
|
83
97
|
anchor_flags?: string;
|
|
84
98
|
anchor_last?: boolean;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
99
|
+
/**
|
|
100
|
+
* Context guard(s) for the anchor. A single object applies to every anchor
|
|
101
|
+
* pattern. An array is matched positionally against an `anchor` array (entry
|
|
102
|
+
* i guards anchor i); a positional `undefined`/null entry means "no guard"
|
|
103
|
+
* for that anchor. A scalar `anchor` ignores array form beyond index 0.
|
|
104
|
+
*/
|
|
105
|
+
anchor_context?: AnchorContext | (AnchorContext | null)[];
|
|
91
106
|
lines?: number;
|
|
92
107
|
until_regex?: string;
|
|
93
108
|
until_regex_flags?: string;
|
|
@@ -99,6 +99,17 @@ export interface RepoMeshPolicy {
|
|
|
99
99
|
* watch-the-agents behavior; hidden sessions remain discoverable and manually openable.
|
|
100
100
|
*/
|
|
101
101
|
spawnedSessionVisibility?: RepoMeshSpawnedSessionVisibility;
|
|
102
|
+
/**
|
|
103
|
+
* Whether worker sessions the coordinator dispatches should auto-approve agent
|
|
104
|
+
* approval modals (tool/command prompts) without firing a user-facing approval
|
|
105
|
+
* notification. Delegated workers are coordinator-driven, so a human should not
|
|
106
|
+
* have to approve each one; defaults to true. Set to false to make delegated
|
|
107
|
+
* worker sessions stop at approval modals like an interactive session.
|
|
108
|
+
* Stamped into the worker launch settings envelope as `autoApprove`, which wins
|
|
109
|
+
* over the global per-provider-type autoApprove config via the settings merge.
|
|
110
|
+
* A node policy may override this per-node (RepoMeshNodePolicy.delegatedWorkerAutoApprove).
|
|
111
|
+
*/
|
|
112
|
+
delegatedWorkerAutoApprove?: boolean;
|
|
102
113
|
/**
|
|
103
114
|
* What to do with delegated session-host records for a node when it is removed.
|
|
104
115
|
* Defaults to 'preserve' so completed work can be reviewed later and live
|
|
@@ -129,6 +140,11 @@ export interface RepoMeshNodePolicy {
|
|
|
129
140
|
maxConcurrentSessions?: number;
|
|
130
141
|
/** Ordered provider preference used when mesh_launch_session omits an explicit type. */
|
|
131
142
|
providerPriority?: string[];
|
|
143
|
+
/**
|
|
144
|
+
* Per-node override for RepoMeshPolicy.delegatedWorkerAutoApprove. When set, takes
|
|
145
|
+
* precedence over the mesh-level policy for worker sessions launched onto this node.
|
|
146
|
+
*/
|
|
147
|
+
delegatedWorkerAutoApprove?: boolean;
|
|
132
148
|
/**
|
|
133
149
|
* Optional associated/external repos that must be checked alongside this node.
|
|
134
150
|
* These are explicit policy/config entries only; Repo Mesh does not auto-discover
|
|
@@ -152,6 +168,14 @@ export interface RepoMeshNodePolicy {
|
|
|
152
168
|
initSubmodulesOnClone?: boolean;
|
|
153
169
|
}
|
|
154
170
|
export declare const DEFAULT_MESH_POLICY: RepoMeshPolicy;
|
|
171
|
+
/**
|
|
172
|
+
* Resolve whether a delegated worker session launched onto `nodePolicy` (within a mesh
|
|
173
|
+
* governed by `meshPolicy`) should auto-approve. Precedence: node override → mesh policy
|
|
174
|
+
* → default true. The result is stamped into the worker launch settings envelope as
|
|
175
|
+
* `autoApprove`; it wins over the global per-provider-type autoApprove config because the
|
|
176
|
+
* launch path merges the envelope as a settingsOverride on top of the provider defaults.
|
|
177
|
+
*/
|
|
178
|
+
export declare function resolveDelegatedWorkerAutoApprove(meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerAutoApprove'> | null, nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerAutoApprove'> | null): boolean;
|
|
155
179
|
export interface RepoMeshNodeCapabilities {
|
|
156
180
|
platform?: string;
|
|
157
181
|
packageManagers?: string[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.270",
|
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.270",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
package/src/commands/router.ts
CHANGED
|
@@ -45,6 +45,7 @@ import { buildSessionEntries } from '../status/builders.js';
|
|
|
45
45
|
import { registerMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
|
|
46
46
|
import { handleMeshForwardEvent, drainPendingMeshCoordinatorEvents, getPendingMeshCoordinatorEvents, queuePendingMeshCoordinatorEvent, type PendingMeshCoordinatorEvent } from '../mesh/mesh-events.js';
|
|
47
47
|
import { getRecentUnroutableDeliveries } from '../mesh/mesh-routing.js';
|
|
48
|
+
import { buildMeshWorkerRelayStamp } from '../mesh/mesh-events-utils.js';
|
|
48
49
|
import { buildMeshHostRequiredFailure, normalizeMeshDaemonRole, resolveMeshHostStatus } from '../mesh/mesh-host-ownership.js';
|
|
49
50
|
import { fastForwardMeshNode } from '../mesh/mesh-fast-forward.js';
|
|
50
51
|
import { analyzeMeshRefineNodeChangeArea, orderMeshRefineBatchNodes } from '../mesh/mesh-refine-batch.js';
|
|
@@ -5307,6 +5308,34 @@ export class DaemonCommandRouter {
|
|
|
5307
5308
|
return this.deps.cliManager.handleCliCommand(cmd, args);
|
|
5308
5309
|
}
|
|
5309
5310
|
case 'agent_command': {
|
|
5311
|
+
// Relay-safety stamp: a dispatch carrying meshContext.coordinatorDaemonId
|
|
5312
|
+
// (mesh_send_task / queue assignment over P2P) is the worker daemon's chance
|
|
5313
|
+
// to persist the coordinator routing anchor onto the target session BEFORE the
|
|
5314
|
+
// turn runs. Without meshCoordinatorDaemonId on the session, the core forwarder
|
|
5315
|
+
// (injectMeshSystemMessage) cannot resolve a remote coordinator target, so the
|
|
5316
|
+
// completion event sits in the pending queue until a read_chat reconcile drains
|
|
5317
|
+
// it. Stamping here makes a reused/relaunched remote session relay-safe at
|
|
5318
|
+
// dispatch time even when it was not launched via mesh_launch_session.
|
|
5319
|
+
{
|
|
5320
|
+
const dispatchSessionId = readStringValue(args?.targetSessionId, (args as any)?.sessionId, (args as any)?.instanceId);
|
|
5321
|
+
const dispatchMeshContext = args?.meshContext as Record<string, unknown> | undefined;
|
|
5322
|
+
if (dispatchSessionId && dispatchMeshContext) {
|
|
5323
|
+
try {
|
|
5324
|
+
const inst = this.deps.instanceManager.getInstance(dispatchSessionId);
|
|
5325
|
+
if (inst && typeof inst.updateSettings === 'function') {
|
|
5326
|
+
const stamp = buildMeshWorkerRelayStamp(
|
|
5327
|
+
inst.getState?.()?.settings as Record<string, unknown> | undefined,
|
|
5328
|
+
{
|
|
5329
|
+
meshId: dispatchMeshContext.meshId,
|
|
5330
|
+
nodeId: dispatchMeshContext.nodeId,
|
|
5331
|
+
coordinatorDaemonId: dispatchMeshContext.coordinatorDaemonId,
|
|
5332
|
+
},
|
|
5333
|
+
);
|
|
5334
|
+
if (stamp) inst.updateSettings(stamp);
|
|
5335
|
+
}
|
|
5336
|
+
} catch { /* best-effort — dispatch still proceeds without the stamp */ }
|
|
5337
|
+
}
|
|
5338
|
+
}
|
|
5310
5339
|
const agentResult = await this.deps.cliManager.handleCliCommand(cmd, args);
|
|
5311
5340
|
// Bug C fix (part 2): when dispatching a task to a mesh node session, override
|
|
5312
5341
|
// the dispatch acknowledgement risk reason to 'bootstrap_still_running' when
|
package/src/index.ts
CHANGED
|
@@ -132,7 +132,7 @@ export type {
|
|
|
132
132
|
RepoMeshLedgerStatus,
|
|
133
133
|
MeshAsyncJobLifecycle,
|
|
134
134
|
} from './repo-mesh-types.js';
|
|
135
|
-
export { DEFAULT_MESH_POLICY } from './repo-mesh-types.js';
|
|
135
|
+
export { DEFAULT_MESH_POLICY, resolveDelegatedWorkerAutoApprove } from './repo-mesh-types.js';
|
|
136
136
|
|
|
137
137
|
// ── Git Surface ──
|
|
138
138
|
export * from './git/index.js';
|
|
@@ -13,6 +13,7 @@ import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
|
13
13
|
import { queuePendingMeshCoordinatorEvent, drainPendingMeshCoordinatorEvents, markMeshCoordinatorEventDirectDelivered } from './mesh-events-pending.js';
|
|
14
14
|
import type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
15
15
|
import { resolveWorkerDelegateRouting, recordUnroutableDelegateEvent } from './mesh-routing.js';
|
|
16
|
+
import { resolveDelegatedWorkerAutoApprove } from '../repo-mesh-types.js';
|
|
16
17
|
import {
|
|
17
18
|
findRecentTerminalLedgerEvidence,
|
|
18
19
|
hasDispatchAfterTerminal,
|
|
@@ -302,7 +303,24 @@ export function tryAssignQueueTask(
|
|
|
302
303
|
try {
|
|
303
304
|
const inst = components.instanceManager.getInstance(sessionId);
|
|
304
305
|
if (inst && typeof inst.updateSettings === 'function') {
|
|
305
|
-
|
|
306
|
+
// Adopting a (possibly manually-opened) local session as a worker: apply the
|
|
307
|
+
// delegated-worker auto-approve policy here too, so a session that was launched
|
|
308
|
+
// without autoApprove still auto-approves once the coordinator dispatches a task
|
|
309
|
+
// to it (the "approval notification fires only for certain delegated sessions"
|
|
310
|
+
// case). updateSettings preserves runtime mesh keys; passing autoApprove keeps it.
|
|
311
|
+
//
|
|
312
|
+
// This local-dispatch branch also runs on the coordinator daemon for a co-located
|
|
313
|
+
// session, so the coordinator daemon id IS this daemon's id. Stamp it alongside
|
|
314
|
+
// the node identity so the session is fully relay-safe (meshCoordinatorDaemonId is
|
|
315
|
+
// the anchor the forwarder keys on), matching what mesh_launch_session stamps.
|
|
316
|
+
const localDaemonId = readNonEmptyString(loadConfig().machineId);
|
|
317
|
+
inst.updateSettings({
|
|
318
|
+
meshNodeFor: meshId,
|
|
319
|
+
meshNodeId: nodeId,
|
|
320
|
+
launchedByCoordinator: true,
|
|
321
|
+
autoApprove: resolveDelegatedWorkerAutoApprove(mesh?.policy, node?.policy),
|
|
322
|
+
...(localDaemonId ? { meshCoordinatorDaemonId: localDaemonId } : {}),
|
|
323
|
+
});
|
|
306
324
|
}
|
|
307
325
|
} catch { /* best-effort — dispatch still proceeds */ }
|
|
308
326
|
|
|
@@ -666,6 +684,10 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
666
684
|
meshNodeFor: meshId,
|
|
667
685
|
meshNodeId: nodeId,
|
|
668
686
|
spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
|
|
687
|
+
// Coordinator-dispatched worker: auto-approve unless mesh/node policy
|
|
688
|
+
// opts out (default true). Lands in settingsOverride and beats the
|
|
689
|
+
// global per-provider-type autoApprove config (see shouldAutoApprove).
|
|
690
|
+
autoApprove: resolveDelegatedWorkerAutoApprove(mesh?.policy, node?.policy),
|
|
669
691
|
launchedByCoordinator: true,
|
|
670
692
|
autoLaunchedForQueueTaskId: task.id,
|
|
671
693
|
},
|
|
@@ -1343,6 +1365,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
1343
1365
|
meshNodeFor: args.meshId,
|
|
1344
1366
|
meshNodeId: node.id,
|
|
1345
1367
|
spawnedSessionVisibility: mesh?.policy?.spawnedSessionVisibility || 'hidden',
|
|
1368
|
+
// Coordinator-dispatched recovery relaunch: same auto-approve
|
|
1369
|
+
// policy as the primary worker launch path.
|
|
1370
|
+
autoApprove: resolveDelegatedWorkerAutoApprove(mesh?.policy, node?.policy),
|
|
1346
1371
|
launchedByCoordinator: true,
|
|
1347
1372
|
}
|
|
1348
1373
|
}).catch((e: any) => LOG.error('MeshRecovery', `Failed to auto-relaunch session for ${node.id}: ${e?.message}`));
|
|
@@ -30,6 +30,65 @@ export function sameDaemonId(a: unknown, b: unknown): boolean {
|
|
|
30
30
|
return ca !== '' && ca === cb;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The relay-safety metadata a worker session must carry so that its completion /
|
|
35
|
+
* generating events route back to the coordinator proactively (without waiting for
|
|
36
|
+
* a mesh_read_chat reconcile). meshCoordinatorDaemonId is the routing anchor the
|
|
37
|
+
* core forwarder (injectMeshSystemMessage) keys on to pick a remote coordinator
|
|
38
|
+
* target; meshNodeFor/meshNodeId identify the worker, and launchedByCoordinator is
|
|
39
|
+
* the delegation proof. See resolveWorkerDelegateRouting().
|
|
40
|
+
*/
|
|
41
|
+
export interface MeshWorkerRelayStamp {
|
|
42
|
+
meshNodeFor?: string;
|
|
43
|
+
meshNodeId?: string;
|
|
44
|
+
meshCoordinatorDaemonId?: string;
|
|
45
|
+
launchedByCoordinator?: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Build the relay-safety stamp from a dispatch's meshContext, only including fields
|
|
50
|
+
* the session does not already carry. Returns undefined when there is nothing new to
|
|
51
|
+
* stamp, so callers can skip a no-op updateSettings() write.
|
|
52
|
+
*
|
|
53
|
+
* This closes the remote-session relay gap: a worker session reached by a dispatch
|
|
54
|
+
* that carries coordinatorDaemonId (mesh_send_task / queue assignment over P2P) gets
|
|
55
|
+
* the coordinator anchor persisted onto its settings at dispatch time, even if it was
|
|
56
|
+
* not launched via mesh_launch_session. Without the stamp the forwarder cannot resolve
|
|
57
|
+
* a remote coordinator and the completion event sits in the pending queue until a
|
|
58
|
+
* read_chat-triggered reconcile drains it.
|
|
59
|
+
*/
|
|
60
|
+
export function buildMeshWorkerRelayStamp(
|
|
61
|
+
currentSettings: Record<string, unknown> | undefined,
|
|
62
|
+
meshContext: {
|
|
63
|
+
meshId?: unknown;
|
|
64
|
+
nodeId?: unknown;
|
|
65
|
+
coordinatorDaemonId?: unknown;
|
|
66
|
+
} | undefined,
|
|
67
|
+
): MeshWorkerRelayStamp | undefined {
|
|
68
|
+
if (!meshContext) return undefined;
|
|
69
|
+
const settings = currentSettings && typeof currentSettings === 'object' ? currentSettings : {};
|
|
70
|
+
const stamp: MeshWorkerRelayStamp = {};
|
|
71
|
+
|
|
72
|
+
const meshId = readNonEmptyString(meshContext.meshId);
|
|
73
|
+
if (meshId && !readNonEmptyString(settings.meshNodeFor)) stamp.meshNodeFor = meshId;
|
|
74
|
+
|
|
75
|
+
const nodeId = readNonEmptyString(meshContext.nodeId);
|
|
76
|
+
if (nodeId && !readNonEmptyString(settings.meshNodeId)) stamp.meshNodeId = nodeId;
|
|
77
|
+
|
|
78
|
+
const coordinatorDaemonId = readNonEmptyString(meshContext.coordinatorDaemonId);
|
|
79
|
+
if (coordinatorDaemonId && !readNonEmptyString(settings.meshCoordinatorDaemonId)) {
|
|
80
|
+
stamp.meshCoordinatorDaemonId = coordinatorDaemonId;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// A dispatch from a coordinator is itself proof of delegation; stamp it when the
|
|
84
|
+
// session is being given any mesh routing context but has no marker yet.
|
|
85
|
+
if ((meshId || nodeId || coordinatorDaemonId) && settings.launchedByCoordinator !== true) {
|
|
86
|
+
stamp.launchedByCoordinator = true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return Object.keys(stamp).length > 0 ? stamp : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
33
92
|
export function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
34
93
|
return readNonEmptyString(event.targetSessionId)
|
|
35
94
|
|| readNonEmptyString(event.sessionId)
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
10
|
import type {
|
|
11
|
-
SectionDef, Condition, RegexCondition,
|
|
11
|
+
SectionDef, AnchorContext, Condition, RegexCondition,
|
|
12
12
|
ChangedCondition, AllCondition, AnyCondition,
|
|
13
13
|
ExtractTitle, ExtractButtons,
|
|
14
14
|
} from './types.js';
|
|
@@ -52,20 +52,44 @@ export function resolveSections(
|
|
|
52
52
|
|
|
53
53
|
if (sec.anchor !== undefined) {
|
|
54
54
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
55
|
+
// Normalize anchor + context into parallel candidate lists. A
|
|
56
|
+
// scalar anchor becomes a single-entry array; a single context
|
|
57
|
+
// object applies to every entry; an array context is positional.
|
|
58
|
+
// Candidates are tried IN ORDER as independent passes: the first
|
|
59
|
+
// pattern that finds any line wins. This keeps a scalar anchor's
|
|
60
|
+
// behavior identical, and lets an array express a preferred shape
|
|
61
|
+
// (e.g. a box divider) with later entries as fallbacks (e.g. a
|
|
62
|
+
// divider-less modal anchored on its question line) — the
|
|
63
|
+
// fallback only takes over when the preferred pattern is absent.
|
|
64
|
+
const anchorPatterns = Array.isArray(sec.anchor) ? sec.anchor : [sec.anchor];
|
|
65
|
+
const sharedCtx: AnchorContext | null = Array.isArray(sec.anchor_context)
|
|
66
|
+
? null
|
|
67
|
+
: (sec.anchor_context ?? null);
|
|
68
|
+
const ctxList: (AnchorContext | null)[] = Array.isArray(sec.anchor_context)
|
|
69
|
+
? sec.anchor_context
|
|
70
|
+
: anchorPatterns.map(() => sharedCtx);
|
|
71
|
+
const candidates = anchorPatterns.map((pattern, k) => {
|
|
72
|
+
const ctx = ctxList[k] ?? null;
|
|
73
|
+
return {
|
|
74
|
+
re: new RegExp(pattern, sec.anchor_flags ?? ''),
|
|
75
|
+
prevRe: ctx?.prev !== undefined
|
|
76
|
+
? new RegExp(ctx.prev, ctx.prev_flags ?? '') : null,
|
|
77
|
+
nextRe: ctx?.next !== undefined
|
|
78
|
+
? new RegExp(ctx.next, ctx.next_flags ?? '') : null,
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
const matchesCandidate = (c: typeof candidates[number], i: number) =>
|
|
82
|
+
c.re.test(lines[i])
|
|
83
|
+
&& (c.prevRe === null || (i > 0 && c.prevRe.test(lines[i - 1])))
|
|
84
|
+
&& (c.nextRe === null || (i < total - 1 && c.nextRe.test(lines[i + 1])));
|
|
64
85
|
let idx = -1;
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
86
|
+
for (const c of candidates) {
|
|
87
|
+
if (sec.anchor_last) {
|
|
88
|
+
for (let i = total - 1; i >= 0; i--) { if (matchesCandidate(c, i)) { idx = i; break; } }
|
|
89
|
+
} else {
|
|
90
|
+
for (let i = 0; i < total; i++) { if (matchesCandidate(c, i)) { idx = i; break; } }
|
|
91
|
+
}
|
|
92
|
+
if (idx !== -1) break;
|
|
69
93
|
}
|
|
70
94
|
if (idx !== -1) {
|
|
71
95
|
from = idx;
|
|
@@ -246,11 +246,27 @@ export class FsmDriver implements ISpecDriver {
|
|
|
246
246
|
this.stateEnteredAt = now;
|
|
247
247
|
this.prevStateAt = now;
|
|
248
248
|
this.adapter.start();
|
|
249
|
+
// Prime focus-gated TUIs (see CliSpecV4.send_on_spawn). Written once,
|
|
250
|
+
// shortly after spawn, so the input stream is awake before the first
|
|
251
|
+
// delegated message — without this, a focus-event CLI like antigravity
|
|
252
|
+
// drops the first programmatic write until a manual keystroke.
|
|
253
|
+
this.scheduleSpawnPrime();
|
|
249
254
|
// The initial state may have a purely time-based exit (elapsed_ms);
|
|
250
255
|
// schedule a wake so we leave it even if the PTY goes quiet.
|
|
251
256
|
this.scheduleWakeForState();
|
|
252
257
|
}
|
|
253
258
|
|
|
259
|
+
private scheduleSpawnPrime(): void {
|
|
260
|
+
const seqs = this.spec.send_on_spawn;
|
|
261
|
+
if (!Array.isArray(seqs) || seqs.length === 0) return;
|
|
262
|
+
const delay = Math.max(0, this.spec.send_on_spawn_delay_ms ?? 250);
|
|
263
|
+
setTimeout(() => {
|
|
264
|
+
for (const seq of seqs) {
|
|
265
|
+
if (typeof seq === 'string' && seq.length > 0) this.adapter.send_keys(seq);
|
|
266
|
+
}
|
|
267
|
+
}, delay);
|
|
268
|
+
}
|
|
269
|
+
|
|
254
270
|
dispatch(cmd: DashboardCommand): void {
|
|
255
271
|
switch (cmd.kind) {
|
|
256
272
|
case 'send_message': this.handleSendMessage(cmd.text); return;
|
|
@@ -440,6 +456,18 @@ export class FsmDriver implements ISpecDriver {
|
|
|
440
456
|
// that's already satisfied doesn't wait for the next PTY frame.
|
|
441
457
|
this.emitStateChanged(forceEmit);
|
|
442
458
|
this.scheduleWakeForState();
|
|
459
|
+
// Drain queued sends on the SAME frame the machine reaches "ready".
|
|
460
|
+
// The first delegated message is queued in pendingSends until the
|
|
461
|
+
// FSM first enters a non-initial idle state (the prompt is drawn).
|
|
462
|
+
// That readiness is normally reached BY a transition (e.g.
|
|
463
|
+
// signing_in→idle / starting→idle) — and an idle state has no
|
|
464
|
+
// pending time-condition, so scheduleWakeForState() arms no timer
|
|
465
|
+
// and, agy being quiet at the prompt, no further PTY frame arrives.
|
|
466
|
+
// Without this call the queued first message would strand here
|
|
467
|
+
// forever (the "first input never processed" bug). maybeMarkReady is
|
|
468
|
+
// idempotent (guarded by readySeenOnce) so calling it on both
|
|
469
|
+
// branches is safe.
|
|
470
|
+
this.maybeMarkReady();
|
|
443
471
|
return;
|
|
444
472
|
}
|
|
445
473
|
|
|
@@ -130,6 +130,21 @@ export interface CliSpecV4 {
|
|
|
130
130
|
spawn_args?: string[];
|
|
131
131
|
env?: Record<string, string>;
|
|
132
132
|
cli_version_range?: string;
|
|
133
|
+
/**
|
|
134
|
+
* Optional raw byte sequences written to the PTY once, shortly after spawn,
|
|
135
|
+
* to prime a TUI that gates its input handling on a terminal event the
|
|
136
|
+
* daemon would otherwise never emit. The canonical case is a focus-event
|
|
137
|
+
* TUI (Ink `useStdin`/`useFocus`, e.g. antigravity's `agy`) that enables
|
|
138
|
+
* focus reporting (`CSI ?1004h`) and treats its input box as unfocused —
|
|
139
|
+
* silently dropping the first programmatic write — until it receives a
|
|
140
|
+
* focus-in event (`ESC [ I`). Declaring `["[I"]` here wakes the input
|
|
141
|
+
* stream on spawn so the first delegated message lands without a manual
|
|
142
|
+
* keystroke. CLIs that do not focus-gate input simply omit this field; the
|
|
143
|
+
* engine writes nothing extra for them, so it stays CLI-agnostic.
|
|
144
|
+
*/
|
|
145
|
+
send_on_spawn?: string[];
|
|
146
|
+
/** Delay (ms) after spawn before writing `send_on_spawn`. Default 250. */
|
|
147
|
+
send_on_spawn_delay_ms?: number;
|
|
133
148
|
send_message: {
|
|
134
149
|
submit_key: string;
|
|
135
150
|
delay_ms_before_submit?: number;
|
|
@@ -104,19 +104,35 @@ export interface NativeHistoryMessageMap {
|
|
|
104
104
|
// Section definition
|
|
105
105
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
106
106
|
|
|
107
|
+
export interface AnchorContext {
|
|
108
|
+
prev?: string;
|
|
109
|
+
next?: string;
|
|
110
|
+
prev_flags?: string;
|
|
111
|
+
next_flags?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
107
114
|
export interface SectionDef {
|
|
108
115
|
from_top?: Size;
|
|
109
116
|
from_bottom?: Size;
|
|
110
117
|
until?: string; // section id OR regex (starts with ^)
|
|
111
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Anchor regex(es). A single string anchors on the first/last matching line
|
|
120
|
+
* (per `anchor_last`). An array is an OR-set: every candidate line is one
|
|
121
|
+
* that matches ANY entry; with `anchor_last` the LAST such line across all
|
|
122
|
+
* patterns wins, otherwise the FIRST. This lets one section capture two
|
|
123
|
+
* different shapes — e.g. a box-divider modal AND a divider-less modal whose
|
|
124
|
+
* only stable landmark is the question line above its numbered choices.
|
|
125
|
+
*/
|
|
126
|
+
anchor?: string | string[];
|
|
112
127
|
anchor_flags?: string;
|
|
113
128
|
anchor_last?: boolean;
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
129
|
+
/**
|
|
130
|
+
* Context guard(s) for the anchor. A single object applies to every anchor
|
|
131
|
+
* pattern. An array is matched positionally against an `anchor` array (entry
|
|
132
|
+
* i guards anchor i); a positional `undefined`/null entry means "no guard"
|
|
133
|
+
* for that anchor. A scalar `anchor` ignores array form beyond index 0.
|
|
134
|
+
*/
|
|
135
|
+
anchor_context?: AnchorContext | (AnchorContext | null)[];
|
|
120
136
|
lines?: number;
|
|
121
137
|
until_regex?: string;
|
|
122
138
|
until_regex_flags?: string;
|
package/src/repo-mesh-types.ts
CHANGED
|
@@ -120,6 +120,17 @@ export interface RepoMeshPolicy {
|
|
|
120
120
|
* watch-the-agents behavior; hidden sessions remain discoverable and manually openable.
|
|
121
121
|
*/
|
|
122
122
|
spawnedSessionVisibility?: RepoMeshSpawnedSessionVisibility;
|
|
123
|
+
/**
|
|
124
|
+
* Whether worker sessions the coordinator dispatches should auto-approve agent
|
|
125
|
+
* approval modals (tool/command prompts) without firing a user-facing approval
|
|
126
|
+
* notification. Delegated workers are coordinator-driven, so a human should not
|
|
127
|
+
* have to approve each one; defaults to true. Set to false to make delegated
|
|
128
|
+
* worker sessions stop at approval modals like an interactive session.
|
|
129
|
+
* Stamped into the worker launch settings envelope as `autoApprove`, which wins
|
|
130
|
+
* over the global per-provider-type autoApprove config via the settings merge.
|
|
131
|
+
* A node policy may override this per-node (RepoMeshNodePolicy.delegatedWorkerAutoApprove).
|
|
132
|
+
*/
|
|
133
|
+
delegatedWorkerAutoApprove?: boolean;
|
|
123
134
|
/**
|
|
124
135
|
* What to do with delegated session-host records for a node when it is removed.
|
|
125
136
|
* Defaults to 'preserve' so completed work can be reviewed later and live
|
|
@@ -152,6 +163,11 @@ export interface RepoMeshNodePolicy {
|
|
|
152
163
|
maxConcurrentSessions?: number;
|
|
153
164
|
/** Ordered provider preference used when mesh_launch_session omits an explicit type. */
|
|
154
165
|
providerPriority?: string[];
|
|
166
|
+
/**
|
|
167
|
+
* Per-node override for RepoMeshPolicy.delegatedWorkerAutoApprove. When set, takes
|
|
168
|
+
* precedence over the mesh-level policy for worker sessions launched onto this node.
|
|
169
|
+
*/
|
|
170
|
+
delegatedWorkerAutoApprove?: boolean;
|
|
155
171
|
/**
|
|
156
172
|
* Optional associated/external repos that must be checked alongside this node.
|
|
157
173
|
* These are explicit policy/config entries only; Repo Mesh does not auto-discover
|
|
@@ -184,11 +200,32 @@ export const DEFAULT_MESH_POLICY: RepoMeshPolicy = {
|
|
|
184
200
|
dirtyWorkspaceBehavior: 'warn',
|
|
185
201
|
maxParallelTasks: 2,
|
|
186
202
|
spawnedSessionVisibility: 'visible',
|
|
203
|
+
delegatedWorkerAutoApprove: true,
|
|
187
204
|
sessionCleanupOnNodeRemove: 'preserve',
|
|
188
205
|
autoFastForward: { enabled: true },
|
|
189
206
|
maxTaskRetries: 1,
|
|
190
207
|
};
|
|
191
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Resolve whether a delegated worker session launched onto `nodePolicy` (within a mesh
|
|
211
|
+
* governed by `meshPolicy`) should auto-approve. Precedence: node override → mesh policy
|
|
212
|
+
* → default true. The result is stamped into the worker launch settings envelope as
|
|
213
|
+
* `autoApprove`; it wins over the global per-provider-type autoApprove config because the
|
|
214
|
+
* launch path merges the envelope as a settingsOverride on top of the provider defaults.
|
|
215
|
+
*/
|
|
216
|
+
export function resolveDelegatedWorkerAutoApprove(
|
|
217
|
+
meshPolicy?: Pick<RepoMeshPolicy, 'delegatedWorkerAutoApprove'> | null,
|
|
218
|
+
nodePolicy?: Pick<RepoMeshNodePolicy, 'delegatedWorkerAutoApprove'> | null,
|
|
219
|
+
): boolean {
|
|
220
|
+
if (typeof nodePolicy?.delegatedWorkerAutoApprove === 'boolean') {
|
|
221
|
+
return nodePolicy.delegatedWorkerAutoApprove;
|
|
222
|
+
}
|
|
223
|
+
if (typeof meshPolicy?.delegatedWorkerAutoApprove === 'boolean') {
|
|
224
|
+
return meshPolicy.delegatedWorkerAutoApprove;
|
|
225
|
+
}
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
192
229
|
// ─── Capabilities ───────────────────────────────
|
|
193
230
|
|
|
194
231
|
export interface RepoMeshNodeCapabilities {
|