pim-agent 0.0.1 → 0.9.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 +51 -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 +216 -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 +28 -0
- package/packages/core/src/extensions/system-prompt/prompt.ts +228 -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 +110 -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 +70 -0
- package/packages/core/src/session/AgentRuntime.ts +85 -0
- package/packages/core/src/session/EventLog.ts +213 -0
- package/packages/core/src/session/MessageText.ts +21 -0
- package/packages/core/src/session/ReadCursors.ts +94 -0
- package/packages/core/src/session/SessionCache.ts +68 -0
- package/packages/core/src/session/SessionHost.ts +876 -0
- package/packages/core/src/session/SessionLease.ts +279 -0
- package/packages/core/src/session/SessionRegistry.ts +169 -0
- package/packages/core/src/session/WriteMark.ts +37 -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/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 +107 -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 +89 -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 +30 -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 +30 -0
- package/packages/core/src/shared/RateLimiter.ts +50 -0
- package/packages/core/src/shared/Renderer.ts +371 -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/Sweeper.ts +32 -0
- package/packages/core/src/shared/Tools.ts +801 -0
- package/packages/core/src/shared/Updater.ts +193 -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/DiffLayout.ts +69 -0
- package/packages/core/src/view/MarkdownPainter.ts +314 -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 +57 -0
- package/packages/daemon/src/index.ts +37 -0
- package/packages/protocol/src/Command.ts +92 -0
- package/packages/protocol/src/ImageRoute.ts +23 -0
- package/packages/protocol/src/Protocol.ts +6 -0
- package/packages/protocol/src/ServerEvent.ts +251 -0
- package/packages/server/src/AttachmentEndpoint.ts +184 -0
- package/packages/server/src/ClientConnection.ts +265 -0
- package/packages/server/src/ImageEndpoint.ts +36 -0
- package/packages/server/src/ProbeClient.ts +312 -0
- package/packages/server/src/Reloader.ts +93 -0
- package/packages/server/src/SendFileTool.ts +88 -0
- package/packages/server/src/SessionCatalogue.ts +202 -0
- package/packages/server/src/SessionProjection.ts +194 -0
- package/packages/server/src/SessionStream.ts +601 -0
- package/packages/server/src/StaticClient.ts +95 -0
- package/packages/server/src/WebOptions.ts +72 -0
- package/packages/server/src/WsGateway.ts +563 -0
- package/packages/server/src/probe.ts +146 -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 +252 -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 +43 -0
- package/packages/tui/src/extensions/file-picker/index.ts +156 -0
- package/packages/tui/src/extensions/footer/index.ts +117 -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 +248 -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-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-CGdQx9la.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-BMNwleWO.js +40 -0
- package/packages/web/dist/client/assets/index-CqjB2RPr.css +1 -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,279 @@
|
|
|
1
|
+
import { readFileSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { mkdir, open, stat, unlink, utimes } from "node:fs/promises";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { dirname } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { FileWatch } from "../shared/FileWatch";
|
|
7
|
+
import { FsErrors } from "../shared/FsErrors";
|
|
8
|
+
import { Fs } from "../shared/Fs";
|
|
9
|
+
import { Json } from "../shared/Json";
|
|
10
|
+
|
|
11
|
+
export type LeaseFrontend = "tui" | "daemon";
|
|
12
|
+
|
|
13
|
+
/** What one holder wrote into `<sessionPath>.lease`. */
|
|
14
|
+
export type LeaseRecord = {
|
|
15
|
+
readonly pid: number;
|
|
16
|
+
readonly hostname: string;
|
|
17
|
+
readonly frontend: LeaseFrontend;
|
|
18
|
+
readonly startedAt: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type LeaseHandle = {
|
|
22
|
+
release(): Promise<void>;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type AcquireResult =
|
|
26
|
+
| { readonly ok: true; readonly handle: LeaseHandle }
|
|
27
|
+
/** Absent when the holder's file is torn or vanished mid-read. */
|
|
28
|
+
| { readonly ok: false; readonly holder?: LeaseRecord };
|
|
29
|
+
|
|
30
|
+
export type LeaseOptions = {
|
|
31
|
+
readonly heartbeatMs?: number;
|
|
32
|
+
readonly pollMs?: number;
|
|
33
|
+
readonly timeoutMs?: number;
|
|
34
|
+
/** Called with the current holder each time an acquire is denied and another poll follows. */
|
|
35
|
+
readonly onBlocked?: (holder: LeaseRecord | undefined) => void;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const HEARTBEAT_MS = 5_000;
|
|
39
|
+
const STALE_MS = 30_000;
|
|
40
|
+
const POLL_MS = 250;
|
|
41
|
+
const TIMEOUT_MS = 5 * 60_000;
|
|
42
|
+
const STEAL_ATTEMPTS = 3;
|
|
43
|
+
const STEAL_BACKOFF_MS = 50;
|
|
44
|
+
|
|
45
|
+
const HOST = hostname();
|
|
46
|
+
|
|
47
|
+
const held = new Set<string>();
|
|
48
|
+
let exitHooked = false;
|
|
49
|
+
|
|
50
|
+
function pathFor(sessionPath: string): string {
|
|
51
|
+
return `${sessionPath}.lease`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toRecord(value: unknown): LeaseRecord | undefined {
|
|
55
|
+
const raw = Json.asRecord(value);
|
|
56
|
+
if (
|
|
57
|
+
typeof raw?.pid !== "number" ||
|
|
58
|
+
typeof raw.hostname !== "string" ||
|
|
59
|
+
typeof raw.startedAt !== "number" ||
|
|
60
|
+
(raw.frontend !== "tui" && raw.frontend !== "daemon")
|
|
61
|
+
) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
pid: raw.pid,
|
|
66
|
+
hostname: raw.hostname,
|
|
67
|
+
frontend: raw.frontend,
|
|
68
|
+
startedAt: raw.startedAt,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function recordAt(path: string): Promise<LeaseRecord | undefined> {
|
|
73
|
+
return toRecord(await Fs.readJsonOr<unknown>(path, undefined));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The current holder, or undefined when the file is absent, torn, or half-written. */
|
|
77
|
+
async function read(sessionPath: string): Promise<LeaseRecord | undefined> {
|
|
78
|
+
return await recordAt(pathFor(sessionPath));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function isAlive(pid: number): boolean {
|
|
82
|
+
try {
|
|
83
|
+
process.kill(pid, 0);
|
|
84
|
+
return true;
|
|
85
|
+
} catch (err) {
|
|
86
|
+
// EPERM is someone else's live process.
|
|
87
|
+
return FsErrors.code(err) !== "ESRCH";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A holder is stale once its pid is gone, or once it stops heartbeating; never across hosts. */
|
|
92
|
+
function isStale(
|
|
93
|
+
record: LeaseRecord,
|
|
94
|
+
mtimeMs: number,
|
|
95
|
+
now = Date.now()
|
|
96
|
+
): boolean {
|
|
97
|
+
if (record.hostname !== HOST) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
return !isAlive(record.pid) || now - mtimeMs > STALE_MS;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Whether this very process wrote that record — the one holder allowed to release it. */
|
|
104
|
+
function isOurs(record: LeaseRecord | undefined): boolean {
|
|
105
|
+
return record?.pid === process.pid && record.hostname === HOST;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function stealable(
|
|
109
|
+
path: string,
|
|
110
|
+
record: LeaseRecord | undefined
|
|
111
|
+
): Promise<boolean> {
|
|
112
|
+
const mtimeMs = await stat(path).then(
|
|
113
|
+
(stats) => stats.mtimeMs,
|
|
114
|
+
() => undefined
|
|
115
|
+
);
|
|
116
|
+
if (mtimeMs === undefined) {
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
// A torn file is a holder mid-write until it stops being refreshed.
|
|
120
|
+
return record === undefined
|
|
121
|
+
? Date.now() - mtimeMs > STALE_MS
|
|
122
|
+
: isStale(record, mtimeMs);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function claim(
|
|
126
|
+
path: string,
|
|
127
|
+
frontend: LeaseFrontend
|
|
128
|
+
): Promise<LeaseRecord | undefined> {
|
|
129
|
+
const record: LeaseRecord = {
|
|
130
|
+
pid: process.pid,
|
|
131
|
+
hostname: HOST,
|
|
132
|
+
frontend,
|
|
133
|
+
startedAt: Date.now(),
|
|
134
|
+
};
|
|
135
|
+
const file = await open(path, "wx").catch((err: unknown) => {
|
|
136
|
+
if (FsErrors.code(err) === "EEXIST") {
|
|
137
|
+
return undefined;
|
|
138
|
+
}
|
|
139
|
+
// The session's directory is pi's to create, and a lease can precede the file it guards.
|
|
140
|
+
if (FsErrors.code(err) === "ENOENT") {
|
|
141
|
+
return mkdir(dirname(path), { recursive: true }).then(() =>
|
|
142
|
+
open(path, "wx")
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
throw err;
|
|
146
|
+
});
|
|
147
|
+
if (file === undefined) {
|
|
148
|
+
return undefined;
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
await file.writeFile(`${JSON.stringify(record)}\n`);
|
|
152
|
+
} finally {
|
|
153
|
+
await file.close();
|
|
154
|
+
}
|
|
155
|
+
return record;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function hookExit(): void {
|
|
159
|
+
if (exitHooked) {
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
exitHooked = true;
|
|
163
|
+
process.on("exit", () => {
|
|
164
|
+
for (const path of held) {
|
|
165
|
+
try {
|
|
166
|
+
if (isOurs(toRecord(Json.tryParseJson(readFileSync(path, "utf8"))))) {
|
|
167
|
+
unlinkSync(path);
|
|
168
|
+
}
|
|
169
|
+
} catch {}
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function handleFor(path: string, options: LeaseOptions): LeaseHandle {
|
|
175
|
+
held.add(path);
|
|
176
|
+
hookExit();
|
|
177
|
+
// Bump mtime rather than rewrite: a rename would clobber whoever holds the file next.
|
|
178
|
+
const beat = setInterval(() => {
|
|
179
|
+
const now = new Date();
|
|
180
|
+
void utimes(path, now, now).catch(() => undefined);
|
|
181
|
+
}, options.heartbeatMs ?? HEARTBEAT_MS);
|
|
182
|
+
beat.unref?.();
|
|
183
|
+
|
|
184
|
+
let released = false;
|
|
185
|
+
return {
|
|
186
|
+
release: async (): Promise<void> => {
|
|
187
|
+
if (released) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
released = true;
|
|
191
|
+
clearInterval(beat);
|
|
192
|
+
held.delete(path);
|
|
193
|
+
if (isOurs(await recordAt(path))) {
|
|
194
|
+
await unlink(path).catch(() => undefined);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Create-exclusive, stealing only a lease whose holder is provably gone. */
|
|
201
|
+
async function acquire(
|
|
202
|
+
sessionPath: string,
|
|
203
|
+
frontend: LeaseFrontend,
|
|
204
|
+
options: LeaseOptions = {}
|
|
205
|
+
): Promise<AcquireResult> {
|
|
206
|
+
const path = pathFor(sessionPath);
|
|
207
|
+
for (let attempt = 0; ; attempt++) {
|
|
208
|
+
if (await claim(path, frontend)) {
|
|
209
|
+
return { ok: true, handle: handleFor(path, options) };
|
|
210
|
+
}
|
|
211
|
+
const holder = await recordAt(path);
|
|
212
|
+
if (attempt >= STEAL_ATTEMPTS || !(await stealable(path, holder))) {
|
|
213
|
+
return holder === undefined ? { ok: false } : { ok: false, holder };
|
|
214
|
+
}
|
|
215
|
+
await unlink(path).catch(() => undefined);
|
|
216
|
+
await Bun.sleep(STEAL_BACKOFF_MS);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** Polls until the lease is free, the deadline passes, or the holder is stealable. */
|
|
221
|
+
async function waitFor(
|
|
222
|
+
sessionPath: string,
|
|
223
|
+
frontend: LeaseFrontend,
|
|
224
|
+
options: LeaseOptions = {}
|
|
225
|
+
): Promise<AcquireResult> {
|
|
226
|
+
const deadline = Date.now() + (options.timeoutMs ?? TIMEOUT_MS);
|
|
227
|
+
while (true) {
|
|
228
|
+
const result = await acquire(sessionPath, frontend, options);
|
|
229
|
+
if (result.ok || Date.now() >= deadline) {
|
|
230
|
+
return result;
|
|
231
|
+
}
|
|
232
|
+
options.onBlocked?.(result.holder);
|
|
233
|
+
await Bun.sleep(options.pollMs ?? POLL_MS);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function denial(result: { readonly holder?: LeaseRecord }): string {
|
|
238
|
+
const holder = result.holder;
|
|
239
|
+
const who =
|
|
240
|
+
holder === undefined
|
|
241
|
+
? "another process"
|
|
242
|
+
: `${holder.frontend} (pid ${holder.pid})`;
|
|
243
|
+
return `Session is busy: ${who} still holds its turn lease.`;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/** The only safe way to take a lease: waits for it, and always gives it back. */
|
|
247
|
+
async function hold<T>(
|
|
248
|
+
sessionPath: string,
|
|
249
|
+
frontend: LeaseFrontend,
|
|
250
|
+
work: () => Promise<T>,
|
|
251
|
+
options: LeaseOptions = {}
|
|
252
|
+
): Promise<T> {
|
|
253
|
+
const result = await waitFor(sessionPath, frontend, options);
|
|
254
|
+
if (!result.ok) {
|
|
255
|
+
throw new Error(denial(result));
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
return await work();
|
|
259
|
+
} finally {
|
|
260
|
+
await result.handle.release();
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Fires whenever the lease appears, changes hands, or is released. */
|
|
265
|
+
function watch(sessionPath: string, onChange: () => void): () => void {
|
|
266
|
+
return FileWatch.file(pathFor(sessionPath), onChange);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/** A turn lease over one session file, held as a sibling `<sessionPath>.lease`. */
|
|
270
|
+
export const SessionLease = {
|
|
271
|
+
pathFor,
|
|
272
|
+
read,
|
|
273
|
+
acquire,
|
|
274
|
+
hold,
|
|
275
|
+
waitFor,
|
|
276
|
+
watch,
|
|
277
|
+
isStale,
|
|
278
|
+
isOurs,
|
|
279
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { type ToolDefinition } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { stat } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { Directories } from "../shared/Directories";
|
|
6
|
+
import { AgentRuntime, type ModelChoice } from "./AgentRuntime";
|
|
7
|
+
import { EventLog } from "./EventLog";
|
|
8
|
+
import { SessionCache } from "./SessionCache";
|
|
9
|
+
import {
|
|
10
|
+
SessionHost,
|
|
11
|
+
type CustomToolContext,
|
|
12
|
+
type HostSettings,
|
|
13
|
+
} from "./SessionHost";
|
|
14
|
+
|
|
15
|
+
/** A session as pi stores it: one JSONL file under a cwd-encoded directory. */
|
|
16
|
+
export type SessionSummary = {
|
|
17
|
+
readonly sessionId: string;
|
|
18
|
+
readonly cwd: string;
|
|
19
|
+
readonly path: string;
|
|
20
|
+
readonly createdAt: number;
|
|
21
|
+
readonly modifiedAt: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export type SessionRegistryDeps = {
|
|
25
|
+
readonly defaults: { readonly cwd: string; readonly model?: string };
|
|
26
|
+
/** The process-wide pi installation; one is built here when a caller has none to share. */
|
|
27
|
+
readonly runtime?: AgentRuntime;
|
|
28
|
+
/** Where auth, models, settings and sessions live; must agree with pi's own `getAgentDir()`. */
|
|
29
|
+
readonly agentDir?: string;
|
|
30
|
+
readonly capacity?: number;
|
|
31
|
+
readonly customTools?: (
|
|
32
|
+
context: CustomToolContext
|
|
33
|
+
) => readonly ToolDefinition[];
|
|
34
|
+
/** Appended to every session's system prompt. */
|
|
35
|
+
readonly systemInstruction?: () => Promise<string | undefined>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type SessionCreateOptions = {
|
|
39
|
+
readonly cwd?: string;
|
|
40
|
+
/** Open the new session like this one: model, chosen thinking level, and cwd when none is given. */
|
|
41
|
+
readonly like?: SessionHost;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
/** Live sessions keyed on pi's session UUID; the catalogue is pi's sessions directory, read on demand. */
|
|
45
|
+
export class SessionRegistry {
|
|
46
|
+
private readonly deps: SessionRegistryDeps;
|
|
47
|
+
private readonly runtime: AgentRuntime;
|
|
48
|
+
private readonly hosts: SessionCache<SessionHost>;
|
|
49
|
+
|
|
50
|
+
public constructor(deps: SessionRegistryDeps) {
|
|
51
|
+
this.deps = deps;
|
|
52
|
+
this.runtime = deps.runtime ?? new AgentRuntime(deps.agentDir);
|
|
53
|
+
this.hosts = new SessionCache(deps.capacity);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public get sessionsRoot(): string {
|
|
57
|
+
return join(this.runtime.agentDir, "sessions");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
public async init(): Promise<void> {
|
|
61
|
+
await this.runtime.init();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Sessions on disk, newest first; reads only each file's header line. */
|
|
65
|
+
public async list(cwd?: string): Promise<readonly SessionSummary[]> {
|
|
66
|
+
const root = this.sessionsRoot;
|
|
67
|
+
if (!(await stat(root).catch(() => undefined))?.isDirectory()) {
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
const paths: string[] = [];
|
|
71
|
+
for await (const relative of new Bun.Glob("*/*.jsonl").scan({
|
|
72
|
+
cwd: root,
|
|
73
|
+
onlyFiles: true,
|
|
74
|
+
})) {
|
|
75
|
+
paths.push(join(root, relative));
|
|
76
|
+
}
|
|
77
|
+
const summaries = (await Promise.all(paths.map(readSummary))).filter(
|
|
78
|
+
(summary): summary is SessionSummary =>
|
|
79
|
+
summary !== undefined && (cwd === undefined || summary.cwd === cwd)
|
|
80
|
+
);
|
|
81
|
+
return summaries.sort((a, b) => b.modifiedAt - a.modifiedAt);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The live host for `sessionId`, if one is currently loaded. */
|
|
85
|
+
public peek(sessionId: string): SessionHost | undefined {
|
|
86
|
+
return this.hosts.peek(sessionId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Every model this machine has credentials for, qualified as `SessionHost.setModel` takes them. */
|
|
90
|
+
public models(): readonly ModelChoice[] {
|
|
91
|
+
return this.runtime.models();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public async open(sessionId: string): Promise<SessionHost> {
|
|
95
|
+
const cached = this.hosts.touch(sessionId);
|
|
96
|
+
if (cached) {
|
|
97
|
+
return cached;
|
|
98
|
+
}
|
|
99
|
+
const summary = (await this.list()).find((s) => s.sessionId === sessionId);
|
|
100
|
+
if (!summary) {
|
|
101
|
+
throw new Error(`unknown session: ${sessionId}`);
|
|
102
|
+
}
|
|
103
|
+
return this.hosts.adopt(
|
|
104
|
+
sessionId,
|
|
105
|
+
this.buildHost(sessionId, {
|
|
106
|
+
cwd: summary.cwd,
|
|
107
|
+
sessionPath: summary.path,
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Start a new session; the agent is built eagerly because pi assigns the UUID and file. */
|
|
113
|
+
public async create(
|
|
114
|
+
options: SessionCreateOptions = {}
|
|
115
|
+
): Promise<SessionHost> {
|
|
116
|
+
const like = options.like;
|
|
117
|
+
const cwd = options.cwd ?? like?.cwd ?? this.deps.defaults.cwd;
|
|
118
|
+
const reason = await Directories.check(cwd);
|
|
119
|
+
if (reason !== undefined) {
|
|
120
|
+
throw new Error(reason);
|
|
121
|
+
}
|
|
122
|
+
const model = like?.currentModelId;
|
|
123
|
+
const thinkingLevel = like?.chosenThinkingLevel;
|
|
124
|
+
const host = this.buildHost("pending", {
|
|
125
|
+
cwd,
|
|
126
|
+
...(model === undefined ? {} : { model }),
|
|
127
|
+
...(thinkingLevel === undefined ? {} : { thinkingLevel }),
|
|
128
|
+
});
|
|
129
|
+
const agent = await host.ensureAgent();
|
|
130
|
+
return this.hosts.adopt(agent.sessionId, host);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
public async disposeAll(): Promise<void> {
|
|
134
|
+
await this.hosts.disposeAll();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
private buildHost(label: string, settings: HostSettings): SessionHost {
|
|
138
|
+
return new SessionHost({
|
|
139
|
+
label: `session ${label}`,
|
|
140
|
+
settings,
|
|
141
|
+
defaults: this.deps.defaults,
|
|
142
|
+
agentDir: this.runtime.agentDir,
|
|
143
|
+
modelRuntime: this.runtime.modelRuntime,
|
|
144
|
+
modelRegistry: this.runtime.modelRegistry,
|
|
145
|
+
settingsManagerFor: (cwd) => this.runtime.settingsManagerFor(cwd),
|
|
146
|
+
persistSettings: async () => {},
|
|
147
|
+
// The terminal can hold the same file open, so every mutation goes through the turn lease.
|
|
148
|
+
lease: "daemon",
|
|
149
|
+
customTools: this.deps.customTools,
|
|
150
|
+
...(this.deps.systemInstruction === undefined
|
|
151
|
+
? {}
|
|
152
|
+
: { systemInstruction: this.deps.systemInstruction }),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function readSummary(path: string): Promise<SessionSummary | undefined> {
|
|
158
|
+
const header = await new EventLog(path).header();
|
|
159
|
+
if (!header) {
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
sessionId: header.id,
|
|
164
|
+
cwd: header.cwd,
|
|
165
|
+
path,
|
|
166
|
+
createdAt: Date.parse(header.timestamp),
|
|
167
|
+
modifiedAt: Math.floor((await Bun.file(path).stat()).mtimeMs),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { EventLog } from "./EventLog";
|
|
2
|
+
|
|
3
|
+
/** How much of a session file one process can account for: durable lines, and pi's own entries. */
|
|
4
|
+
export type WriteMark = {
|
|
5
|
+
readonly head: number;
|
|
6
|
+
readonly entries: number;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** Pi's in-memory entry list: an `AgentSession`'s `sessionManager`, or one held directly. */
|
|
10
|
+
type EntrySource = {
|
|
11
|
+
getEntries(): readonly unknown[];
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/** Nothing accounted for yet, which `foreignSince` reads as "the file is pi's to write". */
|
|
15
|
+
const UNREAD: WriteMark = { head: 0, entries: 0 };
|
|
16
|
+
|
|
17
|
+
/** Head first: an append between the two reads must under-count the file, never over. */
|
|
18
|
+
async function of(log: EventLog, source: EntrySource): Promise<WriteMark> {
|
|
19
|
+
return { head: await log.head(), entries: source.getEntries().length };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Whether the file gained lines beyond the ones pi appended from memory. A turn, a
|
|
24
|
+
* model change, a compaction, a `/name` and an idle `!bash` all move both counts
|
|
25
|
+
* together, so only a writer that is not this process makes the difference positive.
|
|
26
|
+
*
|
|
27
|
+
* pi names a session file before it writes one, buffering entries until the first
|
|
28
|
+
* assistant reply, so a mark taken before the file exists is answered by the whole of
|
|
29
|
+
* it: header plus every entry pi was holding.
|
|
30
|
+
*/
|
|
31
|
+
function foreignSince(mark: WriteMark, next: WriteMark): boolean {
|
|
32
|
+
const base = mark.head === 0 ? { head: 1, entries: 0 } : mark;
|
|
33
|
+
return next.head - base.head - (next.entries - base.entries) > 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Write accounting over one session file: whose lines are these? */
|
|
37
|
+
export const WriteMark = { UNREAD, of, foreignSince };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Never throw on an unknown flag or positional: the same argv reaches every mode.
|
|
2
|
+
function scan(
|
|
3
|
+
args: ReadonlyArray<string>,
|
|
4
|
+
visit: (key: string, take: () => string | undefined) => void
|
|
5
|
+
): void {
|
|
6
|
+
for (let i = 0; i < args.length; i++) {
|
|
7
|
+
const arg = args[i]!;
|
|
8
|
+
if (!arg.startsWith("--")) {
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
const eqIdx = arg.indexOf("=");
|
|
12
|
+
const key = eqIdx >= 0 ? arg.slice(0, eqIdx) : arg;
|
|
13
|
+
const inline = eqIdx >= 0 ? arg.slice(eqIdx + 1) : undefined;
|
|
14
|
+
|
|
15
|
+
visit(key, () => {
|
|
16
|
+
if (inline !== undefined) {
|
|
17
|
+
return inline;
|
|
18
|
+
}
|
|
19
|
+
i += 1;
|
|
20
|
+
return args[i];
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const Cli = { scan };
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Unit } from "./Supervisor";
|
|
2
|
+
|
|
3
|
+
/** The single unit every pim surface runs inside; `--surfaces` picks which of them come up. */
|
|
4
|
+
export const DaemonUnit = {
|
|
5
|
+
mode: "daemon",
|
|
6
|
+
description: "Pim daemon",
|
|
7
|
+
} satisfies Unit;
|
|
8
|
+
|
|
9
|
+
/** One unit per surface, which is how pim shipped until the surfaces shared a process. */
|
|
10
|
+
export const SupersededUnits: ReadonlyArray<Unit> = [
|
|
11
|
+
{ mode: "web", description: "Pim web daemon" },
|
|
12
|
+
{ mode: "telegram", description: "Pim Telegram daemon" },
|
|
13
|
+
];
|