@adhdev/daemon-core 1.0.18-rc.19 → 1.0.18-rc.2
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 +0 -77
- package/dist/cli-adapters/provider-cli-shared.d.ts +0 -33
- package/dist/cli-adapters/pty-transport.d.ts +1 -2
- package/dist/commands/cli-manager.d.ts +0 -9
- package/dist/commands/upgrade-helper.d.ts +0 -1
- package/dist/commands/windows-atomic-upgrade.d.ts +1 -17
- package/dist/config/mesh-json-config.d.ts +0 -62
- package/dist/index.d.ts +2 -4
- package/dist/index.js +4143 -6578
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +4138 -6570
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/coordinator-prompt.d.ts +0 -76
- package/dist/mesh/mesh-active-work.d.ts +1 -1
- package/dist/mesh/mesh-ledger.d.ts +1 -28
- package/dist/mesh/mesh-node-identity.d.ts +0 -23
- package/dist/mesh/mesh-refine-gates.d.ts +0 -89
- package/dist/mesh/mesh-remote-event-pull.d.ts +0 -3
- package/dist/mesh/mesh-runtime-store.d.ts +0 -11
- package/dist/mesh/mesh-work-queue.d.ts +1 -24
- package/dist/providers/chat-message-normalization.d.ts +0 -22
- package/dist/providers/cli-provider-instance-types.d.ts +0 -4
- package/dist/providers/cli-provider-instance.d.ts +0 -78
- package/dist/providers/contracts.d.ts +0 -17
- package/dist/providers/provider-schema.d.ts +0 -1
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +0 -14
- package/dist/providers/types/interactive-prompt.d.ts +0 -18
- package/dist/repo-mesh-types.d.ts +6 -38
- package/dist/shared-types.d.ts +2 -4
- package/package.json +3 -3
- package/src/boot/daemon-lifecycle.ts +0 -10
- package/src/cli-adapters/cli-state-engine.ts +3 -220
- package/src/cli-adapters/provider-cli-adapter.ts +11 -41
- package/src/cli-adapters/provider-cli-shared.ts +0 -41
- package/src/cli-adapters/pty-transport.d.ts +1 -2
- package/src/cli-adapters/pty-transport.ts +1 -1
- package/src/cli-adapters/session-host-transport.ts +3 -8
- package/src/commands/cli-manager.ts +8 -72
- package/src/commands/high-family/mesh-coordinator-launch.ts +2 -17
- package/src/commands/high-family/mesh-events.ts +2 -13
- package/src/commands/med-family/mesh-crud.ts +0 -155
- package/src/commands/med-family/mesh-queue.ts +1 -74
- package/src/commands/router-refine.ts +4 -71
- package/src/commands/router.ts +0 -8
- package/src/commands/upgrade-helper.ts +82 -106
- package/src/commands/windows-atomic-upgrade.ts +35 -281
- package/src/config/mesh-json-config.ts +0 -111
- package/src/index.ts +1 -21
- package/src/mesh/coordinator-prompt.ts +11 -258
- package/src/mesh/mesh-active-work.ts +1 -11
- package/src/mesh/mesh-completion-synthesis.ts +1 -12
- package/src/mesh/mesh-event-classify.ts +0 -19
- package/src/mesh/mesh-event-forwarding.ts +6 -64
- package/src/mesh/mesh-events-utils.ts +0 -42
- package/src/mesh/mesh-ledger.ts +0 -77
- package/src/mesh/mesh-node-identity.ts +0 -49
- package/src/mesh/mesh-queue-assignment.ts +11 -210
- package/src/mesh/mesh-reconcile-loop.ts +3 -306
- package/src/mesh/mesh-refine-gates.ts +0 -300
- package/src/mesh/mesh-remote-event-pull.ts +47 -68
- package/src/mesh/mesh-runtime-store.ts +0 -21
- package/src/mesh/mesh-work-queue.ts +2 -85
- package/src/providers/chat-message-normalization.ts +0 -53
- package/src/providers/cli-provider-instance-types.ts +0 -53
- package/src/providers/cli-provider-instance.ts +15 -497
- package/src/providers/contracts.ts +1 -25
- package/src/providers/provider-schema.ts +0 -83
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +0 -23
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +0 -20
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +0 -44
- package/src/providers/types/interactive-prompt.ts +0 -77
- package/src/repo-mesh-types.ts +12 -112
- package/src/session-host/managed-host.ts +0 -34
- package/src/shared-types.ts +1 -9
- package/src/status/reporter.ts +0 -1
- package/src/status/snapshot.ts +0 -2
- package/dist/commands/process-lifecycle.d.ts +0 -43
- package/dist/mesh/mesh-disk-retention.d.ts +0 -105
- package/dist/providers/auto-approve-modes.d.ts +0 -14
- package/src/commands/process-lifecycle.ts +0 -248
- package/src/mesh/mesh-disk-retention.ts +0 -370
- package/src/providers/auto-approve-modes.ts +0 -97
|
@@ -34,7 +34,6 @@ 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';
|
|
38
37
|
|
|
39
38
|
// ─── external dependency interface ──────────────────────────
|
|
40
39
|
|
|
@@ -453,40 +452,6 @@ export function expandThinkingLaunchArgs(
|
|
|
453
452
|
return template.map((part) => part.includes('{{level}}') ? part.replace('{{level}}', mapped) : part);
|
|
454
453
|
}
|
|
455
454
|
|
|
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
|
-
|
|
490
455
|
function readSubcommandSessionId(args: string[], subcommands: string[]): string | undefined {
|
|
491
456
|
const resumeIndex = args.findIndex((arg) => subcommands.includes(arg));
|
|
492
457
|
if (resumeIndex < 0) return undefined;
|
|
@@ -742,24 +707,6 @@ export class DaemonCliManager {
|
|
|
742
707
|
clearInterval(checkStopped);
|
|
743
708
|
setTimeout(() => {
|
|
744
709
|
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
|
-
}
|
|
763
710
|
this.adapters.delete(key);
|
|
764
711
|
this.deps.removeAgentTracking(key);
|
|
765
712
|
sessionRegistry?.unregisterByInstanceKey(key);
|
|
@@ -1083,17 +1030,6 @@ export class DaemonCliManager {
|
|
|
1083
1030
|
console.log(colorize('cyan', ` 📦 Using provider: ${provider.name} (${provider.type})`));
|
|
1084
1031
|
}
|
|
1085
1032
|
|
|
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
|
-
|
|
1097
1033
|
// ─── Model axis (MAGI kind-panel): expand initialModel → launch args ───
|
|
1098
1034
|
// For a plain CLI provider the model is selected at spawn time via the manifest's
|
|
1099
1035
|
// modelLaunchArgs template ('{{model}}' → the requested model). ACP providers took
|
|
@@ -1101,10 +1037,10 @@ export class DaemonCliManager {
|
|
|
1101
1037
|
// or no requested model, is a no-op — model selection is best-effort and must never
|
|
1102
1038
|
// fail a launch. The model args are prepended so a caller's explicit cliArgs (e.g. a
|
|
1103
1039
|
// resume flag) still win positionally where order matters.
|
|
1104
|
-
const modelLaunchArgs = expandModelLaunchArgs(
|
|
1040
|
+
const modelLaunchArgs = expandModelLaunchArgs(provider?.modelLaunchArgs, initialModel);
|
|
1105
1041
|
const cliArgsWithModel = modelLaunchArgs
|
|
1106
|
-
? [...modelLaunchArgs, ...(
|
|
1107
|
-
:
|
|
1042
|
+
? [...modelLaunchArgs, ...(cliArgs || [])]
|
|
1043
|
+
: cliArgs;
|
|
1108
1044
|
if (initialModel && !modelLaunchArgs) {
|
|
1109
1045
|
LOG.warn('CLI', `[${normalizedType}] initialModel='${initialModel}' requested but provider declares no modelLaunchArgs template — launching without model selection.`);
|
|
1110
1046
|
}
|
|
@@ -1115,7 +1051,7 @@ export class DaemonCliManager {
|
|
|
1115
1051
|
// Best-effort; a provider with no template (or no requested level) is a no-op. ACP
|
|
1116
1052
|
// providers route thinking through setConfigOption('thought_level') above.
|
|
1117
1053
|
const initialThinkingLevel = options?.initialThinkingLevel;
|
|
1118
|
-
const thinkingLaunchArgs = expandThinkingLaunchArgs(
|
|
1054
|
+
const thinkingLaunchArgs = expandThinkingLaunchArgs(provider?.thinkingLaunchArgs, initialThinkingLevel, provider?.thinkingLevelMap);
|
|
1119
1055
|
const cliArgsWithBrain = thinkingLaunchArgs
|
|
1120
1056
|
? [...thinkingLaunchArgs, ...(cliArgsWithModel || [])]
|
|
1121
1057
|
: cliArgsWithModel;
|
|
@@ -1124,13 +1060,13 @@ export class DaemonCliManager {
|
|
|
1124
1060
|
}
|
|
1125
1061
|
|
|
1126
1062
|
// ─── Resolve launch options → provider session binding ───
|
|
1127
|
-
const sessionBinding = resolveCliSessionBinding(
|
|
1063
|
+
const sessionBinding = resolveCliSessionBinding(provider, normalizedType, cliArgsWithBrain, options?.resumeSessionId);
|
|
1128
1064
|
const resolvedCliArgs = sessionBinding.cliArgs;
|
|
1129
1065
|
|
|
1130
1066
|
// If InstanceManager exists, manage as CliProviderInstance unified
|
|
1131
1067
|
const instanceManager = this.deps.getInstanceManager();
|
|
1132
|
-
if (
|
|
1133
|
-
const resolvedProvider =
|
|
1068
|
+
if (provider && instanceManager) {
|
|
1069
|
+
const resolvedProvider = this.providerLoader.resolve(cliType, { version: cliInfo.version }) || provider;
|
|
1134
1070
|
await this.registerCliInstance(
|
|
1135
1071
|
key,
|
|
1136
1072
|
normalizedType,
|
|
@@ -1138,7 +1074,7 @@ export class DaemonCliManager {
|
|
|
1138
1074
|
resolvedDir,
|
|
1139
1075
|
resolvedCliArgs,
|
|
1140
1076
|
resolvedProvider,
|
|
1141
|
-
|
|
1077
|
+
{ ...this.providerLoader.getSettings(normalizedType), ...(options?.settingsOverride || {}) },
|
|
1142
1078
|
false,
|
|
1143
1079
|
{
|
|
1144
1080
|
providerSessionId: sessionBinding.providerSessionId,
|
|
@@ -115,12 +115,8 @@ 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
|
-
|
|
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 });
|
|
118
|
+
// retracted lesson never rides into the prompt. Newest last; tail 20.
|
|
119
|
+
const noteEntries = readOperatingNotes(id, { tail: 20 });
|
|
124
120
|
const notes = noteEntries
|
|
125
121
|
.map((e) => {
|
|
126
122
|
const p = (e.payload || {}) as Record<string, unknown>;
|
|
@@ -135,17 +131,6 @@ export const meshCoordinatorLaunchHandlers: Record<string, HighFamilyHandler> =
|
|
|
135
131
|
category,
|
|
136
132
|
createdAt: typeof p.createdAt === 'string' ? p.createdAt : e.timestamp,
|
|
137
133
|
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 } : {}),
|
|
149
134
|
};
|
|
150
135
|
})
|
|
151
136
|
.filter((n): n is NonNullable<typeof n> => n !== null);
|
|
@@ -93,21 +93,10 @@ 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 response = normalizeInteractivePromptResponse(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
|
-
|
|
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);
|
|
99
|
+
ctx.deps.instanceManager.sendEvent(sessionId, 'interactive_prompt_response', response);
|
|
111
100
|
return { success: true };
|
|
112
101
|
},
|
|
113
102
|
};
|
|
@@ -399,161 +399,6 @@ 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
|
-
|
|
557
402
|
delete_mesh: async (_ctx: MedFamilyContext, args: any) => {
|
|
558
403
|
const meshId = typeof args?.meshId === 'string' ? args.meshId.trim() : '';
|
|
559
404
|
if (!meshId) return { success: false, error: 'meshId required' };
|
|
@@ -9,67 +9,6 @@
|
|
|
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
|
-
}
|
|
73
12
|
|
|
74
13
|
export const meshQueueHandlers: Record<string, MedFamilyHandler> = {
|
|
75
14
|
get_mesh_queue: async (_ctx: MedFamilyContext, args: any) => {
|
|
@@ -111,22 +50,10 @@ export const meshQueueHandlers: Record<string, MedFamilyHandler> = {
|
|
|
111
50
|
const ownerFailure = await ctx.requireMeshHostMutationOwner(meshId, args?.inlineMesh, 'queue cancellation');
|
|
112
51
|
if (ownerFailure) return ownerFailure;
|
|
113
52
|
try {
|
|
114
|
-
const { cancelTask
|
|
53
|
+
const { cancelTask } = await import('../../mesh/mesh-work-queue.js');
|
|
115
54
|
const reason = typeof args?.reason === 'string' ? args.reason : undefined;
|
|
116
55
|
const task = cancelTask(meshId, taskId, { reason });
|
|
117
56
|
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
|
-
}
|
|
130
57
|
return { success: true, task };
|
|
131
58
|
} catch (e: any) {
|
|
132
59
|
return { success: false, error: e.message };
|
|
@@ -36,10 +36,8 @@ import {
|
|
|
36
36
|
RefineExecFileAsync,
|
|
37
37
|
RefineStageOutcome,
|
|
38
38
|
classifyPatchEquivalenceFailure,
|
|
39
|
-
convergeDivergedSubmoduleGitlinks,
|
|
40
39
|
recordMeshRefineStage,
|
|
41
40
|
resolveRefineryAutoPublishSubmoduleMainCommits,
|
|
42
|
-
rootRebaseResolvingGitlinks,
|
|
43
41
|
runMeshRefineEffectiveDiffGate,
|
|
44
42
|
runMeshRefinePatchEquivalenceGate,
|
|
45
43
|
runMeshRefineSubmoduleReachabilityGate,
|
|
@@ -541,9 +539,6 @@ async function computeBranchBaseDivergence(
|
|
|
541
539
|
export async function refineSyncBaseStage(self: DaemonCommandRouter, ctx: RefineContext): Promise<RefineStageOutcome> {
|
|
542
540
|
const { repoRoot, baseHead, node, branch, baseBranch, refineStages, execFileAsync } = ctx;
|
|
543
541
|
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 }> = [];
|
|
547
542
|
const syncStarted = Date.now();
|
|
548
543
|
const divergence = await computeBranchBaseDivergence(execFileAsync, node.workspace, baseHead, branchHead);
|
|
549
544
|
|
|
@@ -576,87 +571,25 @@ export async function refineSyncBaseStage(self: DaemonCommandRouter, ctx: Refine
|
|
|
576
571
|
const preRebasePe = await runMeshRefinePatchEquivalenceGate(repoRoot, baseHead, branchHead);
|
|
577
572
|
const alreadyMerged = !preRebasePe.actualPatchId && !!preRebasePe.expectedPatchId;
|
|
578
573
|
const submoduleConflict = preRebasePe.actionableHint?.kind === 'submodule_conflict';
|
|
579
|
-
if (alreadyMerged) {
|
|
574
|
+
if (alreadyMerged || submoduleConflict) {
|
|
580
575
|
recordMeshRefineStage(refineStages, 'sync_base', 'passed', syncStarted, {
|
|
581
576
|
ahead: divergence.ahead,
|
|
582
577
|
behind: divergence.behind,
|
|
583
578
|
rebased: false,
|
|
584
|
-
reason: 'already_merged_via_other_path_skip_rebase',
|
|
579
|
+
reason: alreadyMerged ? 'already_merged_via_other_path_skip_rebase' : 'submodule_conflict_defer_to_patch_equivalence',
|
|
585
580
|
});
|
|
586
581
|
return { kind: 'continue', ctx };
|
|
587
582
|
}
|
|
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
|
-
}
|
|
627
583
|
} catch { /* fail-open: on gate error, fall through to the rebase */ }
|
|
628
584
|
|
|
629
585
|
// behind>0: strictly-behind OR diverged. Rebase the branch onto the pinned
|
|
630
586
|
// baseHead. A conflict aborts and terminates blocked_review (retryable=false —
|
|
631
587
|
// 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).
|
|
639
588
|
const rebaseStarted = Date.now();
|
|
640
589
|
try {
|
|
641
|
-
|
|
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
|
-
}
|
|
590
|
+
execFileSync('git', ['rebase', baseHead], { cwd: node.workspace, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
656
591
|
} catch (rebaseErr: any) {
|
|
657
|
-
|
|
658
|
-
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
659
|
-
}
|
|
592
|
+
try { execFileSync('git', ['rebase', '--abort'], { cwd: node.workspace, stdio: 'ignore' }); } catch { /* ignore */ }
|
|
660
593
|
// A rebase conflict on a submodule/gitlink divergence is a SPECIAL case the
|
|
661
594
|
// patch-equivalence gate describes with a rich actionable hint (which
|
|
662
595
|
// submodule, base vs branch commit, how to resolve). Run that gate against
|
package/src/commands/router.ts
CHANGED
|
@@ -227,14 +227,6 @@ 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',
|
|
238
230
|
]);
|
|
239
231
|
|
|
240
232
|
function normalizeCommandSource(source: string): CommandLogEntry['source'] {
|