pim-agent 0.0.1 → 0.10.0
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/LICENSE +21 -0
- package/README.md +293 -7
- package/bin/pim.ts +151 -0
- package/package.json +82 -4
- package/packages/core/src/attachments/AttachmentStore.ts +99 -0
- package/packages/core/src/attachments/Attachments.ts +69 -0
- package/packages/core/src/extensions/CoreExtensions.ts +54 -0
- package/packages/core/src/extensions/apply-patch/coordinator.ts +53 -0
- package/packages/core/src/extensions/apply-patch/executor.ts +550 -0
- package/packages/core/src/extensions/apply-patch/index.ts +69 -0
- package/packages/core/src/extensions/apply-patch/matcher.ts +47 -0
- package/packages/core/src/extensions/apply-patch/model.ts +39 -0
- package/packages/core/src/extensions/apply-patch/parser.ts +379 -0
- package/packages/core/src/extensions/apply-patch/render.ts +177 -0
- package/packages/core/src/extensions/apply-patch/schema.ts +37 -0
- package/packages/core/src/extensions/apply-patch/types.ts +30 -0
- package/packages/core/src/extensions/bash/capture.ts +86 -0
- package/packages/core/src/extensions/bash/format.ts +110 -0
- package/packages/core/src/extensions/bash/image.ts +31 -0
- package/packages/core/src/extensions/bash/index.ts +88 -0
- package/packages/core/src/extensions/bash/render.ts +31 -0
- package/packages/core/src/extensions/bash/run.ts +221 -0
- package/packages/core/src/extensions/bash/schema.ts +64 -0
- package/packages/core/src/extensions/edit/edit.ts +366 -0
- package/packages/core/src/extensions/edit/index.ts +36 -0
- package/packages/core/src/extensions/edit/render.ts +16 -0
- package/packages/core/src/extensions/edit/schema.ts +37 -0
- package/packages/core/src/extensions/glob/glob.ts +34 -0
- package/packages/core/src/extensions/glob/index.ts +83 -0
- package/packages/core/src/extensions/glob/render.ts +72 -0
- package/packages/core/src/extensions/glob/schema.ts +66 -0
- package/packages/core/src/extensions/grep/grep.ts +256 -0
- package/packages/core/src/extensions/grep/index.ts +114 -0
- package/packages/core/src/extensions/grep/render.ts +271 -0
- package/packages/core/src/extensions/grep/schema.ts +92 -0
- package/packages/core/src/extensions/read/ImageMemory.ts +42 -0
- package/packages/core/src/extensions/read/image.ts +57 -0
- package/packages/core/src/extensions/read/index.ts +88 -0
- package/packages/core/src/extensions/read/read.ts +191 -0
- package/packages/core/src/extensions/read/render.ts +58 -0
- package/packages/core/src/extensions/read/schema.ts +49 -0
- package/packages/core/src/extensions/subagent/index.ts +33 -0
- package/packages/core/src/extensions/subagent/render.ts +127 -0
- package/packages/core/src/extensions/subagent/schema.ts +9 -0
- package/packages/core/src/extensions/subagent/subagent.ts +419 -0
- package/packages/core/src/extensions/system-prompt/index.ts +36 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +232 -0
- package/packages/core/src/extensions/todo/index.ts +123 -0
- package/packages/core/src/extensions/todo/render.ts +156 -0
- package/packages/core/src/extensions/todo/schema.ts +24 -0
- package/packages/core/src/extensions/todo/todo.ts +185 -0
- package/packages/core/src/extensions/web-fetch/JinaReaderClient.ts +187 -0
- package/packages/core/src/extensions/web-fetch/WebViewFetchClient.ts +183 -0
- package/packages/core/src/extensions/web-fetch/WebViewMarkdownSnapshot.ts +512 -0
- package/packages/core/src/extensions/web-fetch/fetch.ts +265 -0
- package/packages/core/src/extensions/web-fetch/image.ts +165 -0
- package/packages/core/src/extensions/web-fetch/index.ts +83 -0
- package/packages/core/src/extensions/web-fetch/render.ts +100 -0
- package/packages/core/src/extensions/web-fetch/schema.ts +46 -0
- package/packages/core/src/extensions/web-search/ExaMcpClient.ts +251 -0
- package/packages/core/src/extensions/web-search/SearchBreaker.ts +147 -0
- package/packages/core/src/extensions/web-search/SearchChain.ts +77 -0
- package/packages/core/src/extensions/web-search/index.ts +90 -0
- package/packages/core/src/extensions/web-search/providers/DuckDuckGoProvider.ts +195 -0
- package/packages/core/src/extensions/web-search/providers/ExaProvider.ts +54 -0
- package/packages/core/src/extensions/web-search/providers/FirecrawlProvider.ts +144 -0
- package/packages/core/src/extensions/web-search/providers/SearchProvider.ts +118 -0
- package/packages/core/src/extensions/web-search/render.ts +49 -0
- package/packages/core/src/extensions/web-search/schema.ts +29 -0
- package/packages/core/src/extensions/web-search/search.ts +23 -0
- package/packages/core/src/extensions/write/index.ts +58 -0
- package/packages/core/src/extensions/write/render.ts +16 -0
- package/packages/core/src/extensions/write/schema.ts +14 -0
- package/packages/core/src/extensions/write/write.ts +87 -0
- package/packages/core/src/picker/FilePickerSuggestionEngine.ts +14 -0
- package/packages/core/src/picker/InProcessFilePickerSuggestionEngine.ts +52 -0
- package/packages/core/src/picker/PickerItem.ts +6 -0
- package/packages/core/src/picker/PickerService.ts +132 -0
- package/packages/core/src/picker/RemoteFilePickerSuggestionEngine.ts +56 -0
- package/packages/core/src/picker/WorkerFilePickerSuggestionEngine.ts +268 -0
- package/packages/core/src/picker/catalog.ts +220 -0
- package/packages/core/src/picker/commandRanker.ts +17 -0
- package/packages/core/src/picker/filePickerWorker.ts +66 -0
- package/packages/core/src/picker/filePickerWorkerMessages.ts +39 -0
- package/packages/core/src/picker/ranker.ts +245 -0
- package/packages/core/src/picker/token.ts +77 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +156 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SearchIndex.ts +752 -0
- package/packages/core/src/session/SearchTokens.ts +97 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionDigest.ts +161 -0
- package/packages/core/src/session/SessionHost.ts +892 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionMeta.ts +267 -0
- package/packages/core/src/session/SessionName.ts +18 -0
- package/packages/core/src/session/SessionRegistry.ts +244 -0
- package/packages/core/src/session/WriteMark.ts +51 -0
- package/packages/core/src/shared/Cli.ts +25 -0
- package/packages/core/src/shared/DaemonUnit.ts +13 -0
- package/packages/core/src/shared/DiffLines.ts +304 -0
- package/packages/core/src/shared/DiffPatch.ts +70 -0
- package/packages/core/src/shared/DiffRenderer.ts +283 -0
- package/packages/core/src/shared/DiffView.ts +29 -0
- package/packages/core/src/shared/Directories.ts +61 -0
- package/packages/core/src/shared/EditMatcher.ts +767 -0
- package/packages/core/src/shared/Errors.ts +9 -0
- package/packages/core/src/shared/ExtensionToggles.ts +130 -0
- package/packages/core/src/shared/FileEnumerator.ts +361 -0
- package/packages/core/src/shared/FileScanner.ts +57 -0
- package/packages/core/src/shared/FileWatch.ts +121 -0
- package/packages/core/src/shared/Format.ts +74 -0
- package/packages/core/src/shared/Fs.ts +91 -0
- package/packages/core/src/shared/FsErrors.ts +67 -0
- package/packages/core/src/shared/FuzzyMatcher.ts +71 -0
- package/packages/core/src/shared/Git.ts +530 -0
- package/packages/core/src/shared/GitMonitor.ts +221 -0
- package/packages/core/src/shared/GlobExclusions.ts +13 -0
- package/packages/core/src/shared/Http.ts +14 -0
- package/packages/core/src/shared/ImageMime.ts +36 -0
- package/packages/core/src/shared/Images.ts +357 -0
- package/packages/core/src/shared/Json.ts +19 -0
- package/packages/core/src/shared/Languages.ts +95 -0
- package/packages/core/src/shared/Levenshtein.ts +83 -0
- package/packages/core/src/shared/Lines.ts +79 -0
- package/packages/core/src/shared/McpClient.ts +444 -0
- package/packages/core/src/shared/OutputBudget.ts +84 -0
- package/packages/core/src/shared/PatchSummary.ts +40 -0
- package/packages/core/src/shared/Paths.ts +80 -0
- package/packages/core/src/shared/PimSettings.ts +128 -0
- package/packages/core/src/shared/PimVersion.ts +93 -0
- package/packages/core/src/shared/Pool.ts +26 -0
- package/packages/core/src/shared/Proc.ts +50 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -0
- package/packages/core/src/shared/RepoDiff.ts +741 -0
- package/packages/core/src/shared/SafePath.ts +17 -0
- package/packages/core/src/shared/SearchRender.ts +82 -0
- package/packages/core/src/shared/SendFile.ts +23 -0
- package/packages/core/src/shared/SpillCache.ts +78 -0
- package/packages/core/src/shared/SubagentLogs.ts +102 -0
- package/packages/core/src/shared/Supervisor.ts +460 -0
- package/packages/core/src/shared/Surface.ts +2 -0
- package/packages/core/src/shared/Sweeper.ts +32 -0
- package/packages/core/src/shared/Tools.ts +801 -0
- package/packages/core/src/shared/Updater.ts +209 -0
- package/packages/core/src/view/AnsiPainter.ts +227 -0
- package/packages/core/src/view/BodyRenderer.ts +102 -0
- package/packages/core/src/view/DiffBlocks.ts +52 -0
- package/packages/core/src/view/DiffExpand.ts +214 -0
- package/packages/core/src/view/DiffLayout.ts +69 -0
- package/packages/core/src/view/DiffPairs.ts +47 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -0
- package/packages/core/src/view/MovePath.ts +70 -0
- package/packages/core/src/view/Painting.ts +182 -0
- package/packages/core/src/view/ViewBlock.ts +124 -0
- package/packages/daemon/src/Daemon.ts +63 -0
- package/packages/daemon/src/DaemonInstall.ts +115 -0
- package/packages/daemon/src/Surfaces.ts +66 -0
- package/packages/daemon/src/TelegramSurface.ts +30 -0
- package/packages/daemon/src/WebSurface.ts +56 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +219 -0
- package/packages/protocol/src/Diff.ts +15 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/ServerEvent.ts +406 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +277 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +374 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +456 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +627 -0
- package/packages/server/src/StaticClient.ts +178 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +798 -0
- package/packages/server/src/probe.ts +200 -0
- package/packages/telegram/src/Bot.ts +229 -0
- package/packages/telegram/src/Commands.ts +768 -0
- package/packages/telegram/src/Config.ts +148 -0
- package/packages/telegram/src/Markdown.ts +235 -0
- package/packages/telegram/src/Message.ts +152 -0
- package/packages/telegram/src/Renderer.ts +456 -0
- package/packages/telegram/src/SendFileSchema.ts +18 -0
- package/packages/telegram/src/SendFileTool.ts +65 -0
- package/packages/telegram/src/Session.ts +253 -0
- package/packages/telegram/src/SessionRegistry.ts +126 -0
- package/packages/telegram/src/TaskScheduler.ts +296 -0
- package/packages/telegram/src/TaskSchema.ts +88 -0
- package/packages/telegram/src/TaskStore.ts +67 -0
- package/packages/telegram/src/TaskTool.ts +208 -0
- package/packages/telegram/src/TelegramHtml.ts +288 -0
- package/packages/telegram/src/TypingIndicator.ts +43 -0
- package/packages/telegram/src/UpdateConfirm.ts +61 -0
- package/packages/tui/src/extensions/_init/index.ts +144 -0
- package/packages/tui/src/extensions/command-picker/index.ts +75 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +93 -0
- package/packages/tui/src/extensions/footer/powerline.ts +87 -0
- package/packages/tui/src/extensions/footer/segments.ts +202 -0
- package/packages/tui/src/extensions/pim/index.ts +89 -0
- package/packages/tui/src/extensions/session-lease/index.ts +253 -0
- package/packages/tui/src/extensions/tps/index.ts +118 -0
- package/packages/tui/src/extensions/working-indicator/index.ts +75 -0
- package/packages/tui/src/extensions/wrapProvider.ts +18 -0
- package/packages/tui/src/themes/pim-dark.json +84 -0
- package/packages/tui/src/themes/pim-light.json +84 -0
- package/packages/tui/src/themes/themeCliArgs.ts +20 -0
- package/packages/web/dist/client/apple-touch-icon.png +0 -0
- package/packages/web/dist/client/assets/bash-LmRjUKlR.js +1 -0
- package/packages/web/dist/client/assets/c-DBVbJFpC.js +1 -0
- package/packages/web/dist/client/assets/clojure-CR7dvLJ4.js +1 -0
- package/packages/web/dist/client/assets/cmake-CgP6gB4W.js +1 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-B48RJMeK.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-italic-DBq5bJCE.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-B-iV2FbL.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-300-normal-kM0OTZCv.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-BXinMwCi.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-italic-DjSHLl2N.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-s0S3qwFW.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-400-normal-wzhe4RuD.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-BuQDXT0M.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-600-normal-SwgSWHaV.woff2 +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-1-GU0IUE.woff +0 -0
- package/packages/web/dist/client/assets/commit-mono-latin-700-normal-DU8mrtj2.woff2 +0 -0
- package/packages/web/dist/client/assets/core-CA0aSzPu.js +3 -0
- package/packages/web/dist/client/assets/cpp-N2d8M7Nc.js +1 -0
- package/packages/web/dist/client/assets/csharp-yYt0Ikks.js +1 -0
- package/packages/web/dist/client/assets/css-D8M8GYaw.js +1 -0
- package/packages/web/dist/client/assets/diff-BhdZizse.js +1 -0
- package/packages/web/dist/client/assets/dockerfile-C0zufGsU.js +1 -0
- package/packages/web/dist/client/assets/elixir-DXIHlGsR.js +1 -0
- package/packages/web/dist/client/assets/erlang-BjYI8dhz.js +1 -0
- package/packages/web/dist/client/assets/go-CYstVVJV.js +1 -0
- package/packages/web/dist/client/assets/graphql-BHHhacaR.js +1 -0
- package/packages/web/dist/client/assets/haskell-DUkhXIda.js +1 -0
- package/packages/web/dist/client/assets/index-B9be-i9d.css +1 -0
- package/packages/web/dist/client/assets/index-CpinM5NK.js +45 -0
- package/packages/web/dist/client/assets/ini-B1CroMsf.js +1 -0
- package/packages/web/dist/client/assets/java-Dde-Wnb4.js +1 -0
- package/packages/web/dist/client/assets/javascript-8AmHuH5O.js +1 -0
- package/packages/web/dist/client/assets/json-PmyKWHmm.js +1 -0
- package/packages/web/dist/client/assets/kotlin-DgSK8bu-.js +2 -0
- package/packages/web/dist/client/assets/less-Bp0mgmpG.js +1 -0
- package/packages/web/dist/client/assets/lua-1dXYaL-H.js +1 -0
- package/packages/web/dist/client/assets/makefile-BX-o3EIz.js +1 -0
- package/packages/web/dist/client/assets/markdown-D6FrnlqJ.js +1 -0
- package/packages/web/dist/client/assets/ocaml-B-P1Uyq8.js +1 -0
- package/packages/web/dist/client/assets/perl-CvqMZYuf.js +1 -0
- package/packages/web/dist/client/assets/php-DuZl5e-L.js +2 -0
- package/packages/web/dist/client/assets/powershell-BUe16zE4.js +1 -0
- package/packages/web/dist/client/assets/protobuf-DGOW1qT8.js +1 -0
- package/packages/web/dist/client/assets/python-gHmu6VPG.js +1 -0
- package/packages/web/dist/client/assets/r-DNWPCnjT.js +1 -0
- package/packages/web/dist/client/assets/ruby-BGpZ3p4_.js +1 -0
- package/packages/web/dist/client/assets/rust-BIKPVoi6.js +1 -0
- package/packages/web/dist/client/assets/scala-C9lP4FWY.js +1 -0
- package/packages/web/dist/client/assets/scss-gOyOKiXd.js +1 -0
- package/packages/web/dist/client/assets/sql-DWRKyvJ-.js +1 -0
- package/packages/web/dist/client/assets/swift-BTn9__hL.js +1 -0
- package/packages/web/dist/client/assets/typescript-CxkPLmSc.js +1 -0
- package/packages/web/dist/client/assets/vim-Dhhcn_CA.js +1 -0
- package/packages/web/dist/client/assets/xml-COl5yFSC.js +1 -0
- package/packages/web/dist/client/assets/yaml-DjOhhVLG.js +1 -0
- package/packages/web/dist/client/favicon.svg +1 -0
- package/packages/web/dist/client/icon-192.png +0 -0
- package/packages/web/dist/client/icon-512.png +0 -0
- package/packages/web/dist/client/icon-maskable.png +0 -0
- package/packages/web/dist/client/index.html +24 -0
- package/packages/web/dist/client/manifest.webmanifest +21 -0
- package/packages/web/dist/client/wordmark.svg +1 -0
|
@@ -0,0 +1,627 @@
|
|
|
1
|
+
import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
import { PickerService } from "#core/picker/PickerService";
|
|
4
|
+
import { MessageText } from "#core/session/MessageText";
|
|
5
|
+
import type { LeaseState, SessionHost } from "#core/session/SessionHost";
|
|
6
|
+
import { SessionLease, type LeaseRecord } from "#core/session/SessionLease";
|
|
7
|
+
import { FileWatch } from "#core/shared/FileWatch";
|
|
8
|
+
import { GitMonitor } from "#core/shared/GitMonitor";
|
|
9
|
+
import { Tools } from "#core/shared/Tools";
|
|
10
|
+
import type { ToolView } from "#core/view/ViewBlock";
|
|
11
|
+
import type {
|
|
12
|
+
EphemeralEvent,
|
|
13
|
+
ServerEvent,
|
|
14
|
+
StreamEvent,
|
|
15
|
+
} from "#protocol/ServerEvent";
|
|
16
|
+
import { SessionProjection } from "./SessionProjection";
|
|
17
|
+
|
|
18
|
+
export type StreamListener = (event: ServerEvent) => void;
|
|
19
|
+
|
|
20
|
+
type LiveTool = {
|
|
21
|
+
readonly callId: string;
|
|
22
|
+
readonly name: string;
|
|
23
|
+
readonly args: unknown;
|
|
24
|
+
view: ToolView;
|
|
25
|
+
isError: boolean;
|
|
26
|
+
done: boolean;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
type LiveMessage = {
|
|
30
|
+
readonly messageId: string;
|
|
31
|
+
text: string;
|
|
32
|
+
thinking: string;
|
|
33
|
+
ended: boolean;
|
|
34
|
+
retired: boolean;
|
|
35
|
+
readonly tools: LiveTool[];
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type SessionStreamDeps = {
|
|
39
|
+
/** How often the file watch falls back to a poll; the default is a second. */
|
|
40
|
+
readonly pollMs?: number;
|
|
41
|
+
/** Shared with every other session in the same directory; one is made here when none is given. */
|
|
42
|
+
readonly git?: GitMonitor;
|
|
43
|
+
/**
|
|
44
|
+
* Whether a session in this stream's directory is mid-turn, this one
|
|
45
|
+
* included. Only the gateway can see the others, so a stream standing alone
|
|
46
|
+
* reports nothing rather than guessing from itself.
|
|
47
|
+
*/
|
|
48
|
+
readonly repoBusy?: () => boolean;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function sameLease(a: LeaseState, b: LeaseState): boolean {
|
|
52
|
+
return (
|
|
53
|
+
a.writable === b.writable &&
|
|
54
|
+
a.heldBy?.pid === b.heldBy?.pid &&
|
|
55
|
+
a.heldBy?.frontend === b.heldBy?.frontend
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** One session's view of the world, shared by every client attached to it and kept running when none are. */
|
|
60
|
+
export class SessionStream {
|
|
61
|
+
public readonly sessionId: string;
|
|
62
|
+
public readonly host: SessionHost;
|
|
63
|
+
public readonly picker: PickerService;
|
|
64
|
+
private readonly sessionPath: string;
|
|
65
|
+
private readonly projection: SessionProjection;
|
|
66
|
+
private readonly listeners = new Set<StreamListener>();
|
|
67
|
+
private readonly pollMs: number | undefined;
|
|
68
|
+
private readonly git: GitMonitor;
|
|
69
|
+
private readonly repoBusy: (() => boolean) | undefined;
|
|
70
|
+
private liveTurn: LiveMessage[] = [];
|
|
71
|
+
private unsubscribe: (() => void) | undefined;
|
|
72
|
+
private unsubscribeLease: (() => void) | undefined;
|
|
73
|
+
private unsubscribeForeign: (() => void) | undefined;
|
|
74
|
+
private unwatch: (() => void) | undefined;
|
|
75
|
+
private holder: LeaseRecord | undefined;
|
|
76
|
+
private lease: LeaseState = { writable: true };
|
|
77
|
+
private draining: Promise<void> = Promise.resolve();
|
|
78
|
+
private drainQueued = false;
|
|
79
|
+
private sentSeq = 0;
|
|
80
|
+
private liveMessageId = 0;
|
|
81
|
+
private turnStartedAt = 0;
|
|
82
|
+
private gitCwd: string | undefined;
|
|
83
|
+
private gitStop: (() => void) | undefined;
|
|
84
|
+
private gitBranch: string | null = null;
|
|
85
|
+
|
|
86
|
+
public constructor(
|
|
87
|
+
sessionId: string,
|
|
88
|
+
host: SessionHost,
|
|
89
|
+
sessionPath: string,
|
|
90
|
+
deps: SessionStreamDeps = {}
|
|
91
|
+
) {
|
|
92
|
+
this.sessionId = sessionId;
|
|
93
|
+
this.host = host;
|
|
94
|
+
this.sessionPath = sessionPath;
|
|
95
|
+
this.pollMs = deps.pollMs;
|
|
96
|
+
this.git = deps.git ?? new GitMonitor();
|
|
97
|
+
this.repoBusy = deps.repoBusy;
|
|
98
|
+
this.projection = new SessionProjection(sessionPath, () => host.cwd);
|
|
99
|
+
this.picker = new PickerService({
|
|
100
|
+
cwd: () => host.cwd,
|
|
101
|
+
agentDir: host.agentDir,
|
|
102
|
+
agent: () => host.agentSession,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** Follow the host, not one agent: a rehydrated session is a new `AgentSession`. */
|
|
107
|
+
public start(): void {
|
|
108
|
+
this.unsubscribe ??= this.host.subscribe((event) => {
|
|
109
|
+
this.onAgentEvent(event);
|
|
110
|
+
});
|
|
111
|
+
this.unsubscribeLease ??= this.host.onLeaseChange(() => {
|
|
112
|
+
void this.pushLease();
|
|
113
|
+
});
|
|
114
|
+
this.unsubscribeForeign ??= this.host.onForeignWrite(() => {
|
|
115
|
+
this.push({
|
|
116
|
+
type: "error",
|
|
117
|
+
message:
|
|
118
|
+
"Another process is writing this session; its history may be inconsistent.",
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Watch the lease and the session file while a client is reading: un-greying
|
|
125
|
+
* has to feel instant, and a turn another process runs reaches the browser
|
|
126
|
+
* only through the file, which emits no agent event here. Idle sessions pay
|
|
127
|
+
* nothing.
|
|
128
|
+
*/
|
|
129
|
+
public watchFiles(active: boolean): void {
|
|
130
|
+
if (active === (this.unwatch !== undefined)) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
if (!active) {
|
|
134
|
+
this.unwatch?.();
|
|
135
|
+
this.unwatch = undefined;
|
|
136
|
+
this.syncGit();
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const stops = [
|
|
140
|
+
SessionLease.watch(this.sessionPath, () => {
|
|
141
|
+
void this.pushLease();
|
|
142
|
+
}),
|
|
143
|
+
FileWatch.file(
|
|
144
|
+
this.sessionPath,
|
|
145
|
+
() => {
|
|
146
|
+
this.scheduleDrain();
|
|
147
|
+
},
|
|
148
|
+
this.pollMs
|
|
149
|
+
),
|
|
150
|
+
];
|
|
151
|
+
this.unwatch = (): void => {
|
|
152
|
+
for (const stop of stops) {
|
|
153
|
+
stop();
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
this.syncGit();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Re-points the git watch after the session moves; the monitor is shared, so a repeat is free. */
|
|
160
|
+
public syncGit(): void {
|
|
161
|
+
const wanted = this.unwatch === undefined ? undefined : this.host.cwd;
|
|
162
|
+
if (wanted === this.gitCwd) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
this.gitStop?.();
|
|
166
|
+
this.gitStop = undefined;
|
|
167
|
+
this.gitCwd = wanted;
|
|
168
|
+
if (wanted === undefined) {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
this.gitStop = this.git.watch(wanted, (state) => {
|
|
172
|
+
// Whoever moved it — this client, the terminal, another window — every
|
|
173
|
+
// path the pickers hold is from the branch that just left. The first
|
|
174
|
+
// reading is a discovery rather than a move, and drops nothing.
|
|
175
|
+
const moved = this.gitBranch !== null && state.branch !== this.gitBranch;
|
|
176
|
+
this.gitBranch = state.branch;
|
|
177
|
+
if (moved) {
|
|
178
|
+
this.invalidatePickers("files");
|
|
179
|
+
}
|
|
180
|
+
this.emit(this.sessionState());
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Reads the repository again for a client that has reason to think its picture is old. */
|
|
185
|
+
public async refreshGit(fetch: boolean): Promise<void> {
|
|
186
|
+
await this.git.refresh(this.host.cwd, { fetch });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Project whatever pi has appended since the last read, telling every client; returns the head. */
|
|
190
|
+
public async refresh(): Promise<number> {
|
|
191
|
+
await this.flushDurable();
|
|
192
|
+
return this.projection.head;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
public subscribe(listener: StreamListener): () => void {
|
|
196
|
+
this.listeners.add(listener);
|
|
197
|
+
return () => {
|
|
198
|
+
this.listeners.delete(listener);
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** Whether `callId` names a tool this session is still running, log written or not. */
|
|
203
|
+
public isRunning(callId: string): boolean {
|
|
204
|
+
return this.findTool(callId)?.done === false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Everything a client at `fromSeq` has not seen: the durable tail, the in-flight turn coalesced, then state. */
|
|
208
|
+
public async replay(fromSeq: number): Promise<readonly StreamEvent[]> {
|
|
209
|
+
await Promise.all([this.flushDurable(), this.readLease()]);
|
|
210
|
+
return [...this.projection.since(fromSeq), ...this.inFlight()];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private inFlight(): readonly StreamEvent[] {
|
|
214
|
+
const events: StreamEvent[] = [];
|
|
215
|
+
for (const message of this.liveTurn) {
|
|
216
|
+
const { messageId } = message;
|
|
217
|
+
events.push({ type: "message_start", role: "assistant", messageId });
|
|
218
|
+
if (message.thinking) {
|
|
219
|
+
events.push({
|
|
220
|
+
type: "thinking_delta",
|
|
221
|
+
messageId,
|
|
222
|
+
delta: message.thinking,
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
if (message.text) {
|
|
226
|
+
events.push({ type: "text_delta", messageId, delta: message.text });
|
|
227
|
+
}
|
|
228
|
+
for (const tool of message.tools) {
|
|
229
|
+
events.push({
|
|
230
|
+
type: "tool_call",
|
|
231
|
+
callId: tool.callId,
|
|
232
|
+
name: tool.name,
|
|
233
|
+
messageId,
|
|
234
|
+
view: tool.view,
|
|
235
|
+
});
|
|
236
|
+
if (tool.done) {
|
|
237
|
+
events.push({
|
|
238
|
+
type: "tool_end",
|
|
239
|
+
callId: tool.callId,
|
|
240
|
+
view: tool.view,
|
|
241
|
+
isError: tool.isError,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
events.push(this.sessionState());
|
|
247
|
+
return events;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Broadcast an event the stream did not derive itself, e.g. a state push. */
|
|
251
|
+
public push(event: ServerEvent): void {
|
|
252
|
+
this.emit(event);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Drop the server's picker caches and tell every client to drop theirs. */
|
|
256
|
+
public invalidatePickers(scope: "files" | "commands" | "all"): void {
|
|
257
|
+
this.picker.invalidate();
|
|
258
|
+
this.emit({ type: "picker_invalidate", scope, cwd: this.host.cwd });
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
public sessionState(): EphemeralEvent {
|
|
262
|
+
const tps = this.host.tps;
|
|
263
|
+
const usage = this.host.usage();
|
|
264
|
+
const cwd = this.host.cwd;
|
|
265
|
+
const { branch, dirtyCount, ahead, behind, revision } =
|
|
266
|
+
this.git.stateOf(cwd);
|
|
267
|
+
const modelLabel = this.host.currentModelLabel;
|
|
268
|
+
const turnElapsedMs =
|
|
269
|
+
this.host.status === "idle" || this.turnStartedAt === 0
|
|
270
|
+
? undefined
|
|
271
|
+
: Date.now() - this.turnStartedAt;
|
|
272
|
+
return {
|
|
273
|
+
type: "session_state",
|
|
274
|
+
cwd,
|
|
275
|
+
model: this.host.currentModelId ?? "",
|
|
276
|
+
...(modelLabel === undefined ? {} : { modelLabel }),
|
|
277
|
+
thinking: this.host.currentThinkingLevel,
|
|
278
|
+
cost: this.host.settings.cumulativeCost ?? 0,
|
|
279
|
+
status: this.host.status,
|
|
280
|
+
...this.leaseState(),
|
|
281
|
+
...(this.repoBusy?.() === true ? { repoBusy: true } : {}),
|
|
282
|
+
...(tps === undefined ? {} : { tps }),
|
|
283
|
+
...(turnElapsedMs === undefined ? {} : { turnElapsedMs }),
|
|
284
|
+
...(usage?.percent === null || usage === undefined
|
|
285
|
+
? {}
|
|
286
|
+
: {
|
|
287
|
+
contextPercent: usage.percent,
|
|
288
|
+
contextWindow: usage.contextWindow,
|
|
289
|
+
}),
|
|
290
|
+
...(branch === null
|
|
291
|
+
? {}
|
|
292
|
+
: { branch, dirtyCount, ahead, behind, repoRevision: revision }),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Two sources, and both are needed: the host knows only about a mutation of
|
|
298
|
+
* its own parked behind someone else, and reads writable for a session
|
|
299
|
+
* merely sitting idle under a foreign lease — which the cached record covers.
|
|
300
|
+
*/
|
|
301
|
+
private leaseState(): LeaseState {
|
|
302
|
+
const own = this.host.leaseState;
|
|
303
|
+
if (!own.writable) {
|
|
304
|
+
return own;
|
|
305
|
+
}
|
|
306
|
+
const holder = this.holder;
|
|
307
|
+
// Our own turn: the lease exists because this process took it.
|
|
308
|
+
if (
|
|
309
|
+
holder === undefined ||
|
|
310
|
+
(holder.pid === process.pid && holder.frontend === "daemon")
|
|
311
|
+
) {
|
|
312
|
+
return { writable: true };
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
writable: false,
|
|
316
|
+
heldBy: { frontend: holder.frontend, pid: holder.pid },
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Re-reads the holder; true when what a client would render changed. Our own
|
|
322
|
+
* lease comes and goes on every turn we take and says nothing, so only a
|
|
323
|
+
* holder that is not us ever moves this.
|
|
324
|
+
*/
|
|
325
|
+
private async readLease(): Promise<boolean> {
|
|
326
|
+
this.holder = await SessionLease.read(this.sessionPath);
|
|
327
|
+
const next = this.leaseState();
|
|
328
|
+
if (sameLease(this.lease, next)) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
this.lease = next;
|
|
332
|
+
return true;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
private async pushLease(): Promise<void> {
|
|
336
|
+
if (await this.readLease()) {
|
|
337
|
+
this.emit(this.sessionState());
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
public dispose(): void {
|
|
342
|
+
this.unsubscribe?.();
|
|
343
|
+
this.unsubscribe = undefined;
|
|
344
|
+
this.unsubscribeLease?.();
|
|
345
|
+
this.unsubscribeLease = undefined;
|
|
346
|
+
this.unsubscribeForeign?.();
|
|
347
|
+
this.unsubscribeForeign = undefined;
|
|
348
|
+
this.watchFiles(false);
|
|
349
|
+
this.listeners.clear();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
private onAgentEvent(event: AgentSessionEvent): void {
|
|
353
|
+
switch (event.type) {
|
|
354
|
+
case "agent_start":
|
|
355
|
+
this.turnStartedAt = Date.now();
|
|
356
|
+
this.liveTurn = [];
|
|
357
|
+
this.emit(this.sessionState());
|
|
358
|
+
return;
|
|
359
|
+
case "message_start":
|
|
360
|
+
if (event.message.role === "assistant") {
|
|
361
|
+
this.startMessage();
|
|
362
|
+
this.emit(this.sessionState());
|
|
363
|
+
}
|
|
364
|
+
return;
|
|
365
|
+
case "message_update": {
|
|
366
|
+
if (event.message.role !== "assistant") {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
const message = this.currentMessage();
|
|
370
|
+
const text = MessageText.textOf(event.message.content);
|
|
371
|
+
const thinking = MessageText.textOf(event.message.content, "thinking");
|
|
372
|
+
this.emitDelta(message, "thinking", thinking);
|
|
373
|
+
this.emitDelta(message, "text", text);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
case "tool_execution_start": {
|
|
377
|
+
const view = Tools.viewOf({
|
|
378
|
+
name: event.toolName,
|
|
379
|
+
args: event.args,
|
|
380
|
+
isPartial: true,
|
|
381
|
+
cwd: this.host.cwd,
|
|
382
|
+
});
|
|
383
|
+
const message = this.currentMessage();
|
|
384
|
+
message.tools.push({
|
|
385
|
+
callId: event.toolCallId,
|
|
386
|
+
name: event.toolName,
|
|
387
|
+
args: event.args,
|
|
388
|
+
view,
|
|
389
|
+
isError: false,
|
|
390
|
+
done: false,
|
|
391
|
+
});
|
|
392
|
+
this.emit({
|
|
393
|
+
type: "tool_call",
|
|
394
|
+
callId: event.toolCallId,
|
|
395
|
+
name: event.toolName,
|
|
396
|
+
messageId: message.messageId,
|
|
397
|
+
view,
|
|
398
|
+
});
|
|
399
|
+
this.emit(this.sessionState());
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
case "tool_execution_update": {
|
|
403
|
+
const view = Tools.viewOf({
|
|
404
|
+
name: event.toolName,
|
|
405
|
+
args: event.args,
|
|
406
|
+
result: event.partialResult,
|
|
407
|
+
isPartial: true,
|
|
408
|
+
cwd: this.host.cwd,
|
|
409
|
+
});
|
|
410
|
+
const tool = this.findTool(event.toolCallId);
|
|
411
|
+
if (tool) {
|
|
412
|
+
tool.view = view;
|
|
413
|
+
}
|
|
414
|
+
this.emit({ type: "tool_update", callId: event.toolCallId, view });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
case "tool_execution_end": {
|
|
418
|
+
const tool = this.findTool(event.toolCallId);
|
|
419
|
+
const view = Tools.viewOf({
|
|
420
|
+
name: event.toolName,
|
|
421
|
+
args: tool?.args,
|
|
422
|
+
result: event.result,
|
|
423
|
+
isError: event.isError,
|
|
424
|
+
isPartial: false,
|
|
425
|
+
cwd: this.host.cwd,
|
|
426
|
+
});
|
|
427
|
+
if (tool) {
|
|
428
|
+
tool.view = view;
|
|
429
|
+
tool.isError = event.isError;
|
|
430
|
+
tool.done = true;
|
|
431
|
+
}
|
|
432
|
+
this.emit({
|
|
433
|
+
type: "tool_end",
|
|
434
|
+
callId: event.toolCallId,
|
|
435
|
+
view,
|
|
436
|
+
isError: event.isError,
|
|
437
|
+
});
|
|
438
|
+
if (Tools.effectOf(event.toolName)?.kind !== "readOnly") {
|
|
439
|
+
this.invalidatePickers("files");
|
|
440
|
+
// The tool just wrote the worktree, which `.git` never reports.
|
|
441
|
+
void this.git.refresh(this.host.cwd);
|
|
442
|
+
}
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
case "entry_appended":
|
|
446
|
+
void this.flushDurable();
|
|
447
|
+
return;
|
|
448
|
+
// Never await the flush: pi appends the entry only after this listener returns.
|
|
449
|
+
case "message_end": {
|
|
450
|
+
if (event.message.role === "assistant") {
|
|
451
|
+
const open = this.liveTurn.at(-1);
|
|
452
|
+
if (open) {
|
|
453
|
+
open.ended = true;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
void this.flushDurable();
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
case "turn_end": {
|
|
460
|
+
const usage =
|
|
461
|
+
event.message.role === "assistant" ? event.message.usage : undefined;
|
|
462
|
+
this.emit({
|
|
463
|
+
type: "turn_end",
|
|
464
|
+
stats: {
|
|
465
|
+
inputTokens: usage?.input ?? 0,
|
|
466
|
+
outputTokens: usage?.output ?? 0,
|
|
467
|
+
costUsd: usage?.cost.total ?? 0,
|
|
468
|
+
durationMs: Date.now() - this.turnStartedAt,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
case "agent_settled":
|
|
474
|
+
void this.flushDurable().then(() => {
|
|
475
|
+
// Clear only once the durable events superseding it are on the wire.
|
|
476
|
+
this.liveTurn = [];
|
|
477
|
+
this.emit(this.sessionState());
|
|
478
|
+
});
|
|
479
|
+
return;
|
|
480
|
+
default:
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
private currentMessage(): LiveMessage {
|
|
486
|
+
return this.liveTurn.at(-1) ?? this.startMessage();
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
private startMessage(): LiveMessage {
|
|
490
|
+
this.liveMessageId += 1;
|
|
491
|
+
const message: LiveMessage = {
|
|
492
|
+
messageId: `${this.sessionId}:live:${this.liveMessageId}`,
|
|
493
|
+
text: "",
|
|
494
|
+
thinking: "",
|
|
495
|
+
ended: false,
|
|
496
|
+
retired: false,
|
|
497
|
+
tools: [],
|
|
498
|
+
};
|
|
499
|
+
this.liveTurn.push(message);
|
|
500
|
+
this.emit({
|
|
501
|
+
type: "message_start",
|
|
502
|
+
role: "assistant",
|
|
503
|
+
messageId: message.messageId,
|
|
504
|
+
});
|
|
505
|
+
return message;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Pi re-states the whole message on each update; only a suffix is expressible as a delta.
|
|
509
|
+
private emitDelta(
|
|
510
|
+
message: LiveMessage,
|
|
511
|
+
channel: "text" | "thinking",
|
|
512
|
+
next: string
|
|
513
|
+
): void {
|
|
514
|
+
const sent = message[channel];
|
|
515
|
+
if (!next.startsWith(sent) || next === sent) {
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
this.emit({
|
|
519
|
+
type: channel === "text" ? "text_delta" : "thinking_delta",
|
|
520
|
+
messageId: message.messageId,
|
|
521
|
+
delta: next.slice(sent.length),
|
|
522
|
+
});
|
|
523
|
+
message[channel] = next;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
private locate(
|
|
527
|
+
callId: string
|
|
528
|
+
): { readonly message: LiveMessage; readonly at: number } | undefined {
|
|
529
|
+
for (const message of this.liveTurn) {
|
|
530
|
+
const at = message.tools.findIndex((tool) => tool.callId === callId);
|
|
531
|
+
if (at !== -1) {
|
|
532
|
+
return { message, at };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
private findTool(callId: string): LiveTool | undefined {
|
|
539
|
+
const found = this.locate(callId);
|
|
540
|
+
return found === undefined ? undefined : found.message.tools[found.at];
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Serialised: two reads in flight would race on `sentSeq` and hand a client
|
|
545
|
+
* the same lines twice, or out of order. The agent-event path and the file
|
|
546
|
+
* watch both come through here, so they can only ever queue behind one
|
|
547
|
+
* another.
|
|
548
|
+
*/
|
|
549
|
+
private flushDurable(): Promise<void> {
|
|
550
|
+
const done = this.draining.then(() => this.drainDurable());
|
|
551
|
+
this.draining = done.catch(() => {});
|
|
552
|
+
return done;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// A burst of appends is one drain: a trigger waits for the read already running, and one arriving after that starts gets its own.
|
|
556
|
+
private scheduleDrain(): void {
|
|
557
|
+
if (this.drainQueued) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
this.drainQueued = true;
|
|
561
|
+
void this.draining
|
|
562
|
+
.then(() => {
|
|
563
|
+
this.drainQueued = false;
|
|
564
|
+
return this.flushDurable();
|
|
565
|
+
})
|
|
566
|
+
.catch(() => {});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// One frame: a retire split from the durable message that caused it paints the step twice.
|
|
570
|
+
private async drainDurable(): Promise<void> {
|
|
571
|
+
await this.projection.drain();
|
|
572
|
+
// The watermark is what every listener has been offered, so one client's read cannot swallow another's tail.
|
|
573
|
+
const fresh = this.projection.since(this.sentSeq);
|
|
574
|
+
this.sentSeq = this.projection.head;
|
|
575
|
+
const batch: StreamEvent[] = [];
|
|
576
|
+
for (const event of fresh) {
|
|
577
|
+
batch.push(event);
|
|
578
|
+
if (event.type === "message" && event.role === "assistant") {
|
|
579
|
+
const retired = this.retireLive();
|
|
580
|
+
if (retired) {
|
|
581
|
+
batch.push(retired);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
if (event.type === "tool_result") {
|
|
585
|
+
this.settleLive(event.callId);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
if (batch.length > 0) {
|
|
589
|
+
this.emit({ type: "replay", events: batch });
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Retire the oldest *finished* message, never the oldest: a retired shell may still be collecting calls.
|
|
594
|
+
private retireLive(): EphemeralEvent | undefined {
|
|
595
|
+
const retired = this.liveTurn.find(
|
|
596
|
+
(message) => message.ended && !message.retired
|
|
597
|
+
);
|
|
598
|
+
if (!retired) {
|
|
599
|
+
return undefined;
|
|
600
|
+
}
|
|
601
|
+
retired.text = "";
|
|
602
|
+
retired.thinking = "";
|
|
603
|
+
retired.retired = true;
|
|
604
|
+
if (retired.tools.length === 0) {
|
|
605
|
+
this.liveTurn = this.liveTurn.filter((message) => message !== retired);
|
|
606
|
+
}
|
|
607
|
+
return { type: "message_retire", messageId: retired.messageId };
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
private settleLive(callId: string): void {
|
|
611
|
+
const found = this.locate(callId);
|
|
612
|
+
if (found === undefined) {
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
const { message } = found;
|
|
616
|
+
message.tools.splice(found.at, 1);
|
|
617
|
+
if (message.retired && message.tools.length === 0) {
|
|
618
|
+
this.liveTurn = this.liveTurn.filter((held) => held !== message);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
private emit(event: ServerEvent): void {
|
|
623
|
+
for (const listener of this.listeners) {
|
|
624
|
+
listener(event);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|