@adhdev/daemon-core 0.9.82-rc.365 → 0.9.82-rc.367
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/commands/chat-commands.d.ts +27 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +60 -0
- package/dist/commands/router.d.ts +242 -0
- package/dist/index.js +2100 -1769
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2085 -1754
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-coordinator.d.ts +8 -0
- package/package.json +2 -2
- package/src/commands/chat-commands.ts +96 -2
- package/src/commands/cli-manager.ts +28 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +592 -0
- package/src/commands/high-family/mesh-events.ts +47 -0
- package/src/commands/high-family/mesh-status.ts +639 -0
- package/src/commands/high-family/types.ts +76 -0
- package/src/commands/router.ts +407 -1254
- package/src/mesh/mesh-events-coordinator.ts +78 -13
- package/src/mesh/mesh-reconcile-loop.ts +38 -0
|
@@ -59,6 +59,14 @@ export interface MeshQueueTriggerResult {
|
|
|
59
59
|
status?: string;
|
|
60
60
|
}>;
|
|
61
61
|
autoLaunchStarted: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* True when a worker session is already on its way to claim a still-pending task —
|
|
64
|
+
* either launched this tick (autoLaunchStarted) or launched on a prior tick and still
|
|
65
|
+
* booting/awaiting-claim. Callers MUST treat this as "wait, do not launch another
|
|
66
|
+
* session": a second launch double-edits the worktree. Mutually informative with
|
|
67
|
+
* `noIdleMeshSessionAvailable`, which is suppressed whenever this is true.
|
|
68
|
+
*/
|
|
69
|
+
autoLaunchPending?: boolean;
|
|
62
70
|
noIdleMeshSessionAvailable?: boolean;
|
|
63
71
|
}
|
|
64
72
|
export declare function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<MeshQueueTriggerResult>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.367",
|
|
4
4
|
"description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.367",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
import type { ChatMessage } from '../types.js';
|
|
31
31
|
import type { SessionTransport } from '../shared-types.js';
|
|
32
32
|
import { filterUserFacingChatMessages, isActivityChatMessage, isUserFacingChatMessage, normalizeChatMessages } from '../providers/chat-message-normalization.js';
|
|
33
|
+
import { normalizeMeshWorkspaceForCompare } from '@adhdev/mesh-shared';
|
|
33
34
|
|
|
34
35
|
const RECENT_SEND_WINDOW_MS = 1200;
|
|
35
36
|
export const READ_CHAT_PROVIDER_EVAL_TIMEOUT_MS = 25_000;
|
|
@@ -989,6 +990,53 @@ function normalizeComparableWorkspace(value: unknown): string {
|
|
|
989
990
|
return path.resolve(text);
|
|
990
991
|
}
|
|
991
992
|
|
|
993
|
+
/**
|
|
994
|
+
* read_chat node scope verdict. One physical daemon hosts a base node plus several
|
|
995
|
+
* worktree nodes; mesh_read_chat always dispatches read_chat with the requested
|
|
996
|
+
* node's workspace (`args.workspace`). When the resolved target session actually
|
|
997
|
+
* lives in a DIFFERENT worktree, returning its transcript — or worse, letting the
|
|
998
|
+
* native-history-by-workspace fallback splice sibling worktree turns into the
|
|
999
|
+
* reply — makes the coordinator believe one session received every worktree's
|
|
1000
|
+
* work. This guard refuses a CONFIRMED cross-workspace read instead of mixing.
|
|
1001
|
+
*
|
|
1002
|
+
* Conservative by design (mirrors the WTCLAIM fix-B "unknown → allow" rule): only
|
|
1003
|
+
* a session id that resolves to a known workspace which is unequal to a known
|
|
1004
|
+
* intended workspace blocks. When either side is unknown — no targetSessionId, no
|
|
1005
|
+
* args.workspace, an unregistered session, the coordinator self-session, or a
|
|
1006
|
+
* plain dashboard read that never passes a node workspace — the read proceeds
|
|
1007
|
+
* untouched, so base-node and same-daemon coordinator reads never regress.
|
|
1008
|
+
*/
|
|
1009
|
+
export function evaluateReadChatNodeWorkspaceScope(args: {
|
|
1010
|
+
targetSessionId?: string;
|
|
1011
|
+
intendedWorkspace?: string;
|
|
1012
|
+
sessionWorkspace?: string;
|
|
1013
|
+
}): { scoped: false } | { scoped: true; intended: string; actual: string } {
|
|
1014
|
+
const targetSessionId = typeof args.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
1015
|
+
if (!targetSessionId) return { scoped: false };
|
|
1016
|
+
const intended = normalizeMeshWorkspaceForCompare(args.intendedWorkspace);
|
|
1017
|
+
const actual = normalizeMeshWorkspaceForCompare(args.sessionWorkspace);
|
|
1018
|
+
if (!intended || !actual) return { scoped: false };
|
|
1019
|
+
if (intended === actual) return { scoped: false };
|
|
1020
|
+
return { scoped: true, intended, actual };
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
/**
|
|
1024
|
+
* Resolve the target session's ACTUAL workspace from the most authoritative source
|
|
1025
|
+
* available on this daemon: the session registry record (stamped at register
|
|
1026
|
+
* time), then the live CLI adapter's working directory, then the bound instance
|
|
1027
|
+
* state. Returns '' when nothing knows the session's workspace — the caller treats
|
|
1028
|
+
* that as "unknown" and does not block.
|
|
1029
|
+
*/
|
|
1030
|
+
function resolveTargetSessionActualWorkspace(h: CommandHelpers, targetSessionId: string): string {
|
|
1031
|
+
const registryWorkspace = (h.ctx?.sessionRegistry?.get?.(targetSessionId) as any)?.workspace;
|
|
1032
|
+
if (typeof registryWorkspace === 'string' && registryWorkspace.trim()) return registryWorkspace;
|
|
1033
|
+
const adapter = h.getCliAdapter?.(targetSessionId);
|
|
1034
|
+
if (adapter && typeof adapter.workingDir === 'string' && adapter.workingDir.trim()) return adapter.workingDir;
|
|
1035
|
+
const instanceWorkspace = (getTargetInstance(h, { targetSessionId })?.getState?.() as any)?.workspace;
|
|
1036
|
+
if (typeof instanceWorkspace === 'string' && instanceWorkspace.trim()) return instanceWorkspace;
|
|
1037
|
+
return '';
|
|
1038
|
+
}
|
|
1039
|
+
|
|
992
1040
|
function isCurrentRuntimePtySafelyAttributed(args: {
|
|
993
1041
|
adapter: CliAdapter;
|
|
994
1042
|
helpers: CommandHelpers;
|
|
@@ -2100,6 +2148,29 @@ export async function handleChatHistory(h: CommandHelpers, args: any): Promise<C
|
|
|
2100
2148
|
}
|
|
2101
2149
|
|
|
2102
2150
|
export async function handleReadChat(h: CommandHelpers, args: any): Promise<CommandResult> {
|
|
2151
|
+
// Node scope guard: a daemon hosting a base node + several worktree nodes must
|
|
2152
|
+
// not serve worktree A's transcript (or splice sibling worktree turns via the
|
|
2153
|
+
// native-history-by-workspace fallback) when a coordinator scoped the read to
|
|
2154
|
+
// worktree B. mesh_read_chat always passes the requested node's workspace as
|
|
2155
|
+
// args.workspace; refuse a CONFIRMED cross-workspace read rather than mix.
|
|
2156
|
+
{
|
|
2157
|
+
const guardSessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim() : '';
|
|
2158
|
+
if (guardSessionId && typeof args?.workspace === 'string' && args.workspace.trim()) {
|
|
2159
|
+
const verdict = evaluateReadChatNodeWorkspaceScope({
|
|
2160
|
+
targetSessionId: guardSessionId,
|
|
2161
|
+
intendedWorkspace: args.workspace,
|
|
2162
|
+
sessionWorkspace: resolveTargetSessionActualWorkspace(h, guardSessionId),
|
|
2163
|
+
});
|
|
2164
|
+
if (verdict.scoped) {
|
|
2165
|
+
LOG.info('Command', `[read_chat] node scope mismatch: session ${guardSessionId} workspace "${verdict.actual}" ≠ requested node workspace "${verdict.intended}" — refusing cross-worktree transcript`);
|
|
2166
|
+
return {
|
|
2167
|
+
success: false,
|
|
2168
|
+
code: 'read_chat_session_node_scope_mismatch',
|
|
2169
|
+
error: `Session ${guardSessionId} belongs to a different worktree (workspace "${verdict.actual}") than the requested node (workspace "${verdict.intended}"). Refusing to return a cross-worktree transcript — target the node that owns this session.`,
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
}
|
|
2103
2174
|
// Resolve provider in order: explicit agentType/providerType > registered session.
|
|
2104
2175
|
// Without this fallback, callers that only have a sessionId (e.g. a chat tail
|
|
2105
2176
|
// controller that just got handed a session ID over WS) get an empty result
|
|
@@ -2649,10 +2720,33 @@ export async function handleReadChat(h: CommandHelpers, args: any): Promise<Comm
|
|
|
2649
2720
|
});
|
|
2650
2721
|
|
|
2651
2722
|
if (supportsNative && !decision.nativeSelected) {
|
|
2723
|
+
// Dead-end: we are in the history-only path (no live PTY/ACP
|
|
2724
|
+
// adapter was found for this target session) AND provider-native
|
|
2725
|
+
// history is not safely mappable to the requested session
|
|
2726
|
+
// (no historySessionId stamp / workspace mismatch). Previously
|
|
2727
|
+
// this returned `success:false`, which the command logger emits
|
|
2728
|
+
// at warn level on EVERY poll (handler.ts logCommandEnd) —
|
|
2729
|
+
// mesh coordinators poll read_chat continuously, so a worker whose
|
|
2730
|
+
// transcript can never be safely mapped produced a 100% warn-log
|
|
2731
|
+
// storm with no recovery. Switch to a SOFT response: success with
|
|
2732
|
+
// empty messages + pending:true so the coordinator treats it as
|
|
2733
|
+
// "no live messages readable yet" rather than a hard failure, and
|
|
2734
|
+
// carry the machine-readable reason for debuggability. The normal
|
|
2735
|
+
// live-adapter path (above) and the safe-native return (below) are
|
|
2736
|
+
// unaffected — this is strictly the both-absent dead end.
|
|
2737
|
+
LOG.debug('Command', `[read_chat] soft pending: no live adapter and native history not safely mappable target=${String(args?.targetSessionId || '')} provider=${agentStr} reason=native_history_not_safely_available`);
|
|
2652
2738
|
return {
|
|
2653
|
-
success:
|
|
2739
|
+
success: true,
|
|
2740
|
+
pending: true,
|
|
2741
|
+
// Both signals are true here: we reached the history-only path
|
|
2742
|
+
// because no live adapter was found (`live_adapter_not_found`),
|
|
2743
|
+
// and native history is not safely mappable
|
|
2744
|
+
// (`native_history_not_safely_available`).
|
|
2745
|
+
reason: 'native_history_not_safely_available',
|
|
2746
|
+
reasons: ['live_adapter_not_found', 'native_history_not_safely_available'],
|
|
2654
2747
|
code: 'native_history_not_safely_available',
|
|
2655
|
-
|
|
2748
|
+
messages: [],
|
|
2749
|
+
status: 'idle',
|
|
2656
2750
|
providerSessionId: historyProviderSessionId,
|
|
2657
2751
|
messageSource: decision.messageSource,
|
|
2658
2752
|
transcriptProvenance: decision.messageSource,
|
|
@@ -19,7 +19,7 @@ import { loadState, saveState } from '../config/state-store.js';
|
|
|
19
19
|
import { getWorkspaceState, resolveLaunchDirectory } from '../config/workspaces.js';
|
|
20
20
|
import { appendRecentActivity } from '../config/recent-activity.js';
|
|
21
21
|
import { shortHash } from '../system/hash.js';
|
|
22
|
-
import { unregisterMeshCoordinator, getCoordinatorForSession } from '../mesh/coordinator-registry.js';
|
|
22
|
+
import { unregisterMeshCoordinator, getCoordinatorForSession, listCoordinatorsForWorkspace } from '../mesh/coordinator-registry.js';
|
|
23
23
|
import { upsertSavedProviderSession } from '../config/saved-sessions.js';
|
|
24
24
|
import { buildLegacyModelModeSummaryMetadata, normalizeProviderSummaryMetadata } from '../providers/summary-metadata.js';
|
|
25
25
|
import { CliProviderInstance } from '../providers/cli-provider-instance.js';
|
|
@@ -1069,7 +1069,33 @@ export class DaemonCliManager {
|
|
|
1069
1069
|
// Both restores are provider-agnostic — getSettings is keyed by provider type and the
|
|
1070
1070
|
// registry mark is type-independent.
|
|
1071
1071
|
const restoredSettings: Record<string, any> = { ...this.providerLoader.getSettings(normalizedType) };
|
|
1072
|
-
|
|
1072
|
+
// Primary rebind: exact persisted-registry match by runtimeId (stable across
|
|
1073
|
+
// restart, see session-host runtimeId = runtimeRecord.sessionId).
|
|
1074
|
+
let coordinatorEntry = getCoordinatorForSession(record.runtimeId);
|
|
1075
|
+
// CORDBADGE fallback: the by-id match misses when a coordinator's runtime
|
|
1076
|
+
// re-attaches under a different runtimeId than the one it was registered with
|
|
1077
|
+
// (the registry survived, but its key no longer lines up). Without a rebind the
|
|
1078
|
+
// restored session silently loses meshCoordinatorFor → the coordinator badge and
|
|
1079
|
+
// selfIdentification block vanish and pending mesh events stop draining into its
|
|
1080
|
+
// PTY, and the only recovery is a manual coordinator restart. Recover the mark
|
|
1081
|
+
// from the persisted registry scoped to this exact workspace, but ONLY when it is
|
|
1082
|
+
// UNAMBIGUOUS: exactly one registered coordinator for this workspace AND its
|
|
1083
|
+
// cliType matches the restored session's type. The registry never holds worker
|
|
1084
|
+
// sessions (only launch_mesh_coordinator registrations), so this cannot mis-mark a
|
|
1085
|
+
// delegated worker; the uniqueness + cliType gate keeps it from guessing when two
|
|
1086
|
+
// coordinators ever shared a workspace. Anything ambiguous stays unbound (we would
|
|
1087
|
+
// rather miss a badge than mis-attribute one).
|
|
1088
|
+
if (!coordinatorEntry?.meshId && record.workspace) {
|
|
1089
|
+
const workspaceCoordinators = listCoordinatorsForWorkspace(record.workspace)
|
|
1090
|
+
.filter(e => e.meshId && (!e.cliType || e.cliType === record.cliType));
|
|
1091
|
+
if (workspaceCoordinators.length === 1) {
|
|
1092
|
+
coordinatorEntry = workspaceCoordinators[0];
|
|
1093
|
+
LOG.info(
|
|
1094
|
+
'CLI',
|
|
1095
|
+
`↻ Rebound coordinator mark by workspace for ${record.runtimeKey || record.runtimeId} (mesh ${coordinatorEntry.meshId} @ ${record.workspace}); registry key did not match runtimeId`
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1073
1099
|
if (coordinatorEntry?.meshId) {
|
|
1074
1100
|
restoredSettings.meshCoordinatorFor = coordinatorEntry.meshId;
|
|
1075
1101
|
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER HIGH family — command registry.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates the high-coupling command handlers extracted from
|
|
5
|
+
* DaemonCommandRouter.executeDaemonCommand into a single cmd → handler map. The
|
|
6
|
+
* router consults this registry after the LOW- and MED-family registries and
|
|
7
|
+
* before its remaining switch (registry hit → return the handler result; miss →
|
|
8
|
+
* fall through to the switch / CommandHandler delegation), so the facade and
|
|
9
|
+
* dispatch semantics are unchanged.
|
|
10
|
+
*
|
|
11
|
+
* HIGH handlers are the most router-coupled: beyond the MED collaborators they
|
|
12
|
+
* reach the router's aggregate-status memory cache and running-refine-job table.
|
|
13
|
+
* The router binds those onto HighFamilyContext at dispatch — see types.ts.
|
|
14
|
+
*/
|
|
15
|
+
import { meshEventsHandlers } from './mesh-events.js';
|
|
16
|
+
import { meshCoordinatorLaunchHandlers } from './mesh-coordinator-launch.js';
|
|
17
|
+
import { meshStatusHandlers } from './mesh-status.js';
|
|
18
|
+
import type { HighFamilyRegistry } from './types.js';
|
|
19
|
+
|
|
20
|
+
export type { HighFamilyContext, HighFamilyHandler, HighFamilyRegistry } from './types.js';
|
|
21
|
+
|
|
22
|
+
export const highFamilyRegistry: HighFamilyRegistry = new Map(
|
|
23
|
+
Object.entries({
|
|
24
|
+
...meshEventsHandlers,
|
|
25
|
+
...meshCoordinatorLaunchHandlers,
|
|
26
|
+
...meshStatusHandlers,
|
|
27
|
+
}),
|
|
28
|
+
);
|