@adhdev/daemon-core 1.0.18-rc.2 → 1.0.18-rc.20
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/cli-state-engine.d.ts +77 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +33 -0
- package/dist/cli-adapters/pty-transport.d.ts +2 -1
- package/dist/commands/cli-manager.d.ts +9 -0
- package/dist/commands/process-lifecycle.d.ts +43 -0
- package/dist/commands/upgrade-helper.d.ts +1 -0
- package/dist/commands/windows-atomic-upgrade.d.ts +17 -1
- package/dist/config/mesh-json-config.d.ts +62 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.js +6674 -4237
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6361 -3927
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +76 -0
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-disk-retention.d.ts +105 -0
- package/dist/mesh/mesh-ledger.d.ts +28 -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-remote-event-pull.d.ts +3 -0
- package/dist/mesh/mesh-runtime-store.d.ts +11 -0
- package/dist/mesh/mesh-work-queue.d.ts +24 -1
- package/dist/providers/auto-approve-modes.d.ts +14 -0
- package/dist/providers/chat-message-normalization.d.ts +22 -0
- package/dist/providers/cli-provider-instance-types.d.ts +4 -0
- package/dist/providers/cli-provider-instance.d.ts +78 -0
- package/dist/providers/contracts.d.ts +17 -0
- package/dist/providers/provider-schema.d.ts +1 -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/repo-mesh-types.d.ts +38 -6
- package/dist/shared-types.d.ts +4 -2
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +10 -0
- package/src/cli-adapters/cli-state-engine.ts +220 -3
- package/src/cli-adapters/provider-cli-adapter.ts +41 -11
- package/src/cli-adapters/provider-cli-shared.ts +48 -0
- package/src/cli-adapters/pty-transport.d.ts +2 -1
- package/src/cli-adapters/pty-transport.ts +1 -1
- package/src/cli-adapters/session-host-transport.ts +8 -3
- package/src/commands/cli-manager.ts +72 -8
- package/src/commands/high-family/mesh-coordinator-launch.ts +17 -2
- package/src/commands/high-family/mesh-events.ts +13 -2
- package/src/commands/med-family/mesh-crud.ts +155 -0
- package/src/commands/med-family/mesh-queue.ts +74 -1
- package/src/commands/process-lifecycle.ts +248 -0
- package/src/commands/router-refine.ts +71 -4
- package/src/commands/router.ts +8 -0
- package/src/commands/upgrade-helper.ts +106 -82
- package/src/commands/windows-atomic-upgrade.ts +281 -35
- package/src/config/mesh-json-config.ts +111 -0
- package/src/index.ts +21 -1
- package/src/mesh/coordinator-prompt.ts +258 -11
- package/src/mesh/mesh-active-work.ts +11 -1
- package/src/mesh/mesh-completion-synthesis.ts +12 -1
- package/src/mesh/mesh-disk-retention.ts +370 -0
- package/src/mesh/mesh-event-classify.ts +19 -0
- package/src/mesh/mesh-event-forwarding.ts +64 -6
- package/src/mesh/mesh-events-utils.ts +42 -0
- package/src/mesh/mesh-ledger.ts +77 -0
- package/src/mesh/mesh-node-identity.ts +49 -0
- package/src/mesh/mesh-queue-assignment.ts +210 -11
- package/src/mesh/mesh-reconcile-loop.ts +306 -3
- package/src/mesh/mesh-refine-gates.ts +300 -0
- package/src/mesh/mesh-remote-event-pull.ts +68 -47
- package/src/mesh/mesh-runtime-store.ts +21 -0
- package/src/mesh/mesh-work-queue.ts +85 -2
- package/src/providers/auto-approve-modes.ts +103 -0
- package/src/providers/chat-message-normalization.ts +53 -0
- package/src/providers/cli-provider-instance-types.ts +53 -0
- package/src/providers/cli-provider-instance.ts +497 -15
- package/src/providers/contracts.ts +25 -1
- package/src/providers/provider-schema.ts +83 -0
- 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/sdk/v1/schemas/cli/provider.schema.json +44 -0
- package/src/providers/types/interactive-prompt.ts +77 -0
- package/src/repo-mesh-types.ts +112 -12
- package/src/session-host/managed-host.ts +34 -0
- package/src/shared-types.ts +9 -1
- package/src/status/reporter.ts +1 -0
- package/src/status/snapshot.ts +2 -0
|
@@ -254,6 +254,54 @@ export interface CliProviderModule {
|
|
|
254
254
|
_versionWarning?: string | null;
|
|
255
255
|
}
|
|
256
256
|
|
|
257
|
+
/**
|
|
258
|
+
* PURE-PTY TRANSCRIPT CLASS predicate (kimi and kin).
|
|
259
|
+
*
|
|
260
|
+
* A provider is "pure-PTY full-buffer" when it reconstructs its ENTIRE transcript
|
|
261
|
+
* from the rendered PTY buffer on every read and has NO alternate transcript
|
|
262
|
+
* source:
|
|
263
|
+
* - transcriptAuthority !== 'provider' (the daemon PTY parser owns the transcript,
|
|
264
|
+
* not a provider-side canonical source)
|
|
265
|
+
* - NO nativeHistory (no on-disk / native-source history to fall back to)
|
|
266
|
+
* - tui.transcriptPty.scope === 'buffer' (the parser walks the full rendered buffer)
|
|
267
|
+
*
|
|
268
|
+
* This class is invisible to two provider-authority-keyed code paths that other
|
|
269
|
+
* providers rely on for mesh completion semantics:
|
|
270
|
+
* 1. CliStateEngine.onTurnStarted only promotes to 'generating' for
|
|
271
|
+
* transcriptAuthority==='provider' providers — so a pure-PTY session that
|
|
272
|
+
* submits a prompt while already idle collapses idle→idle, the
|
|
273
|
+
* generating→idle edge never occurs, and agent:generating_completed is never
|
|
274
|
+
* emitted (the coordinator ledger leaves the task 'assigned' forever).
|
|
275
|
+
* 2. checkMeshWorkerStall's native-transcript completion reconcile is gated on
|
|
276
|
+
* the native-source shape, so a finished pure-PTY worker's static idle is
|
|
277
|
+
* misread as monitor:no_progress (a false task_stalled).
|
|
278
|
+
*
|
|
279
|
+
* The runtime capability, NOT any single spec field value, is authoritative: a
|
|
280
|
+
* given kimi manifest checkout may declare nativeHistory/transcriptAuthority, but
|
|
281
|
+
* the live-loaded pure-PTY session has none. Callers that only hold a
|
|
282
|
+
* CliProviderModule (the engine) share this exact predicate with the adapter
|
|
283
|
+
* (parsesFullPtyTranscriptFromBuffer) so the two never drift.
|
|
284
|
+
*/
|
|
285
|
+
export function isPurePtyTranscriptProvider(provider: {
|
|
286
|
+
transcriptAuthority?: 'provider' | 'daemon';
|
|
287
|
+
nativeHistory?: unknown;
|
|
288
|
+
tui?: Record<string, unknown>;
|
|
289
|
+
} | null | undefined): boolean {
|
|
290
|
+
// No provider (e.g. an instance whose module isn't set when the stall watchdog
|
|
291
|
+
// ticks) ⇒ not the pure-PTY class. Guarding here protects every caller — the
|
|
292
|
+
// stall-reconcile and turn-start paths pass this.provider unguarded, and
|
|
293
|
+
// this.provider is genuinely nullable at runtime (see this.provider?.nativeHistory
|
|
294
|
+
// in cli-provider-instance.ts). Matches resolveLocalSessionPurePty's own
|
|
295
|
+
// no-provider ⇒ not-pure-PTY contract.
|
|
296
|
+
if (!provider) return false;
|
|
297
|
+
if (provider.transcriptAuthority === 'provider') return false;
|
|
298
|
+
// nativeHistory is a top-level provider field not surfaced on
|
|
299
|
+
// CliProviderModule; read it via the same structural shape the adapter uses.
|
|
300
|
+
if (provider.nativeHistory) return false;
|
|
301
|
+
const transcriptPty = (provider.tui as { transcriptPty?: { scope?: unknown } } | undefined)?.transcriptPty;
|
|
302
|
+
return transcriptPty?.scope === 'buffer';
|
|
303
|
+
}
|
|
304
|
+
|
|
257
305
|
function stripAnsi(str: string): string {
|
|
258
306
|
// eslint-disable-next-line no-control-regex
|
|
259
307
|
return str
|
|
@@ -37,7 +37,8 @@ export interface PtyRuntimeTransport {
|
|
|
37
37
|
getMetadata?(): PtyRuntimeMetadata | null;
|
|
38
38
|
onData(callback: (data: string) => void): void;
|
|
39
39
|
onExit(callback: (info: {
|
|
40
|
-
exitCode: number;
|
|
40
|
+
exitCode: number | null;
|
|
41
|
+
signal?: number | null;
|
|
41
42
|
}) => void): void;
|
|
42
43
|
}
|
|
43
44
|
export interface PtyTransportFactory {
|
|
@@ -62,7 +62,7 @@ export interface PtyRuntimeTransport {
|
|
|
62
62
|
updateMeta?(meta: Record<string, unknown>, replace?: boolean): void;
|
|
63
63
|
getMetadata?(): PtyRuntimeMetadata | null;
|
|
64
64
|
onData(callback: (data: string) => void): void;
|
|
65
|
-
onExit(callback: (info: { exitCode: number }) => void): void;
|
|
65
|
+
onExit(callback: (info: { exitCode: number | null; signal?: number | null }) => void): void;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export interface PtyTransportFactory {
|
|
@@ -42,7 +42,7 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
|
42
42
|
|
|
43
43
|
private readonly client: SessionHostClient;
|
|
44
44
|
private readonly dataCallbacks = new Set<(data: string) => void>();
|
|
45
|
-
private readonly exitCallbacks = new Set<(info: { exitCode: number }) => void>();
|
|
45
|
+
private readonly exitCallbacks = new Set<(info: { exitCode: number | null; signal?: number | null }) => void>();
|
|
46
46
|
private readonly pendingOutput: string[] = [];
|
|
47
47
|
private operationChain = Promise.resolve();
|
|
48
48
|
private unsubscribe: (() => void) | null = null;
|
|
@@ -79,7 +79,7 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
onExit(callback: (info: { exitCode: number }) => void): void {
|
|
82
|
+
onExit(callback: (info: { exitCode: number | null; signal?: number | null }) => void): void {
|
|
83
83
|
this.exitCallbacks.add(callback);
|
|
84
84
|
}
|
|
85
85
|
|
|
@@ -361,8 +361,13 @@ class SessionHostRuntimeTransport implements PtyRuntimeTransport {
|
|
|
361
361
|
return;
|
|
362
362
|
}
|
|
363
363
|
if (event.type === 'session_exit') {
|
|
364
|
+
// Preserve the nullable/unknown exitCode and signal exactly as the
|
|
365
|
+
// session host reported them — never collapse null to 0, which would
|
|
366
|
+
// make a signal-terminated process indistinguishable from a clean exit.
|
|
367
|
+
const exitCode = typeof event.exitCode === 'number' ? event.exitCode : null;
|
|
368
|
+
const signal = typeof event.signal === 'number' ? event.signal : null;
|
|
364
369
|
for (const callback of this.exitCallbacks) {
|
|
365
|
-
callback({ exitCode
|
|
370
|
+
callback({ exitCode, signal });
|
|
366
371
|
}
|
|
367
372
|
void this.closeClient(false);
|
|
368
373
|
}
|
|
@@ -34,6 +34,7 @@ import type { SessionRegistry } from '../sessions/registry.js';
|
|
|
34
34
|
import type { ProviderInstance } from '../providers/provider-instance.js';
|
|
35
35
|
import { LOG } from '../logging/logger.js';
|
|
36
36
|
import { shouldRestoreHostedRuntime } from './hosted-runtime-restore.js';
|
|
37
|
+
import { findProviderAutoApproveMode, resolveProviderAutoApproveMode } from '../providers/auto-approve-modes.js';
|
|
37
38
|
|
|
38
39
|
// ─── external dependency interface ──────────────────────────
|
|
39
40
|
|
|
@@ -452,6 +453,40 @@ export function expandThinkingLaunchArgs(
|
|
|
452
453
|
return template.map((part) => part.includes('{{level}}') ? part.replace('{{level}}', mapped) : part);
|
|
453
454
|
}
|
|
454
455
|
|
|
456
|
+
function matchesRemovedLaunchArg(arg: string, removeArg: string): boolean {
|
|
457
|
+
return arg === removeArg || (removeArg.startsWith('--') && arg.startsWith(`${removeArg}=`));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Apply a selected launch-args auto-approve mode without mutating provider metadata.
|
|
462
|
+
* removeArgs only targets provider-owned base spawn.args; launchArgs are prepended to
|
|
463
|
+
* per-launch args beside model/thinking args, making conflict removal order-independent.
|
|
464
|
+
*/
|
|
465
|
+
export function applyAutoApproveModeLaunchArgs(
|
|
466
|
+
provider: ProviderModule | undefined,
|
|
467
|
+
cliArgs: string[] | undefined,
|
|
468
|
+
settings: Record<string, unknown> | undefined,
|
|
469
|
+
): { provider: ProviderModule | undefined; cliArgs: string[] | undefined } {
|
|
470
|
+
if (!provider) return { provider, cliArgs };
|
|
471
|
+
const resolved = resolveProviderAutoApproveMode(provider, settings);
|
|
472
|
+
if (!resolved.active || resolved.strategy !== 'launch-args') return { provider, cliArgs };
|
|
473
|
+
const mode = findProviderAutoApproveMode(provider, resolved.modeId);
|
|
474
|
+
if (!mode || !Array.isArray(mode.launchArgs) || mode.launchArgs.length === 0) return { provider, cliArgs };
|
|
475
|
+
|
|
476
|
+
const removeArgs = Array.isArray(mode.removeArgs) ? mode.removeArgs : [];
|
|
477
|
+
const baseArgs = provider.spawn?.args;
|
|
478
|
+
const filteredBaseArgs = Array.isArray(baseArgs) && removeArgs.length > 0
|
|
479
|
+
? baseArgs.filter((arg) => !removeArgs.some((removeArg) => matchesRemovedLaunchArg(arg, removeArg)))
|
|
480
|
+
: baseArgs;
|
|
481
|
+
const launchProvider = filteredBaseArgs === baseArgs
|
|
482
|
+
? provider
|
|
483
|
+
: { ...provider, spawn: { ...provider.spawn!, args: filteredBaseArgs } };
|
|
484
|
+
return {
|
|
485
|
+
provider: launchProvider,
|
|
486
|
+
cliArgs: [...mode.launchArgs, ...(cliArgs || [])],
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
455
490
|
function readSubcommandSessionId(args: string[], subcommands: string[]): string | undefined {
|
|
456
491
|
const resumeIndex = args.findIndex((arg) => subcommands.includes(arg));
|
|
457
492
|
if (resumeIndex < 0) return undefined;
|
|
@@ -707,6 +742,24 @@ export class DaemonCliManager {
|
|
|
707
742
|
clearInterval(checkStopped);
|
|
708
743
|
setTimeout(() => {
|
|
709
744
|
if (this.adapters.has(key)) {
|
|
745
|
+
// KIMI-MESH-COMPLETION-EMIT (axis 2): before removeInstance closes the
|
|
746
|
+
// event-emit window, give a mesh DELEGATED worker one last chance to emit
|
|
747
|
+
// its completion. A native-source worker (e.g. kimi) can have its PTY
|
|
748
|
+
// killed by a false stall AFTER it finished the task (transcript written)
|
|
749
|
+
// but BEFORE the FSM's idle→completed event fired — the instance is the
|
|
750
|
+
// only thing that can emit that event, and it is about to be removed. The
|
|
751
|
+
// instance-side method is a no-op for a non-mesh session or when the
|
|
752
|
+
// turn's completion already fired (double-emit guard) or when there is no
|
|
753
|
+
// transcript evidence of a finished turn. Best-effort — never blocks cleanup.
|
|
754
|
+
try {
|
|
755
|
+
const inst = instanceManager?.getInstance(key) as (ProviderInstance & { flushMeshCompletionBeforeCleanup?: () => boolean }) | undefined;
|
|
756
|
+
if (typeof inst?.flushMeshCompletionBeforeCleanup === 'function') {
|
|
757
|
+
const emitted = inst.flushMeshCompletionBeforeCleanup();
|
|
758
|
+
if (emitted) LOG.info('CLI', `Emitted pre-cleanup mesh completion for ${cliType} session ${key} before auto-clean`);
|
|
759
|
+
}
|
|
760
|
+
} catch (e) {
|
|
761
|
+
LOG.warn('CLI', `pre-cleanup mesh completion flush failed for ${key}: ${(e as Error)?.message || e}`);
|
|
762
|
+
}
|
|
710
763
|
this.adapters.delete(key);
|
|
711
764
|
this.deps.removeAgentTracking(key);
|
|
712
765
|
sessionRegistry?.unregisterByInstanceKey(key);
|
|
@@ -1030,6 +1083,17 @@ export class DaemonCliManager {
|
|
|
1030
1083
|
console.log(colorize('cyan', ` 📦 Using provider: ${provider.name} (${provider.type})`));
|
|
1031
1084
|
}
|
|
1032
1085
|
|
|
1086
|
+
const launchSettings = {
|
|
1087
|
+
...this.providerLoader.getSettings(normalizedType),
|
|
1088
|
+
...(options?.settingsOverride || {}),
|
|
1089
|
+
};
|
|
1090
|
+
const versionResolvedProvider = provider
|
|
1091
|
+
? (this.providerLoader.resolve(cliType, { version: cliInfo.version }) || provider)
|
|
1092
|
+
: undefined;
|
|
1093
|
+
const autoApproveLaunch = applyAutoApproveModeLaunchArgs(versionResolvedProvider, cliArgs, launchSettings);
|
|
1094
|
+
const launchProvider = autoApproveLaunch.provider || provider;
|
|
1095
|
+
const cliArgsWithAutoApprove = autoApproveLaunch.cliArgs;
|
|
1096
|
+
|
|
1033
1097
|
// ─── Model axis (MAGI kind-panel): expand initialModel → launch args ───
|
|
1034
1098
|
// For a plain CLI provider the model is selected at spawn time via the manifest's
|
|
1035
1099
|
// modelLaunchArgs template ('{{model}}' → the requested model). ACP providers took
|
|
@@ -1037,10 +1101,10 @@ export class DaemonCliManager {
|
|
|
1037
1101
|
// or no requested model, is a no-op — model selection is best-effort and must never
|
|
1038
1102
|
// fail a launch. The model args are prepended so a caller's explicit cliArgs (e.g. a
|
|
1039
1103
|
// resume flag) still win positionally where order matters.
|
|
1040
|
-
const modelLaunchArgs = expandModelLaunchArgs(
|
|
1104
|
+
const modelLaunchArgs = expandModelLaunchArgs(launchProvider?.modelLaunchArgs, initialModel);
|
|
1041
1105
|
const cliArgsWithModel = modelLaunchArgs
|
|
1042
|
-
? [...modelLaunchArgs, ...(
|
|
1043
|
-
:
|
|
1106
|
+
? [...modelLaunchArgs, ...(cliArgsWithAutoApprove || [])]
|
|
1107
|
+
: cliArgsWithAutoApprove;
|
|
1044
1108
|
if (initialModel && !modelLaunchArgs) {
|
|
1045
1109
|
LOG.warn('CLI', `[${normalizedType}] initialModel='${initialModel}' requested but provider declares no modelLaunchArgs template — launching without model selection.`);
|
|
1046
1110
|
}
|
|
@@ -1051,7 +1115,7 @@ export class DaemonCliManager {
|
|
|
1051
1115
|
// Best-effort; a provider with no template (or no requested level) is a no-op. ACP
|
|
1052
1116
|
// providers route thinking through setConfigOption('thought_level') above.
|
|
1053
1117
|
const initialThinkingLevel = options?.initialThinkingLevel;
|
|
1054
|
-
const thinkingLaunchArgs = expandThinkingLaunchArgs(
|
|
1118
|
+
const thinkingLaunchArgs = expandThinkingLaunchArgs(launchProvider?.thinkingLaunchArgs, initialThinkingLevel, launchProvider?.thinkingLevelMap);
|
|
1055
1119
|
const cliArgsWithBrain = thinkingLaunchArgs
|
|
1056
1120
|
? [...thinkingLaunchArgs, ...(cliArgsWithModel || [])]
|
|
1057
1121
|
: cliArgsWithModel;
|
|
@@ -1060,13 +1124,13 @@ export class DaemonCliManager {
|
|
|
1060
1124
|
}
|
|
1061
1125
|
|
|
1062
1126
|
// ─── Resolve launch options → provider session binding ───
|
|
1063
|
-
const sessionBinding = resolveCliSessionBinding(
|
|
1127
|
+
const sessionBinding = resolveCliSessionBinding(launchProvider, normalizedType, cliArgsWithBrain, options?.resumeSessionId);
|
|
1064
1128
|
const resolvedCliArgs = sessionBinding.cliArgs;
|
|
1065
1129
|
|
|
1066
1130
|
// If InstanceManager exists, manage as CliProviderInstance unified
|
|
1067
1131
|
const instanceManager = this.deps.getInstanceManager();
|
|
1068
|
-
if (
|
|
1069
|
-
const resolvedProvider =
|
|
1132
|
+
if (launchProvider && instanceManager) {
|
|
1133
|
+
const resolvedProvider = launchProvider;
|
|
1070
1134
|
await this.registerCliInstance(
|
|
1071
1135
|
key,
|
|
1072
1136
|
normalizedType,
|
|
@@ -1074,7 +1138,7 @@ export class DaemonCliManager {
|
|
|
1074
1138
|
resolvedDir,
|
|
1075
1139
|
resolvedCliArgs,
|
|
1076
1140
|
resolvedProvider,
|
|
1077
|
-
|
|
1141
|
+
launchSettings,
|
|
1078
1142
|
false,
|
|
1079
1143
|
{
|
|
1080
1144
|
providerSessionId: sessionBinding.providerSessionId,
|
|
@@ -115,8 +115,12 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
115
115
|
try {
|
|
116
116
|
const { readOperatingNotes } = await import('../../mesh/mesh-ledger.js');
|
|
117
117
|
// readOperatingNotes filters out tombstoned (forgotten) notes so a
|
|
118
|
-
// retracted lesson never rides into the prompt. Newest last
|
|
119
|
-
|
|
118
|
+
// retracted lesson never rides into the prompt. Newest last.
|
|
119
|
+
// Phase 2 (d): the byte-budget/count cap now bounds the injected list
|
|
120
|
+
// (selectOperatingNotesForPrompt), so read a larger candidate tail than
|
|
121
|
+
// the old fixed 20 and let injection-side ranking + budget do the
|
|
122
|
+
// bounding. Keep a sane store-read ceiling to avoid unbounded arrays.
|
|
123
|
+
const noteEntries = readOperatingNotes(id, { tail: 100 });
|
|
120
124
|
const notes = noteEntries
|
|
121
125
|
.map((e) => {
|
|
122
126
|
const p = (e.payload || {}) as Record<string, unknown>;
|
|
@@ -131,6 +135,17 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
131
135
|
category,
|
|
132
136
|
createdAt: typeof p.createdAt === 'string' ? p.createdAt : e.timestamp,
|
|
133
137
|
sourceCoordinator: typeof p.sourceCoordinator === 'string' ? p.sourceCoordinator : undefined,
|
|
138
|
+
// Operating-notes lifecycle: thread pinned/expiresAt so the
|
|
139
|
+
// injection-side selection can honor them. Legacy notes lack
|
|
140
|
+
// these → pinned defaults false, expiry governed by category TTL.
|
|
141
|
+
pinned: p.pinned === true,
|
|
142
|
+
...(typeof p.expiresAt === 'string' ? { expiresAt: p.expiresAt } : {}),
|
|
143
|
+
// Phase 2 (b)/(c): thread the ledger id + supersedes/subjectKey
|
|
144
|
+
// so version-supersede targeting and same-class folding work.
|
|
145
|
+
// Legacy notes lack them → no supersede, fold by leading [tag].
|
|
146
|
+
noteId: e.id,
|
|
147
|
+
...(typeof p.supersedes === 'string' ? { supersedes: p.supersedes } : {}),
|
|
148
|
+
...(typeof p.subjectKey === 'string' ? { subjectKey: p.subjectKey } : {}),
|
|
134
149
|
};
|
|
135
150
|
})
|
|
136
151
|
.filter((n): n is NonNullable<typeof n> => n !== null);
|
|
@@ -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
|
};
|
|
@@ -399,6 +399,161 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
399
399
|
}
|
|
400
400
|
},
|
|
401
401
|
|
|
402
|
+
// READ path for `.adhdev/mesh.json` — returns the currently-committed repo
|
|
403
|
+
// config (parsed + normalized) for a workspace so a UI can render/edit the
|
|
404
|
+
// existing declarative zones (notably `providerDefaults.autoApproveModes`)
|
|
405
|
+
// WITHOUT re-deriving them from the machine-local scaffold. Never writes.
|
|
406
|
+
// `config` is undefined when no repo file exists (sourceType 'unavailable') or
|
|
407
|
+
// it is unparseable (sourceType 'invalid', with the parse error surfaced).
|
|
408
|
+
read_mesh_json_config: async (_ctx: MedFamilyContext, args: any) => {
|
|
409
|
+
const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
|
|
410
|
+
try {
|
|
411
|
+
const { loadRepoMeshJsonConfig } = await import('../../config/mesh-json-config.js');
|
|
412
|
+
const loaded = loadRepoMeshJsonConfig(workspace);
|
|
413
|
+
return {
|
|
414
|
+
success: true,
|
|
415
|
+
workspace,
|
|
416
|
+
sourceType: loaded.sourceType,
|
|
417
|
+
source: loaded.source,
|
|
418
|
+
...(loaded.path ? { path: loaded.path } : {}),
|
|
419
|
+
...(loaded.error ? { error: loaded.error } : {}),
|
|
420
|
+
config: loaded.config,
|
|
421
|
+
// Convenience projection so the UI does not have to reach into config.
|
|
422
|
+
providerDefaults: loaded.config?.providerDefaults,
|
|
423
|
+
};
|
|
424
|
+
} catch (e: any) {
|
|
425
|
+
return { success: false, error: e.message };
|
|
426
|
+
}
|
|
427
|
+
},
|
|
428
|
+
|
|
429
|
+
// Partial-edit WRITE path for `.adhdev/mesh.json` `providerDefaults` — a
|
|
430
|
+
// READ-MODIFY-WRITE that preserves operator hand-edits. Unlike
|
|
431
|
+
// write_mesh_json_config (which rebuilds the WHOLE file from the machine-local
|
|
432
|
+
// scaffold and can silently drop hand-edited zones), this parses the existing
|
|
433
|
+
// repo file, merges ONLY the providerDefaults.autoApproveModes zone, and
|
|
434
|
+
// re-serializes — coordinator prompt, operating notes and limits authored in
|
|
435
|
+
// the repo are carried through untouched. Defaults to dry-run.
|
|
436
|
+
//
|
|
437
|
+
// args: { workspace?, autoApproveModes: Record<providerType,modeId>, write?, merge? }
|
|
438
|
+
// merge=true (default): per-provider merge into the existing map; a modeId of
|
|
439
|
+
// '' | null removes that provider's entry. merge=false: REPLACE the whole
|
|
440
|
+
// autoApproveModes map with the supplied one.
|
|
441
|
+
set_mesh_provider_defaults: async (_ctx: MedFamilyContext, args: any) => {
|
|
442
|
+
const workspace = typeof args?.workspace === 'string' && args.workspace.trim() ? args.workspace.trim() : process.cwd();
|
|
443
|
+
const write = args?.write === true;
|
|
444
|
+
const merge = args?.merge !== false; // default true
|
|
445
|
+
const inputModes = args?.autoApproveModes;
|
|
446
|
+
if (inputModes !== undefined && (typeof inputModes !== 'object' || inputModes === null || Array.isArray(inputModes))) {
|
|
447
|
+
return { success: false, error: 'autoApproveModes must be an object (providerType → modeId) when provided' };
|
|
448
|
+
}
|
|
449
|
+
try {
|
|
450
|
+
const {
|
|
451
|
+
loadRepoMeshJsonConfig,
|
|
452
|
+
normalizeRepoMeshDeclarativeConfig,
|
|
453
|
+
MESH_JSON_CONFIG_LOCATIONS,
|
|
454
|
+
} = await import('../../config/mesh-json-config.js');
|
|
455
|
+
const { existsSync, readFileSync, mkdirSync, writeFileSync } = await import('fs');
|
|
456
|
+
const { dirname, join } = await import('path');
|
|
457
|
+
const yaml = await import('js-yaml');
|
|
458
|
+
|
|
459
|
+
const relativePath = MESH_JSON_CONFIG_LOCATIONS[0];
|
|
460
|
+
|
|
461
|
+
// Read-modify-write: parse the EXISTING on-disk document (preferring the
|
|
462
|
+
// first existing json/yaml variant) so unrelated zones survive verbatim.
|
|
463
|
+
let baseDoc: Record<string, any> = { version: 1 };
|
|
464
|
+
let existingPath = join(workspace, relativePath);
|
|
465
|
+
let existedAsYaml = false;
|
|
466
|
+
for (const relative of MESH_JSON_CONFIG_LOCATIONS) {
|
|
467
|
+
const candidate = join(workspace, relative);
|
|
468
|
+
if (!existsSync(candidate)) continue;
|
|
469
|
+
try {
|
|
470
|
+
const text = readFileSync(candidate, 'utf-8');
|
|
471
|
+
const parsed = /\.json$/i.test(candidate) ? JSON.parse(text) : yaml.load(text);
|
|
472
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
473
|
+
baseDoc = parsed as Record<string, any>;
|
|
474
|
+
existingPath = candidate;
|
|
475
|
+
existedAsYaml = !/\.json$/i.test(candidate);
|
|
476
|
+
}
|
|
477
|
+
} catch (e: any) {
|
|
478
|
+
return { success: false, error: `existing ${relative} is unparseable, refusing to overwrite: ${e?.message || e}` };
|
|
479
|
+
}
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
// Merge ONLY the providerDefaults.autoApproveModes zone.
|
|
484
|
+
const existingPd = baseDoc.providerDefaults && typeof baseDoc.providerDefaults === 'object' && !Array.isArray(baseDoc.providerDefaults)
|
|
485
|
+
? baseDoc.providerDefaults as Record<string, any>
|
|
486
|
+
: {};
|
|
487
|
+
const existingModes = existingPd.autoApproveModes && typeof existingPd.autoApproveModes === 'object' && !Array.isArray(existingPd.autoApproveModes)
|
|
488
|
+
? { ...existingPd.autoApproveModes as Record<string, string> }
|
|
489
|
+
: {};
|
|
490
|
+
|
|
491
|
+
const nextModes: Record<string, string> = merge ? existingModes : {};
|
|
492
|
+
if (inputModes) {
|
|
493
|
+
for (const [providerType, modeId] of Object.entries(inputModes as Record<string, unknown>)) {
|
|
494
|
+
const type = typeof providerType === 'string' ? providerType.trim() : '';
|
|
495
|
+
if (!type) continue;
|
|
496
|
+
const id = typeof modeId === 'string' ? modeId.trim() : '';
|
|
497
|
+
if (id) nextModes[type] = id;
|
|
498
|
+
else delete nextModes[type]; // '' / null → remove this provider's entry
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const nextDoc: Record<string, any> = { ...baseDoc, version: 1 };
|
|
503
|
+
if (Object.keys(nextModes).length) {
|
|
504
|
+
nextDoc.providerDefaults = { ...existingPd, autoApproveModes: nextModes };
|
|
505
|
+
} else {
|
|
506
|
+
// No entries left → drop the zone entirely so we don't leave an empty stub.
|
|
507
|
+
if (nextDoc.providerDefaults) {
|
|
508
|
+
const { autoApproveModes, ...restPd } = nextDoc.providerDefaults;
|
|
509
|
+
if (Object.keys(restPd).length) nextDoc.providerDefaults = restPd;
|
|
510
|
+
else delete nextDoc.providerDefaults;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Validate the merged document before it ever touches disk.
|
|
515
|
+
const validation = normalizeRepoMeshDeclarativeConfig(nextDoc);
|
|
516
|
+
if (!validation.valid) {
|
|
517
|
+
return { success: false, error: `merged mesh.json is invalid: ${validation.errors.join('; ')}` };
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
// Serialize in the on-disk format (JSON unless the existing file was YAML).
|
|
521
|
+
const absolutePath = existingPath;
|
|
522
|
+
const serialized = existedAsYaml
|
|
523
|
+
? yaml.dump(nextDoc, { indent: 2 })
|
|
524
|
+
: `${JSON.stringify(nextDoc, null, 2)}\n`;
|
|
525
|
+
|
|
526
|
+
if (!write) {
|
|
527
|
+
return {
|
|
528
|
+
success: true,
|
|
529
|
+
written: false,
|
|
530
|
+
dryRun: true,
|
|
531
|
+
path: absolutePath,
|
|
532
|
+
relativePath,
|
|
533
|
+
merge,
|
|
534
|
+
providerDefaults: nextDoc.providerDefaults,
|
|
535
|
+
preview: serialized,
|
|
536
|
+
note: 'Dry-run: nothing written. Re-run with write=true to persist. Only the providerDefaults zone is merged; other repo zones are preserved.',
|
|
537
|
+
};
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
mkdirSync(dirname(absolutePath), { recursive: true });
|
|
541
|
+
writeFileSync(absolutePath, serialized, 'utf-8');
|
|
542
|
+
return {
|
|
543
|
+
success: true,
|
|
544
|
+
written: true,
|
|
545
|
+
dryRun: false,
|
|
546
|
+
path: absolutePath,
|
|
547
|
+
relativePath,
|
|
548
|
+
merge,
|
|
549
|
+
providerDefaults: nextDoc.providerDefaults,
|
|
550
|
+
note: 'Wrote providerDefaults into .adhdev/mesh.json (read-modify-write; other zones preserved). Commit it to the repo.',
|
|
551
|
+
};
|
|
552
|
+
} catch (e: any) {
|
|
553
|
+
return { success: false, error: e.message };
|
|
554
|
+
}
|
|
555
|
+
},
|
|
556
|
+
|
|
402
557
|
delete_mesh: async (_ctx: MedFamilyContext, args: any) => {
|
|
403
558
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
404
559
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -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 };
|