@adhdev/daemon-core 0.9.82-rc.360 → 0.9.82-rc.361
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/cli-manager.d.ts +13 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/types.d.ts +16 -0
- package/dist/commands/router.d.ts +0 -1
- package/dist/index.js +2074 -2008
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2073 -2007
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-utils.d.ts +0 -2
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +69 -4
- package/src/commands/low-family/index.ts +23 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +274 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/types.ts +19 -0
- package/src/commands/router.ts +15 -555
- package/src/mesh/contracts.ts +12 -3
- package/src/mesh/mesh-events-coordinator.ts +1 -1
- package/src/mesh/mesh-events-utils.ts +0 -20
- package/src/providers/cli-provider-instance.ts +16 -3
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { SessionRecoveryContext } from './mesh-ledger.js';
|
|
2
2
|
export declare function readNonEmptyString(value: unknown): string;
|
|
3
3
|
export declare function readRecord(value: unknown): Record<string, unknown> | undefined;
|
|
4
|
-
export declare function canonicalDaemonId(value: unknown): string;
|
|
5
|
-
export declare function sameDaemonId(a: unknown, b: unknown): boolean;
|
|
6
4
|
/**
|
|
7
5
|
* The relay-safety metadata a worker session must carry so that its completion /
|
|
8
6
|
* generating events route back to the coordinator proactively (without waiting for
|
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.361",
|
|
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.361",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -148,6 +148,18 @@ function hasCompletedStartingLaunch(adapter: any): boolean {
|
|
|
148
148
|
return hasFinalAssistantMessage(parsedStatus?.messages);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
+
/**
|
|
152
|
+
* WTCLAIM (B): compare two workspace paths for node scoping. Normalizes separator
|
|
153
|
+
* style, trailing slashes, and case (Windows paths are case-insensitive; the
|
|
154
|
+
* coordinator-supplied node.workspace and the adapter's launch workingDir can
|
|
155
|
+
* differ only in those) so a base node and a worktree clone are still told apart
|
|
156
|
+
* by their distinct workspace roots.
|
|
157
|
+
*/
|
|
158
|
+
function normalizeDirForCompare(dir?: string): string {
|
|
159
|
+
if (typeof dir !== 'string') return '';
|
|
160
|
+
return dir.trim().replace(/[\\/]+/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
161
|
+
}
|
|
162
|
+
|
|
151
163
|
function shouldSuppressStaleParsedBusyStatus(adapterStatus: string, parsedStatus: any, adapter: any): boolean {
|
|
152
164
|
const parsedRawStatus = normalizeAgentStatus(parsedStatus?.status);
|
|
153
165
|
if (!BUSY_AGENT_STATUSES.has(parsedRawStatus)) return false;
|
|
@@ -1142,6 +1154,40 @@ export class DaemonCliManager {
|
|
|
1142
1154
|
return adapter ? { adapter, key: ik } : null;
|
|
1143
1155
|
}
|
|
1144
1156
|
|
|
1157
|
+
/**
|
|
1158
|
+
* WTCLAIM (B): resolve the adapter for a mesh dispatch that named a node
|
|
1159
|
+
* (meshContext.nodeId) but carried no explicit session. Matches by the
|
|
1160
|
+
* instance's bound mesh node id (settings.meshNodeId, falling back to the
|
|
1161
|
+
* sticky meshLastNodeId) first, then by the node workspace (workingDir).
|
|
1162
|
+
*
|
|
1163
|
+
* Unlike findAdapter's step-2 fuzzy fallback, this NEVER degrades to a
|
|
1164
|
+
* provider-only first-match: on a daemon hosting BOTH a base node and a
|
|
1165
|
+
* cloned worktree node (same daemonId), that fuzzy match could land a
|
|
1166
|
+
* worktree-targeted task on the base session. Returns null when no session
|
|
1167
|
+
* is bound to this node so the caller fails closed.
|
|
1168
|
+
*/
|
|
1169
|
+
private findMeshNodeAdapter(agentType: string, nodeId: string, dir?: string): { adapter: CliAdapter; key: string } | null {
|
|
1170
|
+
const instanceManager = this.deps.getInstanceManager();
|
|
1171
|
+
const targetDir = normalizeDirForCompare(dir);
|
|
1172
|
+
let workspaceMatch: { adapter: CliAdapter; key: string } | null = null;
|
|
1173
|
+
for (const [k, a] of this.adapters) {
|
|
1174
|
+
if (a.cliType !== agentType) continue;
|
|
1175
|
+
const settings = (instanceManager?.getInstance(k) as any)?.getState?.()?.settings as Record<string, unknown> | undefined;
|
|
1176
|
+
const boundNodeId = (typeof settings?.meshNodeId === 'string' && settings.meshNodeId.trim())
|
|
1177
|
+
? settings.meshNodeId.trim()
|
|
1178
|
+
: (typeof settings?.meshLastNodeId === 'string' ? settings.meshLastNodeId.trim() : '');
|
|
1179
|
+
// Exact node binding wins immediately (most precise).
|
|
1180
|
+
if (boundNodeId && boundNodeId === nodeId) return { adapter: a, key: k };
|
|
1181
|
+
// Workspace identity is the secondary signal for a session not (yet)
|
|
1182
|
+
// stamped with a node id — a base node and a worktree clone always have
|
|
1183
|
+
// distinct workspaces, so this still separates them.
|
|
1184
|
+
if (!workspaceMatch && targetDir && normalizeDirForCompare(a.workingDir) === targetDir) {
|
|
1185
|
+
workspaceMatch = { adapter: a, key: k };
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
return workspaceMatch;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1145
1191
|
// ─── CLI command handling ────────────────────────────
|
|
1146
1192
|
|
|
1147
1193
|
async handleCliCommand(cmd: string, args: any): Promise<CommandResult | null> {
|
|
@@ -1343,10 +1389,29 @@ export class DaemonCliManager {
|
|
|
1343
1389
|
const action = args?.action;
|
|
1344
1390
|
if (!agentType || !action) throw new Error('agentType and action required');
|
|
1345
1391
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1392
|
+
// WTCLAIM (B): a mesh dispatch that named a node (meshContext.nodeId)
|
|
1393
|
+
// but resolved no explicit session must be scoped to THAT node's
|
|
1394
|
+
// session — never routed by findAdapter's provider-only fuzzy fallback,
|
|
1395
|
+
// which on a daemon hosting both a base node and a cloned worktree node
|
|
1396
|
+
// (same daemonId) could land a worktree task on the base session. Fail
|
|
1397
|
+
// closed when no session is bound to the node so the coordinator
|
|
1398
|
+
// launches/retries instead of mis-landing the work.
|
|
1399
|
+
const meshScopeNodeId = (() => {
|
|
1400
|
+
const mc = (args as any)?.meshContext;
|
|
1401
|
+
return mc && typeof mc === 'object' && typeof mc.nodeId === 'string' ? mc.nodeId.trim() : '';
|
|
1402
|
+
})();
|
|
1403
|
+
let found: { adapter: CliAdapter; key: string } | null;
|
|
1404
|
+
if (meshScopeNodeId && !args?.targetSessionId) {
|
|
1405
|
+
found = this.findMeshNodeAdapter(agentType, meshScopeNodeId, args?.dir);
|
|
1406
|
+
if (!found) {
|
|
1407
|
+
throw new Error(`No mesh worker session bound to node '${meshScopeNodeId}' for agent '${agentType}' on this daemon; refusing provider-only fuzzy match to avoid cross-node dispatch`);
|
|
1408
|
+
}
|
|
1409
|
+
} else {
|
|
1410
|
+
found = this.findAdapter(agentType, {
|
|
1411
|
+
dir: args?.dir,
|
|
1412
|
+
instanceKey: args?.targetSessionId,
|
|
1413
|
+
});
|
|
1414
|
+
}
|
|
1350
1415
|
if (!found) throw new Error(`CLI agent not running: ${agentType}`);
|
|
1351
1416
|
const { adapter, key } = found;
|
|
1352
1417
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER LOW family — command registry.
|
|
3
|
+
*
|
|
4
|
+
* Aggregates the low-coupling command handlers extracted from
|
|
5
|
+
* DaemonCommandRouter.executeDaemonCommand into a single cmd → handler map.
|
|
6
|
+
* The router consults this registry before its switch (registry hit → return the
|
|
7
|
+
* handler result; miss → fall through to the remaining switch / CommandHandler
|
|
8
|
+
* delegation), so the facade and dispatch semantics are unchanged.
|
|
9
|
+
*/
|
|
10
|
+
import { sessionHostHandlers } from './session-host.js';
|
|
11
|
+
import { specProviderDevHandlers } from './spec-providerdev.js';
|
|
12
|
+
import { refineConfigHandlers } from './refine-config.js';
|
|
13
|
+
import type { LowFamilyRegistry } from './types.js';
|
|
14
|
+
|
|
15
|
+
export type { LowFamilyContext, LowFamilyHandler, LowFamilyRegistry } from './types.js';
|
|
16
|
+
|
|
17
|
+
export const lowFamilyRegistry: LowFamilyRegistry = new Map(
|
|
18
|
+
Object.entries({
|
|
19
|
+
...sessionHostHandlers,
|
|
20
|
+
...specProviderDevHandlers,
|
|
21
|
+
...refineConfigHandlers,
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER LOW family — mesh refine-config / change-impact config commands.
|
|
3
|
+
*
|
|
4
|
+
* Schema/validate/suggest endpoints for the repo mesh refine config and the
|
|
5
|
+
* change-impact config. Pure: every handler is a function of args only (it reads
|
|
6
|
+
* no router deps), so ctx is unused. Extracted verbatim from executeDaemonCommand;
|
|
7
|
+
* the config helpers are imported from their source modules, identical to router.ts.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
CHANGE_IMPACT_CONFIG_LOCATIONS,
|
|
11
|
+
CHANGE_IMPACT_CONFIG_SCHEMA,
|
|
12
|
+
loadChangeImpactConfig,
|
|
13
|
+
suggestChangeImpactConfig,
|
|
14
|
+
validateChangeImpactConfig,
|
|
15
|
+
} from '../../git/change-impact-config.js';
|
|
16
|
+
import {
|
|
17
|
+
MESH_REFINE_CONFIG_LOCATIONS,
|
|
18
|
+
MESH_REFINE_CONFIG_SCHEMA,
|
|
19
|
+
loadMeshRefineConfig,
|
|
20
|
+
suggestMeshRefineConfig,
|
|
21
|
+
validateMeshRefineConfig,
|
|
22
|
+
} from '../../mesh/refine-config.js';
|
|
23
|
+
import {
|
|
24
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
25
|
+
MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
26
|
+
} from '../../mesh/worktree-bootstrap-config.js';
|
|
27
|
+
import type { LowFamilyContext, LowFamilyHandler } from './types.js';
|
|
28
|
+
|
|
29
|
+
export const refineConfigHandlers: Record<string, LowFamilyHandler> = {
|
|
30
|
+
get_mesh_refine_config_schema: async (_ctx: LowFamilyContext, _args: any) => {
|
|
31
|
+
return {
|
|
32
|
+
success: true,
|
|
33
|
+
schema: MESH_REFINE_CONFIG_SCHEMA,
|
|
34
|
+
locations: MESH_REFINE_CONFIG_LOCATIONS,
|
|
35
|
+
worktreeBootstrap: {
|
|
36
|
+
schema: MESH_WORKTREE_BOOTSTRAP_CONFIG_SCHEMA,
|
|
37
|
+
locations: MESH_WORKTREE_BOOTSTRAP_CONFIG_LOCATIONS,
|
|
38
|
+
sourceOfTruth: 'repo worktree bootstrap config',
|
|
39
|
+
runBehavior: 'When present and enabled, clone_mesh_node runs commands after submodule initialization and records status on the worktree node.',
|
|
40
|
+
},
|
|
41
|
+
sourceOfTruth: 'repo mesh/refine config',
|
|
42
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
43
|
+
};
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
validate_mesh_refine_config: async (_ctx: LowFamilyContext, args: any) => {
|
|
47
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
48
|
+
const mesh = args?.inlineMesh || {};
|
|
49
|
+
const loaded = args?.config !== undefined
|
|
50
|
+
? { config: args.config, source: 'inline', sourceType: 'mesh_policy' as const }
|
|
51
|
+
: loadMeshRefineConfig(mesh, workspace);
|
|
52
|
+
const validation = loaded.config
|
|
53
|
+
? validateMeshRefineConfig(loaded.config, loaded.source)
|
|
54
|
+
: { valid: false, errors: [((loaded as { error?: string }).error) || 'repo mesh/refine config unavailable'], commands: [], rejectedCommands: [] };
|
|
55
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
56
|
+
},
|
|
57
|
+
|
|
58
|
+
suggest_mesh_refine_config: async (_ctx: LowFamilyContext, args: any) => {
|
|
59
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
60
|
+
const mesh = args?.inlineMesh || {};
|
|
61
|
+
return {
|
|
62
|
+
success: true,
|
|
63
|
+
...suggestMeshRefineConfig(mesh, workspace),
|
|
64
|
+
note: 'Suggestions are heuristic scaffold only; Refinery will not execute them until saved into repo mesh/refine config.',
|
|
65
|
+
};
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
get_mesh_change_impact_config_schema: async (_ctx: LowFamilyContext, _args: any) => {
|
|
69
|
+
return {
|
|
70
|
+
success: true,
|
|
71
|
+
schema: CHANGE_IMPACT_CONFIG_SCHEMA,
|
|
72
|
+
locations: CHANGE_IMPACT_CONFIG_LOCATIONS,
|
|
73
|
+
sourceOfTruth: 'repo change-impact config',
|
|
74
|
+
heuristicRole: 'suggestions_only_not_execution_path',
|
|
75
|
+
note: 'Declarative config only — JSON/YAML are parsed but never executed. Defines which package/file changes require a daemon rebuild/restart vs. a web-only redeploy vs. nothing.',
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
|
|
79
|
+
validate_mesh_change_impact_config: async (_ctx: LowFamilyContext, args: any) => {
|
|
80
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
81
|
+
if (args?.config !== undefined) {
|
|
82
|
+
const validation = validateChangeImpactConfig(args.config, 'inline');
|
|
83
|
+
return { success: validation.valid, source: 'inline', sourceType: 'mesh_policy', ...validation };
|
|
84
|
+
}
|
|
85
|
+
const loaded = loadChangeImpactConfig(workspace);
|
|
86
|
+
if (loaded.sourceType === 'repo_file') {
|
|
87
|
+
const validation = validateChangeImpactConfig(loaded.config, loaded.source);
|
|
88
|
+
return { success: validation.valid, ...loaded, ...validation };
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
success: false,
|
|
92
|
+
...loaded,
|
|
93
|
+
valid: false,
|
|
94
|
+
errors: [loaded.error || 'repo change-impact config unavailable'],
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
suggest_mesh_change_impact_config: async (_ctx: LowFamilyContext, args: any) => {
|
|
99
|
+
const workspace = typeof args?.workspace === 'string' ? args.workspace : process.cwd();
|
|
100
|
+
return {
|
|
101
|
+
success: true,
|
|
102
|
+
...suggestChangeImpactConfig(workspace),
|
|
103
|
+
note: 'Suggestions are heuristic scaffold only; the draft must be reviewed and saved into repo change-impact config before it takes effect. Nothing is executed.',
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER LOW family — session-host control commands.
|
|
3
|
+
*
|
|
4
|
+
* Extracted verbatim from DaemonCommandRouter.executeDaemonCommand. Each handler
|
|
5
|
+
* reads only ctx.deps.sessionHostControl (+ cliManager for the resume/restart
|
|
6
|
+
* hosted-session restore) and returns the same CommandRouterResult as before.
|
|
7
|
+
* The trace/summarize helpers were `this`-free module functions in router.ts and
|
|
8
|
+
* are relocated here unchanged.
|
|
9
|
+
*/
|
|
10
|
+
import type { HostedCliRuntimeDescriptor } from '../cli-manager.js';
|
|
11
|
+
import { recordDebugTrace } from '../../logging/debug-trace.js';
|
|
12
|
+
import { getSessionHostSurfaceKind, partitionSessionHostRecords } from '../../session-host/runtime-surface.js';
|
|
13
|
+
import type { LowFamilyContext, LowFamilyHandler } from './types.js';
|
|
14
|
+
|
|
15
|
+
function toHostedCliRuntimeDescriptor(record: any): HostedCliRuntimeDescriptor | null {
|
|
16
|
+
if (!record || typeof record !== 'object') return null;
|
|
17
|
+
const runtimeId = typeof record.sessionId === 'string' ? record.sessionId : '';
|
|
18
|
+
const cliType = typeof record.providerType === 'string' ? record.providerType : '';
|
|
19
|
+
const workspace = typeof record.workspace === 'string' ? record.workspace : '';
|
|
20
|
+
if (!runtimeId || !cliType || !workspace) return null;
|
|
21
|
+
return {
|
|
22
|
+
runtimeId,
|
|
23
|
+
runtimeKey: typeof record.runtimeKey === 'string' ? record.runtimeKey : undefined,
|
|
24
|
+
displayName: typeof record.displayName === 'string' ? record.displayName : undefined,
|
|
25
|
+
workspaceLabel: typeof record.workspaceLabel === 'string' ? record.workspaceLabel : undefined,
|
|
26
|
+
lifecycle: typeof record.lifecycle === 'string' ? record.lifecycle as HostedCliRuntimeDescriptor['lifecycle'] : undefined,
|
|
27
|
+
recoveryState: typeof record.meta?.runtimeRecoveryState === 'string'
|
|
28
|
+
? String(record.meta.runtimeRecoveryState)
|
|
29
|
+
: null,
|
|
30
|
+
cliType,
|
|
31
|
+
workspace,
|
|
32
|
+
cliArgs: Array.isArray(record.meta?.cliArgs) ? record.meta.cliArgs as string[] : [],
|
|
33
|
+
providerSessionId: typeof record.meta?.providerSessionId === 'string'
|
|
34
|
+
? String(record.meta.providerSessionId)
|
|
35
|
+
: undefined,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function getWriteConflictOwnerClientId(error: unknown): string | undefined {
|
|
40
|
+
const message = typeof error === 'string'
|
|
41
|
+
? error
|
|
42
|
+
: error instanceof Error
|
|
43
|
+
? error.message
|
|
44
|
+
: '';
|
|
45
|
+
const match = /^Write owned by\s+(.+)$/.exec(message.trim());
|
|
46
|
+
return match?.[1]?.trim() || undefined;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function summarizeSessionHostRecord(result: unknown): Record<string, unknown> {
|
|
50
|
+
if (!result || typeof result !== 'object') return {};
|
|
51
|
+
const record = result as Record<string, any>;
|
|
52
|
+
return {
|
|
53
|
+
runtimeKey: typeof record.runtimeKey === 'string' ? record.runtimeKey : undefined,
|
|
54
|
+
lifecycle: typeof record.lifecycle === 'string' ? record.lifecycle : undefined,
|
|
55
|
+
surfaceKind: getSessionHostSurfaceKind(record as any),
|
|
56
|
+
attachedClientCount: Array.isArray(record.attachedClients) ? record.attachedClients.length : undefined,
|
|
57
|
+
hasWriteOwner: !!record.writeOwner,
|
|
58
|
+
writeOwnerClientId: typeof record.writeOwner?.clientId === 'string' ? record.writeOwner.clientId : undefined,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function summarizeSessionHostRecords(result: unknown): Record<string, unknown> {
|
|
63
|
+
const records = Array.isArray(result) ? result : [];
|
|
64
|
+
const groups = partitionSessionHostRecords(records as any[]);
|
|
65
|
+
return {
|
|
66
|
+
sessionCount: records.length,
|
|
67
|
+
liveRuntimeCount: groups.liveRuntimes.length,
|
|
68
|
+
recoverySnapshotCount: groups.recoverySnapshots.length,
|
|
69
|
+
inactiveRecordCount: groups.inactiveRecords.length,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function summarizeSessionHostDiagnostics(result: unknown): Record<string, unknown> {
|
|
74
|
+
const diagnostics = result && typeof result === 'object' ? result as Record<string, any> : {};
|
|
75
|
+
const sessions = Array.isArray(diagnostics.sessions) ? diagnostics.sessions : [];
|
|
76
|
+
return {
|
|
77
|
+
runtimeCount: typeof diagnostics.runtimeCount === 'number' ? diagnostics.runtimeCount : undefined,
|
|
78
|
+
...summarizeSessionHostRecords(sessions),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function summarizeSessionHostPruneResult(result: unknown): Record<string, unknown> {
|
|
83
|
+
const value = result && typeof result === 'object' ? result as Record<string, any> : {};
|
|
84
|
+
return {
|
|
85
|
+
duplicateGroupCount: typeof value.duplicateGroupCount === 'number' ? value.duplicateGroupCount : undefined,
|
|
86
|
+
prunedCount: Array.isArray(value.prunedSessionIds) ? value.prunedSessionIds.length : undefined,
|
|
87
|
+
keptCount: Array.isArray(value.keptSessionIds) ? value.keptSessionIds.length : undefined,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function traceSessionHostAction<T>(
|
|
92
|
+
action: string,
|
|
93
|
+
args: any,
|
|
94
|
+
run: () => Promise<T>,
|
|
95
|
+
summarizeResult?: (result: T) => Record<string, unknown>,
|
|
96
|
+
): Promise<T> {
|
|
97
|
+
const interactionId = typeof args?._interactionId === 'string' ? args._interactionId : undefined;
|
|
98
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : undefined;
|
|
99
|
+
const requestedPayload: Record<string, unknown> = { action };
|
|
100
|
+
if (sessionId) requestedPayload.sessionId = sessionId;
|
|
101
|
+
if (typeof args?.clientId === 'string') requestedPayload.clientId = args.clientId;
|
|
102
|
+
if (typeof args?.signal === 'string') requestedPayload.signal = args.signal;
|
|
103
|
+
if (typeof args?.providerType === 'string') requestedPayload.providerType = args.providerType;
|
|
104
|
+
if (typeof args?.workspace === 'string') requestedPayload.workspace = args.workspace;
|
|
105
|
+
if (typeof args?.dryRun === 'boolean') requestedPayload.dryRun = args.dryRun;
|
|
106
|
+
|
|
107
|
+
recordDebugTrace({
|
|
108
|
+
interactionId,
|
|
109
|
+
category: 'session_host',
|
|
110
|
+
stage: 'action_requested',
|
|
111
|
+
level: 'info',
|
|
112
|
+
sessionId,
|
|
113
|
+
payload: requestedPayload,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const result = await run();
|
|
118
|
+
recordDebugTrace({
|
|
119
|
+
interactionId,
|
|
120
|
+
category: 'session_host',
|
|
121
|
+
stage: 'action_result',
|
|
122
|
+
level: 'info',
|
|
123
|
+
sessionId,
|
|
124
|
+
payload: {
|
|
125
|
+
...requestedPayload,
|
|
126
|
+
success: true,
|
|
127
|
+
...(summarizeResult ? summarizeResult(result) : {}),
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
return result;
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
recordDebugTrace({
|
|
133
|
+
interactionId,
|
|
134
|
+
category: 'session_host',
|
|
135
|
+
stage: 'action_failed',
|
|
136
|
+
level: 'error',
|
|
137
|
+
sessionId,
|
|
138
|
+
payload: {
|
|
139
|
+
...requestedPayload,
|
|
140
|
+
error: error?.message || String(error),
|
|
141
|
+
failureKind: getWriteConflictOwnerClientId(error) ? 'write_conflict' : 'request_failed',
|
|
142
|
+
conflictOwnerClientId: getWriteConflictOwnerClientId(error),
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
throw error;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const sessionHostHandlers: Record<string, LowFamilyHandler> = {
|
|
150
|
+
session_host_get_diagnostics: async (ctx: LowFamilyContext, args: any) => {
|
|
151
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
152
|
+
const diagnostics = await traceSessionHostAction('session_host_get_diagnostics', args, () => ctx.deps.sessionHostControl!.getDiagnostics({
|
|
153
|
+
includeSessions: args?.includeSessions !== false,
|
|
154
|
+
limit: Number(args?.limit) || undefined,
|
|
155
|
+
}), (result) => ({
|
|
156
|
+
includeSessions: args?.includeSessions !== false,
|
|
157
|
+
limit: Number(args?.limit) || undefined,
|
|
158
|
+
...summarizeSessionHostDiagnostics(result),
|
|
159
|
+
}));
|
|
160
|
+
return { success: true, diagnostics };
|
|
161
|
+
},
|
|
162
|
+
|
|
163
|
+
session_host_list_sessions: async (ctx: LowFamilyContext, args: any) => {
|
|
164
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
165
|
+
const sessions = await traceSessionHostAction('session_host_list_sessions', args, () => ctx.deps.sessionHostControl!.listSessions(), (records) => summarizeSessionHostRecords(records));
|
|
166
|
+
return { success: true, sessions };
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
session_host_stop_session: async (ctx: LowFamilyContext, args: any) => {
|
|
170
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
171
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
172
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
173
|
+
const record = await traceSessionHostAction('session_host_stop_session', args, () => ctx.deps.sessionHostControl!.stopSession(sessionId), (result) => summarizeSessionHostRecord(result));
|
|
174
|
+
return { success: true, record };
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
session_host_resume_session: async (ctx: LowFamilyContext, args: any) => {
|
|
178
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
179
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
180
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
181
|
+
const record = await traceSessionHostAction('session_host_resume_session', args, async () => {
|
|
182
|
+
const nextRecord = await ctx.deps.sessionHostControl!.resumeSession(sessionId);
|
|
183
|
+
const hosted = toHostedCliRuntimeDescriptor(nextRecord);
|
|
184
|
+
if (hosted) {
|
|
185
|
+
await ctx.deps.cliManager.restoreHostedSessions([hosted]);
|
|
186
|
+
}
|
|
187
|
+
return nextRecord;
|
|
188
|
+
}, (result) => ({
|
|
189
|
+
...summarizeSessionHostRecord(result),
|
|
190
|
+
restoredHostedSession: !!toHostedCliRuntimeDescriptor(result),
|
|
191
|
+
}));
|
|
192
|
+
return { success: true, record };
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
session_host_restart_session: async (ctx: LowFamilyContext, args: any) => {
|
|
196
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
197
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
198
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
199
|
+
const record = await traceSessionHostAction('session_host_restart_session', args, async () => {
|
|
200
|
+
const nextRecord = await ctx.deps.sessionHostControl!.restartSession(sessionId);
|
|
201
|
+
const hosted = toHostedCliRuntimeDescriptor(nextRecord);
|
|
202
|
+
if (hosted) {
|
|
203
|
+
await ctx.deps.cliManager.restoreHostedSessions([hosted]);
|
|
204
|
+
}
|
|
205
|
+
return nextRecord;
|
|
206
|
+
}, (result) => ({
|
|
207
|
+
...summarizeSessionHostRecord(result),
|
|
208
|
+
restoredHostedSession: !!toHostedCliRuntimeDescriptor(result),
|
|
209
|
+
}));
|
|
210
|
+
return { success: true, record };
|
|
211
|
+
},
|
|
212
|
+
|
|
213
|
+
session_host_send_signal: async (ctx: LowFamilyContext, args: any) => {
|
|
214
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
215
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
216
|
+
const signal = typeof args?.signal === 'string' ? args.signal : '';
|
|
217
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
218
|
+
if (!signal) return { success: false, error: 'signal required' };
|
|
219
|
+
const record = await traceSessionHostAction('session_host_send_signal', args, () => ctx.deps.sessionHostControl!.sendSignal(sessionId, signal), (result) => summarizeSessionHostRecord(result));
|
|
220
|
+
return { success: true, record };
|
|
221
|
+
},
|
|
222
|
+
|
|
223
|
+
session_host_force_detach_client: async (ctx: LowFamilyContext, args: any) => {
|
|
224
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
225
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
226
|
+
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
227
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
228
|
+
if (!clientId) return { success: false, error: 'clientId required' };
|
|
229
|
+
const record = await traceSessionHostAction('session_host_force_detach_client', args, () => ctx.deps.sessionHostControl!.forceDetachClient(sessionId, clientId), (result) => summarizeSessionHostRecord(result));
|
|
230
|
+
return { success: true, record };
|
|
231
|
+
},
|
|
232
|
+
|
|
233
|
+
session_host_prune_duplicate_sessions: async (ctx: LowFamilyContext, args: any) => {
|
|
234
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
235
|
+
const result = await traceSessionHostAction('session_host_prune_duplicate_sessions', args, () => ctx.deps.sessionHostControl!.pruneDuplicateSessions({
|
|
236
|
+
providerType: typeof args?.providerType === 'string' ? args.providerType : undefined,
|
|
237
|
+
workspace: typeof args?.workspace === 'string' ? args.workspace : undefined,
|
|
238
|
+
dryRun: args?.dryRun === true,
|
|
239
|
+
}), (value) => summarizeSessionHostPruneResult(value));
|
|
240
|
+
return { success: true, result };
|
|
241
|
+
},
|
|
242
|
+
|
|
243
|
+
session_host_acquire_write: async (ctx: LowFamilyContext, args: any) => {
|
|
244
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
245
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
246
|
+
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
247
|
+
const ownerType = args?.ownerType === 'agent' ? 'agent' : 'user';
|
|
248
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
249
|
+
if (!clientId) return { success: false, error: 'clientId required' };
|
|
250
|
+
const record = await traceSessionHostAction('session_host_acquire_write', args, () => ctx.deps.sessionHostControl!.acquireWrite({
|
|
251
|
+
sessionId,
|
|
252
|
+
clientId,
|
|
253
|
+
ownerType,
|
|
254
|
+
force: args?.force !== false,
|
|
255
|
+
}), (result) => ({
|
|
256
|
+
...summarizeSessionHostRecord(result),
|
|
257
|
+
ownerType,
|
|
258
|
+
}));
|
|
259
|
+
return { success: true, record };
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
session_host_release_write: async (ctx: LowFamilyContext, args: any) => {
|
|
263
|
+
if (!ctx.deps.sessionHostControl) return { success: false, error: 'Session host control unavailable' };
|
|
264
|
+
const sessionId = typeof args?.sessionId === 'string' ? args.sessionId : '';
|
|
265
|
+
const clientId = typeof args?.clientId === 'string' ? args.clientId : '';
|
|
266
|
+
if (!sessionId) return { success: false, error: 'sessionId required' };
|
|
267
|
+
if (!clientId) return { success: false, error: 'clientId required' };
|
|
268
|
+
const record = await traceSessionHostAction('session_host_release_write', args, () => ctx.deps.sessionHostControl!.releaseWrite({
|
|
269
|
+
sessionId,
|
|
270
|
+
clientId,
|
|
271
|
+
}), (result) => summarizeSessionHostRecord(result));
|
|
272
|
+
return { success: true, record };
|
|
273
|
+
},
|
|
274
|
+
};
|