@adhdev/daemon-core 1.0.18-rc.18 → 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.
Files changed (82) hide show
  1. package/dist/cli-adapters/cli-state-engine.d.ts +0 -77
  2. package/dist/cli-adapters/provider-cli-shared.d.ts +0 -33
  3. package/dist/cli-adapters/pty-transport.d.ts +1 -2
  4. package/dist/commands/cli-manager.d.ts +0 -9
  5. package/dist/commands/upgrade-helper.d.ts +0 -1
  6. package/dist/commands/windows-atomic-upgrade.d.ts +1 -17
  7. package/dist/config/mesh-json-config.d.ts +0 -62
  8. package/dist/index.d.ts +2 -4
  9. package/dist/index.js +4143 -6578
  10. package/dist/index.js.map +1 -1
  11. package/dist/index.mjs +4138 -6570
  12. package/dist/index.mjs.map +1 -1
  13. package/dist/mesh/coordinator-prompt.d.ts +0 -76
  14. package/dist/mesh/mesh-active-work.d.ts +1 -1
  15. package/dist/mesh/mesh-ledger.d.ts +1 -28
  16. package/dist/mesh/mesh-node-identity.d.ts +0 -23
  17. package/dist/mesh/mesh-refine-gates.d.ts +0 -89
  18. package/dist/mesh/mesh-remote-event-pull.d.ts +0 -3
  19. package/dist/mesh/mesh-runtime-store.d.ts +0 -11
  20. package/dist/mesh/mesh-work-queue.d.ts +1 -24
  21. package/dist/providers/chat-message-normalization.d.ts +0 -22
  22. package/dist/providers/cli-provider-instance-types.d.ts +0 -4
  23. package/dist/providers/cli-provider-instance.d.ts +0 -78
  24. package/dist/providers/contracts.d.ts +0 -17
  25. package/dist/providers/provider-schema.d.ts +0 -1
  26. package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +0 -14
  27. package/dist/providers/types/interactive-prompt.d.ts +0 -18
  28. package/dist/repo-mesh-types.d.ts +6 -38
  29. package/dist/shared-types.d.ts +2 -4
  30. package/package.json +3 -3
  31. package/src/boot/daemon-lifecycle.ts +0 -10
  32. package/src/cli-adapters/cli-state-engine.ts +3 -220
  33. package/src/cli-adapters/provider-cli-adapter.ts +11 -41
  34. package/src/cli-adapters/provider-cli-shared.ts +0 -41
  35. package/src/cli-adapters/pty-transport.d.ts +1 -2
  36. package/src/cli-adapters/pty-transport.ts +1 -1
  37. package/src/cli-adapters/session-host-transport.ts +3 -8
  38. package/src/commands/cli-manager.ts +8 -72
  39. package/src/commands/high-family/mesh-coordinator-launch.ts +2 -17
  40. package/src/commands/high-family/mesh-events.ts +2 -13
  41. package/src/commands/med-family/mesh-crud.ts +0 -155
  42. package/src/commands/med-family/mesh-queue.ts +1 -74
  43. package/src/commands/router-refine.ts +4 -71
  44. package/src/commands/router.ts +0 -8
  45. package/src/commands/upgrade-helper.ts +82 -106
  46. package/src/commands/windows-atomic-upgrade.ts +35 -281
  47. package/src/config/mesh-json-config.ts +0 -111
  48. package/src/index.ts +1 -21
  49. package/src/mesh/coordinator-prompt.ts +11 -258
  50. package/src/mesh/mesh-active-work.ts +1 -11
  51. package/src/mesh/mesh-completion-synthesis.ts +1 -12
  52. package/src/mesh/mesh-event-classify.ts +0 -19
  53. package/src/mesh/mesh-event-forwarding.ts +6 -64
  54. package/src/mesh/mesh-events-utils.ts +0 -42
  55. package/src/mesh/mesh-ledger.ts +0 -77
  56. package/src/mesh/mesh-node-identity.ts +0 -49
  57. package/src/mesh/mesh-queue-assignment.ts +11 -210
  58. package/src/mesh/mesh-reconcile-loop.ts +3 -306
  59. package/src/mesh/mesh-refine-gates.ts +0 -300
  60. package/src/mesh/mesh-remote-event-pull.ts +47 -68
  61. package/src/mesh/mesh-runtime-store.ts +0 -21
  62. package/src/mesh/mesh-work-queue.ts +2 -85
  63. package/src/providers/chat-message-normalization.ts +0 -53
  64. package/src/providers/cli-provider-instance-types.ts +0 -53
  65. package/src/providers/cli-provider-instance.ts +15 -497
  66. package/src/providers/contracts.ts +1 -25
  67. package/src/providers/provider-schema.ts +0 -83
  68. package/src/providers/sdk/v1/builders/cli/detect-status.ts +0 -23
  69. package/src/providers/sdk/v1/builders/cli/parse-approval.ts +0 -20
  70. package/src/providers/sdk/v1/schemas/cli/provider.schema.json +0 -44
  71. package/src/providers/types/interactive-prompt.ts +0 -77
  72. package/src/repo-mesh-types.ts +12 -112
  73. package/src/session-host/managed-host.ts +0 -34
  74. package/src/shared-types.ts +1 -9
  75. package/src/status/reporter.ts +0 -1
  76. package/src/status/snapshot.ts +0 -2
  77. package/dist/commands/process-lifecycle.d.ts +0 -43
  78. package/dist/mesh/mesh-disk-retention.d.ts +0 -105
  79. package/dist/providers/auto-approve-modes.d.ts +0 -14
  80. package/src/commands/process-lifecycle.ts +0 -248
  81. package/src/mesh/mesh-disk-retention.ts +0 -370
  82. 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(launchProvider?.modelLaunchArgs, initialModel);
