@zgeoff/atc 2.14.0 → 2.15.1
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/package.json +2 -1
- package/src/agents/agent-adapter.ts +3 -38
- package/src/agents/build-hook-settings.ts +1 -1
- package/src/agents/claude-adapter.ts +3 -3
- package/src/agents/codex-adapter.ts +3 -3
- package/src/agents/gateway-adapter.ts +3 -3
- package/src/agents/grok-adapter.ts +3 -3
- package/src/agents/start-claude-headless-run.ts +1 -1
- package/src/client/boot-daemon.ts +2 -2
- package/src/client/collect-agent-picks.ts +1 -1
- package/src/client/daemon-client.ts +7 -23
- package/src/client/format-overlay-agent-mark.ts +1 -1
- package/src/client/index.ts +4 -2
- package/src/client/pick-tab-target.ts +2 -2
- package/src/client/spawn-picker.ts +1 -1
- package/src/client/to-mirror-session.ts +3 -3
- package/src/client/ui.ts +3 -3
- package/src/daemon/build-imp-provider.ts +70 -23
- package/src/daemon/build-message-trail-entry.ts +1 -1
- package/src/daemon/build-report-trail-entry.ts +1 -1
- package/src/daemon/build-scoped-context.ts +1 -1
- package/src/daemon/build-session-message-event.ts +1 -1
- package/src/daemon/daemon-connection.ts +9 -253
- package/src/daemon/daemon-context.ts +240 -0
- package/src/daemon/daemon.ts +9 -5
- package/src/daemon/execution-provider.ts +12 -0
- package/src/daemon/hooks.ts +6 -24
- package/src/daemon/idempotency-ledger.ts +78 -18
- package/src/daemon/imp-client-port.ts +83 -27
- package/src/daemon/imp-harness.ts +41 -0
- package/src/daemon/imp-provider.ts +5 -11
- package/src/daemon/local-pty-provider.ts +45 -0
- package/src/daemon/parse-hook-line.ts +1 -1
- package/src/daemon/read-imp-token.ts +30 -0
- package/src/daemon/sessions.ts +134 -107
- package/src/daemon/start-headless-turn.ts +3 -3
- package/src/daemon/start-session-bridge.ts +2 -2
- package/src/mcp/collect-clients.ts +1 -1
- package/src/mcp/collect-grants.ts +1 -1
- package/src/mcp/find-client-name.ts +1 -1
- package/src/mcp/mcp-auth-schema.ts +37 -0
- package/src/mcp/open-mcp-auth.ts +2 -2
- package/src/mcp/reconnecting-caller.ts +15 -6
- package/src/mcp/remove-client.ts +1 -1
- package/src/mcp/revoke-grant.ts +1 -1
- package/src/mcp/types.ts +0 -38
- package/src/mcp-http-server.ts +2 -1
- package/src/mcp-server.ts +3 -14
- package/src/protocol/adapter-event.ts +25 -0
- package/src/protocol/count-session-states.ts +16 -0
- package/src/protocol/daemon-channel.ts +17 -0
- package/src/protocol/hook-event.ts +11 -0
- package/src/protocol/line-decoder.ts +34 -0
- package/src/{shared → protocol}/open-bridge-socket.ts +4 -7
- package/src/protocol/pinned-group-key.ts +2 -0
- package/src/protocol/session-state.ts +5 -0
- package/src/protocol/sort-grouped-session-views.ts +29 -0
- package/src/protocol/sort-session-views.ts +41 -0
- package/src/protocol/sortable-session-view.ts +13 -0
- package/src/report.ts +1 -1
- package/src/run-bridge-tap.ts +2 -2
- package/src/shared/agent-id.ts +6 -0
- package/src/shared/collect-gateways.ts +1 -1
- package/src/shared/to-agent-id.ts +11 -0
- package/src/statusline.ts +1 -1
- package/src/store/fleet-entry.ts +6 -3
- package/src/store/state-store.ts +80 -19
- /package/src/{shared → protocol}/send-bridge-request.ts +0 -0
- /package/src/{store → shared}/bun-sqlite-driver.ts +0 -0
- /package/src/{agents → shared}/truncate-detail.ts +0 -0
- /package/src/{daemon → shared}/truncate-summary.ts +0 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One open protocol connection to a daemon, as a caller that does not own
|
|
3
|
+
* the transport drives it: the handshake, correlated requests, a callback
|
|
4
|
+
* for when the connection ends, and closing it.
|
|
5
|
+
*/
|
|
6
|
+
export interface DaemonChannel {
|
|
7
|
+
onClose: () => void;
|
|
8
|
+
readonly sendHello: (build: string) => Promise<Readonly<Record<string, unknown>>>;
|
|
9
|
+
|
|
10
|
+
// A request with a principal acts as that principal.
|
|
11
|
+
readonly sendRequest: (
|
|
12
|
+
m: string,
|
|
13
|
+
p?: Readonly<Record<string, unknown>>,
|
|
14
|
+
as?: string,
|
|
15
|
+
) => Promise<Readonly<Record<string, unknown>>>;
|
|
16
|
+
readonly stop: () => void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SessionID } from '../shared/session-id';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One line a hook reporter sends: the atc session it reports on, the agent's
|
|
5
|
+
* hook event name, and the hook's payload as the agent gave it.
|
|
6
|
+
*/
|
|
7
|
+
export interface HookEvent {
|
|
8
|
+
atcId: SessionID;
|
|
9
|
+
event: string;
|
|
10
|
+
payload: Record<string, unknown>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Splits one stream of newline-framed text into whole lines. Bytes decode
|
|
3
|
+
* as UTF-8 with state kept across reads, so a multi-byte character split
|
|
4
|
+
* between two reads decodes whole. The unterminated tail waits for the read
|
|
5
|
+
* that ends it, and a line holding only whitespace is dropped. A carriage
|
|
6
|
+
* return before the newline stays on the line.
|
|
7
|
+
*/
|
|
8
|
+
export class LineDecoder {
|
|
9
|
+
private readonly decoder = new TextDecoder();
|
|
10
|
+
|
|
11
|
+
private pending = '';
|
|
12
|
+
|
|
13
|
+
get pendingLength(): number {
|
|
14
|
+
return this.pending.length;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- a socket read buffer has no readonly form
|
|
18
|
+
splitChunk(bytes: Uint8Array): string[] {
|
|
19
|
+
return this.splitText(this.decodeText(bytes));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// oxlint-disable-next-line prefer-readonly-parameter-types -- a socket read buffer has no readonly form
|
|
23
|
+
decodeText(bytes: Uint8Array): string {
|
|
24
|
+
return this.decoder.decode(bytes, { stream: true });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
splitText(text: string): string[] {
|
|
28
|
+
const lines = `${this.pending}${text}`.split('\n');
|
|
29
|
+
|
|
30
|
+
this.pending = lines.pop() ?? '';
|
|
31
|
+
|
|
32
|
+
return lines.filter((line) => line.trim() !== '');
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { LineDecoder } from './line-decoder';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* One line-framed connection to a session bridge, from inside a remote
|
|
3
5
|
* session: each line the bridge writes arrives whole, and each line written
|
|
@@ -20,10 +22,9 @@ export async function openBridgeSocket(
|
|
|
20
22
|
onLine: (line: Readonly<Record<string, unknown>>) => void,
|
|
21
23
|
): Promise<BridgeSocket> {
|
|
22
24
|
const encoder = new TextEncoder();
|
|
23
|
-
const decoder = new
|
|
25
|
+
const decoder = new LineDecoder();
|
|
24
26
|
|
|
25
27
|
const closed = Promise.withResolvers<void>();
|
|
26
|
-
let pending = '';
|
|
27
28
|
|
|
28
29
|
let unsent = new Uint8Array(0);
|
|
29
30
|
|
|
@@ -31,11 +32,7 @@ export async function openBridgeSocket(
|
|
|
31
32
|
unix: path,
|
|
32
33
|
socket: {
|
|
33
34
|
data(_socket, buf) {
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
pending = lines.pop() ?? '';
|
|
37
|
-
|
|
38
|
-
for (const line of lines) {
|
|
35
|
+
for (const line of decoder.splitChunk(buf)) {
|
|
39
36
|
const parsed = parseLine(line);
|
|
40
37
|
|
|
41
38
|
if (parsed !== null) {
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { PINNED_GROUP_KEY } from './pinned-group-key';
|
|
2
|
+
import { sortSessionViews } from './sort-session-views';
|
|
3
|
+
import type { SortableSessionView } from './sortable-session-view';
|
|
4
|
+
|
|
5
|
+
// Overlay display order for the grouped view: the flat sort with each
|
|
6
|
+
// repository's sessions pulled together at the position of its best-ranked
|
|
7
|
+
// member, so the renderer's adjacency-based headers appear once per group.
|
|
8
|
+
// Pinned sessions form their own leading group. A sub-session keys by its
|
|
9
|
+
// parent, so a set never splits across groups.
|
|
10
|
+
export function sortGroupedSessionViews<
|
|
11
|
+
T extends SortableSessionView & { readonly repoRoot: string },
|
|
12
|
+
>(list: readonly T[]): T[] {
|
|
13
|
+
const byID = new Map(list.map((s) => [s.id, s]));
|
|
14
|
+
const buckets = new Map<string, T[]>();
|
|
15
|
+
|
|
16
|
+
for (const s of sortSessionViews(list)) {
|
|
17
|
+
const owner = (s.parent === null ? undefined : byID.get(s.parent)) ?? s;
|
|
18
|
+
const key = owner.pinned ? PINNED_GROUP_KEY : owner.repoRoot;
|
|
19
|
+
const bucket = buckets.get(key);
|
|
20
|
+
|
|
21
|
+
if (bucket === undefined) {
|
|
22
|
+
buckets.set(key, [s]);
|
|
23
|
+
} else {
|
|
24
|
+
bucket.push(s);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return [...buckets.values()].flat();
|
|
29
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { SessionState } from './session-state';
|
|
2
|
+
import type { SortableSessionView } from './sortable-session-view';
|
|
3
|
+
|
|
4
|
+
// Overlay order: pinned sessions first in most-recently-attached order, then
|
|
5
|
+
// everyone else by urgency — who needs you, finished turns, busy, dead —
|
|
6
|
+
// with most-recently-attached breaking ties inside each state. A
|
|
7
|
+
// sub-session sits directly under its parent, ranked among its siblings
|
|
8
|
+
// alone, so its attention never moves the parent's row; a sub-session whose
|
|
9
|
+
// parent is not listed ranks as a top-level row.
|
|
10
|
+
export function sortSessionViews<T extends SortableSessionView>(list: readonly T[]): T[] {
|
|
11
|
+
const rank: Record<SessionState, number> = {
|
|
12
|
+
needs_you: 0,
|
|
13
|
+
done: 1,
|
|
14
|
+
running: 2,
|
|
15
|
+
exited: 3,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const ranked = [...list].toSorted((a, b) => {
|
|
19
|
+
if (a.pinned !== b.pinned) {
|
|
20
|
+
return a.pinned ? -1 : 1;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const recency = b.lastAttachedAt - a.lastAttachedAt || b.createdAt - a.createdAt;
|
|
24
|
+
|
|
25
|
+
return a.pinned ? recency : rank[a.state] - rank[b.state] || recency;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const listed = new Set(ranked.map((s) => s.id));
|
|
29
|
+
|
|
30
|
+
const sorted: T[] = [];
|
|
31
|
+
|
|
32
|
+
for (const s of ranked) {
|
|
33
|
+
if (s.parent !== null && listed.has(s.parent)) {
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
sorted.push(s, ...ranked.filter((child) => child.parent === s.id));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return sorted;
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SessionState } from './session-state';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The fields of a session row that the overlay orders by.
|
|
5
|
+
*/
|
|
6
|
+
export interface SortableSessionView {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly parent: string | null;
|
|
9
|
+
readonly state: SessionState;
|
|
10
|
+
readonly pinned: boolean;
|
|
11
|
+
readonly lastAttachedAt: number;
|
|
12
|
+
readonly createdAt: number;
|
|
13
|
+
}
|
package/src/report.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { sendBridgeRequest } from './protocol/send-bridge-request';
|
|
4
5
|
import { sendReport } from './shared/report';
|
|
5
6
|
import { REPORT_KINDS } from './shared/report-kinds';
|
|
6
|
-
import { sendBridgeRequest } from './shared/send-bridge-request';
|
|
7
7
|
|
|
8
8
|
interface ReportOptions {
|
|
9
9
|
readonly message: string;
|
package/src/run-bridge-tap.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, rmSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { openBridgeSocket } from './
|
|
5
|
-
import type { BridgeSocket } from './
|
|
4
|
+
import { openBridgeSocket } from './protocol/open-bridge-socket';
|
|
5
|
+
import type { BridgeSocket } from './protocol/open-bridge-socket';
|
|
6
6
|
|
|
7
7
|
const INBOX_MESSAGE_SCHEMA = z.looseObject({
|
|
8
8
|
ev: z.literal('InboxMessage'),
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { AgentID } from './agent-id';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Missing and empty values become Claude so a fleet written before the agent
|
|
5
|
+
* column still restores as Claude. Any other string is returned as it stands,
|
|
6
|
+
* registered or not: an id whose adapter is gone must reach the caller intact
|
|
7
|
+
* so the session can be shown and refused, never quietly run as Claude.
|
|
8
|
+
*/
|
|
9
|
+
export function toAgentID(raw: unknown): AgentID {
|
|
10
|
+
return typeof raw === 'string' && raw !== '' ? raw : 'claude';
|
|
11
|
+
}
|
package/src/statusline.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
|
+
import { sendBridgeRequest } from './protocol/send-bridge-request';
|
|
4
5
|
import { statusFile } from './shared/config';
|
|
5
6
|
import { isRecord, sendReport } from './shared/report';
|
|
6
|
-
import { sendBridgeRequest } from './shared/send-bridge-request';
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Runs as the statusLine command injected into wrangled sessions. Chains the
|
package/src/store/fleet-entry.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import {
|
|
3
|
-
import type { AgentID } from '../agents/agent-adapter';
|
|
2
|
+
import type { AgentID } from '../shared/agent-id';
|
|
4
3
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
5
4
|
import { buildOptionalBoolean } from '../shared/build-optional-boolean';
|
|
6
5
|
import { buildOptionalString } from '../shared/build-optional-string';
|
|
7
6
|
import type { DaemonID } from '../shared/daemon-id';
|
|
8
7
|
import { isRecord } from '../shared/report';
|
|
9
8
|
import type { SessionID } from '../shared/session-id';
|
|
9
|
+
import { toAgentID } from '../shared/to-agent-id';
|
|
10
10
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
11
11
|
import type { SessionWorkspace } from './workspace-materialization';
|
|
12
12
|
|
|
@@ -72,7 +72,10 @@ export interface FleetEntry {
|
|
|
72
72
|
export interface FleetStore {
|
|
73
73
|
readonly daemonID: DaemonID;
|
|
74
74
|
readonly loadFleet: () => Promise<FleetEntry[]>;
|
|
75
|
-
readonly writeFleet: (
|
|
75
|
+
readonly writeFleet: (
|
|
76
|
+
entries: readonly FleetEntry[],
|
|
77
|
+
removed?: readonly SessionID[],
|
|
78
|
+
) => Promise<void>;
|
|
76
79
|
readonly updateFleetEntry: (sessionID: SessionID, fields: FleetEntryUpdate) => Promise<void>;
|
|
77
80
|
}
|
|
78
81
|
|
package/src/store/state-store.ts
CHANGED
|
@@ -3,19 +3,20 @@ import { randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { Kysely, SqliteAdapter, SqliteIntrospector, SqliteQueryCompiler, sql } from 'kysely';
|
|
5
5
|
import type { Expression, ExpressionBuilder, SqlBool, Transaction } from 'kysely';
|
|
6
|
-
import {
|
|
7
|
-
import type { AdapterEvent, AgentID } from '../agents/agent-adapter';
|
|
8
|
-
import type { HookEvent } from '../daemon/hooks';
|
|
6
|
+
import type { AdapterEvent } from '../protocol/adapter-event';
|
|
9
7
|
import { DaemonError } from '../protocol/daemon-error';
|
|
8
|
+
import type { HookEvent } from '../protocol/hook-event';
|
|
9
|
+
import type { AgentID } from '../shared/agent-id';
|
|
10
10
|
import type { AgentSessionID } from '../shared/agent-session-id';
|
|
11
|
+
import { BunSqliteDriver } from '../shared/bun-sqlite-driver';
|
|
11
12
|
import type { DaemonID } from '../shared/daemon-id';
|
|
12
13
|
import type { MessageID } from '../shared/message-id';
|
|
13
14
|
import type { SessionID } from '../shared/session-id';
|
|
15
|
+
import { toAgentID } from '../shared/to-agent-id';
|
|
14
16
|
import { toAgentSessionID } from '../shared/to-agent-session-id';
|
|
15
17
|
import { toDaemonID } from '../shared/to-daemon-id';
|
|
16
18
|
import { toMessageID } from '../shared/to-message-id';
|
|
17
19
|
import { toSessionID } from '../shared/to-session-id';
|
|
18
|
-
import { BunSqliteDriver } from './bun-sqlite-driver';
|
|
19
20
|
import { parseFleetEntry } from './fleet-entry';
|
|
20
21
|
import type { FleetEntry, FleetEntryUpdate, LegacyFleetEntry } from './fleet-entry';
|
|
21
22
|
import type { EffectTarget, IdempotencyClaim, IdempotencyRecord } from './idempotency-record';
|
|
@@ -241,12 +242,22 @@ export class StateStore {
|
|
|
241
242
|
return recency;
|
|
242
243
|
}
|
|
243
244
|
|
|
244
|
-
//
|
|
245
|
-
//
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
245
|
+
// Rewrites this daemon's rows for the given entries and drops its rows
|
|
246
|
+
// for the removed sessions, plus any row of its own that an entry
|
|
247
|
+
// replaces by holding the same agent session id. Every other row stays as
|
|
248
|
+
// it stands, so a row the daemon never loaded survives a write. A session
|
|
249
|
+
// another daemon owns, or one whose stored ownership epoch has moved past
|
|
250
|
+
// this daemon's, rejects the whole write with stale_epoch.
|
|
251
|
+
async writeFleet(
|
|
252
|
+
entries: readonly FleetEntry[],
|
|
253
|
+
removed: readonly SessionID[] = [],
|
|
254
|
+
): Promise<void> {
|
|
249
255
|
const kept = buildFleetWithoutReplacedRows(entries);
|
|
256
|
+
const sessionIDs = [...entries.map((entry) => entry.sessionID), ...removed];
|
|
257
|
+
|
|
258
|
+
const agentSessionIDs = entries.flatMap((entry) =>
|
|
259
|
+
entry.agentSessionID === undefined ? [] : [entry.agentSessionID],
|
|
260
|
+
);
|
|
250
261
|
|
|
251
262
|
await this.db.transaction().execute(async (trx) => {
|
|
252
263
|
await requireCurrentOwnership(
|
|
@@ -255,17 +266,35 @@ export class StateStore {
|
|
|
255
266
|
entries.map((entry) => entry.sessionID),
|
|
256
267
|
);
|
|
257
268
|
|
|
258
|
-
|
|
259
|
-
.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
.execute();
|
|
269
|
+
const replaced =
|
|
270
|
+
agentSessionIDs.length === 0
|
|
271
|
+
? []
|
|
272
|
+
: await trx
|
|
273
|
+
.selectFrom('fleet')
|
|
274
|
+
.select(['session_id', 'agent_session_id'])
|
|
275
|
+
.where('agent_session_id', 'in', agentSessionIDs)
|
|
276
|
+
.execute();
|
|
267
277
|
|
|
268
|
-
|
|
278
|
+
const dropped = [...new Set([...sessionIDs, ...replaced.map((row) => row.session_id)])];
|
|
279
|
+
|
|
280
|
+
if (dropped.length > 0) {
|
|
281
|
+
await trx
|
|
282
|
+
.deleteFrom('fleet')
|
|
283
|
+
.where('session_id', 'in', dropped)
|
|
284
|
+
.where('session_id', 'in', (eb) =>
|
|
285
|
+
eb
|
|
286
|
+
.selectFrom('session_owner')
|
|
287
|
+
.select('session_id')
|
|
288
|
+
.where('daemon_id', '=', this.daemonID),
|
|
289
|
+
)
|
|
290
|
+
.execute();
|
|
291
|
+
|
|
292
|
+
await trx
|
|
293
|
+
.deleteFrom('session_owner')
|
|
294
|
+
.where('daemon_id', '=', this.daemonID)
|
|
295
|
+
.where('session_id', 'in', dropped)
|
|
296
|
+
.execute();
|
|
297
|
+
}
|
|
269
298
|
|
|
270
299
|
const updatedAt = Date.now();
|
|
271
300
|
|
|
@@ -304,6 +333,38 @@ export class StateStore {
|
|
|
304
333
|
})
|
|
305
334
|
.execute();
|
|
306
335
|
}
|
|
336
|
+
|
|
337
|
+
// A stored row this write leaves in place follows its replaced parent
|
|
338
|
+
// to the session that replaced it, or to that session's own parent
|
|
339
|
+
// when the replacement is itself a sub-session.
|
|
340
|
+
const survivors = new Map(
|
|
341
|
+
kept.flatMap((entry) =>
|
|
342
|
+
entry.agentSessionID === undefined ? [] : [[entry.agentSessionID, entry] as const],
|
|
343
|
+
),
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
for (const row of replaced) {
|
|
347
|
+
const survivor =
|
|
348
|
+
row.agent_session_id === null
|
|
349
|
+
? undefined
|
|
350
|
+
: survivors.get(toAgentSessionID(row.agent_session_id));
|
|
351
|
+
|
|
352
|
+
if (survivor === undefined || survivor.sessionID === row.session_id) {
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
await trx
|
|
357
|
+
.updateTable('fleet')
|
|
358
|
+
.set({ parent_session_id: survivor.parent ?? survivor.sessionID })
|
|
359
|
+
.where('parent_session_id', '=', row.session_id)
|
|
360
|
+
.where('session_id', 'in', (eb) =>
|
|
361
|
+
eb
|
|
362
|
+
.selectFrom('session_owner')
|
|
363
|
+
.select('session_id')
|
|
364
|
+
.where('daemon_id', '=', this.daemonID),
|
|
365
|
+
)
|
|
366
|
+
.execute();
|
|
367
|
+
}
|
|
307
368
|
});
|
|
308
369
|
}
|
|
309
370
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|