@adhdev/daemon-core 0.9.82-rc.448 → 0.9.82-rc.449
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 +10 -0
- package/dist/commands/med-family/types.d.ts +10 -0
- package/dist/index.js +64 -20
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +64 -20
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/adapter.d.ts +7 -0
- package/dist/providers/spec/fsm-driver.d.ts +6 -0
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +41 -17
- package/src/commands/med-family/mesh-crud.ts +12 -1
- package/src/commands/med-family/types.ts +10 -0
- package/src/commands/router.ts +46 -1
- package/src/providers/spec/adapter.ts +11 -0
- package/src/providers/spec/cli-adapter.ts +9 -1
- package/src/providers/spec/fsm-driver.ts +9 -0
|
@@ -80,6 +80,13 @@ export declare class TerminalAdapter {
|
|
|
80
80
|
col: number;
|
|
81
81
|
};
|
|
82
82
|
send_keys(text: string): void;
|
|
83
|
+
/** Forward runtime metadata (meshNodeId, workspaceLabel, lifecycle, …) to
|
|
84
|
+
* the underlying transport so it reaches the session registry. The spec
|
|
85
|
+
* path previously dropped everything but providerSessionId here, which
|
|
86
|
+
* left autoLaunch's meshNodeId stamp unbound on the record (see
|
|
87
|
+
* SESSION-ACCUMULATION-LEAK). No-op when the transport does not support
|
|
88
|
+
* metadata updates (e.g. plain node-pty). */
|
|
89
|
+
updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
|
|
83
90
|
/** Debug-only: most-recent PTY input/output/resize/cursor events, oldest
|
|
84
91
|
* first. Pure observation — never consulted by the FSM. */
|
|
85
92
|
getEventTimeline(limit?: number): SpecPtyEvent[];
|
|
@@ -119,6 +119,7 @@ export interface ISpecDriver {
|
|
|
119
119
|
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
120
120
|
start(): void;
|
|
121
121
|
dispatch(cmd: DashboardCommand): void;
|
|
122
|
+
updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
|
|
122
123
|
snapshot(): string;
|
|
123
124
|
getCursorPosition(): {
|
|
124
125
|
row: number;
|
|
@@ -232,6 +233,11 @@ export declare class FsmDriver implements ISpecDriver {
|
|
|
232
233
|
* declares no prime, so non-focus-gated CLIs are never poked. */
|
|
233
234
|
private sendSpawnPrime;
|
|
234
235
|
dispatch(cmd: DashboardCommand): void;
|
|
236
|
+
/** Forward runtime metadata to the terminal transport so mesh binding
|
|
237
|
+
* fields (meshNodeId / meshNodeFor / workspaceLabel / lifecycle) reach
|
|
238
|
+
* the session registry. Not a DashboardCommand — this is a control-plane
|
|
239
|
+
* update, not user input. */
|
|
240
|
+
updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
|
|
235
241
|
snapshot(): string;
|
|
236
242
|
getCursorPosition(): {
|
|
237
243
|
row: number;
|
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.449",
|
|
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.449",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -209,6 +209,16 @@ export interface CliTransportFactoryParams {
|
|
|
209
209
|
cliArgs?: string[];
|
|
210
210
|
providerSessionId?: string;
|
|
211
211
|
attachExisting?: boolean;
|
|
212
|
+
/**
|
|
213
|
+
* Launch-time record meta (meshNodeId / meshNodeFor / launchedByCoordinator /
|
|
214
|
+
* autoLaunchedForQueueTaskId). Defense-in-depth for SESSION-ACCUMULATION-LEAK:
|
|
215
|
+
* a session-host factory impl SHOULD seed the create_session record with this
|
|
216
|
+
* so the node binding is present the instant the record exists, rather than
|
|
217
|
+
* relying solely on the post-spawn updateRuntimeMeta round-trip. Optional and
|
|
218
|
+
* additive — factory impls that ignore it keep the prior behavior; the
|
|
219
|
+
* post-spawn WTDISPATCH stamp in register() still runs as the primary path.
|
|
220
|
+
*/
|
|
221
|
+
initialMeta?: Record<string, unknown>;
|
|
212
222
|
}
|
|
213
223
|
|
|
214
224
|
export interface HostedCliRuntimeDescriptor {
|
|
@@ -578,6 +588,7 @@ export class DaemonCliManager {
|
|
|
578
588
|
cliArgs?: string[],
|
|
579
589
|
providerSessionId?: string,
|
|
580
590
|
attachExisting = false,
|
|
591
|
+
initialMeta?: Record<string, unknown>,
|
|
581
592
|
): PtyTransportFactory | undefined {
|
|
582
593
|
return this.deps.createPtyTransportFactory?.({
|
|
583
594
|
runtimeId,
|
|
@@ -586,6 +597,7 @@ export class DaemonCliManager {
|
|
|
586
597
|
cliArgs,
|
|
587
598
|
providerSessionId,
|
|
588
599
|
attachExisting,
|
|
600
|
+
...(initialMeta && Object.keys(initialMeta).length ? { initialMeta } : {}),
|
|
589
601
|
}) || undefined;
|
|
590
602
|
}
|
|
591
603
|
|
|
@@ -674,6 +686,24 @@ export class DaemonCliManager {
|
|
|
674
686
|
const instanceManager = this.deps.getInstanceManager();
|
|
675
687
|
const sessionRegistry = this.deps.getSessionRegistry?.() || null;
|
|
676
688
|
if (!instanceManager) throw new Error('InstanceManager not available');
|
|
689
|
+
|
|
690
|
+
// Launch-time record meta (mesh node binding) — computed BEFORE the
|
|
691
|
+
// transport factory so the session-host record can be seeded with the
|
|
692
|
+
// binding at create_session time (Fix ②, defense-in-depth for
|
|
693
|
+
// SESSION-ACCUMULATION-LEAK), not only via the post-spawn WTDISPATCH
|
|
694
|
+
// updateRuntimeMeta round-trip below. See CliTransportFactoryParams.initialMeta.
|
|
695
|
+
const launchMeshNodeId = typeof settings?.meshNodeId === 'string' ? settings.meshNodeId.trim() : '';
|
|
696
|
+
const launchMeshNodeFor = typeof settings?.meshNodeFor === 'string' ? settings.meshNodeFor.trim() : '';
|
|
697
|
+
const launchAutoLaunchedForQueueTaskId = typeof settings?.autoLaunchedForQueueTaskId === 'string'
|
|
698
|
+
? settings.autoLaunchedForQueueTaskId.trim()
|
|
699
|
+
: '';
|
|
700
|
+
const launchRecordMeta: Record<string, unknown> = {
|
|
701
|
+
...(launchMeshNodeId ? { meshNodeId: launchMeshNodeId } : {}),
|
|
702
|
+
...(launchMeshNodeFor ? { meshNodeFor: launchMeshNodeFor } : {}),
|
|
703
|
+
...(settings?.launchedByCoordinator === true ? { launchedByCoordinator: true } : {}),
|
|
704
|
+
...(launchAutoLaunchedForQueueTaskId ? { autoLaunchedForQueueTaskId: launchAutoLaunchedForQueueTaskId } : {}),
|
|
705
|
+
};
|
|
706
|
+
|
|
677
707
|
const transportFactory = this.getTransportFactory(
|
|
678
708
|
key,
|
|
679
709
|
normalizedType,
|
|
@@ -681,6 +711,9 @@ export class DaemonCliManager {
|
|
|
681
711
|
cliArgs,
|
|
682
712
|
options?.providerSessionId,
|
|
683
713
|
attachExisting,
|
|
714
|
+
// Only seed at create time for fresh launches — an attach restores an
|
|
715
|
+
// existing record whose meta is already stamped; re-seeding could clobber.
|
|
716
|
+
attachExisting ? undefined : launchRecordMeta,
|
|
684
717
|
);
|
|
685
718
|
const cliInstance = new CliProviderInstance(provider, resolvedDir, cliArgs, key, transportFactory, options);
|
|
686
719
|
try {
|
|
@@ -728,24 +761,15 @@ export class DaemonCliManager {
|
|
|
728
761
|
// sibling worktree nodes cannot tell two co-located clones apart. Push the launch-time node
|
|
729
762
|
// binding to the record meta so the record is 1:1 bound to its node (the spawned pty exists
|
|
730
763
|
// by now, so updateMeta reaches the session-host store). Best-effort; guarded.
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
//
|
|
734
|
-
//
|
|
735
|
-
//
|
|
736
|
-
//
|
|
737
|
-
|
|
738
|
-
const launchAutoLaunchedForQueueTaskId = typeof settings?.autoLaunchedForQueueTaskId === 'string'
|
|
739
|
-
? settings.autoLaunchedForQueueTaskId.trim()
|
|
740
|
-
: '';
|
|
741
|
-
if (launchMeshNodeId || launchMeshNodeFor || launchAutoLaunchedForQueueTaskId) {
|
|
764
|
+
//
|
|
765
|
+
// With the spec-CLI updateRuntimeMeta fix (SpecCliAdapter now forwards the FULL
|
|
766
|
+
// meta down to the transport, not just providerSessionId), this stamp finally
|
|
767
|
+
// reaches the session-host record for spec-backed providers too — previously it
|
|
768
|
+
// was silently dropped, which is what let orphans accumulate. launchRecordMeta
|
|
769
|
+
// is hoisted above and also seeds the create_session record via initialMeta.
|
|
770
|
+
if (Object.keys(launchRecordMeta).length) {
|
|
742
771
|
try {
|
|
743
|
-
cliInstance.getAdapter().updateRuntimeMeta?.({
|
|
744
|
-
...(launchMeshNodeId ? { meshNodeId: launchMeshNodeId } : {}),
|
|
745
|
-
...(launchMeshNodeFor ? { meshNodeFor: launchMeshNodeFor } : {}),
|
|
746
|
-
...(settings?.launchedByCoordinator === true ? { launchedByCoordinator: true } : {}),
|
|
747
|
-
...(launchAutoLaunchedForQueueTaskId ? { autoLaunchedForQueueTaskId: launchAutoLaunchedForQueueTaskId } : {}),
|
|
748
|
-
});
|
|
772
|
+
cliInstance.getAdapter().updateRuntimeMeta?.({ ...launchRecordMeta });
|
|
749
773
|
} catch { /* best-effort — record-meta stamp is cleanup hygiene, not on the dispatch path */ }
|
|
750
774
|
}
|
|
751
775
|
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Extracted verbatim from executeDaemonCommand; the inline-cache, session/worktree
|
|
10
10
|
* cleanup and aggregate-status collaborators come from ctx.
|
|
11
11
|
*/
|
|
12
|
-
import { daemonIdsEquivalent, meshNodeIdMatches } from '@adhdev/mesh-shared';
|
|
12
|
+
import { daemonIdsEquivalent, meshNodeIdMatches, normalizeMeshNodeId } from '@adhdev/mesh-shared';
|
|
13
13
|
import { resolveMeshHostStatus, normalizeMeshDaemonRole } from '../../mesh/mesh-host-ownership.js';
|
|
14
14
|
import {
|
|
15
15
|
loadMeshWorktreeBootstrapConfig,
|
|
@@ -558,6 +558,15 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
558
558
|
&& !Array.isArray(args.requireAutoLaunchedForTaskIds))
|
|
559
559
|
? args.requireAutoLaunchedForTaskIds as Record<string, string>
|
|
560
560
|
: undefined;
|
|
561
|
+
// Opt-in orphan reclaim (SESSION-ACCUMULATION-LEAK). The live-node id set
|
|
562
|
+
// is the CURRENT mesh membership; a matched live session bound to a node
|
|
563
|
+
// still in this set is an active sibling and is never reclaimed. Only when
|
|
564
|
+
// the caller passes reclaimOrphans:true does the router loosen the
|
|
565
|
+
// shared-daemon guard for workspace-only / dead-node-bound live sessions.
|
|
566
|
+
const reclaimOrphans = args?.reclaimOrphans === true;
|
|
567
|
+
const liveMeshNodeIds = Array.isArray(mesh?.nodes)
|
|
568
|
+
? mesh.nodes.map((n: any) => normalizeMeshNodeId(n)).filter(Boolean) as string[]
|
|
569
|
+
: [];
|
|
561
570
|
const result = await ctx.cleanupMeshSessions({
|
|
562
571
|
meshId,
|
|
563
572
|
nodeId,
|
|
@@ -567,6 +576,8 @@ export const meshCrudHandlers: Record<string, MedFamilyHandler> = {
|
|
|
567
576
|
dryRun: args?.dryRun === true,
|
|
568
577
|
source,
|
|
569
578
|
requireAutoLaunchedForTaskIds,
|
|
579
|
+
reclaimOrphans,
|
|
580
|
+
liveMeshNodeIds,
|
|
570
581
|
});
|
|
571
582
|
return result;
|
|
572
583
|
} catch (e: any) {
|
|
@@ -86,6 +86,16 @@ export interface MedFamilyContext {
|
|
|
86
86
|
dryRun?: boolean;
|
|
87
87
|
source?: 'mesh_cleanup_sessions' | 'mesh_remove_node' | 'magi_session_cleanup';
|
|
88
88
|
requireAutoLaunchedForTaskIds?: Record<string, string>;
|
|
89
|
+
/**
|
|
90
|
+
* Opt-in orphan reclaim (default false). When true, a workspace-only live
|
|
91
|
+
* session (no node binding) OR a session bound to a node that no longer
|
|
92
|
+
* exists in `liveMeshNodeIds` is stopped instead of skipped — reclaims the
|
|
93
|
+
* SESSION-ACCUMULATION-LEAK orphans. `liveMeshNodeIds` is the current mesh's
|
|
94
|
+
* node id set; a session whose meshNodeId is still present is treated as an
|
|
95
|
+
* active sibling and left alone.
|
|
96
|
+
*/
|
|
97
|
+
reclaimOrphans?: boolean;
|
|
98
|
+
liveMeshNodeIds?: string[];
|
|
89
99
|
}) => Promise<{ success: boolean; [key: string]: unknown }>;
|
|
90
100
|
|
|
91
101
|
/** Bound `DaemonCommandRouter.cleanupLocalWorktreeNode`. */
|
package/src/commands/router.ts
CHANGED
|
@@ -1468,6 +1468,16 @@ export class DaemonCommandRouter {
|
|
|
1468
1468
|
* didn't itself spawn for this fan-out. Only consulted alongside explicit sessionIds.
|
|
1469
1469
|
*/
|
|
1470
1470
|
requireAutoLaunchedForTaskIds?: Record<string, string>;
|
|
1471
|
+
/**
|
|
1472
|
+
* Opt-in orphan reclaim (default false). See SESSION-ACCUMULATION-LEAK.
|
|
1473
|
+
* When true, a workspace-only live_runtime session (no node binding) OR a
|
|
1474
|
+
* live_runtime session bound to a node absent from `liveMeshNodeIds` is
|
|
1475
|
+
* stopped instead of skipped by the conservative shared-daemon guard. A
|
|
1476
|
+
* session whose meshNodeId is STILL in `liveMeshNodeIds` (active sibling)
|
|
1477
|
+
* is never reclaimed — that is the shared-daemon safety this preserves.
|
|
1478
|
+
*/
|
|
1479
|
+
reclaimOrphans?: boolean;
|
|
1480
|
+
liveMeshNodeIds?: string[];
|
|
1471
1481
|
}): Promise<{ success: boolean; [key: string]: unknown }> {
|
|
1472
1482
|
if (args.mode === 'preserve') {
|
|
1473
1483
|
return { success: true, mode: 'preserve', matchedCount: 0, stoppedSessionIds: [], deletedSessionIds: [], skippedSessionIds: [] };
|
|
@@ -1477,6 +1487,23 @@ export class DaemonCommandRouter {
|
|
|
1477
1487
|
const requestedSessionIds = Array.isArray(args.sessionIds)
|
|
1478
1488
|
? new Set(args.sessionIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean))
|
|
1479
1489
|
: undefined;
|
|
1490
|
+
// Orphan-reclaim support (opt-in). The live-node id list lets us tell an
|
|
1491
|
+
// orphan (owning node gone from the mesh) apart from a still-active sibling
|
|
1492
|
+
// sharing this daemon. daemonIdsEquivalent (not raw ===) guards against the
|
|
1493
|
+
// canon-identity defect class: daemonId forms (daemon_mach_ vs mach_) and
|
|
1494
|
+
// normalized vs raw meshNodeId must compare equivalent, not literally equal.
|
|
1495
|
+
const reclaimOrphans = args.reclaimOrphans === true;
|
|
1496
|
+
const liveMeshNodeIds = Array.isArray(args.liveMeshNodeIds)
|
|
1497
|
+
? args.liveMeshNodeIds.map(id => typeof id === 'string' ? id.trim() : '').filter(Boolean)
|
|
1498
|
+
: [];
|
|
1499
|
+
const isNodeStillLive = (candidateNodeId: string): boolean => {
|
|
1500
|
+
if (!candidateNodeId) return false;
|
|
1501
|
+
return liveMeshNodeIds.some(liveId =>
|
|
1502
|
+
liveId === candidateNodeId
|
|
1503
|
+
|| meshNodeIdMatches({ id: liveId }, candidateNodeId)
|
|
1504
|
+
|| daemonIdsEquivalent(liveId, candidateNodeId));
|
|
1505
|
+
};
|
|
1506
|
+
const reclaimedOrphanSessionIds: string[] = [];
|
|
1480
1507
|
const sessions = await this.deps.sessionHostControl.listSessions();
|
|
1481
1508
|
const matched = sessions.filter(record => this.sessionMatchesMeshNode(record, args.node, args.nodeId, requestedSessionIds));
|
|
1482
1509
|
const hasExplicitSessionIds = !!requestedSessionIds?.size;
|
|
@@ -1579,7 +1606,18 @@ export class DaemonCommandRouter {
|
|
|
1579
1606
|
const matchedByWorkspaceOnly = !recordNodeId;
|
|
1580
1607
|
const isWorktreeNodeRemoval = cleanupSource === 'mesh_remove_node' && args.node?.isLocalWorktree === true;
|
|
1581
1608
|
const cleanWorkspaceOnlyForWorktree = isWorktreeNodeRemoval && matchedByWorkspaceOnly;
|
|
1582
|
-
|
|
1609
|
+
// Opt-in orphan reclaim (SESSION-ACCUMULATION-LEAK): a live_runtime that
|
|
1610
|
+
// is either workspace-only (no node binding — the leaked spec-CLI case)
|
|
1611
|
+
// or bound to a node no longer present in the mesh is an orphan and safe
|
|
1612
|
+
// to stop. A session bound to a node STILL live in the mesh is an active
|
|
1613
|
+
// sibling on this shared daemon and is left to the normal guard. Node
|
|
1614
|
+
// equivalence uses isNodeStillLive (daemonIdsEquivalent under the hood),
|
|
1615
|
+
// never a raw === on the id form.
|
|
1616
|
+
const reclaimableOrphan = reclaimOrphans
|
|
1617
|
+
&& liveRuntime
|
|
1618
|
+
&& !delegateBoundToThisNode
|
|
1619
|
+
&& (matchedByWorkspaceOnly || !isNodeStillLive(recordNodeId));
|
|
1620
|
+
if (!hasExplicitSessionIds && liveRuntime && !delegateBoundToThisNode && !cleanWorkspaceOnlyForWorktree && !reclaimableOrphan) {
|
|
1583
1621
|
skippedSessionIds.push(sessionId);
|
|
1584
1622
|
skippedLiveSessionIds.push(sessionId);
|
|
1585
1623
|
const reason = recordNodeId && recordNodeId !== args.nodeId
|
|
@@ -1595,6 +1633,12 @@ export class DaemonCommandRouter {
|
|
|
1595
1633
|
// like a bound delegate for accounting (so callers can see it was acted on).
|
|
1596
1634
|
actedLiveDelegateSessionIds.push(sessionId);
|
|
1597
1635
|
}
|
|
1636
|
+
if (reclaimableOrphan && !cleanWorkspaceOnlyForWorktree && !delegateBoundToThisNode) {
|
|
1637
|
+
// Reclaimed orphan — surface it distinctly so callers can audit what
|
|
1638
|
+
// the opt-in reclaim path stopped that the normal guard would have kept.
|
|
1639
|
+
reclaimedOrphanSessionIds.push(sessionId);
|
|
1640
|
+
actedLiveDelegateSessionIds.push(sessionId);
|
|
1641
|
+
}
|
|
1598
1642
|
if (!hasExplicitSessionIds && liveRuntime && delegateBoundToThisNode && args.mode === 'delete_stopped') {
|
|
1599
1643
|
// delete_stopped never stops live runtimes by contract — even bound delegates.
|
|
1600
1644
|
// Surface a clear reason instead of an unexplained skip so callers know to use
|
|
@@ -1674,6 +1718,7 @@ export class DaemonCommandRouter {
|
|
|
1674
1718
|
skippedCoordinatorSessionIds,
|
|
1675
1719
|
...(skippedMarkerMismatchSessionIds.length ? { skippedMarkerMismatchSessionIds } : {}),
|
|
1676
1720
|
...(actedLiveDelegateSessionIds.length ? { actedLiveDelegateSessionIds } : {}),
|
|
1721
|
+
...(reclaimedOrphanSessionIds.length ? { reclaimedOrphanSessionIds } : {}),
|
|
1677
1722
|
...(skippedLiveSessionReasons.length ? { skippedLiveSessionReasons } : {}),
|
|
1678
1723
|
...(deleteUnsupported ? {
|
|
1679
1724
|
deleteUnsupported: true,
|
|
@@ -180,6 +180,17 @@ export class TerminalAdapter {
|
|
|
180
180
|
this.pty?.write(text);
|
|
181
181
|
}
|
|
182
182
|
|
|
183
|
+
/** Forward runtime metadata (meshNodeId, workspaceLabel, lifecycle, …) to
|
|
184
|
+
* the underlying transport so it reaches the session registry. The spec
|
|
185
|
+
* path previously dropped everything but providerSessionId here, which
|
|
186
|
+
* left autoLaunch's meshNodeId stamp unbound on the record (see
|
|
187
|
+
* SESSION-ACCUMULATION-LEAK). No-op when the transport does not support
|
|
188
|
+
* metadata updates (e.g. plain node-pty). */
|
|
189
|
+
updateMeta(meta: Record<string, unknown>, replace = false): void {
|
|
190
|
+
if (!this.pty || typeof this.pty.updateMeta !== 'function') return;
|
|
191
|
+
this.pty.updateMeta(meta, replace);
|
|
192
|
+
}
|
|
193
|
+
|
|
183
194
|
/** Debug-only: most-recent PTY input/output/resize/cursor events, oldest
|
|
184
195
|
* first. Pure observation — never consulted by the FSM. */
|
|
185
196
|
getEventTimeline(limit = MAX_PTY_EVENTS): SpecPtyEvent[] {
|
|
@@ -641,9 +641,17 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
641
641
|
};
|
|
642
642
|
}
|
|
643
643
|
updateRuntimeMeta(meta?: Record<string, unknown>): void {
|
|
644
|
-
if (meta
|
|
644
|
+
if (!meta) return;
|
|
645
|
+
if (typeof meta.providerSessionId === 'string') {
|
|
645
646
|
this.providerSessionId = meta.providerSessionId;
|
|
646
647
|
}
|
|
648
|
+
// Forward the FULL meta (meshNodeId / meshNodeFor / workspaceLabel /
|
|
649
|
+
// lifecycle / …) down to the transport so it reaches the session
|
|
650
|
+
// registry. The legacy ProviderCliAdapter.updateRuntimeMeta does the
|
|
651
|
+
// same via ptyProcess.updateMeta; the spec path previously dropped
|
|
652
|
+
// everything but providerSessionId, leaving autoLaunch's meshNodeId
|
|
653
|
+
// stamp unbound on the record — the root of SESSION-ACCUMULATION-LEAK.
|
|
654
|
+
try { this.driver.updateMeta(meta); } catch { /* transport may not support meta */ }
|
|
647
655
|
}
|
|
648
656
|
refreshProviderDefinition(): void { /* hot reload handled by SpecDriver fs.watch */ }
|
|
649
657
|
|
|
@@ -116,6 +116,7 @@ export interface ISpecDriver {
|
|
|
116
116
|
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
117
117
|
start(): void;
|
|
118
118
|
dispatch(cmd: DashboardCommand): void;
|
|
119
|
+
updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
|
|
119
120
|
snapshot(): string;
|
|
120
121
|
getCursorPosition(): { row: number; col: number };
|
|
121
122
|
getScreen(): string;
|
|
@@ -422,6 +423,14 @@ export class FsmDriver implements ISpecDriver {
|
|
|
422
423
|
}
|
|
423
424
|
}
|
|
424
425
|
|
|
426
|
+
/** Forward runtime metadata to the terminal transport so mesh binding
|
|
427
|
+
* fields (meshNodeId / meshNodeFor / workspaceLabel / lifecycle) reach
|
|
428
|
+
* the session registry. Not a DashboardCommand — this is a control-plane
|
|
429
|
+
* update, not user input. */
|
|
430
|
+
updateMeta(meta: Record<string, unknown>, replace = false): void {
|
|
431
|
+
this.adapter.updateMeta(meta, replace);
|
|
432
|
+
}
|
|
433
|
+
|
|
425
434
|
snapshot(): string { return this.adapter.snapshot(); }
|
|
426
435
|
getCursorPosition(): { row: number; col: number } { return this.adapter.getCursorPosition(); }
|
|
427
436
|
getScreen(): string { return this.adapter.snapshot(); }
|