1040
+ const modelLaunchArgs = expandModelLaunchArgs(provider?.modelLaunchArgs, initialModel);
1105
1041
  const cliArgsWithModel = modelLaunchArgs
1106
- ? [...modelLaunchArgs, ...(cliArgsWithAutoApprove || [])]
1107
- : cliArgsWithAutoApprove;
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(launchProvider?.thinkingLaunchArgs, initialThinkingLevel, launchProvider?.thinkingLevelMap);
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(launchProvider, normalizedType, cliArgsWithBrain, options?.resumeSessionId);
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 (launchProvider && instanceManager) {
1133
- const resolvedProvider = launchProvider;
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
- launchSettings,
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
- // 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 });
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 rawResponse = args?.response ?? args;
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
- // 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);
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, takeCancelledTaskAssignment } = await import('../../mesh/mesh-work-queue.js');
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
- if (gitlinkResolutions.length > 0) {
642
- const gitlinkRebase = rootRebaseResolvingGitlinks(node.workspace, baseHead, gitlinkResolutions);
643
- if (!gitlinkRebase.ok) {
644
- // Surface as a rebase failure so the shared blocked_review handling
645
- // (submodule-hint recovery included) runs — nothing was left mid-rebase
646
- // (rootRebaseResolvingGitlinks aborts on failure).
647
- const err: any = new Error(`gitlink-aware rebase aborted: ${gitlinkRebase.reason || 'unknown'}`);
648
- err.gitlinkRebaseReason = gitlinkRebase.reason;
649
- err.gitlinkRebaseConflicts = gitlinkRebase.conflictPaths;
650
- err.alreadyAborted = true;
651
- throw err;
652
- }
653
- } else {
654
- execFileSync('git', ['rebase', baseHead], { cwd: node.workspace, stdio: ['ignore', 'pipe', 'pipe'] });
655
- }
590
+ execFileSync('git', ['rebase', baseHead], { cwd: node.workspace, stdio: ['ignore', 'pipe', 'pipe'] });
656
591
  } catch (rebaseErr: any) {
657
- if (!rebaseErr?.alreadyAborted) {
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
@@ -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'] {