@linkshell/host 0.4.69 → 0.4.70
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/host/src/drivers/acp/antigravity.d.ts +13 -0
- package/dist/host/src/drivers/acp/antigravity.js +13 -0
- package/dist/host/src/drivers/acp/antigravity.js.map +1 -1
- package/dist/host/src/drivers/acp/driver.d.ts +14 -1
- package/dist/host/src/drivers/acp/driver.js +45 -3
- package/dist/host/src/drivers/acp/driver.js.map +1 -1
- package/dist/host/src/drivers/acp/mapper.d.ts +2 -0
- package/dist/host/src/drivers/acp/mapper.js +6 -1
- package/dist/host/src/drivers/acp/mapper.js.map +1 -1
- package/dist/host/src/drivers/claude/app-sessions.d.ts +58 -0
- package/dist/host/src/drivers/claude/app-sessions.js +183 -0
- package/dist/host/src/drivers/claude/app-sessions.js.map +1 -0
- package/dist/host/src/drivers/claude/driver.d.ts +22 -1
- package/dist/host/src/drivers/claude/driver.js +95 -10
- package/dist/host/src/drivers/claude/driver.js.map +1 -1
- package/dist/host/src/drivers/claude/holders.d.ts +3 -1
- package/dist/host/src/drivers/claude/holders.js +4 -0
- package/dist/host/src/drivers/claude/holders.js.map +1 -1
- package/dist/host/src/drivers/claude/routing.d.ts +19 -0
- package/dist/host/src/drivers/claude/routing.js +145 -0
- package/dist/host/src/drivers/claude/routing.js.map +1 -0
- package/dist/host/src/drivers/claude/transcript.d.ts +10 -0
- package/dist/host/src/drivers/claude/transcript.js +97 -22
- package/dist/host/src/drivers/claude/transcript.js.map +1 -1
- package/dist/host/src/drivers/codex/driver.d.ts +15 -0
- package/dist/host/src/drivers/codex/driver.js +47 -9
- package/dist/host/src/drivers/codex/driver.js.map +1 -1
- package/dist/host/src/drivers/codex/mapper.d.ts +41 -7
- package/dist/host/src/drivers/codex/mapper.js +167 -26
- package/dist/host/src/drivers/codex/mapper.js.map +1 -1
- package/dist/host/src/drivers/codex/mcp-apps.d.ts +69 -0
- package/dist/host/src/drivers/codex/mcp-apps.js +340 -0
- package/dist/host/src/drivers/codex/mcp-apps.js.map +1 -0
- package/dist/host/src/drivers/codex/settings.d.ts +13 -0
- package/dist/host/src/drivers/codex/settings.js +22 -6
- package/dist/host/src/drivers/codex/settings.js.map +1 -1
- package/dist/host/src/drivers/types.d.ts +50 -1
- package/dist/host/src/hub.d.ts +33 -1
- package/dist/host/src/hub.js +157 -13
- package/dist/host/src/hub.js.map +1 -1
- package/dist/host/src/rpc/mcp.d.ts +8 -0
- package/dist/host/src/rpc/mcp.js +33 -0
- package/dist/host/src/rpc/mcp.js.map +1 -0
- package/dist/host/src/rpc/server.js +16 -2
- package/dist/host/src/rpc/server.js.map +1 -1
- package/dist/host/src/store.d.ts +7 -0
- package/dist/host/src/store.js +27 -3
- package/dist/host/src/store.js.map +1 -1
- package/dist/host/src/terminals.d.ts +6 -0
- package/dist/host/src/terminals.js +29 -2
- package/dist/host/src/terminals.js.map +1 -1
- package/dist/host/src/usage.d.ts +5 -0
- package/dist/host/src/usage.js +10 -2
- package/dist/host/src/usage.js.map +1 -1
- package/dist/host/tsconfig.tsbuildinfo +1 -1
- package/dist/wire/src/envelopes.d.ts +53 -0
- package/dist/wire/src/envelopes.js +251 -0
- package/dist/wire/src/envelopes.js.map +1 -0
- package/dist/wire/src/index.d.ts +2 -0
- package/dist/wire/src/index.js +2 -0
- package/dist/wire/src/index.js.map +1 -1
- package/dist/wire/src/model.d.ts +32 -0
- package/dist/wire/src/model.js +24 -0
- package/dist/wire/src/model.js.map +1 -1
- package/dist/wire/src/rpc-mcp.d.ts +199 -0
- package/dist/wire/src/rpc-mcp.js +78 -0
- package/dist/wire/src/rpc-mcp.js.map +1 -0
- package/dist/wire/src/rpc.d.ts +494 -4
- package/dist/wire/src/rpc.js +30 -2
- package/dist/wire/src/rpc.js.map +1 -1
- package/dist/wire/src/updates.d.ts +211 -0
- package/dist/wire/src/updates.js +14 -1
- package/dist/wire/src/updates.js.map +1 -1
- package/package.json +3 -3
package/dist/host/src/hub.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type GoalChange, type BackgroundTask, type AgentInfo, type ContentBlock, type SessionEvent, type SessionSummary, type SubagentInfo, type QuestionAnswer, type GitInfo, type ProjectSummary, type WorktreeEntry, type SessionNotice, type AcpAgentSettings, type AcpRemoteAgent, type PendingPermissionSummary, type EditorRequest, type SideChat } from "@linkshell/wire";
|
|
1
|
+
import { type GoalChange, type BackgroundTask, type AgentInfo, type ContentBlock, type SessionEvent, type SessionSummary, type SubagentInfo, type QuestionAnswer, type GitInfo, type ProjectSummary, type WorktreeEntry, type SessionNotice, type AcpAgentSettings, type AcpRemoteAgent, type PendingPermissionSummary, type EditorRequest, type SideChat, type MethodResult } from "@linkshell/wire";
|
|
2
2
|
import type { AcpSettingsStore } from "./acp-settings.js";
|
|
3
3
|
import type { AgentDriver, DesktopController, DesktopLaunchContext, DriverHost, LaunchSpec } from "./drivers/types.js";
|
|
4
4
|
import { type HostStore } from "./store.js";
|
|
@@ -9,6 +9,17 @@ export interface Subscriber {
|
|
|
9
9
|
/** The backlog that follows starts after `startSeq`, not where the subscriber left off. */
|
|
10
10
|
window?(startSeq: number): void;
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Archived here or in the app, unless it was unarchived here after the app
|
|
14
|
+
* archived it (`local.at` 0: chosen before times were kept, so it never outranks the app).
|
|
15
|
+
*/
|
|
16
|
+
export declare function mergedArchive(local: {
|
|
17
|
+
archived: boolean;
|
|
18
|
+
at: number;
|
|
19
|
+
}, app: {
|
|
20
|
+
archived: boolean;
|
|
21
|
+
since: number;
|
|
22
|
+
} | undefined): boolean;
|
|
12
23
|
/**
|
|
13
24
|
* The host's source of truth for sessions. Drivers push native events in;
|
|
14
25
|
* the hub logs them with a per-session seq, keeps summaries current and fans
|
|
@@ -315,6 +326,11 @@ export declare class SessionHub {
|
|
|
315
326
|
/** Asked for every project each time the list is: remembered for a moment. */
|
|
316
327
|
private branchOf;
|
|
317
328
|
getSession(sessionId: string): SessionSummary;
|
|
329
|
+
/** A session and its agent's driver, unchecked: for what one driver offers itself (`mcp.*`), which says what it needs of it. */
|
|
330
|
+
sessionDriver(sessionId: string): {
|
|
331
|
+
session: SessionSummary;
|
|
332
|
+
driver: AgentDriver | undefined;
|
|
333
|
+
};
|
|
318
334
|
/** Adds the live-only fields (current activity, first pending permission) to a stored summary. */
|
|
319
335
|
/** Marks a session that works in one of LinkShell's worktrees. */
|
|
320
336
|
private withWorktree;
|
|
@@ -322,7 +338,20 @@ export declare class SessionHub {
|
|
|
322
338
|
private worktrees;
|
|
323
339
|
private readonly pendingQuestions;
|
|
324
340
|
private questionsOf;
|
|
341
|
+
/** Per session, the session it was forked from (null: none), read from the store once. */
|
|
342
|
+
private readonly forkOrigins;
|
|
343
|
+
/** Per session, the sessions whose lineage names it: their rows change when it comes or goes. */
|
|
344
|
+
private readonly related;
|
|
345
|
+
private forkOrigin;
|
|
346
|
+
/** Where the session came from, naming only sessions this host has: a list can't nest under one it doesn't show. */
|
|
347
|
+
private withLineage;
|
|
348
|
+
/** As the Claude app nests: a session still in use stands alone when the one it came from is archived. */
|
|
349
|
+
private firstKnown;
|
|
350
|
+
/** The session came, went or was (un)archived: the rows that name it as parent or origin say so now. */
|
|
351
|
+
private announceRelated;
|
|
325
352
|
private decorate;
|
|
353
|
+
/** What a new session of `agent` could run on (see `agents.models`): nothing from a driver that can't tell before a session exists. */
|
|
354
|
+
agentModels(agent: string, cwd?: string): Promise<MethodResult<"agents.models">>;
|
|
326
355
|
createSession(input: {
|
|
327
356
|
agent: string;
|
|
328
357
|
cwd: string;
|
|
@@ -416,6 +445,9 @@ export declare class SessionHub {
|
|
|
416
445
|
private sendHeld;
|
|
417
446
|
onRemoved(listener: (sessionId: string) => void): () => void;
|
|
418
447
|
archive(sessionId: string, archived: boolean): Promise<SessionSummary>;
|
|
448
|
+
/** Folds in the archive state of the agent's app that LinkShell only reads (see `mergedArchive`). */
|
|
449
|
+
private reconcileAppArchive;
|
|
450
|
+
private localArchive;
|
|
419
451
|
rename(sessionId: string, title: string): Promise<SessionSummary>;
|
|
420
452
|
/**
|
|
421
453
|
* Deletes a session. A worktree only it used goes too when nothing in it
|
package/dist/host/src/hub.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RpcError, questionReplies, questionAnswerError, updateAsyncQuestions, sessionIdFor, } from "@linkshell/wire";
|
|
1
|
+
import { RpcError, questionReplies, readUserText, questionAnswerError, updateAsyncQuestions, sessionIdFor, } from "@linkshell/wire";
|
|
2
2
|
import { inOrder } from "./drivers/acp/driver.js";
|
|
3
3
|
import { canForkNatively } from "./drivers/behavior.js";
|
|
4
4
|
import { homedir } from "node:os";
|
|
@@ -57,6 +57,24 @@ function worktreeLossMessage(state) {
|
|
|
57
57
|
const RENAMED = "renamed";
|
|
58
58
|
/** Driver-state key: the conversation a forked session's agent is still to be told, with its first message. */
|
|
59
59
|
const CARRY = "carry";
|
|
60
|
+
/** Driver-state key: the session a session was forked from (made here, or as its agent recorded it). */
|
|
61
|
+
const FORKED_FROM = "forkedFrom";
|
|
62
|
+
/**
|
|
63
|
+
* Driver-state keys, for agents with an app LinkShell only reads (`appArchive`):
|
|
64
|
+
* LinkShell's own archive choice and when it was made, and the app's state as
|
|
65
|
+
* last seen and since when.
|
|
66
|
+
*/
|
|
67
|
+
const LOCAL_ARCHIVE = "archive-local";
|
|
68
|
+
const APP_ARCHIVE = "archive-app";
|
|
69
|
+
/**
|
|
70
|
+
* Archived here or in the app, unless it was unarchived here after the app
|
|
71
|
+
* archived it (`local.at` 0: chosen before times were kept, so it never outranks the app).
|
|
72
|
+
*/
|
|
73
|
+
export function mergedArchive(local, app) {
|
|
74
|
+
if (local.archived)
|
|
75
|
+
return true;
|
|
76
|
+
return !!app?.archived && !(local.at > app.since);
|
|
77
|
+
}
|
|
60
78
|
/** A returning client catches up from where it was, unless it missed more than this. */
|
|
61
79
|
const CATCH_UP = { events: 800, bytes: 4 * 1024 * 1024 };
|
|
62
80
|
/** State a client needs from before its window: the latest of each applies. */
|
|
@@ -172,6 +190,17 @@ export class SessionHub {
|
|
|
172
190
|
this.emitSummary(stored);
|
|
173
191
|
},
|
|
174
192
|
removed: (agent, nativeId) => this.forget(sessionIdFor(agent, nativeId)),
|
|
193
|
+
nativeRecordsChanged: (agent, nativeIds) => {
|
|
194
|
+
for (const nativeId of nativeIds) {
|
|
195
|
+
const stored = this.store.getSession(sessionIdFor(agent, nativeId));
|
|
196
|
+
if (!stored)
|
|
197
|
+
continue;
|
|
198
|
+
const merged = this.reconcileAppArchive(stored);
|
|
199
|
+
this.emitSummary(merged);
|
|
200
|
+
if (merged.archived !== stored.archived)
|
|
201
|
+
this.announceRelated(merged.id);
|
|
202
|
+
}
|
|
203
|
+
},
|
|
175
204
|
desktop: (agent, nativeId) => this.desktops.get(sessionIdFor(agent, nativeId)),
|
|
176
205
|
state: (agent, nativeId) => {
|
|
177
206
|
const sessionId = sessionIdFor(agent, nativeId);
|
|
@@ -180,6 +209,10 @@ export class SessionHub {
|
|
|
180
209
|
set: (key, value) => this.store.setDriverState(sessionId, key, value),
|
|
181
210
|
};
|
|
182
211
|
},
|
|
212
|
+
agentState: (agent) => ({
|
|
213
|
+
get: (key) => this.store.getAgentState(agent, key),
|
|
214
|
+
set: (key, value) => this.store.setAgentState(agent, key, value),
|
|
215
|
+
}),
|
|
183
216
|
log: (message) => this.log(message),
|
|
184
217
|
};
|
|
185
218
|
constructor(store, drivers, log = () => { },
|
|
@@ -291,6 +324,7 @@ export class SessionHub {
|
|
|
291
324
|
else if (availability === "available" && this.store.isNativeMissing(session.id)) {
|
|
292
325
|
this.store.setNativeMissing(session.id, false);
|
|
293
326
|
this.emitSummary(session);
|
|
327
|
+
this.announceRelated(session.id);
|
|
294
328
|
}
|
|
295
329
|
}
|
|
296
330
|
hideMissingSession(session) {
|
|
@@ -305,6 +339,7 @@ export class SessionHub {
|
|
|
305
339
|
driver?.watched?.(session.nativeId, false);
|
|
306
340
|
this.live.delete(session.id);
|
|
307
341
|
this.emitRemoved(session.id);
|
|
342
|
+
this.announceRelated(session.id);
|
|
308
343
|
void driver?.detach(session.nativeId).catch((error) => this.log(`[hub] detach missing ${session.id}: ${String(error)}`));
|
|
309
344
|
}
|
|
310
345
|
async stop() {
|
|
@@ -425,6 +460,8 @@ export class SessionHub {
|
|
|
425
460
|
await existing.configureAcp?.(services.settings.settings(id));
|
|
426
461
|
await existing.stop();
|
|
427
462
|
this.drivers.delete(id);
|
|
463
|
+
// What the old definition's agent reported (its models) needn't hold for the new one.
|
|
464
|
+
this.store.clearAgentState(id);
|
|
428
465
|
}
|
|
429
466
|
if (save && driver) {
|
|
430
467
|
services.settings.saveAgent(save);
|
|
@@ -493,6 +530,11 @@ export class SessionHub {
|
|
|
493
530
|
throw RpcError.app("not_found", "这个会话的原始记录已不存在,已从列表隐藏;本地缓存仍保留。");
|
|
494
531
|
return this.decorate(summary);
|
|
495
532
|
}
|
|
533
|
+
/** A session and its agent's driver, unchecked: for what one driver offers itself (`mcp.*`), which says what it needs of it. */
|
|
534
|
+
sessionDriver(sessionId) {
|
|
535
|
+
const session = this.getSession(sessionId);
|
|
536
|
+
return { session, driver: this.drivers.get(session.agent) };
|
|
537
|
+
}
|
|
496
538
|
/** Adds the live-only fields (current activity, first pending permission) to a stored summary. */
|
|
497
539
|
/** Marks a session that works in one of LinkShell's worktrees. */
|
|
498
540
|
withWorktree(summary) {
|
|
@@ -512,8 +554,55 @@ export class SessionHub {
|
|
|
512
554
|
}
|
|
513
555
|
return questions;
|
|
514
556
|
}
|
|
557
|
+
/** Per session, the session it was forked from (null: none), read from the store once. */
|
|
558
|
+
forkOrigins = new Map();
|
|
559
|
+
/** Per session, the sessions whose lineage names it: their rows change when it comes or goes. */
|
|
560
|
+
related = new Map();
|
|
561
|
+
forkOrigin(sessionId) {
|
|
562
|
+
let origin = this.forkOrigins.get(sessionId);
|
|
563
|
+
if (origin === undefined) {
|
|
564
|
+
origin = this.store.getDriverState(sessionId, FORKED_FROM) ?? null;
|
|
565
|
+
this.forkOrigins.set(sessionId, origin);
|
|
566
|
+
}
|
|
567
|
+
return origin ?? undefined;
|
|
568
|
+
}
|
|
569
|
+
/** Where the session came from, naming only sessions this host has: a list can't nest under one it doesn't show. */
|
|
570
|
+
withLineage(summary) {
|
|
571
|
+
const native = this.drivers.get(summary.agent)?.lineage?.(summary.nativeId);
|
|
572
|
+
const ids = (nativeIds) => nativeIds?.map((nativeId) => sessionIdFor(summary.agent, nativeId)) ?? [];
|
|
573
|
+
const origin = this.forkOrigin(summary.id);
|
|
574
|
+
const parentId = this.firstKnown(summary, ids(native?.parent));
|
|
575
|
+
const forkedFrom = this.firstKnown(summary, [...(origin ? [origin] : []), ...ids(native?.forkedFrom)]);
|
|
576
|
+
const appId = native?.appId;
|
|
577
|
+
if (!parentId && !forkedFrom && !appId)
|
|
578
|
+
return summary;
|
|
579
|
+
return { ...summary, ...(parentId ? { parentId } : {}), ...(forkedFrom ? { forkedFrom } : {}), ...(appId ? { appId } : {}) };
|
|
580
|
+
}
|
|
581
|
+
/** As the Claude app nests: a session still in use stands alone when the one it came from is archived. */
|
|
582
|
+
firstKnown(summary, candidates) {
|
|
583
|
+
let found;
|
|
584
|
+
for (const candidate of candidates) {
|
|
585
|
+
if (candidate === summary.id)
|
|
586
|
+
continue;
|
|
587
|
+
let children = this.related.get(candidate);
|
|
588
|
+
if (!children)
|
|
589
|
+
this.related.set(candidate, (children = new Set()));
|
|
590
|
+
children.add(summary.id);
|
|
591
|
+
if (found)
|
|
592
|
+
continue;
|
|
593
|
+
const other = this.store.getSession(candidate);
|
|
594
|
+
if (other && !(other.archived && !summary.archived) && !this.store.isNativeMissing(candidate))
|
|
595
|
+
found = candidate;
|
|
596
|
+
}
|
|
597
|
+
return found;
|
|
598
|
+
}
|
|
599
|
+
/** The session came, went or was (un)archived: the rows that name it as parent or origin say so now. */
|
|
600
|
+
announceRelated(sessionId) {
|
|
601
|
+
for (const child of this.related.get(sessionId) ?? [])
|
|
602
|
+
this.announce(child);
|
|
603
|
+
}
|
|
515
604
|
decorate(summary) {
|
|
516
|
-
summary = { ...summary, asyncQuestions: this.questionsOf(summary.id) };
|
|
605
|
+
summary = this.withLineage({ ...summary, asyncQuestions: this.questionsOf(summary.id) });
|
|
517
606
|
const live = this.live.get(summary.id);
|
|
518
607
|
if (!live)
|
|
519
608
|
return this.withWorktree(summary);
|
|
@@ -561,6 +650,11 @@ export class SessionHub {
|
|
|
561
650
|
}
|
|
562
651
|
return decorated;
|
|
563
652
|
}
|
|
653
|
+
/** What a new session of `agent` could run on (see `agents.models`): nothing from a driver that can't tell before a session exists. */
|
|
654
|
+
async agentModels(agent, cwd) {
|
|
655
|
+
const driver = this.requireDriver(agent);
|
|
656
|
+
return (await driver.listModels?.(cwd)) ?? { models: [] };
|
|
657
|
+
}
|
|
564
658
|
async createSession(input) {
|
|
565
659
|
const driver = this.requireDriver(input.agent);
|
|
566
660
|
const worktree = input.worktree ? await this.newWorktree(input.cwd, promptLabel(input.prompt)) : undefined;
|
|
@@ -615,7 +709,7 @@ export class SessionHub {
|
|
|
615
709
|
}
|
|
616
710
|
if (!discovered)
|
|
617
711
|
return await this.forkByReplay(summary, driver, cwd, cut);
|
|
618
|
-
this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title });
|
|
712
|
+
this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title, forkedFrom: summary.nativeId });
|
|
619
713
|
return this.getSession(sessionIdFor(driver.id, discovered.nativeId));
|
|
620
714
|
}
|
|
621
715
|
catch (error) {
|
|
@@ -633,7 +727,7 @@ export class SessionHub {
|
|
|
633
727
|
const start = this.store.pageStart(summary.id, cut);
|
|
634
728
|
const events = this.store.readEvents(summary.id, start, PAGE.maxEvents + 1, cut).filter((event) => isConversation(event.update));
|
|
635
729
|
const discovered = await driver.createSession({ cwd });
|
|
636
|
-
this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title });
|
|
730
|
+
this.recordDiscovered(driver.id, { ...discovered, title: discovered.title ?? summary.title, forkedFrom: summary.nativeId });
|
|
637
731
|
const forkId = sessionIdFor(driver.id, discovered.nativeId);
|
|
638
732
|
await this.ensureAttached(forkId);
|
|
639
733
|
const live = this.liveFor(forkId);
|
|
@@ -1185,10 +1279,40 @@ export class SessionHub {
|
|
|
1185
1279
|
const driver = this.drivers.get(summary.agent);
|
|
1186
1280
|
if (driver?.archive)
|
|
1187
1281
|
await driver.archive(summary.nativeId, archived);
|
|
1188
|
-
|
|
1282
|
+
// Kept apart from the app's, with when: unarchiving here outranks an older archive there.
|
|
1283
|
+
if (driver?.appArchive)
|
|
1284
|
+
this.store.setDriverState(sessionId, LOCAL_ARCHIVE, JSON.stringify({ archived, at: Date.now() }));
|
|
1285
|
+
const updated = this.reconcileAppArchive(this.store.patchSession(sessionId, { archived }));
|
|
1189
1286
|
this.emitSummary(updated);
|
|
1287
|
+
this.announceRelated(sessionId);
|
|
1190
1288
|
return this.decorate(updated);
|
|
1191
1289
|
}
|
|
1290
|
+
/** Folds in the archive state of the agent's app that LinkShell only reads (see `mergedArchive`). */
|
|
1291
|
+
reconcileAppArchive(summary) {
|
|
1292
|
+
const reported = this.drivers.get(summary.agent)?.appArchive?.(summary.nativeId);
|
|
1293
|
+
// Not in that app (or not read yet): the flag is LinkShell's alone, or stays as last merged.
|
|
1294
|
+
if (!reported)
|
|
1295
|
+
return summary;
|
|
1296
|
+
const saved = this.store.getDriverState(summary.id, APP_ARCHIVE);
|
|
1297
|
+
const seen = saved ? JSON.parse(saved) : undefined;
|
|
1298
|
+
if (!seen && !reported.archived)
|
|
1299
|
+
return summary;
|
|
1300
|
+
// Only a change counts as the app archiving it: the app rewrites its record for much else.
|
|
1301
|
+
const app = seen?.archived === reported.archived ? seen : { archived: reported.archived, since: reported.at };
|
|
1302
|
+
if (app !== seen)
|
|
1303
|
+
this.store.setDriverState(summary.id, APP_ARCHIVE, JSON.stringify(app));
|
|
1304
|
+
const archived = mergedArchive(this.localArchive(summary), app);
|
|
1305
|
+
return archived === summary.archived ? summary : this.store.patchSession(summary.id, { archived });
|
|
1306
|
+
}
|
|
1307
|
+
localArchive(summary) {
|
|
1308
|
+
const saved = this.store.getDriverState(summary.id, LOCAL_ARCHIVE);
|
|
1309
|
+
if (saved)
|
|
1310
|
+
return JSON.parse(saved);
|
|
1311
|
+
// Until the app's state is first merged in, the flag is LinkShell's own choice.
|
|
1312
|
+
const local = { archived: summary.archived, at: 0 };
|
|
1313
|
+
this.store.setDriverState(summary.id, LOCAL_ARCHIVE, JSON.stringify(local));
|
|
1314
|
+
return local;
|
|
1315
|
+
}
|
|
1192
1316
|
async rename(sessionId, title) {
|
|
1193
1317
|
const summary = this.getSession(sessionId);
|
|
1194
1318
|
const name = compact(title, TITLE_LENGTH);
|
|
@@ -1249,7 +1373,9 @@ export class SessionHub {
|
|
|
1249
1373
|
this.previews.forget(sessionId);
|
|
1250
1374
|
this.sides.forget(sessionId);
|
|
1251
1375
|
this.store.removeSession(sessionId);
|
|
1376
|
+
this.forkOrigins.delete(sessionId);
|
|
1252
1377
|
this.emitRemoved(sessionId);
|
|
1378
|
+
this.announceRelated(sessionId);
|
|
1253
1379
|
}
|
|
1254
1380
|
emitRemoved(sessionId) {
|
|
1255
1381
|
for (const listener of this.removedListeners) {
|
|
@@ -1431,10 +1557,19 @@ export class SessionHub {
|
|
|
1431
1557
|
// Items without a recorded time get the time of the one before, never "now".
|
|
1432
1558
|
// (An agent that only says when a session last changed gives that as its creation too.)
|
|
1433
1559
|
let ts = Math.min(summary.createdAt, history.find((item) => item.ts !== undefined)?.ts ?? summary.createdAt);
|
|
1434
|
-
|
|
1560
|
+
// The log only grows at its end. A message before what it already has is one an earlier
|
|
1561
|
+
// reading skipped (a reader that now shows more), not something that happened since:
|
|
1562
|
+
// added now it would land at the bottom, out of place, so it is only marked as seen.
|
|
1563
|
+
const logged = history.map((item) => this.store.isItemLogged(sessionId, item.itemId));
|
|
1564
|
+
const lastLogged = logged.lastIndexOf(true);
|
|
1565
|
+
for (const [index, item] of history.entries()) {
|
|
1435
1566
|
ts = Math.min(Math.max(item.ts ?? ts, ts), summary.updatedAt);
|
|
1436
|
-
if (
|
|
1567
|
+
if (logged[index])
|
|
1437
1568
|
continue;
|
|
1569
|
+
if (index < lastLogged && item.updates.every((update) => update.sessionUpdate === "user_message_chunk")) {
|
|
1570
|
+
this.store.markItemLogged(sessionId, item.itemId);
|
|
1571
|
+
continue;
|
|
1572
|
+
}
|
|
1438
1573
|
for (const update of item.updates)
|
|
1439
1574
|
this.commit(sessionId, update, undefined, ts);
|
|
1440
1575
|
this.store.markItemLogged(sessionId, item.itemId);
|
|
@@ -1500,9 +1635,9 @@ export class SessionHub {
|
|
|
1500
1635
|
updatedAt: session.updatedAt,
|
|
1501
1636
|
});
|
|
1502
1637
|
const { created } = stored;
|
|
1503
|
-
const summary = session.archived !== undefined && session.archived !== stored.summary.archived
|
|
1638
|
+
const summary = this.reconcileAppArchive(session.archived !== undefined && session.archived !== stored.summary.archived
|
|
1504
1639
|
? this.store.patchSession(stored.summary.id, { archived: session.archived })
|
|
1505
|
-
: stored.summary;
|
|
1640
|
+
: stored.summary);
|
|
1506
1641
|
const changed = !!before &&
|
|
1507
1642
|
(before.title !== summary.title ||
|
|
1508
1643
|
before.state !== summary.state ||
|
|
@@ -1517,8 +1652,16 @@ export class SessionHub {
|
|
|
1517
1652
|
if (questionsChanged)
|
|
1518
1653
|
this.store.setDriverState(summary.id, "asyncQuestions", JSON.stringify(session.asyncQuestions));
|
|
1519
1654
|
}
|
|
1520
|
-
|
|
1655
|
+
const origin = session.forkedFrom === undefined ? undefined : sessionIdFor(agent, session.forkedFrom);
|
|
1656
|
+
const forked = origin !== undefined && origin !== summary.id && origin !== this.forkOrigin(summary.id);
|
|
1657
|
+
if (forked) {
|
|
1658
|
+
this.store.setDriverState(summary.id, FORKED_FROM, origin);
|
|
1659
|
+
this.forkOrigins.set(summary.id, origin);
|
|
1660
|
+
}
|
|
1661
|
+
if (created || changed || questionsChanged || restored || forked)
|
|
1521
1662
|
this.emitSummary(summary);
|
|
1663
|
+
if (created || restored || before?.archived !== summary.archived)
|
|
1664
|
+
this.announceRelated(summary.id);
|
|
1522
1665
|
}
|
|
1523
1666
|
ingest(sessionId, update, itemId, ts) {
|
|
1524
1667
|
if (this.store.isNativeMissing(sessionId))
|
|
@@ -1595,8 +1738,9 @@ export class SessionHub {
|
|
|
1595
1738
|
if (update.parentToolCallId || !update.content)
|
|
1596
1739
|
break;
|
|
1597
1740
|
const text = update.content.filter((block) => block.type === "text").map((block) => block.text).join("");
|
|
1598
|
-
|
|
1599
|
-
|
|
1741
|
+
// A title is the person's words, not the context their client or Claude wrapped around them.
|
|
1742
|
+
if (update.role === "user" && !before.title && readUserText(text).text.trim())
|
|
1743
|
+
patch.title = compact(readUserText(text).text, TITLE_LENGTH);
|
|
1600
1744
|
if (update.role === "agent" && text.trim())
|
|
1601
1745
|
patch.preview = compact(text, PREVIEW_LENGTH);
|
|
1602
1746
|
if (live.turnActive && update.role === "agent")
|
|
@@ -1604,7 +1748,7 @@ export class SessionHub {
|
|
|
1604
1748
|
break;
|
|
1605
1749
|
}
|
|
1606
1750
|
case "user_message_chunk": {
|
|
1607
|
-
const text = update.content.type === "text" ? update.content.text : "";
|
|
1751
|
+
const text = update.content.type === "text" ? readUserText(update.content.text).text : "";
|
|
1608
1752
|
if (text && !before.title)
|
|
1609
1753
|
patch.title = compact(text, TITLE_LENGTH);
|
|
1610
1754
|
if (text && !before.preview)
|