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,252 @@
|
|
|
1
|
+
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
|
|
2
|
+
import { AgentSession } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { Api } from "grammy";
|
|
4
|
+
import { rename } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
|
|
7
|
+
import type { AgentRuntime } from "#core/session/AgentRuntime";
|
|
8
|
+
import {
|
|
9
|
+
SessionHost,
|
|
10
|
+
type HostSettings,
|
|
11
|
+
type SessionCompactResult,
|
|
12
|
+
type SetCwdResult,
|
|
13
|
+
type SetModelResult,
|
|
14
|
+
} from "#core/session/SessionHost";
|
|
15
|
+
import { Tools } from "#core/shared/Tools";
|
|
16
|
+
import type { LogsMode, TelegramConfig, ThinkingLevelOpt } from "./Config";
|
|
17
|
+
import { SendFileTool } from "./SendFileTool";
|
|
18
|
+
import type { TaskScheduler } from "./TaskScheduler";
|
|
19
|
+
import { TaskTool } from "./TaskTool";
|
|
20
|
+
|
|
21
|
+
export type { SessionCompactResult, SetCwdResult, SetModelResult };
|
|
22
|
+
|
|
23
|
+
export type SessionId = {
|
|
24
|
+
readonly chatId: number;
|
|
25
|
+
readonly threadId: number | undefined;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type SessionSettings = HostSettings & {
|
|
29
|
+
readonly logsMode?: LogsMode;
|
|
30
|
+
readonly temporary?: boolean;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export type SessionDeps = {
|
|
34
|
+
readonly id: SessionId;
|
|
35
|
+
readonly settings: SessionSettings;
|
|
36
|
+
readonly config: TelegramConfig;
|
|
37
|
+
readonly api: Api;
|
|
38
|
+
/** Pi's installation, shared with every other surface in this process. */
|
|
39
|
+
readonly runtime: AgentRuntime;
|
|
40
|
+
readonly scheduler: TaskScheduler;
|
|
41
|
+
readonly persistSettings: (patch: Partial<SessionSettings>) => Promise<void>;
|
|
42
|
+
readonly getBotUsername: () => string | undefined;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const MAIN = "main";
|
|
46
|
+
|
|
47
|
+
function stamp(): string {
|
|
48
|
+
return new Date().toISOString().replace(/[:.]/g, "-");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Telegram's chat-scoped adapter over the frontend-agnostic `SessionHost`. */
|
|
52
|
+
export class Session {
|
|
53
|
+
public readonly id: SessionId;
|
|
54
|
+
private readonly deps: SessionDeps;
|
|
55
|
+
private readonly host: SessionHost;
|
|
56
|
+
private chatSettings: Pick<SessionSettings, "logsMode" | "temporary">;
|
|
57
|
+
|
|
58
|
+
public constructor(deps: SessionDeps) {
|
|
59
|
+
this.deps = deps;
|
|
60
|
+
this.id = deps.id;
|
|
61
|
+
this.chatSettings = {
|
|
62
|
+
logsMode: deps.settings.logsMode,
|
|
63
|
+
temporary: deps.settings.temporary,
|
|
64
|
+
};
|
|
65
|
+
this.host = new SessionHost({
|
|
66
|
+
label: `session ${encodeId(deps.id)}`,
|
|
67
|
+
settings: deps.settings,
|
|
68
|
+
defaults: { cwd: deps.config.cwd, model: deps.config.model },
|
|
69
|
+
agentDir: deps.runtime.agentDir,
|
|
70
|
+
modelRuntime: deps.runtime.modelRuntime,
|
|
71
|
+
modelRegistry: deps.runtime.modelRegistry,
|
|
72
|
+
settingsManagerFor: (cwd) => deps.runtime.settingsManagerFor(cwd),
|
|
73
|
+
persistSettings: deps.persistSettings,
|
|
74
|
+
// Nothing else opens these files; the lease is here for the turn a crashed
|
|
75
|
+
// daemon left behind, and for an eviction that races its own turn.
|
|
76
|
+
lease: "daemon",
|
|
77
|
+
mainSessionPath: () => this.sessionPath("sessions"),
|
|
78
|
+
isolatedSessionPath: () =>
|
|
79
|
+
this.sessionPath("isolated-sessions", `-${stamp()}`),
|
|
80
|
+
systemInstruction: () => this.getSystemInstruction(),
|
|
81
|
+
customTools: ({ cwd }) => [
|
|
82
|
+
Tools.wrap(
|
|
83
|
+
SendFileTool.build({ api: deps.api, sessionId: deps.id, cwd })
|
|
84
|
+
),
|
|
85
|
+
Tools.wrap(
|
|
86
|
+
TaskTool.build({ scheduler: deps.scheduler, sessionId: deps.id })
|
|
87
|
+
),
|
|
88
|
+
],
|
|
89
|
+
onRetire: (path) => this.archive(path),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public get settings(): SessionSettings {
|
|
94
|
+
return { ...this.host.settings, ...this.chatSettings };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
public get lastUsed(): number {
|
|
98
|
+
return this.host.lastUsed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public set lastUsed(value: number) {
|
|
102
|
+
this.host.lastUsed = value;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public get cwd(): string {
|
|
106
|
+
return this.host.cwd;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
public get isStreaming(): boolean {
|
|
110
|
+
return this.host.isStreaming;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public usage(): ReturnType<SessionHost["usage"]> {
|
|
114
|
+
return this.host.usage();
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
public sessionCost(): number | undefined {
|
|
118
|
+
return this.host.sessionCost();
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Pi's session UUID, once an agent exists. */
|
|
122
|
+
public get sessionId(): string | undefined {
|
|
123
|
+
return this.host.sessionId;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public get currentModelId(): string | undefined {
|
|
127
|
+
return this.host.currentModelId;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
public get supportedThinkingLevels(): readonly ThinkingLevelOpt[] {
|
|
131
|
+
return this.host.supportedThinkingLevels as readonly ThinkingLevelOpt[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
public get currentThinkingLevel(): ThinkingLevelOpt {
|
|
135
|
+
return this.host.currentThinkingLevel as ThinkingLevelOpt;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
public get temporary(): boolean {
|
|
139
|
+
return this.chatSettings.temporary ?? false;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
public run(
|
|
143
|
+
work: (agent: AgentSession) => Promise<void>,
|
|
144
|
+
opts?: { readonly isolated?: boolean }
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
return this.host.run(work, opts);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public async cancel(): Promise<boolean> {
|
|
150
|
+
return (await this.host.cancel()).cancelled;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public clear(): Promise<void> {
|
|
154
|
+
return this.host.clear();
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
public setCwd(newCwd: string): Promise<SetCwdResult> {
|
|
158
|
+
return this.host.setCwd(newCwd);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
public setModel(pattern: string): Promise<SetModelResult> {
|
|
162
|
+
return this.host.setModel(pattern);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
public setThinkingLevel(level: ThinkingLevelOpt): Promise<void> {
|
|
166
|
+
return this.host.setThinkingLevel(level as ThinkingLevel);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
public compact(customInstructions?: string): Promise<SessionCompactResult> {
|
|
170
|
+
return this.host.compact(customInstructions);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
public setLogsMode(mode: LogsMode): Promise<void> {
|
|
174
|
+
return this.host.serialize(async () => {
|
|
175
|
+
if (this.chatSettings.logsMode === mode) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
this.chatSettings = { ...this.chatSettings, logsMode: mode };
|
|
179
|
+
await this.deps.persistSettings({ logsMode: mode });
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
public setTemporary(value: boolean): Promise<void> {
|
|
184
|
+
return this.host.serialize(async () => {
|
|
185
|
+
if (this.chatSettings.temporary === value) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
this.chatSettings = { ...this.chatSettings, temporary: value };
|
|
189
|
+
await this.deps.persistSettings({ temporary: value });
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
public dispose(): Promise<void> {
|
|
194
|
+
return this.host.dispose();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private sessionPath(dir: string, suffix = ""): string {
|
|
198
|
+
return join(
|
|
199
|
+
this.deps.config.configDir,
|
|
200
|
+
dir,
|
|
201
|
+
`${encodeId(this.id)}${suffix}.jsonl`
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
private async archive(path: string): Promise<void> {
|
|
206
|
+
try {
|
|
207
|
+
await rename(path, `${path}.archived-${stamp()}`);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
210
|
+
console.warn(`[session ${encodeId(this.id)}] archive ${path}:`, err);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private async getSystemInstruction(): Promise<string | undefined> {
|
|
216
|
+
const path = join(
|
|
217
|
+
this.deps.config.configDir,
|
|
218
|
+
"instructions",
|
|
219
|
+
`${encodeId(this.id)}.md`
|
|
220
|
+
);
|
|
221
|
+
let userContent: string | undefined;
|
|
222
|
+
try {
|
|
223
|
+
userContent = (await Bun.file(path).text()).trim();
|
|
224
|
+
} catch (err) {
|
|
225
|
+
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
226
|
+
console.warn(
|
|
227
|
+
`[telegram-user-instruction] failed to read ${path}:`,
|
|
228
|
+
err
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const username = this.deps.getBotUsername();
|
|
233
|
+
const handle = username ? ` (@${username})` : "";
|
|
234
|
+
const systemIx = `You are running as a Telegram bot${handle} powered by Pim Agent. The telegram_user_instructions below are your editable instructions - edit the file at its \`path\` attribute to update your instructions. Wrap LaTeX math in a \`\`\`math fenced block without the $$ delimitters; inline math is not supported.`;
|
|
235
|
+
const userIx = `<telegram_user_instructions path="${path}">${userContent ? `\n${userContent}\n` : ""}</telegram_user_instructions>`;
|
|
236
|
+
return `<telegram_system_instructions>\n${systemIx}\n${userIx}\n</telegram_system_instructions>`;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export function encodeId(id: SessionId): string {
|
|
241
|
+
return `${id.chatId}-${id.threadId ?? MAIN}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export function decodeId(s: string): SessionId {
|
|
245
|
+
const idx = s.lastIndexOf("-");
|
|
246
|
+
const chatId = Number(s.slice(0, idx));
|
|
247
|
+
const tail = s.slice(idx + 1);
|
|
248
|
+
return {
|
|
249
|
+
chatId,
|
|
250
|
+
threadId: tail === MAIN ? undefined : Number(tail),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { Api } from "grammy";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
import { AgentRuntime } from "#core/session/AgentRuntime";
|
|
6
|
+
import { SessionCache } from "#core/session/SessionCache";
|
|
7
|
+
import { Fs } from "#core/shared/Fs";
|
|
8
|
+
import { type TelegramConfig } from "./Config";
|
|
9
|
+
import {
|
|
10
|
+
encodeId,
|
|
11
|
+
Session,
|
|
12
|
+
type SessionId,
|
|
13
|
+
type SessionSettings,
|
|
14
|
+
} from "./Session";
|
|
15
|
+
import type { TaskScheduler } from "./TaskScheduler";
|
|
16
|
+
|
|
17
|
+
export class SessionRegistry {
|
|
18
|
+
private readonly config: TelegramConfig;
|
|
19
|
+
private readonly api: Api;
|
|
20
|
+
private readonly scheduler: TaskScheduler;
|
|
21
|
+
private readonly runtime: AgentRuntime;
|
|
22
|
+
private readonly cache = new SessionCache<Session>();
|
|
23
|
+
private settings: Map<string, SessionSettings> = new Map();
|
|
24
|
+
private initialized = false;
|
|
25
|
+
private initPromise: Promise<void> | undefined;
|
|
26
|
+
private botUsername: string | undefined;
|
|
27
|
+
|
|
28
|
+
public constructor(
|
|
29
|
+
config: TelegramConfig,
|
|
30
|
+
api: Api,
|
|
31
|
+
scheduler: TaskScheduler,
|
|
32
|
+
runtime: AgentRuntime
|
|
33
|
+
) {
|
|
34
|
+
this.config = config;
|
|
35
|
+
this.api = api;
|
|
36
|
+
this.scheduler = scheduler;
|
|
37
|
+
this.runtime = runtime;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public setBotUsername(username: string): void {
|
|
41
|
+
this.botUsername = username;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async init(): Promise<void> {
|
|
45
|
+
if (this.initialized) {
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
this.initPromise ??= this.bootstrap().catch((err: unknown) => {
|
|
49
|
+
this.initPromise = undefined;
|
|
50
|
+
throw err;
|
|
51
|
+
});
|
|
52
|
+
await this.initPromise;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public get(sessionId: SessionId): Session {
|
|
56
|
+
this.requireInitialized();
|
|
57
|
+
const key = encodeId(sessionId);
|
|
58
|
+
const cached = this.cache.touch(key);
|
|
59
|
+
if (cached) {
|
|
60
|
+
return cached;
|
|
61
|
+
}
|
|
62
|
+
return this.cache.adopt(
|
|
63
|
+
key,
|
|
64
|
+
new Session({
|
|
65
|
+
id: sessionId,
|
|
66
|
+
settings: this.settings.get(key) ?? {},
|
|
67
|
+
config: this.config,
|
|
68
|
+
api: this.api,
|
|
69
|
+
runtime: this.runtime,
|
|
70
|
+
scheduler: this.scheduler,
|
|
71
|
+
persistSettings: (patch) => this.persistSettings(key, patch),
|
|
72
|
+
getBotUsername: () => this.botUsername,
|
|
73
|
+
})
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async disposeAll(): Promise<void> {
|
|
78
|
+
await this.cache.disposeAll();
|
|
79
|
+
if (this.initialized) {
|
|
80
|
+
await this.flushSettings();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private async bootstrap(): Promise<void> {
|
|
85
|
+
await this.runtime.init();
|
|
86
|
+
const loaded = await Fs.readJsonOrEmpty<Record<string, SessionSettings>>(
|
|
87
|
+
join(this.config.configDir, "state.json"),
|
|
88
|
+
{}
|
|
89
|
+
);
|
|
90
|
+
this.settings = new Map(Object.entries(loaded));
|
|
91
|
+
await mkdir(join(this.config.configDir, "sessions"), { recursive: true });
|
|
92
|
+
await mkdir(join(this.config.configDir, "isolated-sessions"), {
|
|
93
|
+
recursive: true,
|
|
94
|
+
});
|
|
95
|
+
await mkdir(join(this.config.configDir, "instructions"), {
|
|
96
|
+
recursive: true,
|
|
97
|
+
});
|
|
98
|
+
this.initialized = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private requireInitialized(): void {
|
|
102
|
+
if (!this.initialized) {
|
|
103
|
+
throw new Error("SessionRegistry.init() must complete before use");
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async persistSettings(
|
|
108
|
+
key: string,
|
|
109
|
+
patch: Partial<SessionSettings>
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const prev = this.settings.get(key) ?? {};
|
|
112
|
+
this.settings.set(key, { ...prev, ...patch });
|
|
113
|
+
await this.flushSettings();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private async flushSettings(): Promise<void> {
|
|
117
|
+
try {
|
|
118
|
+
await Fs.writeAtomic(
|
|
119
|
+
join(this.config.configDir, "state.json"),
|
|
120
|
+
JSON.stringify(Object.fromEntries(this.settings), null, 2)
|
|
121
|
+
);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.warn(`[registry] state save failed:`, err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
import type { SessionId } from "./Session";
|
|
2
|
+
import type { ScheduleSpec, ScheduledTask } from "./TaskSchema";
|
|
3
|
+
import { TaskStore } from "./TaskStore";
|
|
4
|
+
|
|
5
|
+
export type RunTaskFn = (task: ScheduledTask) => Promise<void>;
|
|
6
|
+
|
|
7
|
+
export type TaskSchedulerOptions = {
|
|
8
|
+
readonly configDir: string;
|
|
9
|
+
readonly runTask: RunTaskFn;
|
|
10
|
+
readonly pollIntervalMs?: number;
|
|
11
|
+
readonly now?: () => number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const MISSED_TASK_WINDOW_MS = 24 * 3600_000;
|
|
15
|
+
const MIN_INTERVAL_MS = 60_000;
|
|
16
|
+
|
|
17
|
+
export class TaskScheduler {
|
|
18
|
+
private readonly configDir: string;
|
|
19
|
+
private readonly runTask: RunTaskFn;
|
|
20
|
+
private readonly pollIntervalMs: number;
|
|
21
|
+
private readonly now: () => number;
|
|
22
|
+
private timer: Timer | undefined;
|
|
23
|
+
private inflight: Promise<void> | undefined;
|
|
24
|
+
|
|
25
|
+
public constructor(opts: TaskSchedulerOptions) {
|
|
26
|
+
this.configDir = opts.configDir;
|
|
27
|
+
this.runTask = opts.runTask;
|
|
28
|
+
this.pollIntervalMs = opts.pollIntervalMs ?? 10_000;
|
|
29
|
+
this.now = opts.now ?? ((): number => Date.now());
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async start(): Promise<void> {
|
|
33
|
+
if (this.timer) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
this.timer = setInterval(() => {
|
|
37
|
+
void this.tick();
|
|
38
|
+
}, this.pollIntervalMs);
|
|
39
|
+
await this.tick();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
public async stop(): Promise<void> {
|
|
43
|
+
if (this.timer) {
|
|
44
|
+
clearInterval(this.timer);
|
|
45
|
+
this.timer = undefined;
|
|
46
|
+
}
|
|
47
|
+
if (this.inflight) {
|
|
48
|
+
await this.inflight;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
public async tick(): Promise<void> {
|
|
53
|
+
if (this.inflight) {
|
|
54
|
+
await this.inflight;
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const run = this.runTick().finally(() => {
|
|
58
|
+
this.inflight = undefined;
|
|
59
|
+
});
|
|
60
|
+
this.inflight = run;
|
|
61
|
+
await run;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
public async list(
|
|
65
|
+
sessionId: SessionId
|
|
66
|
+
): Promise<ReadonlyArray<ScheduledTask>> {
|
|
67
|
+
const all = await TaskStore.loadAll(this.configDir);
|
|
68
|
+
return all
|
|
69
|
+
.filter(
|
|
70
|
+
(t) =>
|
|
71
|
+
t.chatId === sessionId.chatId && t.threadId === sessionId.threadId
|
|
72
|
+
)
|
|
73
|
+
.sort((a, b) => a.nextRun.localeCompare(b.nextRun));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public async create(
|
|
77
|
+
sessionId: SessionId,
|
|
78
|
+
input: {
|
|
79
|
+
readonly prompt: string;
|
|
80
|
+
readonly schedule: ScheduleSpec;
|
|
81
|
+
readonly expires?: string;
|
|
82
|
+
readonly isolatedSession?: boolean;
|
|
83
|
+
}
|
|
84
|
+
): Promise<ScheduledTask> {
|
|
85
|
+
const nextRun = this.computeFirstRun(input.schedule);
|
|
86
|
+
if (input.expires !== undefined) {
|
|
87
|
+
const expMs = Date.parse(input.expires);
|
|
88
|
+
if (!Number.isFinite(expMs)) {
|
|
89
|
+
throw new Error(`invalid expires timestamp: ${input.expires}`);
|
|
90
|
+
}
|
|
91
|
+
if (expMs <= Date.parse(nextRun)) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`expires must be strictly after first nextRun (${nextRun})`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const task: ScheduledTask = {
|
|
98
|
+
id: TaskStore.makeId(input.prompt),
|
|
99
|
+
prompt: input.prompt,
|
|
100
|
+
chatId: sessionId.chatId,
|
|
101
|
+
threadId: sessionId.threadId,
|
|
102
|
+
schedule: input.schedule,
|
|
103
|
+
status: "active",
|
|
104
|
+
nextRun,
|
|
105
|
+
expires: input.expires ?? null,
|
|
106
|
+
isolatedSession: input.isolatedSession ?? false,
|
|
107
|
+
createdAt: new Date(this.now()).toISOString(),
|
|
108
|
+
};
|
|
109
|
+
await TaskStore.save(this.configDir, task);
|
|
110
|
+
return task;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
public async delete(sessionId: SessionId, id: string): Promise<boolean> {
|
|
114
|
+
const t = await this.find(sessionId, id);
|
|
115
|
+
if (!t) {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
await TaskStore.remove(this.configDir, id);
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
public setStatus(
|
|
123
|
+
sessionId: SessionId,
|
|
124
|
+
id: string,
|
|
125
|
+
status: "active" | "paused"
|
|
126
|
+
): Promise<ScheduledTask | undefined> {
|
|
127
|
+
return this.mutate(sessionId, id, (t) =>
|
|
128
|
+
t.status === status ? undefined : { ...t, status }
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
public updatePrompt(
|
|
133
|
+
sessionId: SessionId,
|
|
134
|
+
id: string,
|
|
135
|
+
prompt: string
|
|
136
|
+
): Promise<ScheduledTask | undefined> {
|
|
137
|
+
return this.mutate(sessionId, id, (t) =>
|
|
138
|
+
t.prompt === prompt ? undefined : { ...t, prompt }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async mutate(
|
|
143
|
+
sessionId: SessionId,
|
|
144
|
+
id: string,
|
|
145
|
+
patch: (task: ScheduledTask) => ScheduledTask | undefined
|
|
146
|
+
): Promise<ScheduledTask | undefined> {
|
|
147
|
+
const t = await this.find(sessionId, id);
|
|
148
|
+
if (!t) {
|
|
149
|
+
return undefined;
|
|
150
|
+
}
|
|
151
|
+
const updated = patch(t);
|
|
152
|
+
if (!updated) {
|
|
153
|
+
return t;
|
|
154
|
+
}
|
|
155
|
+
await TaskStore.save(this.configDir, updated);
|
|
156
|
+
return updated;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private async find(
|
|
160
|
+
sessionId: SessionId,
|
|
161
|
+
id: string
|
|
162
|
+
): Promise<ScheduledTask | undefined> {
|
|
163
|
+
const all = await this.list(sessionId);
|
|
164
|
+
return all.find((t) => t.id === id);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private async runTick(): Promise<void> {
|
|
168
|
+
const all = await TaskStore.loadAll(this.configDir);
|
|
169
|
+
const now = this.now();
|
|
170
|
+
const fires: Promise<void>[] = [];
|
|
171
|
+
|
|
172
|
+
for (const task of all) {
|
|
173
|
+
if (task.status !== "active") {
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const nextMs = Date.parse(task.nextRun);
|
|
177
|
+
if (!Number.isFinite(nextMs) || nextMs > now) {
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (task.expires) {
|
|
182
|
+
const expMs = Date.parse(task.expires);
|
|
183
|
+
if (Number.isFinite(expMs) && now > expMs) {
|
|
184
|
+
await TaskStore.remove(this.configDir, task.id);
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (now - nextMs > MISSED_TASK_WINDOW_MS) {
|
|
190
|
+
const advanced = advanceNextRun(task, now);
|
|
191
|
+
if (advanced) {
|
|
192
|
+
await TaskStore.save(this.configDir, advanced);
|
|
193
|
+
} else {
|
|
194
|
+
await TaskStore.remove(this.configDir, task.id);
|
|
195
|
+
}
|
|
196
|
+
console.warn(
|
|
197
|
+
`[scheduler] task ${task.id} missed by >24h, advanced silently`
|
|
198
|
+
);
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
fires.push(this.fireAndReschedule(task, now));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
await Promise.allSettled(fires);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private async fireAndReschedule(
|
|
209
|
+
task: ScheduledTask,
|
|
210
|
+
firedAt: number
|
|
211
|
+
): Promise<void> {
|
|
212
|
+
try {
|
|
213
|
+
await this.runTask(task);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
console.error(`[scheduler] task ${task.id} runTask failed:`, err);
|
|
216
|
+
}
|
|
217
|
+
const next = advanceNextRun(task, firedAt);
|
|
218
|
+
if (next) {
|
|
219
|
+
await TaskStore.save(this.configDir, next);
|
|
220
|
+
} else {
|
|
221
|
+
await TaskStore.remove(this.configDir, task.id);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private computeFirstRun(schedule: ScheduleSpec): string {
|
|
226
|
+
const now = this.now();
|
|
227
|
+
if (schedule.type === "once") {
|
|
228
|
+
const at = Date.parse(schedule.at);
|
|
229
|
+
if (!Number.isFinite(at)) {
|
|
230
|
+
throw new Error(`invalid 'at' timestamp: ${schedule.at}`);
|
|
231
|
+
}
|
|
232
|
+
if (at <= now) {
|
|
233
|
+
throw new Error(`'at' must be strictly in the future`);
|
|
234
|
+
}
|
|
235
|
+
return new Date(at).toISOString();
|
|
236
|
+
}
|
|
237
|
+
if (schedule.type === "interval") {
|
|
238
|
+
const ms = parseDuration(schedule.every);
|
|
239
|
+
if (ms < MIN_INTERVAL_MS) {
|
|
240
|
+
throw new Error(`interval must be at least 1 minute`);
|
|
241
|
+
}
|
|
242
|
+
return new Date(now + ms).toISOString();
|
|
243
|
+
}
|
|
244
|
+
const next = Bun.cron.parse(schedule.expr, new Date(now));
|
|
245
|
+
if (!next) {
|
|
246
|
+
throw new Error(`cron expression has no future match: ${schedule.expr}`);
|
|
247
|
+
}
|
|
248
|
+
return next.toISOString();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function advanceNextRun(
|
|
253
|
+
task: ScheduledTask,
|
|
254
|
+
fromMs: number
|
|
255
|
+
): ScheduledTask | undefined {
|
|
256
|
+
const schedule = task.schedule;
|
|
257
|
+
if (schedule.type === "once") {
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
if (schedule.type === "interval") {
|
|
261
|
+
const ms = parseDuration(schedule.every);
|
|
262
|
+
return { ...task, nextRun: new Date(fromMs + ms).toISOString() };
|
|
263
|
+
}
|
|
264
|
+
const next = Bun.cron.parse(schedule.expr, new Date(fromMs));
|
|
265
|
+
if (!next) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
return { ...task, nextRun: next.toISOString() };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export function parseDuration(input: string): number {
|
|
272
|
+
const s = input.trim();
|
|
273
|
+
if (!s) {
|
|
274
|
+
throw new Error("empty duration");
|
|
275
|
+
}
|
|
276
|
+
let remaining = s;
|
|
277
|
+
let total = 0;
|
|
278
|
+
while (remaining.length > 0) {
|
|
279
|
+
const m = remaining.match(/^(\d+)([smhd])/);
|
|
280
|
+
if (!m) {
|
|
281
|
+
throw new Error(`bad duration: ${input}`);
|
|
282
|
+
}
|
|
283
|
+
const n = Number(m[1]);
|
|
284
|
+
const mult = DURATION_UNITS[m[2]!]!;
|
|
285
|
+
total += n * mult;
|
|
286
|
+
remaining = remaining.slice(m[0].length);
|
|
287
|
+
}
|
|
288
|
+
return total;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const DURATION_UNITS: Record<string, number> = {
|
|
292
|
+
s: 1_000,
|
|
293
|
+
m: 60_000,
|
|
294
|
+
h: 3_600_000,
|
|
295
|
+
d: 86_400_000,
|
|
296
|
+
};
|