codeden 0.1.1 → 0.2.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/README.md +7 -1
- package/dist/cli.js +1 -1
- package/dist/drizzle/20261108041036_session_worktree_cleanup/migration.sql +2 -0
- package/dist/drizzle/20261108041036_session_worktree_cleanup/snapshot.json +3424 -0
- package/dist/drizzle/20261108045102_project_pin_order/migration.sql +4 -0
- package/dist/drizzle/20261108045102_project_pin_order/snapshot.json +3444 -0
- package/dist/drizzle/20261108052259_queued_message_sender/migration.sql +1 -0
- package/dist/drizzle/20261108052259_queued_message_sender/snapshot.json +3454 -0
- package/dist/{main-DaGFUbbW.js → main-CzmG0d8t.js} +2 -2
- package/dist/{main-DaGFUbbW.js.map → main-CzmG0d8t.js.map} +1 -1
- package/dist/{main-BF0aQ9U8.js → main-pXwL7GDQ.js} +1555 -149
- package/dist/main-pXwL7GDQ.js.map +1 -0
- package/dist/{runner-D8m3WdtS.js → runner-CJetH9jW.js} +1947 -389
- package/dist/runner-CJetH9jW.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/web/assets/{core-BmMheb4i.js → core-BeOBW-Wr.js} +1 -1
- package/dist/web/assets/engine-oniguruma-BLpLufIT.js +1 -0
- package/dist/web/assets/{highlighted-body-JLMTFACH-nUCrIe8P.js → highlighted-body-JLMTFACH-D-L40tHI.js} +1 -1
- package/dist/web/assets/{highlights-CdGBlIcw.js → highlights-ZCqssNA3.js} +1 -1
- package/dist/web/assets/index-D8qaPF_E.css +1 -0
- package/dist/web/assets/index-DR-I5EC5.js +892 -0
- package/dist/web/assets/langs-DKjK0GUN.js +1 -0
- package/dist/web/assets/mermaid-WFAXDIZX-fZTzmfWq.js +1 -0
- package/dist/web/assets/{shiki-D8HIISPT.js → shiki-CWXnlX2q.js} +2 -2
- package/dist/web/assets/{stream-tokenizer-BmWmYTv_.js → stream-tokenizer-BpK1CZzW.js} +1 -1
- package/dist/web/assets/{themes-CKsIQs7_.js → themes-Bm7B3n10.js} +2 -2
- package/dist/web/favicon.svg +1 -1
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/main-BF0aQ9U8.js.map +0 -1
- package/dist/runner-D8m3WdtS.js.map +0 -1
- package/dist/web/assets/engine-oniguruma-CVzqqUIi.js +0 -1
- package/dist/web/assets/index-DKWZjMF1.js +0 -885
- package/dist/web/assets/index-DfoaM4E6.css +0 -1
- package/dist/web/assets/langs-kfQh6fa2.js +0 -1
- package/dist/web/assets/mermaid-WFAXDIZX-BIq7_eYY.js +0 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { n as __require, r as __toESM, t as __commonJSMin } from "./rolldown-runtime-DegdyBIC.js";
|
|
2
|
-
import { $ as
|
|
2
|
+
import { $ as ListAutomationRunsQuery, $n as literal, $t as FactoryTuning, A as SessionFileQuery, An as FactoryLimits, At as LocalSttModelParams, B as ImageTestRequest, Bn as McpServerUpsertRequest, Bt as CreateGithubIssueRequest, C as isElectronRuntime, Cn as ProjectConfigFile, Ct as ThreadItemsQuery, D as CODEDEN_VERSION, Dn as Project, Dt as agentEventToNewEvent, E as wrapper_default, En as WorkKey, Et as HealthResponse, F as ListConflictsQuery, Fn as AgentConfigListQuery, Ft as UpdateStatus, G as BrowserNavigateRequest, Gn as _enum, Gt as GithubPr, H as BrowserClientMessage, Hn as ProviderId, Ht as GithubIssue, I as ListNoticesQuery, In as AgentConfigLocator, It as compareVersions, J as BrowserViewportRequest, Jn as boolean, Jt as githubPrId, K as BrowserNewTabRequest, Kn as _null, Kt as MergePrRequest, L as ResolveConflictRequest, Ln as AgentConfigWriteRequest, Lt as isNewerVersion, M as RunnerToServerMessage, Mn as InboxItemKind, Mt as TtsRequest, N as ServerLock, Nn as AgentConfigDeleteQuery, Nt as AppPointer, O as UsageQuery, On as sortProjects, Ot as Settings, P as ArchiveSessionRequest, Pn as AgentConfigDiffRequest, Pt as UpdateInstallRequest, Q as CreateAutomationRequest, Qn as intersection, Qt as FactoryTriageRequest, R as RoleMessageRequest, Rn as McpServerDiffRequest, Rt as ConflictRecord, S as acquireInstallLock, Sn as SessionOptions, St as SteerRequest, T as import_websocket_server, Tn as StartedBy, Tt as UpdateProjectRequest, U as BrowserDevServerRequest, Un as ReasoningEffort, Ut as GithubLabelRequest, V as summarizeActivity, Vn as AutonomyLevel, Vt as GithubCommentRequest, W as BrowserHistoryRequest, Wn as datetime, Wt as GithubListQuery, X as AutomationFileFrontmatter, Xn as discriminatedUnion, Xt as DiscoveryArea, Y as BrowserWsQuery, Yn as custom, Yt as parseGithubPrId, Z as AutomationProjectRequest, Zn as int, Zt as FactoryCandidate, _ as importClaudeSdkFrom, _n as SessionResult, _t as SecretKey, a as Hono, an as Artifact, ar as record, at as TriageReport, b as runNpm, bn as TerminalQuery, bt as SetSecretRequest, c as serve, cn as AutomationRun, cr as unknown, ct as UpdateAutomationRequest, d as createImageEngineRegistry, dn as SessionStatus, dr as safeParse$1, dt as FileWriteRequest, en as KillSwitchRequest, er as looseObject, et as ListAutomationsQuery, f as IMAGE_ASPECT_RATIOS, fn as DiffScope, fr as prettifyError, ft as ListInboxQuery, g as CLAUDE_SDK_VERSION, gn as RestoreFilesRequest, gt as RespondRequest, h as claudeSdkSource, hn as GitPushRequest, ht as ResolveInboxRequest, i as killTree$1, in as isFactoryMode, ir as preprocess, it as TriageDecisionRequest, j as RunnerLock, jn as InboxItem, jt as SttQuery, k as UsageSettings, kn as DEFAULT_VISION_PATH, kt as LOCAL_STT_SAMPLE_RATE, l as upgradeWebSocket, ln as TriagePolicy, lr as ZodError, lt as CreateProjectRequest, m as ImageGenerationError, mn as GitCommitRequest, mt as ReorderProjectsRequest, n as readLock, nn as ReviewVerdict, nr as object, nt as SetAutomationEnabledRequest, o as tryDecodeURIComponent, on as Automation, or as string, ot as UpcomingRunsQuery, p as resolveImageOutput, pn as EditQueuedMessageRequest, pt as ListSessionsQuery, q as BrowserOpenRequest, qn as array, qt as TriageIssueRequest, r as isAlive, rn as SetAutonomyModeRequest, rr as optional, rt as StarterSetRequest, s as bufferToFormData, sn as AutomationGuards, sr as union$1, st as UpdateAutomationProjectSettingsRequest, tn as PutVisionRequest, tr as number, tt as RunAutomationRequest, u as ImageEngineRegistry, un as Session, ur as toJSONSchema, ut as FileReadQuery, v as installClaudeSdk, vn as SpeechSummaryRequest, vt as SendMessageRequest, w as ownNodeEnv, wn as ConflictDecision, wt as UiClientMessage, x as resolveNpm, xn as RunnerCommandResults, xt as StartSessionRequest, y as isClaudeSdkInstalled, yn as TerminalClientMessage, yt as SessionStartMeta, z as UpdateSessionRequest, zn as McpServerTarget, zt as NOTICE_BATCH_CODE } from "./runner-CJetH9jW.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import fs, { accessSync, closeSync, constants, copyFileSync, cpSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, tmpdir } from "node:os";
|
|
@@ -7,6 +7,8 @@ import { basename, delimiter, dirname, extname, isAbsolute, join, parse, relativ
|
|
|
7
7
|
import { isDeepStrictEqual, parseArgs } from "node:util";
|
|
8
8
|
import crypto$1, { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
9
9
|
import { execFile, spawn } from "node:child_process";
|
|
10
|
+
import { gunzipSync } from "node:zlib";
|
|
11
|
+
import { open, rename } from "node:fs/promises";
|
|
10
12
|
import { Readable } from "node:stream";
|
|
11
13
|
import { DatabaseSync } from "node:sqlite";
|
|
12
14
|
import { versions } from "node:process";
|
|
@@ -146,7 +148,9 @@ function homePaths(home) {
|
|
|
146
148
|
/** Copies of config files taken before the editors overwrite them (§12.4). */
|
|
147
149
|
backups: join(home, "backups"),
|
|
148
150
|
/** Dependencies installed on demand, e.g. Claude Code support (§14). */
|
|
149
|
-
deps: join(home, "deps")
|
|
151
|
+
deps: join(home, "deps"),
|
|
152
|
+
/** Models downloaded on demand, e.g. local Whisper for dictation (§11.3). */
|
|
153
|
+
models: join(home, "models")
|
|
150
154
|
};
|
|
151
155
|
}
|
|
152
156
|
/** Create the data directory layout if missing. */
|
|
@@ -7177,6 +7181,9 @@ const projects = sqliteTable("projects", {
|
|
|
7177
7181
|
factoryLimits: json$3("factory_limits"),
|
|
7178
7182
|
killSwitch: integer("kill_switch", { mode: "boolean" }).notNull().default(false),
|
|
7179
7183
|
timezone: text("timezone"),
|
|
7184
|
+
pinned: integer("pinned", { mode: "boolean" }).notNull().default(false),
|
|
7185
|
+
/** Manual list position (§13); the migration backfills it from the alphabetical order. */
|
|
7186
|
+
sortOrder: integer("sort_order").notNull().default(0),
|
|
7180
7187
|
github: json$3("github"),
|
|
7181
7188
|
createdAt: text("created_at").notNull(),
|
|
7182
7189
|
updatedAt: text("updated_at").notNull(),
|
|
@@ -7211,6 +7218,8 @@ const sessions = sqliteTable("sessions", {
|
|
|
7211
7218
|
createdAt: text("created_at").notNull(),
|
|
7212
7219
|
updatedAt: text("updated_at").notNull(),
|
|
7213
7220
|
endedAt: text("ended_at"),
|
|
7221
|
+
archivedAt: text("archived_at"),
|
|
7222
|
+
worktreeRemovedAt: text("worktree_removed_at"),
|
|
7214
7223
|
/** Accumulated usage (server-side projection; not part of the `Session` entity). */
|
|
7215
7224
|
usage: json$3("usage"),
|
|
7216
7225
|
/** How the session was started (workspace fallback, forced conflict reason, queue). */
|
|
@@ -7342,6 +7351,8 @@ const queuedMessages = sqliteTable("queued_messages", {
|
|
|
7342
7351
|
id: text("id").primaryKey(),
|
|
7343
7352
|
sessionId: text("session_id").notNull(),
|
|
7344
7353
|
input: json$3("input").notNull(),
|
|
7354
|
+
/** Who queued it; null for rows written before senders were recorded (the user). */
|
|
7355
|
+
sender: text("sender").$type(),
|
|
7345
7356
|
createdAt: text("created_at").notNull()
|
|
7346
7357
|
}, (t) => [index("queued_messages_session").on(t.sessionId, t.createdAt)]);
|
|
7347
7358
|
/**
|
|
@@ -7541,6 +7552,8 @@ function projectToRow(p) {
|
|
|
7541
7552
|
factoryLimits: p.factoryLimits,
|
|
7542
7553
|
killSwitch: p.killSwitch,
|
|
7543
7554
|
timezone: p.timezone,
|
|
7555
|
+
pinned: p.pinned,
|
|
7556
|
+
sortOrder: p.sortOrder,
|
|
7544
7557
|
github: p.kind === "github" ? p.github : null,
|
|
7545
7558
|
createdAt: p.createdAt,
|
|
7546
7559
|
updatedAt: p.updatedAt,
|
|
@@ -7559,7 +7572,9 @@ function rowToProject(r) {
|
|
|
7559
7572
|
function sessionToRow(s) {
|
|
7560
7573
|
return {
|
|
7561
7574
|
...s,
|
|
7562
|
-
result: s.result ?? null
|
|
7575
|
+
result: s.result ?? null,
|
|
7576
|
+
archivedAt: s.archivedAt ?? null,
|
|
7577
|
+
worktreeRemovedAt: s.worktreeRemovedAt ?? null
|
|
7563
7578
|
};
|
|
7564
7579
|
}
|
|
7565
7580
|
function rowToSession(r) {
|
|
@@ -8185,6 +8200,11 @@ function spawnRunner(config, home) {
|
|
|
8185
8200
|
const logs = join(home, "logs");
|
|
8186
8201
|
mkdirSync(logs, { recursive: true });
|
|
8187
8202
|
const log = openSync(join(logs, "runner.log"), "a");
|
|
8203
|
+
const env = {
|
|
8204
|
+
...process.env,
|
|
8205
|
+
...config.env,
|
|
8206
|
+
CODEDEN_HOME: home
|
|
8207
|
+
};
|
|
8188
8208
|
const child = spawn(config.command, [
|
|
8189
8209
|
...config.args,
|
|
8190
8210
|
"--home",
|
|
@@ -8198,11 +8218,7 @@ function spawnRunner(config, home) {
|
|
|
8198
8218
|
],
|
|
8199
8219
|
windowsHide: true,
|
|
8200
8220
|
cwd: home,
|
|
8201
|
-
env:
|
|
8202
|
-
...process.env,
|
|
8203
|
-
...config.env,
|
|
8204
|
-
CODEDEN_HOME: home
|
|
8205
|
-
}
|
|
8221
|
+
env: config.command === process.execPath ? ownNodeEnv(env) : env
|
|
8206
8222
|
});
|
|
8207
8223
|
child.unref();
|
|
8208
8224
|
return child.pid;
|
|
@@ -8438,6 +8454,12 @@ var RunnerClient = class {
|
|
|
8438
8454
|
case "stream":
|
|
8439
8455
|
this.opts.onStream?.(msg.stream);
|
|
8440
8456
|
return;
|
|
8457
|
+
case "providers":
|
|
8458
|
+
this.setStatus({
|
|
8459
|
+
...this.current,
|
|
8460
|
+
providers: msg.providers
|
|
8461
|
+
});
|
|
8462
|
+
return;
|
|
8441
8463
|
case "ping":
|
|
8442
8464
|
this.send({
|
|
8443
8465
|
type: "pong",
|
|
@@ -9411,6 +9433,7 @@ function applyWorkbenchProjection(tx, e) {
|
|
|
9411
9433
|
id: m.id,
|
|
9412
9434
|
sessionId: m.sessionId,
|
|
9413
9435
|
input: m.input,
|
|
9436
|
+
sender: m.from,
|
|
9414
9437
|
createdAt: m.createdAt
|
|
9415
9438
|
}).onConflictDoNothing().run();
|
|
9416
9439
|
return;
|
|
@@ -9418,6 +9441,15 @@ function applyWorkbenchProjection(tx, e) {
|
|
|
9418
9441
|
case "message.dequeued":
|
|
9419
9442
|
tx.delete(queuedMessages).where(eq(queuedMessages.id, e.payload.messageId)).run();
|
|
9420
9443
|
return;
|
|
9444
|
+
case "message.edited": {
|
|
9445
|
+
const row = tx.select({ input: queuedMessages.input }).from(queuedMessages).where(eq(queuedMessages.id, e.payload.messageId)).get();
|
|
9446
|
+
if (!row) return;
|
|
9447
|
+
tx.update(queuedMessages).set({ input: {
|
|
9448
|
+
...row.input,
|
|
9449
|
+
text: e.payload.text
|
|
9450
|
+
} }).where(eq(queuedMessages.id, e.payload.messageId)).run();
|
|
9451
|
+
return;
|
|
9452
|
+
}
|
|
9421
9453
|
}
|
|
9422
9454
|
}
|
|
9423
9455
|
/**
|
|
@@ -9467,6 +9499,11 @@ function applyProjection(tx, e) {
|
|
|
9467
9499
|
updatedAt: e.ts
|
|
9468
9500
|
}).where(eq(projects.id, e.payload.projectId)).run();
|
|
9469
9501
|
return [];
|
|
9502
|
+
case "projects.reordered":
|
|
9503
|
+
e.payload.projectIds.forEach((id, index) => {
|
|
9504
|
+
tx.update(projects).set({ sortOrder: index }).where(eq(projects.id, id)).run();
|
|
9505
|
+
});
|
|
9506
|
+
return [];
|
|
9470
9507
|
case "session.created":
|
|
9471
9508
|
case "session.updated": {
|
|
9472
9509
|
const row = sessionToRow(e.payload.session);
|
|
@@ -9511,6 +9548,7 @@ function applyProjection(tx, e) {
|
|
|
9511
9548
|
case "checkpoint.created":
|
|
9512
9549
|
case "message.queued":
|
|
9513
9550
|
case "message.dequeued":
|
|
9551
|
+
case "message.edited":
|
|
9514
9552
|
applyWorkbenchProjection(tx, e);
|
|
9515
9553
|
return [];
|
|
9516
9554
|
case "notice.created":
|
|
@@ -20727,6 +20765,16 @@ function toServiceError$1(error) {
|
|
|
20727
20765
|
if (code === "unknown_session" || code === "unsupported" || code === "no_turn") return new ServiceError(409, code, message);
|
|
20728
20766
|
return new ServiceError(502, code, message);
|
|
20729
20767
|
}
|
|
20768
|
+
/**
|
|
20769
|
+
* Ask a connected runner to re-detect its providers, which also refreshes their model lists
|
|
20770
|
+
* (they come back as a runner status update). Fire and forget; failures are only logged.
|
|
20771
|
+
*/
|
|
20772
|
+
function redetectProviders(runner, log) {
|
|
20773
|
+
if (!runner.isConnected()) return;
|
|
20774
|
+
runner.request({ name: "provider.detect" }, 6e4).catch((error) => {
|
|
20775
|
+
log?.(`runner: provider re-detect failed: ${error instanceof Error ? error.message : error}`);
|
|
20776
|
+
});
|
|
20777
|
+
}
|
|
20730
20778
|
/** `RunnerPort` over the server's (possibly absent) `RunnerClient`. */
|
|
20731
20779
|
function runnerPort(get) {
|
|
20732
20780
|
return {
|
|
@@ -21026,6 +21074,97 @@ function buffered(ws) {
|
|
|
21026
21074
|
function send$1(ws, msg) {
|
|
21027
21075
|
if (ws.readyState === 1) ws.send(JSON.stringify(msg));
|
|
21028
21076
|
}
|
|
21077
|
+
// ../server/src/services/sessions/preview.ts
|
|
21078
|
+
/** Tool output kept in transcript pages and live updates; the rest is fetched on demand (§6). */
|
|
21079
|
+
const OUTPUT_PREVIEW_CHARS = 16e3;
|
|
21080
|
+
/**
|
|
21081
|
+
* Trim large tool outputs to their last `max` characters (the end of command output is the
|
|
21082
|
+
* useful part) and mark them truncated with the full size. Pure.
|
|
21083
|
+
*/
|
|
21084
|
+
function previewRecord(record, max = OUTPUT_PREVIEW_CHARS) {
|
|
21085
|
+
const { item } = record;
|
|
21086
|
+
if (item.kind !== "tool_call" || !item.output || item.output.text.length <= max) return record;
|
|
21087
|
+
const text = item.output.text;
|
|
21088
|
+
return {
|
|
21089
|
+
...record,
|
|
21090
|
+
item: {
|
|
21091
|
+
...item,
|
|
21092
|
+
output: {
|
|
21093
|
+
...item.output,
|
|
21094
|
+
text: text.slice(text.length - max),
|
|
21095
|
+
truncated: true,
|
|
21096
|
+
totalBytes: item.output.totalBytes ?? Buffer.byteLength(text, "utf8")
|
|
21097
|
+
}
|
|
21098
|
+
}
|
|
21099
|
+
};
|
|
21100
|
+
}
|
|
21101
|
+
// ../server/src/services/sessions/client-items.ts
|
|
21102
|
+
const isPathImage = (a) => a.type === "image" && a.source.type === "path";
|
|
21103
|
+
/** The last segment of a path, whichever separator it uses. */
|
|
21104
|
+
const fileName = (path) => path.split(/[\\/]/).pop() || "image";
|
|
21105
|
+
/**
|
|
21106
|
+
* Input as the browser sees it. Image attachments are handed to providers as file paths and
|
|
21107
|
+
* providers echo that input back, so a user message can hold a path into the artifacts folder
|
|
21108
|
+
* (older transcripts stored it that way too). Such a path becomes its artifact, which the UI
|
|
21109
|
+
* loads from `/api/artifacts/:id`; any other image path keeps only its file name, so no
|
|
21110
|
+
* filesystem path reaches the browser. Pure apart from the lookup.
|
|
21111
|
+
*/
|
|
21112
|
+
function clientInput(input, artifacts) {
|
|
21113
|
+
if (!input.attachments.some(isPathImage)) return input;
|
|
21114
|
+
const attachments = input.attachments.map((a) => {
|
|
21115
|
+
if (a.type !== "image" || a.source.type !== "path") return a;
|
|
21116
|
+
const artifact = artifacts.artifactForPath(a.source.path);
|
|
21117
|
+
if (artifact) return {
|
|
21118
|
+
...a,
|
|
21119
|
+
source: {
|
|
21120
|
+
type: "artifact",
|
|
21121
|
+
artifactId: artifact.id
|
|
21122
|
+
}
|
|
21123
|
+
};
|
|
21124
|
+
return {
|
|
21125
|
+
...a,
|
|
21126
|
+
source: {
|
|
21127
|
+
type: "path",
|
|
21128
|
+
path: fileName(a.source.path)
|
|
21129
|
+
}
|
|
21130
|
+
};
|
|
21131
|
+
});
|
|
21132
|
+
return {
|
|
21133
|
+
...input,
|
|
21134
|
+
attachments
|
|
21135
|
+
};
|
|
21136
|
+
}
|
|
21137
|
+
/** An item record as served to the browser: large outputs previewed, attachments mapped. */
|
|
21138
|
+
function clientItemRecord(record, artifacts) {
|
|
21139
|
+
const previewed = previewRecord(record);
|
|
21140
|
+
const { item } = previewed;
|
|
21141
|
+
if (item.kind !== "user_message" || !item.input.attachments.some(isPathImage)) return previewed;
|
|
21142
|
+
return {
|
|
21143
|
+
...previewed,
|
|
21144
|
+
item: {
|
|
21145
|
+
...item,
|
|
21146
|
+
input: clientInput(item.input, artifacts)
|
|
21147
|
+
}
|
|
21148
|
+
};
|
|
21149
|
+
}
|
|
21150
|
+
/** A stored event as streamed to the browser: agent items carry mapped attachments too. */
|
|
21151
|
+
function clientEvent(event, artifacts) {
|
|
21152
|
+
if (event.type !== "agent.item.started" && event.type !== "agent.item.updated" && event.type !== "agent.item.completed") return event;
|
|
21153
|
+
const payload = event.payload;
|
|
21154
|
+
if (!("item" in payload) || payload.item.kind !== "user_message") return event;
|
|
21155
|
+
const { item } = payload;
|
|
21156
|
+
if (!item.input.attachments.some(isPathImage)) return event;
|
|
21157
|
+
return {
|
|
21158
|
+
...event,
|
|
21159
|
+
payload: {
|
|
21160
|
+
...payload,
|
|
21161
|
+
item: {
|
|
21162
|
+
...item,
|
|
21163
|
+
input: clientInput(item.input, artifacts)
|
|
21164
|
+
}
|
|
21165
|
+
}
|
|
21166
|
+
};
|
|
21167
|
+
}
|
|
21029
21168
|
// ../server/src/realtime/coalesce.ts
|
|
21030
21169
|
function asDelta(e) {
|
|
21031
21170
|
return e.type === "agent.item.delta" ? e.payload : void 0;
|
|
@@ -21074,30 +21213,6 @@ function coalesceEvents(events) {
|
|
|
21074
21213
|
}
|
|
21075
21214
|
return out;
|
|
21076
21215
|
}
|
|
21077
|
-
// ../server/src/services/sessions/preview.ts
|
|
21078
|
-
/** Tool output kept in transcript pages and live updates; the rest is fetched on demand (§6). */
|
|
21079
|
-
const OUTPUT_PREVIEW_CHARS = 16e3;
|
|
21080
|
-
/**
|
|
21081
|
-
* Trim large tool outputs to their last `max` characters (the end of command output is the
|
|
21082
|
-
* useful part) and mark them truncated with the full size. Pure.
|
|
21083
|
-
*/
|
|
21084
|
-
function previewRecord(record, max = OUTPUT_PREVIEW_CHARS) {
|
|
21085
|
-
const { item } = record;
|
|
21086
|
-
if (item.kind !== "tool_call" || !item.output || item.output.text.length <= max) return record;
|
|
21087
|
-
const text = item.output.text;
|
|
21088
|
-
return {
|
|
21089
|
-
...record,
|
|
21090
|
-
item: {
|
|
21091
|
-
...item,
|
|
21092
|
-
output: {
|
|
21093
|
-
...item.output,
|
|
21094
|
-
text: text.slice(text.length - max),
|
|
21095
|
-
truncated: true,
|
|
21096
|
-
totalBytes: item.output.totalBytes ?? Buffer.byteLength(text, "utf8")
|
|
21097
|
-
}
|
|
21098
|
-
}
|
|
21099
|
-
};
|
|
21100
|
-
}
|
|
21101
21216
|
// ../server/src/realtime/transcript.ts
|
|
21102
21217
|
function emptyDirty() {
|
|
21103
21218
|
return {
|
|
@@ -21148,8 +21263,11 @@ function collectDirty(dirty, events, sessionId, threadIds) {
|
|
|
21148
21263
|
dirty.seq = Math.max(dirty.seq, e.seq);
|
|
21149
21264
|
}
|
|
21150
21265
|
}
|
|
21151
|
-
/**
|
|
21152
|
-
|
|
21266
|
+
/**
|
|
21267
|
+
* Load the latest state of dirty rows as a `transcript` message (outputs previewed, image
|
|
21268
|
+
* paths mapped to artifacts, see `clientItemRecord`).
|
|
21269
|
+
*/
|
|
21270
|
+
function loadTranscript(db, sessionId, dirty, artifacts) {
|
|
21153
21271
|
const ids = (set) => [...set];
|
|
21154
21272
|
const msg = {
|
|
21155
21273
|
type: "transcript",
|
|
@@ -21157,7 +21275,7 @@ function loadTranscript(db, sessionId, dirty) {
|
|
|
21157
21275
|
seq: dirty.seq,
|
|
21158
21276
|
threads: dirty.threads.size === 0 ? [] : db.select().from(threads).where(and(eq(threads.sessionId, sessionId), inArray(threads.id, ids(dirty.threads)))).all().map(rowToThread),
|
|
21159
21277
|
turns: dirty.turns.size === 0 ? [] : db.select().from(turns).where(and(eq(turns.sessionId, sessionId), inArray(turns.id, ids(dirty.turns)))).all().map(rowToTurn),
|
|
21160
|
-
items: dirty.items.size === 0 ? [] : db.select().from(items).where(and(eq(items.sessionId, sessionId), inArray(items.id, ids(dirty.items)))).all().map((r) =>
|
|
21278
|
+
items: dirty.items.size === 0 ? [] : db.select().from(items).where(and(eq(items.sessionId, sessionId), inArray(items.id, ids(dirty.items)))).all().map((r) => clientItemRecord(rowToItemRecord(r), artifacts)).sort((a, b) => a.position - b.position)
|
|
21161
21279
|
};
|
|
21162
21280
|
if (dirty.usage) {
|
|
21163
21281
|
const usage = db.select({ usage: sessions.usage }).from(sessions).where(eq(sessions.id, sessionId)).get()?.usage;
|
|
@@ -21173,6 +21291,7 @@ const MAX_WATCH_REPLAY = 2e4;
|
|
|
21173
21291
|
/** A client with this many undelivered events is told to resync (it cannot keep up). */
|
|
21174
21292
|
const MAX_PENDING = 1e4;
|
|
21175
21293
|
const BATCH = 500;
|
|
21294
|
+
const noArtifacts = { artifactForPath: () => void 0 };
|
|
21176
21295
|
/**
|
|
21177
21296
|
* UI realtime hub (§6). Global stream: snapshot seq -> `subscribe { fromSeq }` -> catch-up ->
|
|
21178
21297
|
* live `events`, batched every ~40ms with text deltas merged. Transcripts: `watch_session`
|
|
@@ -21187,12 +21306,14 @@ var WsHub = class {
|
|
|
21187
21306
|
clients = /* @__PURE__ */ new Set();
|
|
21188
21307
|
unsubscribe;
|
|
21189
21308
|
flushMs;
|
|
21309
|
+
artifacts;
|
|
21190
21310
|
constructor(store, db, runnerStatus, opts = {}) {
|
|
21191
21311
|
this.store = store;
|
|
21192
21312
|
this.db = db;
|
|
21193
21313
|
this.runnerStatus = runnerStatus;
|
|
21194
21314
|
this.opts = opts;
|
|
21195
21315
|
this.flushMs = opts.flushMs ?? 40;
|
|
21316
|
+
this.artifacts = opts.artifacts ?? noArtifacts;
|
|
21196
21317
|
this.unsubscribe = store.subscribe((events) => this.onEvents(events));
|
|
21197
21318
|
}
|
|
21198
21319
|
/** Handlers for `upgradeWebSocket`. */
|
|
@@ -21317,7 +21438,7 @@ var WsHub = class {
|
|
|
21317
21438
|
const visible = batch.filter((e) => this.visible(client, e));
|
|
21318
21439
|
if (visible.length > 0) send(client.ws, {
|
|
21319
21440
|
type: "events",
|
|
21320
|
-
events:
|
|
21441
|
+
events: this.present(visible)
|
|
21321
21442
|
});
|
|
21322
21443
|
cursor = batch.at(-1)?.seq ?? cursor;
|
|
21323
21444
|
if (batch.length < BATCH) break;
|
|
@@ -21388,7 +21509,7 @@ var WsHub = class {
|
|
|
21388
21509
|
client.timer = void 0;
|
|
21389
21510
|
if (!this.clients.has(client)) return;
|
|
21390
21511
|
if (client.pending.length > 0) {
|
|
21391
|
-
const events =
|
|
21512
|
+
const events = this.present(client.pending);
|
|
21392
21513
|
client.pending = [];
|
|
21393
21514
|
send(client.ws, {
|
|
21394
21515
|
type: "events",
|
|
@@ -21398,9 +21519,13 @@ var WsHub = class {
|
|
|
21398
21519
|
for (const [sessionId, watch] of client.watches) if (isDirty(watch.dirty)) this.sendTranscript(client, sessionId, watch);
|
|
21399
21520
|
}
|
|
21400
21521
|
sendTranscript(client, sessionId, watch) {
|
|
21401
|
-
send(client.ws, loadTranscript(this.db, sessionId, watch.dirty));
|
|
21522
|
+
send(client.ws, loadTranscript(this.db, sessionId, watch.dirty, this.artifacts));
|
|
21402
21523
|
watch.dirty = emptyDirty();
|
|
21403
21524
|
}
|
|
21525
|
+
/** Events as sent to a client: deltas merged, attachment paths mapped. */
|
|
21526
|
+
present(events) {
|
|
21527
|
+
return coalesceEvents(events).map((e) => clientEvent(e, this.artifacts));
|
|
21528
|
+
}
|
|
21404
21529
|
};
|
|
21405
21530
|
function send(ws, msg) {
|
|
21406
21531
|
if (ws.readyState === 1) ws.send(JSON.stringify(msg));
|
|
@@ -22069,15 +22194,15 @@ function conflictRoutes(s) {
|
|
|
22069
22194
|
return new Hono().get("/", queryOf(ListConflictsQuery), (c) => c.json(conflicts.list(c.req.valid("query")))).get("/:id", (c) => c.json(conflicts.get(c.req.param("id")))).post("/:id/resolve", jsonBody(ResolveConflictRequest), async (c) => c.json(await conflicts.resolve(c.req.param("id"), c.req.valid("json"))));
|
|
22070
22195
|
}
|
|
22071
22196
|
// ../server/src/routes/projects.ts
|
|
22072
|
-
/** `/api/projects`: add from a folder or clone, list, update, archive. */
|
|
22197
|
+
/** `/api/projects`: add from a folder or clone, list, reorder, update (and pin), archive. */
|
|
22073
22198
|
function projectRoutes(s) {
|
|
22074
|
-
return new Hono().get("/", queryOf(object({ archived: _enum(["true", "false"]).optional() })), (c) => c.json(s.projects.list({ includeArchived: c.req.valid("query").archived === "true" }))).post("/", jsonBody(CreateProjectRequest), async (c) => c.json(await s.projects.create(c.req.valid("json")), 201)).get("/:id", (c) => c.json(s.projects.get(c.req.param("id")))).patch("/:id", jsonBody(UpdateProjectRequest), (c) => c.json(s.projects.update(c.req.param("id"), c.req.valid("json")))).delete("/:id", (c) => {
|
|
22199
|
+
return new Hono().get("/", queryOf(object({ archived: _enum(["true", "false"]).optional() })), (c) => c.json(s.projects.list({ includeArchived: c.req.valid("query").archived === "true" }))).post("/", jsonBody(CreateProjectRequest), async (c) => c.json(await s.projects.create(c.req.valid("json")), 201)).put("/order", jsonBody(ReorderProjectsRequest), (c) => c.json({ projects: s.projects.reorder(c.req.valid("json").projectIds) })).get("/:id", (c) => c.json(s.projects.get(c.req.param("id")))).patch("/:id", jsonBody(UpdateProjectRequest), (c) => c.json(s.projects.update(c.req.param("id"), c.req.valid("json")))).delete("/:id", (c) => {
|
|
22075
22200
|
s.projects.archive(c.req.param("id"));
|
|
22076
22201
|
return c.json({ ok: true });
|
|
22077
22202
|
});
|
|
22078
22203
|
}
|
|
22079
22204
|
// ../server/src/services/files/path-guard.ts
|
|
22080
|
-
const caseInsensitive = process.platform === "win32" || process.platform === "darwin";
|
|
22205
|
+
const caseInsensitive$1 = process.platform === "win32" || process.platform === "darwin";
|
|
22081
22206
|
/** Real path of `p`, or of its nearest existing ancestor with the rest appended. */
|
|
22082
22207
|
function realish(p) {
|
|
22083
22208
|
let current = p;
|
|
@@ -22093,8 +22218,8 @@ function realish(p) {
|
|
|
22093
22218
|
}
|
|
22094
22219
|
/** True when `child` is `root` or inside it (no `..` escape, no other drive). */
|
|
22095
22220
|
function isInside$3(root, child) {
|
|
22096
|
-
const a = caseInsensitive ? root.toLowerCase() : root;
|
|
22097
|
-
const b = caseInsensitive ? child.toLowerCase() : child;
|
|
22221
|
+
const a = caseInsensitive$1 ? root.toLowerCase() : root;
|
|
22222
|
+
const b = caseInsensitive$1 ? child.toLowerCase() : child;
|
|
22098
22223
|
const rel = relative(a, b);
|
|
22099
22224
|
return rel === "" || !rel.startsWith(`..${sep}`) && rel !== ".." && !isAbsolute(rel);
|
|
22100
22225
|
}
|
|
@@ -22191,6 +22316,18 @@ function readSessionFile(root, path) {
|
|
|
22191
22316
|
truncated: stat.size > MAX_READ_BYTES$2
|
|
22192
22317
|
};
|
|
22193
22318
|
}
|
|
22319
|
+
// ../server/src/services/sessions/workdir.ts
|
|
22320
|
+
/** Shown wherever a cleaned-up worktree is needed before the session is resumed. */
|
|
22321
|
+
const WORKTREE_REMOVED_MESSAGE = "This session's worktree was cleaned up. Send the session a message to recreate it from its branch.";
|
|
22322
|
+
/**
|
|
22323
|
+
* Directory a session's files, git commands and terminals use: its worktree, else its cwd.
|
|
22324
|
+
* Throws `worktree_removed` (409) once cleanup removed the worktree; the next message to the
|
|
22325
|
+
* session recreates it (§9).
|
|
22326
|
+
*/
|
|
22327
|
+
function sessionWorkdir(session) {
|
|
22328
|
+
if (session.worktreePath && session.worktreeRemovedAt) throw conflict(WORKTREE_REMOVED_MESSAGE, "worktree_removed");
|
|
22329
|
+
return session.worktreePath ?? session.cwd;
|
|
22330
|
+
}
|
|
22194
22331
|
// ../server/src/services/workbench/diff-files.ts
|
|
22195
22332
|
const CHANGE = {
|
|
22196
22333
|
added: "add",
|
|
@@ -22373,7 +22510,7 @@ var WorkbenchService = class {
|
|
|
22373
22510
|
};
|
|
22374
22511
|
/** Directory git commands run in: the session's worktree, else its cwd. */
|
|
22375
22512
|
function workdir(session) {
|
|
22376
|
-
return session
|
|
22513
|
+
return sessionWorkdir(session);
|
|
22377
22514
|
}
|
|
22378
22515
|
// ../server/src/routes/session-files.ts
|
|
22379
22516
|
/** `/api/sessions/:id/files`: read-only file tree and viewer for the session's worktree (§12.3). */
|
|
@@ -22381,7 +22518,10 @@ function sessionFileRoutes(s) {
|
|
|
22381
22518
|
return new Hono().get("/:id/files", (c) => c.json(listSessionFiles(workdir(s.sessionRepo.get(c.req.param("id")))))).get("/:id/files/content", queryOf(SessionFileQuery), (c) => c.json(readSessionFile(workdir(s.sessionRepo.get(c.req.param("id"))), c.req.valid("query").path)));
|
|
22382
22519
|
}
|
|
22383
22520
|
// ../server/src/routes/sessions.ts
|
|
22384
|
-
/**
|
|
22521
|
+
/**
|
|
22522
|
+
* `/api/sessions`: start (with conflict handling), message, steer, interrupt, respond, stop,
|
|
22523
|
+
* archive (removes the worktree, §9), unarchive and the queued-message tray.
|
|
22524
|
+
*/
|
|
22385
22525
|
function sessionRoutes(s) {
|
|
22386
22526
|
return new Hono().get("/", queryOf(ListSessionsQuery), (c) => c.json(s.sessionRepo.list(c.req.valid("query")))).post("/", jsonBody(StartSessionRequest), async (c) => {
|
|
22387
22527
|
const req = c.req.valid("json");
|
|
@@ -22392,7 +22532,7 @@ function sessionRoutes(s) {
|
|
|
22392
22532
|
...result,
|
|
22393
22533
|
conflictId: record.id
|
|
22394
22534
|
}, 409);
|
|
22395
|
-
}).get("/:id", (c) => c.json(s.sessions.detail(c.req.param("id")))).patch("/:id", jsonBody(UpdateSessionRequest), (c) => c.json(s.sessions.update(c.req.param("id"), c.req.valid("json")))).post("/:id/messages", jsonBody(SendMessageRequest), async (c) => c.json(await s.sessions.send(c.req.param("id"), c.req.valid("json")))).post("/:id/steer", jsonBody(SteerRequest), async (c) => {
|
|
22535
|
+
}).get("/:id", (c) => c.json(s.sessions.detail(c.req.param("id")))).patch("/:id", jsonBody(UpdateSessionRequest), async (c) => c.json(await s.sessions.update(c.req.param("id"), c.req.valid("json")))).post("/:id/messages", jsonBody(SendMessageRequest), async (c) => c.json(await s.sessions.send(c.req.param("id"), c.req.valid("json")))).post("/:id/steer", jsonBody(SteerRequest), async (c) => {
|
|
22396
22536
|
await s.sessions.steer(c.req.param("id"), c.req.valid("json").input);
|
|
22397
22537
|
return c.json({ ok: true });
|
|
22398
22538
|
}).post("/:id/interrupt", async (c) => {
|
|
@@ -22401,13 +22541,19 @@ function sessionRoutes(s) {
|
|
|
22401
22541
|
}).post("/:id/respond", jsonBody(RespondRequest), async (c) => {
|
|
22402
22542
|
await s.sessions.respond(c.req.param("id"), c.req.valid("json"));
|
|
22403
22543
|
return c.json({ ok: true });
|
|
22404
|
-
}).post("/:id/stop", async (c) => c.json(await s.sessions.stop(c.req.param("id")))).
|
|
22544
|
+
}).post("/:id/stop", async (c) => c.json(await s.sessions.stop(c.req.param("id")))).post("/:id/archive", jsonBody(ArchiveSessionRequest), async (c) => c.json(await s.worktreeCleanup.archive(c.req.param("id"), c.req.valid("json")))).post("/:id/unarchive", (c) => c.json(s.worktreeCleanup.unarchive(c.req.param("id")))).get("/:id/queue", (c) => {
|
|
22545
|
+
const body = { messages: s.queue.list(c.req.param("id")) };
|
|
22546
|
+
return c.json(body);
|
|
22547
|
+
}).patch("/:id/queue/:messageId", jsonBody(EditQueuedMessageRequest), (c) => c.json(s.queue.edit(c.req.param("id"), c.req.param("messageId"), c.req.valid("json").text))).delete("/:id/queue/:messageId", (c) => {
|
|
22548
|
+
s.queue.delete(c.req.param("id"), c.req.param("messageId"));
|
|
22549
|
+
return c.json({ ok: true });
|
|
22550
|
+
}).post("/:id/queue/:messageId/send-now", async (c) => c.json(await s.queue.sendNow(c.req.param("id"), c.req.param("messageId")))).get("/:id/threads/:threadId/items", queryOf(ThreadItemsQuery), (c) => {
|
|
22405
22551
|
const id = c.req.param("id");
|
|
22406
22552
|
s.sessionRepo.get(id);
|
|
22407
22553
|
const page = s.sessionRepo.threadPage(id, c.req.param("threadId"), c.req.valid("query"));
|
|
22408
22554
|
return c.json({
|
|
22409
22555
|
...page,
|
|
22410
|
-
items: page.items.map((r) =>
|
|
22556
|
+
items: page.items.map((r) => clientItemRecord(r, s.artifacts))
|
|
22411
22557
|
});
|
|
22412
22558
|
}).get("/:id/items/:itemId/output", (c) => {
|
|
22413
22559
|
const record = s.sessionRepo.item(c.req.param("id"), c.req.param("itemId"));
|
|
@@ -22801,13 +22947,20 @@ var VoiceService = class {
|
|
|
22801
22947
|
signal
|
|
22802
22948
|
});
|
|
22803
22949
|
}
|
|
22804
|
-
/** Transcribe recorded audio with the chosen (or configured)
|
|
22950
|
+
/** Transcribe recorded audio with the chosen (or configured) engine. */
|
|
22805
22951
|
async transcribe(audio, opts) {
|
|
22806
22952
|
if (audio.bytes.length === 0) throw new ServiceError(400, "empty_audio", "No audio was sent");
|
|
22807
22953
|
if (audio.bytes.length > 26214400) throw new ServiceError(413, "audio_too_large", "Audio is larger than 25 MB");
|
|
22808
22954
|
const configured = this.deps.settings.get().voice.dictation;
|
|
22809
|
-
const engine = opts.engine ?? (configured.engine === "openai"
|
|
22955
|
+
const engine = opts.engine ?? (configured.engine === "openai" || configured.engine === "local" ? configured.engine : "elevenlabs");
|
|
22810
22956
|
const language = opts.language ?? configured.language ?? void 0;
|
|
22957
|
+
if (engine === "local") {
|
|
22958
|
+
if (!this.deps.local) throw new ServiceError(503, "local_unavailable", "Local Whisper is not available");
|
|
22959
|
+
return this.deps.local.transcribe(audio.bytes, {
|
|
22960
|
+
language,
|
|
22961
|
+
signal: opts.signal
|
|
22962
|
+
});
|
|
22963
|
+
}
|
|
22811
22964
|
const form = new FormData();
|
|
22812
22965
|
form.append("file", new Blob([audio.bytes], { type: audio.mediaType }), audio.fileName);
|
|
22813
22966
|
if (engine === "elevenlabs") {
|
|
@@ -22874,14 +23027,25 @@ const AUDIO_EXTENSIONS = [
|
|
|
22874
23027
|
[/mp4|m4a|aac/, ".m4a"],
|
|
22875
23028
|
[/mpeg|mp3/, ".mp3"]
|
|
22876
23029
|
];
|
|
22877
|
-
|
|
23030
|
+
function modelId(raw) {
|
|
23031
|
+
const parsed = LocalSttModelParams.safeParse({ id: raw });
|
|
23032
|
+
if (!parsed.success) throw badRequest(`Unknown Whisper model: ${raw}`, "unknown_model");
|
|
23033
|
+
return parsed.data.id;
|
|
23034
|
+
}
|
|
23035
|
+
/**
|
|
23036
|
+
* `/api/voice/*` (§11.3): the ElevenLabs voice picker, and local Whisper's status, downloads
|
|
23037
|
+
* and deletes.
|
|
23038
|
+
*/
|
|
22878
23039
|
function voiceRoutes(s) {
|
|
22879
23040
|
return new Hono().get("/voices", async (c) => {
|
|
22880
23041
|
const body = { voices: await s.voice.voices() };
|
|
22881
23042
|
return c.json(body);
|
|
22882
|
-
});
|
|
23043
|
+
}).get("/local", (c) => c.json(s.localStt.status())).post("/local/models/:id/download", (c) => c.json(s.localStt.download(modelId(c.req.param("id"))))).delete("/local/models/:id", async (c) => c.json(await s.localStt.remove(modelId(c.req.param("id")))));
|
|
22883
23044
|
}
|
|
22884
|
-
/**
|
|
23045
|
+
/**
|
|
23046
|
+
* `POST /api/tts` (streamed MP3) and `POST /api/stt` (transcription with a cloud engine or
|
|
23047
|
+
* local Whisper).
|
|
23048
|
+
*/
|
|
22885
23049
|
function speechRoutes(s) {
|
|
22886
23050
|
return new Hono().post("/tts", jsonBody(TtsRequest), async (c) => {
|
|
22887
23051
|
const upstream = await s.voice.speak(c.req.valid("json"), c.req.raw.signal);
|
|
@@ -22948,7 +23112,7 @@ function createApi(ctx) {
|
|
|
22948
23112
|
uptimeSec: (Date.now() - ctx.startedAt) / 1e3
|
|
22949
23113
|
};
|
|
22950
23114
|
return c.json(body);
|
|
22951
|
-
}).get("/snapshot", (c) => c.json(buildSnapshot(ctx))).get("/sessions/:id/snapshot", (c) => c.json(buildSessionSnapshot(s, c.req.param("id")))).route("/projects", projectRoutes(s)).route("/projects", projectGithubRoutes(s)).route("/projects", projectFactoryRoutes(s)).route("/factory", factoryRoutes(s)).route("/prs", prRoutes(s)).route("/github", githubRoutes(s)).route("/sessions", sessionGithubRoutes(s)).route("/sessions", sessionRoutes(s)).route("/sessions", workbenchRoutes(s)).route("/sessions", sessionBrowserRoutes(s)).route("/browser", browserRoutes(s)).route("/images", imageRoutes(s)).route("/agent-config", agentConfigRoutes(s)).route("/voice", voiceRoutes(s)).route("/", speechRoutes(s)).route("/sessions", sessionAgentConfigRoutes(s)).route("/sessions", sessionFileRoutes(s)).route("/utility", utilityRoutes(s)).route("/inbox", inboxRoutes(s)).route("/orchestration", orchestrationRoutes(s)).route("/conflicts", conflictRoutes(s)).route("/automations", automationRoutes(s)).route("/projects", projectAutomationRoutes(s)).route("/usage", usageRoutes(s)).route("/setup", setupRoutes(s)).route("/updates", updateRoutes(s)).route("/artifacts", artifactRoutes(s)).route("/secrets", secretRoutes(s)).route("/", fileRoutes(s)).get("/settings", (c) => c.json(s.settings.get())).patch("/settings", async (c) => {
|
|
23115
|
+
}).get("/snapshot", (c) => c.json(buildSnapshot(ctx))).get("/activity", (c) => c.json(buildActivity(s))).get("/sessions/:id/snapshot", (c) => c.json(buildSessionSnapshot(s, c.req.param("id")))).route("/projects", projectRoutes(s)).route("/projects", projectGithubRoutes(s)).route("/projects", projectFactoryRoutes(s)).route("/factory", factoryRoutes(s)).route("/prs", prRoutes(s)).route("/github", githubRoutes(s)).route("/sessions", sessionGithubRoutes(s)).route("/sessions", sessionRoutes(s)).route("/sessions", workbenchRoutes(s)).route("/sessions", sessionBrowserRoutes(s)).route("/browser", browserRoutes(s)).route("/images", imageRoutes(s)).route("/agent-config", agentConfigRoutes(s)).route("/voice", voiceRoutes(s)).route("/", speechRoutes(s)).route("/sessions", sessionAgentConfigRoutes(s)).route("/sessions", sessionFileRoutes(s)).route("/utility", utilityRoutes(s)).route("/inbox", inboxRoutes(s)).route("/orchestration", orchestrationRoutes(s)).route("/conflicts", conflictRoutes(s)).route("/automations", automationRoutes(s)).route("/projects", projectAutomationRoutes(s)).route("/usage", usageRoutes(s)).route("/setup", setupRoutes(s)).route("/updates", updateRoutes(s)).route("/artifacts", artifactRoutes(s)).route("/secrets", secretRoutes(s)).route("/", fileRoutes(s)).get("/settings", (c) => c.json(s.settings.get())).patch("/settings", async (c) => {
|
|
22952
23116
|
const patch = await c.req.json().catch(() => void 0);
|
|
22953
23117
|
if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw badRequest("Expected a JSON object");
|
|
22954
23118
|
let settings;
|
|
@@ -22989,6 +23153,16 @@ function buildSnapshot(ctx) {
|
|
|
22989
23153
|
runner: ctx.runnerStatus()
|
|
22990
23154
|
};
|
|
22991
23155
|
}
|
|
23156
|
+
/** Running and waiting sessions and open inbox items, for the desktop tray and badge (§17). */
|
|
23157
|
+
function buildActivity(s) {
|
|
23158
|
+
const active = s.db.select({ status: sessions.status }).from(sessions).where(inArray(sessions.status, [
|
|
23159
|
+
"starting",
|
|
23160
|
+
"running",
|
|
23161
|
+
"waiting"
|
|
23162
|
+
])).all();
|
|
23163
|
+
const open = s.db.select({ status: inboxItems.status }).from(inboxItems).where(eq(inboxItems.status, "open")).all();
|
|
23164
|
+
return summarizeActivity(active, open);
|
|
23165
|
+
}
|
|
22992
23166
|
/** A whole transcript in one response (prefer the paginated thread items route). */
|
|
22993
23167
|
function buildSessionSnapshot(s, sessionId) {
|
|
22994
23168
|
const seq = s.store.headSeq();
|
|
@@ -22999,7 +23173,7 @@ function buildSessionSnapshot(s, sessionId) {
|
|
|
22999
23173
|
session: rowToSession(row),
|
|
23000
23174
|
threads: s.db.select().from(threads).where(eq(threads.sessionId, sessionId)).all().map(rowToThread),
|
|
23001
23175
|
turns: s.db.select().from(turns).where(eq(turns.sessionId, sessionId)).all().map(rowToTurn),
|
|
23002
|
-
items: s.db.select().from(items).where(eq(items.sessionId, sessionId)).orderBy(asc(items.threadId), asc(items.position)).all().map((r) =>
|
|
23176
|
+
items: s.db.select().from(items).where(eq(items.sessionId, sessionId)).orderBy(asc(items.threadId), asc(items.position)).all().map((r) => clientItemRecord(rowToItemRecord(r), s.artifacts))
|
|
23003
23177
|
};
|
|
23004
23178
|
}
|
|
23005
23179
|
// ../../node_modules/.pnpm/@hono+node-server@2.1.6_hono@4.13.17/node_modules/@hono/node-server/dist/utils/stream.mjs
|
|
@@ -24229,7 +24403,7 @@ const githubTools = [
|
|
|
24229
24403
|
* text form, prepended to every orchestrator and PM wake so they know the current state
|
|
24230
24404
|
* without spending a tool call.
|
|
24231
24405
|
*/
|
|
24232
|
-
const DAY_MS = 864e5;
|
|
24406
|
+
const DAY_MS$1 = 864e5;
|
|
24233
24407
|
function buildProjectStatus(input) {
|
|
24234
24408
|
const recent = input.outcomes.flatMap((n) => {
|
|
24235
24409
|
if (n.type === "session.finished") return [{
|
|
@@ -24324,18 +24498,29 @@ function projectLines(s, now, indent = "") {
|
|
|
24324
24498
|
}
|
|
24325
24499
|
return lines;
|
|
24326
24500
|
}
|
|
24501
|
+
/**
|
|
24502
|
+
* CodeDen's own tags (`<codeden_status>`, `<codeden_updates>`) inside free text (a purpose, a
|
|
24503
|
+
* quoted PM message) get a look-alike bracket, so only CodeDen's real tags open and close a
|
|
24504
|
+
* block and the UI can fold exactly the injected part away.
|
|
24505
|
+
*/
|
|
24506
|
+
const OWN_TAG = /<(\/?codeden_(?:status|updates))/gi;
|
|
24507
|
+
const neutralizeTags = (line) => line.replace(OWN_TAG, "‹$1");
|
|
24508
|
+
/** A tagged block: the opening tag, the escaped lines, the closing tag. */
|
|
24509
|
+
function block(open, close, lines) {
|
|
24510
|
+
return [
|
|
24511
|
+
open,
|
|
24512
|
+
...lines.map(neutralizeTags),
|
|
24513
|
+
close
|
|
24514
|
+
].join("\n");
|
|
24515
|
+
}
|
|
24327
24516
|
/** The PM's digest for its project. */
|
|
24328
24517
|
function renderProjectDigest(s, now = Date.now()) {
|
|
24329
24518
|
const at = new Date(now).toISOString();
|
|
24330
|
-
return
|
|
24331
|
-
`<codeden_status project=${quote(s.project.name)} at="${at}">`,
|
|
24332
|
-
...projectLines(s, now),
|
|
24333
|
-
"</codeden_status>"
|
|
24334
|
-
].join("\n");
|
|
24519
|
+
return block(`<codeden_status project=${quote(neutralizeTags(s.project.name))} at="${at}">`, "</codeden_status>", projectLines(s, now));
|
|
24335
24520
|
}
|
|
24336
24521
|
/** The orchestrator's digest: every project rolled up. */
|
|
24337
24522
|
function renderOverviewDigest(o, now = Date.now()) {
|
|
24338
|
-
const lines = [
|
|
24523
|
+
const lines = [];
|
|
24339
24524
|
if (o.projects.length === 0) lines.push("No projects yet.");
|
|
24340
24525
|
for (const p of o.projects) {
|
|
24341
24526
|
lines.push(`## ${p.project.name} (${p.project.id})`);
|
|
@@ -24346,17 +24531,20 @@ function renderOverviewDigest(o, now = Date.now()) {
|
|
|
24346
24531
|
lines.push("Informational PM messages:");
|
|
24347
24532
|
for (const m of o.pmMessages) lines.push(`- ${m.projectId ?? "?"}: ${quote(m.text, 300)}`);
|
|
24348
24533
|
}
|
|
24349
|
-
|
|
24350
|
-
return lines.join("\n");
|
|
24534
|
+
return block(`<codeden_status scope="all projects" at="${new Date(now).toISOString()}">`, "</codeden_status>", lines);
|
|
24351
24535
|
}
|
|
24352
|
-
/**
|
|
24536
|
+
/**
|
|
24537
|
+
* "Updates since your last turn": one entry per notice, for the model, inside
|
|
24538
|
+
* `<codeden_updates>`. A notice can quote several paragraphs (a PM message), so the tags,
|
|
24539
|
+
* not a blank line, mark where the injected part ends and the sender's own text begins.
|
|
24540
|
+
*/
|
|
24353
24541
|
function renderNotices(list, projectName) {
|
|
24354
24542
|
const lines = ["Updates since your last turn:"];
|
|
24355
24543
|
for (const n of list) {
|
|
24356
24544
|
const where = n.projectId && projectName ? ` [${projectName(n.projectId)}]` : "";
|
|
24357
24545
|
lines.push(`- (${n.type})${where} ${n.title}${noticeDetail(n)}`);
|
|
24358
24546
|
}
|
|
24359
|
-
return
|
|
24547
|
+
return block("<codeden_updates>", "</codeden_updates>", lines);
|
|
24360
24548
|
}
|
|
24361
24549
|
function noticeDetail(n) {
|
|
24362
24550
|
switch (n.type) {
|
|
@@ -24377,7 +24565,7 @@ function noticeDetail(n) {
|
|
|
24377
24565
|
}
|
|
24378
24566
|
/** The day window for recent outcomes, from `now`. */
|
|
24379
24567
|
function dayAgo(now = Date.now()) {
|
|
24380
|
-
return (/* @__PURE__ */ new Date(now - DAY_MS)).toISOString();
|
|
24568
|
+
return (/* @__PURE__ */ new Date(now - DAY_MS$1)).toISOString();
|
|
24381
24569
|
}
|
|
24382
24570
|
// ../server/src/mcp/tools/shared/helpers.ts
|
|
24383
24571
|
/** A project by id, as a tool error when it does not exist or is archived. */
|
|
@@ -24950,7 +25138,7 @@ const messageSession = defineTool({
|
|
|
24950
25138
|
attachments: []
|
|
24951
25139
|
},
|
|
24952
25140
|
whenBusy: whenBusy ?? "queue"
|
|
24953
|
-
})).status}.`;
|
|
25141
|
+
}, "pm")).status}.`;
|
|
24954
25142
|
}
|
|
24955
25143
|
});
|
|
24956
25144
|
/** `stop_session` (§8.4, always loaded): stop one of the project's agents. */
|
|
@@ -25139,9 +25327,9 @@ var ArtifactService = class {
|
|
|
25139
25327
|
return this.dir;
|
|
25140
25328
|
}
|
|
25141
25329
|
get(id) {
|
|
25142
|
-
const
|
|
25143
|
-
if (!
|
|
25144
|
-
return
|
|
25330
|
+
const artifact = this.find(id);
|
|
25331
|
+
if (!artifact) throw notFound(`No artifact ${id}`, "artifact_not_found");
|
|
25332
|
+
return artifact;
|
|
25145
25333
|
}
|
|
25146
25334
|
list(filter = {}) {
|
|
25147
25335
|
const conditions = [];
|
|
@@ -25154,10 +25342,13 @@ var ArtifactService = class {
|
|
|
25154
25342
|
return resolveInside(this.dir, artifact.path);
|
|
25155
25343
|
}
|
|
25156
25344
|
/**
|
|
25157
|
-
* Store pasted (base64) images as artifacts and point the attachment at the
|
|
25158
|
-
* payloads never land in the event log
|
|
25345
|
+
* Store pasted (base64) images as artifacts and point the attachment at the artifact, so
|
|
25346
|
+
* large payloads never land in the event log and stored input (queued messages, pending
|
|
25347
|
+
* starts) never carries a filesystem path. `resolveAttachments` turns the reference into a
|
|
25348
|
+
* file path when the input is handed to the runner.
|
|
25159
25349
|
*/
|
|
25160
25350
|
externalizeAttachments(input, owner) {
|
|
25351
|
+
if (!input.attachments.some((a) => a.type === "image" && a.source.type === "base64")) return input;
|
|
25161
25352
|
const attachments = input.attachments.map((a) => {
|
|
25162
25353
|
if (a.type !== "image" || a.source.type !== "base64") return a;
|
|
25163
25354
|
const bytes = Buffer.from(a.source.data, "base64");
|
|
@@ -25169,6 +25360,29 @@ var ArtifactService = class {
|
|
|
25169
25360
|
kind: "image",
|
|
25170
25361
|
...owner
|
|
25171
25362
|
});
|
|
25363
|
+
return {
|
|
25364
|
+
...a,
|
|
25365
|
+
source: {
|
|
25366
|
+
type: "artifact",
|
|
25367
|
+
artifactId: artifact.id
|
|
25368
|
+
}
|
|
25369
|
+
};
|
|
25370
|
+
});
|
|
25371
|
+
return {
|
|
25372
|
+
...input,
|
|
25373
|
+
attachments
|
|
25374
|
+
};
|
|
25375
|
+
}
|
|
25376
|
+
/**
|
|
25377
|
+
* Point artifact image attachments at their file, for the runner: providers read images by
|
|
25378
|
+
* path. Only call this on input that goes to the runner, never on what the browser sees.
|
|
25379
|
+
*/
|
|
25380
|
+
resolveAttachments(input) {
|
|
25381
|
+
if (!input.attachments.some((a) => a.type === "image" && a.source.type === "artifact")) return input;
|
|
25382
|
+
const attachments = input.attachments.map((a) => {
|
|
25383
|
+
if (a.type !== "image" || a.source.type !== "artifact") return a;
|
|
25384
|
+
const artifact = this.find(a.source.artifactId);
|
|
25385
|
+
if (!artifact) throw badRequest(`No artifact ${a.source.artifactId}`, "unknown_artifact");
|
|
25172
25386
|
return {
|
|
25173
25387
|
...a,
|
|
25174
25388
|
source: {
|
|
@@ -25182,7 +25396,27 @@ var ArtifactService = class {
|
|
|
25182
25396
|
attachments
|
|
25183
25397
|
};
|
|
25184
25398
|
}
|
|
25399
|
+
/**
|
|
25400
|
+
* The artifact whose own file is `path`, or `undefined`. Providers echo the input they were
|
|
25401
|
+
* given, so a user message's image comes back as the resolved path (and older transcripts
|
|
25402
|
+
* stored it that way); this maps it back to the artifact the browser can load. Only an exact
|
|
25403
|
+
* match with an artifact's file inside the artifacts folder counts; nothing is read from disk.
|
|
25404
|
+
*/
|
|
25405
|
+
artifactForPath(path) {
|
|
25406
|
+
const id = (path.split(/[\\/]/).pop() ?? "").replace(/\.[^.]*$/, "");
|
|
25407
|
+
if (!id) return void 0;
|
|
25408
|
+
const artifact = this.find(id);
|
|
25409
|
+
if (!artifact) return void 0;
|
|
25410
|
+
const expected = resolve(this.dir, artifact.path);
|
|
25411
|
+
return comparable(resolve(path)) === comparable(expected) ? artifact : void 0;
|
|
25412
|
+
}
|
|
25413
|
+
find(id) {
|
|
25414
|
+
const row = this.db.select().from(artifacts).where(eq(artifacts.id, id)).get();
|
|
25415
|
+
return row ? rowToArtifact(row) : void 0;
|
|
25416
|
+
}
|
|
25185
25417
|
};
|
|
25418
|
+
const caseInsensitive = process.platform === "win32" || process.platform === "darwin";
|
|
25419
|
+
const comparable = (p) => caseInsensitive ? p.toLowerCase() : p;
|
|
25186
25420
|
// ../server/src/mcp/tools/share-to-chat.ts
|
|
25187
25421
|
const MEDIA_TYPES$1 = {
|
|
25188
25422
|
".png": "image/png",
|
|
@@ -42247,7 +42481,7 @@ var AutomationExecutor = class {
|
|
|
42247
42481
|
const res = await this.deps.orchestration().sendToRole("pm", project.id, {
|
|
42248
42482
|
text: `Automation "${automation.name}" (${TRIGGER_TEXT[triggeredBy]}):\n\n${text}`,
|
|
42249
42483
|
attachments: []
|
|
42250
|
-
});
|
|
42484
|
+
}, "queue", "automation");
|
|
42251
42485
|
return this.finished(run, res.sessionId);
|
|
42252
42486
|
}
|
|
42253
42487
|
case "triage": {
|
|
@@ -42736,7 +42970,7 @@ var TriageService = class {
|
|
|
42736
42970
|
return (await this.deps.orchestration().sendToRole("pm", project.id, {
|
|
42737
42971
|
text,
|
|
42738
42972
|
attachments: []
|
|
42739
|
-
})).sessionId;
|
|
42973
|
+
}, "queue", "automation")).sessionId;
|
|
42740
42974
|
}
|
|
42741
42975
|
/** The PM's `report_triage`. */
|
|
42742
42976
|
async report(project, report) {
|
|
@@ -43909,7 +44143,7 @@ function triageInstructions(items) {
|
|
|
43909
44143
|
].join("\n");
|
|
43910
44144
|
}
|
|
43911
44145
|
// ../server/src/services/factory/progress.ts
|
|
43912
|
-
const BUSY$
|
|
44146
|
+
const BUSY$2 = /* @__PURE__ */ new Set([
|
|
43913
44147
|
"created",
|
|
43914
44148
|
"queued",
|
|
43915
44149
|
"starting",
|
|
@@ -43923,7 +44157,7 @@ const BUSY$1 = /* @__PURE__ */ new Set([
|
|
|
43923
44157
|
async function advanceBuilding(ctx, project, loop, item) {
|
|
43924
44158
|
const s = item.builderSessionId ? ctx.repo.find(item.builderSessionId)?.session : void 0;
|
|
43925
44159
|
if (!s) return failItem(ctx, loop, item, "The builder session is gone");
|
|
43926
|
-
if (BUSY$
|
|
44160
|
+
if (BUSY$2.has(s.status)) return loop;
|
|
43927
44161
|
const fixAt = item.attempts > 0 ? item.nudges[`fix_${item.attempts}`] : void 0;
|
|
43928
44162
|
if (fixAt && Date.parse(s.updatedAt) <= Date.parse(fixAt) && s.status === "idle") return loop;
|
|
43929
44163
|
const pr = s.pr ?? ctx.github.prForSession(project.id, s.id, s.branch);
|
|
@@ -45151,7 +45385,7 @@ var FactorySessions = class {
|
|
|
45151
45385
|
attachments: []
|
|
45152
45386
|
},
|
|
45153
45387
|
whenBusy: "queue"
|
|
45154
|
-
});
|
|
45388
|
+
}, "codeden");
|
|
45155
45389
|
}
|
|
45156
45390
|
/** Stop every live factory session of a project (kill switch). Returns the stopped ids. */
|
|
45157
45391
|
async stopAll(projectId) {
|
|
@@ -46441,7 +46675,7 @@ function isInside$1(root, path) {
|
|
|
46441
46675
|
/** Read evidence sources, confined to the session's worktree, the temp folder and its artifacts. */
|
|
46442
46676
|
function readEvidence(session, artifacts, sources) {
|
|
46443
46677
|
if (sources.length > 10) throw badRequest(`Attach at most 10 files at once`);
|
|
46444
|
-
const cwd = session
|
|
46678
|
+
const cwd = sessionWorkdir(session);
|
|
46445
46679
|
const roots = [cwd, tmpdir()].filter(existsSync).map((p) => realpathSync(p));
|
|
46446
46680
|
return sources.map((source) => {
|
|
46447
46681
|
let file;
|
|
@@ -49709,7 +49943,7 @@ function pullRequestBody(text, issue, session) {
|
|
|
49709
49943
|
* §9). The PR is projected right away (`pr.opened` / `pr.updated`) and linked to the session.
|
|
49710
49944
|
*/
|
|
49711
49945
|
async function createPullRequest(deps, session, repo, input) {
|
|
49712
|
-
const cwd = session
|
|
49946
|
+
const cwd = sessionWorkdir(session);
|
|
49713
49947
|
const branch = session.branch;
|
|
49714
49948
|
if (!branch) throw conflict("This session has no branch to open a pull request from", "no_branch");
|
|
49715
49949
|
const base = input.base ?? session.baseBranch ?? repo.defaultBranch;
|
|
@@ -50659,7 +50893,7 @@ var ImageService = class {
|
|
|
50659
50893
|
const eff = this.effective(session.projectId);
|
|
50660
50894
|
const pick = await this.pick(eff.engine);
|
|
50661
50895
|
if (!pick.engine) throw new ServiceError(409, "no_image_engine", pick.reason);
|
|
50662
|
-
const cwd = realpathSync(session
|
|
50896
|
+
const cwd = realpathSync(sessionWorkdir(session));
|
|
50663
50897
|
const root = req.role === "agent" ? cwd : this.ensureDir(join(this.deps.generatedDir, session.id));
|
|
50664
50898
|
let target;
|
|
50665
50899
|
try {
|
|
@@ -51822,13 +52056,16 @@ var OrchestrationService = class {
|
|
|
51822
52056
|
return input;
|
|
51823
52057
|
}
|
|
51824
52058
|
}
|
|
51825
|
-
/**
|
|
51826
|
-
|
|
52059
|
+
/**
|
|
52060
|
+
* Send a message to the orchestrator or a project's PM, creating the session if needed.
|
|
52061
|
+
* `from` labels the message in the queue tray when it has to wait for a running turn.
|
|
52062
|
+
*/
|
|
52063
|
+
async sendToRole(role, projectId, input, whenBusy = "queue", from = "user") {
|
|
51827
52064
|
const session = await this.roles.ensure(role, projectId);
|
|
51828
52065
|
const res = await this.deps.sessions.send(session.id, {
|
|
51829
52066
|
input,
|
|
51830
52067
|
whenBusy
|
|
51831
|
-
});
|
|
52068
|
+
}, from);
|
|
51832
52069
|
return {
|
|
51833
52070
|
sessionId: session.id,
|
|
51834
52071
|
status: res.status
|
|
@@ -51840,7 +52077,7 @@ var OrchestrationService = class {
|
|
|
51840
52077
|
return this.sendToRole("pm", projectId, {
|
|
51841
52078
|
text: `Message from the orchestrator:\n${text}`,
|
|
51842
52079
|
attachments: []
|
|
51843
|
-
});
|
|
52080
|
+
}, "queue", "orchestrator");
|
|
51844
52081
|
}
|
|
51845
52082
|
/** `ask_pm`: ask a PM and wait (up to `timeoutMs`) for the answer. */
|
|
51846
52083
|
async askPm(projectId, question, opts) {
|
|
@@ -51865,7 +52102,7 @@ var OrchestrationService = class {
|
|
|
51865
52102
|
attachments: []
|
|
51866
52103
|
},
|
|
51867
52104
|
whenBusy: "queue"
|
|
51868
|
-
});
|
|
52105
|
+
}, "orchestrator");
|
|
51869
52106
|
return {
|
|
51870
52107
|
...await answer,
|
|
51871
52108
|
pmSessionId: pm.id
|
|
@@ -51975,7 +52212,7 @@ var OrchestrationService = class {
|
|
|
51975
52212
|
attachments: []
|
|
51976
52213
|
},
|
|
51977
52214
|
whenBusy: "reject"
|
|
51978
|
-
});
|
|
52215
|
+
}, "codeden");
|
|
51979
52216
|
return "sent";
|
|
51980
52217
|
}
|
|
51981
52218
|
projectName(id) {
|
|
@@ -52122,8 +52359,10 @@ var ProjectService = class {
|
|
|
52122
52359
|
constructor(deps) {
|
|
52123
52360
|
this.deps = deps;
|
|
52124
52361
|
}
|
|
52362
|
+
/** Projects in the canonical order (§13): pinned first, then manual order, then name. */
|
|
52125
52363
|
list(opts = {}) {
|
|
52126
|
-
|
|
52364
|
+
const all = this.deps.db.select().from(projects).all().map(rowToProject);
|
|
52365
|
+
return sortProjects(all.filter((p) => opts.includeArchived || p.archivedAt === null));
|
|
52127
52366
|
}
|
|
52128
52367
|
get(id) {
|
|
52129
52368
|
const row = this.deps.db.select().from(projects).where(eq(projects.id, id)).get();
|
|
@@ -52181,6 +52420,7 @@ var ProjectService = class {
|
|
|
52181
52420
|
if (patch.autonomy !== void 0) next.autonomy = patch.autonomy;
|
|
52182
52421
|
if (patch.timezone !== void 0) next.timezone = patch.timezone;
|
|
52183
52422
|
if (patch.defaultBranch !== void 0) next.defaultBranch = patch.defaultBranch;
|
|
52423
|
+
if (patch.pinned !== void 0) next.pinned = patch.pinned;
|
|
52184
52424
|
this.deps.store.append([{
|
|
52185
52425
|
type: "project.updated",
|
|
52186
52426
|
sessionId: null,
|
|
@@ -52188,6 +52428,37 @@ var ProjectService = class {
|
|
|
52188
52428
|
}]);
|
|
52189
52429
|
return next;
|
|
52190
52430
|
}
|
|
52431
|
+
/**
|
|
52432
|
+
* Set the manual order of the project list (§13). `projectIds` must name every non-archived
|
|
52433
|
+
* project exactly once. Pinned projects still list first; within each group the order given
|
|
52434
|
+
* here applies.
|
|
52435
|
+
*/
|
|
52436
|
+
reorder(projectIds) {
|
|
52437
|
+
const active = this.list();
|
|
52438
|
+
const known = new Set(active.map((p) => p.id));
|
|
52439
|
+
const seen = /* @__PURE__ */ new Set();
|
|
52440
|
+
const duplicates = /* @__PURE__ */ new Set();
|
|
52441
|
+
for (const id of projectIds) {
|
|
52442
|
+
if (seen.has(id)) duplicates.add(id);
|
|
52443
|
+
seen.add(id);
|
|
52444
|
+
}
|
|
52445
|
+
const unknown = [...seen].filter((id) => !known.has(id));
|
|
52446
|
+
const missing = active.filter((p) => !seen.has(p.id)).map((p) => p.id);
|
|
52447
|
+
if (duplicates.size > 0 || unknown.length > 0 || missing.length > 0) {
|
|
52448
|
+
const problems = [
|
|
52449
|
+
duplicates.size > 0 && `listed more than once: ${[...duplicates].join(", ")}`,
|
|
52450
|
+
unknown.length > 0 && `not active projects: ${unknown.join(", ")}`,
|
|
52451
|
+
missing.length > 0 && `missing: ${missing.join(", ")}`
|
|
52452
|
+
].filter(Boolean);
|
|
52453
|
+
throw badRequest(`The order must list every active project exactly once (${problems.join("; ")})`, "invalid_order");
|
|
52454
|
+
}
|
|
52455
|
+
this.deps.store.append([{
|
|
52456
|
+
type: "projects.reordered",
|
|
52457
|
+
sessionId: null,
|
|
52458
|
+
payload: { projectIds }
|
|
52459
|
+
}]);
|
|
52460
|
+
return this.list();
|
|
52461
|
+
}
|
|
52191
52462
|
archive(id) {
|
|
52192
52463
|
this.get(id);
|
|
52193
52464
|
this.deps.store.append([{
|
|
@@ -52211,6 +52482,8 @@ var ProjectService = class {
|
|
|
52211
52482
|
factoryLimits: FactoryLimits.parse({}),
|
|
52212
52483
|
killSwitch: false,
|
|
52213
52484
|
timezone: null,
|
|
52485
|
+
pinned: false,
|
|
52486
|
+
sortOrder: this.nextSortOrder(),
|
|
52214
52487
|
createdAt: now,
|
|
52215
52488
|
updatedAt: now,
|
|
52216
52489
|
archivedAt: null
|
|
@@ -52224,6 +52497,11 @@ var ProjectService = class {
|
|
|
52224
52497
|
kind: "local"
|
|
52225
52498
|
};
|
|
52226
52499
|
}
|
|
52500
|
+
/** New projects go to the end of the unpinned group, after every slot (archived included). */
|
|
52501
|
+
nextSortOrder() {
|
|
52502
|
+
const row = this.deps.db.select({ last: max(projects.sortOrder) }).from(projects).get();
|
|
52503
|
+
return typeof row?.last === "number" ? row.last + 1 : 0;
|
|
52504
|
+
}
|
|
52227
52505
|
uniqueSlug(base) {
|
|
52228
52506
|
const taken = new Set(this.list({ includeArchived: true }).map((p) => p.slug));
|
|
52229
52507
|
if (!taken.has(base)) return base;
|
|
@@ -52265,7 +52543,8 @@ function redactUrls(text) {
|
|
|
52265
52543
|
const SHARED = `## How CodeDen keeps you informed
|
|
52266
52544
|
|
|
52267
52545
|
- Every message you receive starts with a \`<codeden_status>\` block: a fresh digest of running and queued work (work key, purpose, status, progress, age), the next scheduled automation runs, open inbox items and recent finishes and failures. Trust it; you do not need a tool call to learn the current state.
|
|
52268
|
-
-
|
|
52546
|
+
- After it may come a \`<codeden_updates>\` block, "Updates since your last turn": typed notices (session finished, needs input, failed, PR opened, checks failed, automation skipped, conflict, PM messages). React to the ones that need action and ignore routine ones.
|
|
52547
|
+
- Everything after the last of these blocks is what the sender actually wrote.
|
|
52269
52548
|
- You have no write access to project code. You act only through CodeDen tools on the \`codeden\` MCP server; reading files and read-only git commands are fine.
|
|
52270
52549
|
- Never start work that duplicates running or queued work. Check the digest first; starting tools return \`conflict\` with the existing work instead of starting. Then choose one resolution and say why: \`wait\` (queue behind it), \`skip\`, \`join\` (send your request to the existing session as a follow-up) or \`force\` (only with a concrete reason, which is recorded).`;
|
|
52271
52550
|
const ORCHESTRATOR_PROMPT = `# You are the CodeDen orchestrator
|
|
@@ -52395,11 +52674,12 @@ var SessionLauncher = class {
|
|
|
52395
52674
|
*/
|
|
52396
52675
|
async resume(session, input) {
|
|
52397
52676
|
await this.requireRunner();
|
|
52677
|
+
const current = await this.restoreWorktree(session);
|
|
52398
52678
|
const result = await this.deps.runner.request({
|
|
52399
52679
|
name: "session.start",
|
|
52400
|
-
sessionId:
|
|
52401
|
-
providerId:
|
|
52402
|
-
options: this.sessionOptions(
|
|
52680
|
+
sessionId: current.id,
|
|
52681
|
+
providerId: current.provider,
|
|
52682
|
+
options: this.sessionOptions(current),
|
|
52403
52683
|
input
|
|
52404
52684
|
}, START_TIMEOUT_MS);
|
|
52405
52685
|
this.deps.repo.update(session.id, (s) => ({
|
|
@@ -52408,6 +52688,32 @@ var SessionLauncher = class {
|
|
|
52408
52688
|
runnerId: this.deps.runner.runnerId()
|
|
52409
52689
|
}));
|
|
52410
52690
|
}
|
|
52691
|
+
/**
|
|
52692
|
+
* Recreate a worktree that cleanup removed (§9), at the same path from the session's branch,
|
|
52693
|
+
* running the project's setup again. A no-op for sessions that still have their worktree.
|
|
52694
|
+
*/
|
|
52695
|
+
async restoreWorktree(session) {
|
|
52696
|
+
if (!session.worktreeRemovedAt || !session.worktreePath || !session.projectId) return session;
|
|
52697
|
+
const project = this.deps.projects.get(session.projectId);
|
|
52698
|
+
const result = await this.deps.runner.request({
|
|
52699
|
+
name: "workspace.prepare",
|
|
52700
|
+
sessionId: session.id,
|
|
52701
|
+
repoPath: project.path,
|
|
52702
|
+
strategy: "worktree",
|
|
52703
|
+
path: session.worktreePath,
|
|
52704
|
+
branch: session.branch ?? void 0,
|
|
52705
|
+
baseRef: session.baseBranch ?? project.defaultBranch ?? void 0,
|
|
52706
|
+
setupCommands: []
|
|
52707
|
+
}, PREPARE_TIMEOUT_MS);
|
|
52708
|
+
this.deps.log?.(`session ${session.id}: recreated worktree ${result.cwd}`);
|
|
52709
|
+
return this.deps.repo.update(session.id, (s) => ({
|
|
52710
|
+
...s,
|
|
52711
|
+
cwd: result.cwd,
|
|
52712
|
+
worktreePath: result.cwd,
|
|
52713
|
+
branch: result.branch ?? s.branch,
|
|
52714
|
+
worktreeRemovedAt: null
|
|
52715
|
+
}));
|
|
52716
|
+
}
|
|
52411
52717
|
/** Mark a session failed with an inbox item explaining why. */
|
|
52412
52718
|
fail(sessionId, key, error) {
|
|
52413
52719
|
const message = errorMessage(error);
|
|
@@ -52655,24 +52961,39 @@ var SessionLifecycle = class {
|
|
|
52655
52961
|
}
|
|
52656
52962
|
};
|
|
52657
52963
|
// ../server/src/services/sessions/outbox.ts
|
|
52964
|
+
const toMessage = (r) => ({
|
|
52965
|
+
id: r.id,
|
|
52966
|
+
sessionId: r.sessionId,
|
|
52967
|
+
input: r.input,
|
|
52968
|
+
from: r.sender ?? "user",
|
|
52969
|
+
createdAt: r.createdAt
|
|
52970
|
+
});
|
|
52658
52971
|
/**
|
|
52659
52972
|
* Messages sent while a turn runs, delivered one at a time when the session goes idle.
|
|
52660
|
-
* Stored through `message.queued` / `message.dequeued` events, so the queue
|
|
52661
|
-
* restarts and can be rebuilt from the log.
|
|
52973
|
+
* Stored through `message.queued` / `message.dequeued` / `message.edited` events, so the queue
|
|
52974
|
+
* survives server restarts and can be rebuilt from the log.
|
|
52975
|
+
*
|
|
52976
|
+
* Every method is synchronous. Delivery takes a message out with `remove(…, "sent")` before
|
|
52977
|
+
* it awaits anything, so an edit or delete either lands before delivery (and is what gets
|
|
52978
|
+
* sent) or finds the message gone; a half-edited message can never be sent.
|
|
52662
52979
|
*/
|
|
52663
52980
|
var Outbox = class {
|
|
52664
52981
|
db;
|
|
52665
52982
|
store;
|
|
52983
|
+
/** Last `createdAt` handed out (ms), so timestamps strictly increase. */
|
|
52984
|
+
lastAt = 0;
|
|
52666
52985
|
constructor(db, store) {
|
|
52667
52986
|
this.db = db;
|
|
52668
52987
|
this.store = store;
|
|
52669
52988
|
}
|
|
52670
|
-
push(sessionId, input) {
|
|
52989
|
+
push(sessionId, input, from = "user") {
|
|
52990
|
+
this.lastAt = Math.max(Date.now(), this.lastAt + 1);
|
|
52671
52991
|
const message = {
|
|
52672
52992
|
id: randomUUID(),
|
|
52673
52993
|
sessionId,
|
|
52674
52994
|
input,
|
|
52675
|
-
|
|
52995
|
+
from,
|
|
52996
|
+
createdAt: new Date(this.lastAt).toISOString()
|
|
52676
52997
|
};
|
|
52677
52998
|
this.store.append([{
|
|
52678
52999
|
type: "message.queued",
|
|
@@ -52681,9 +53002,17 @@ var Outbox = class {
|
|
|
52681
53002
|
}]);
|
|
52682
53003
|
return message;
|
|
52683
53004
|
}
|
|
53005
|
+
/** A session's queued messages, next to be delivered first. */
|
|
53006
|
+
list(sessionId) {
|
|
53007
|
+
return this.db.select().from(queuedMessages).where(eq(queuedMessages.sessionId, sessionId)).orderBy(asc(queuedMessages.createdAt), asc(sql`rowid`)).all().map(toMessage);
|
|
53008
|
+
}
|
|
52684
53009
|
/** Oldest queued message of a session, if any. */
|
|
52685
53010
|
peek(sessionId) {
|
|
52686
|
-
return this.
|
|
53011
|
+
return this.list(sessionId)[0];
|
|
53012
|
+
}
|
|
53013
|
+
get(sessionId, messageId) {
|
|
53014
|
+
const row = this.db.select().from(queuedMessages).where(and(eq(queuedMessages.sessionId, sessionId), eq(queuedMessages.id, messageId))).get();
|
|
53015
|
+
return row ? toMessage(row) : void 0;
|
|
52687
53016
|
}
|
|
52688
53017
|
count(sessionId) {
|
|
52689
53018
|
return this.db.select({ id: queuedMessages.id }).from(queuedMessages).where(eq(queuedMessages.sessionId, sessionId)).all().length;
|
|
@@ -52692,6 +53021,26 @@ var Outbox = class {
|
|
|
52692
53021
|
sessionsWithMessages() {
|
|
52693
53022
|
return this.db.selectDistinct({ id: queuedMessages.sessionId }).from(queuedMessages).all().map((r) => r.id);
|
|
52694
53023
|
}
|
|
53024
|
+
/** Replace a queued message's text; its attachments stay. */
|
|
53025
|
+
edit(message, text) {
|
|
53026
|
+
this.store.append([{
|
|
53027
|
+
type: "message.edited",
|
|
53028
|
+
sessionId: message.sessionId,
|
|
53029
|
+
payload: {
|
|
53030
|
+
sessionId: message.sessionId,
|
|
53031
|
+
messageId: message.id,
|
|
53032
|
+
text,
|
|
53033
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
53034
|
+
}
|
|
53035
|
+
}]);
|
|
53036
|
+
return {
|
|
53037
|
+
...message,
|
|
53038
|
+
input: {
|
|
53039
|
+
...message.input,
|
|
53040
|
+
text
|
|
53041
|
+
}
|
|
53042
|
+
};
|
|
53043
|
+
}
|
|
52695
53044
|
remove(message, reason) {
|
|
52696
53045
|
this.store.append([{
|
|
52697
53046
|
type: "message.dequeued",
|
|
@@ -52703,11 +53052,19 @@ var Outbox = class {
|
|
|
52703
53052
|
}
|
|
52704
53053
|
}]);
|
|
52705
53054
|
}
|
|
52706
|
-
/**
|
|
53055
|
+
/** Put a message taken out for delivery back in its place (the delivery failed). */
|
|
53056
|
+
restore(message) {
|
|
53057
|
+
this.store.append([{
|
|
53058
|
+
type: "message.queued",
|
|
53059
|
+
sessionId: message.sessionId,
|
|
53060
|
+
payload: { message }
|
|
53061
|
+
}]);
|
|
53062
|
+
}
|
|
53063
|
+
/** Drop every queued message of a session (interrupt, stop). Returns what was dropped. */
|
|
52707
53064
|
clear(sessionId) {
|
|
52708
|
-
const
|
|
52709
|
-
if (
|
|
52710
|
-
this.store.append(
|
|
53065
|
+
const dropped = this.list(sessionId);
|
|
53066
|
+
if (dropped.length === 0) return [];
|
|
53067
|
+
this.store.append(dropped.map((m) => ({
|
|
52711
53068
|
type: "message.dequeued",
|
|
52712
53069
|
sessionId,
|
|
52713
53070
|
payload: {
|
|
@@ -52716,6 +53073,11 @@ var Outbox = class {
|
|
|
52716
53073
|
reason: "dropped"
|
|
52717
53074
|
}
|
|
52718
53075
|
})));
|
|
53076
|
+
return dropped;
|
|
53077
|
+
}
|
|
53078
|
+
/** Why a message is no longer queued, from the log; `undefined` if it never was. */
|
|
53079
|
+
dequeuedReason(sessionId, messageId) {
|
|
53080
|
+
return (this.db.select({ payload: events.payload }).from(events).where(and(eq(events.sessionId, sessionId), eq(events.type, "message.dequeued"), sql`json_extract(${events.payload}, '$.messageId') = ${messageId}`)).orderBy(desc(events.seq)).limit(1).get()?.payload)?.reason;
|
|
52719
53081
|
}
|
|
52720
53082
|
};
|
|
52721
53083
|
// ../server/src/services/sessions/questions.ts
|
|
@@ -52815,6 +53177,71 @@ var QuestionBroker = class {
|
|
|
52815
53177
|
function formatAnswers(item, answers) {
|
|
52816
53178
|
return item.questions.map((q) => `Q: ${q.question}\nA: ${(answers[q.id] ?? []).join(", ") || "(no answer)"}`).join("\n\n");
|
|
52817
53179
|
}
|
|
53180
|
+
// ../server/src/services/sessions/queue.ts
|
|
53181
|
+
const BUSY$1 = /* @__PURE__ */ new Set(["running", "waiting"]);
|
|
53182
|
+
/**
|
|
53183
|
+
* The queued-message tray (`/api/sessions/:id/queue`): list, edit, delete and send-now.
|
|
53184
|
+
* Messages are taken from the `Outbox` synchronously, so a request racing delivery either
|
|
53185
|
+
* wins or answers 409 `already_sent`; it never acts on a message that is being delivered.
|
|
53186
|
+
*/
|
|
53187
|
+
var QueueService = class {
|
|
53188
|
+
deps;
|
|
53189
|
+
constructor(deps) {
|
|
53190
|
+
this.deps = deps;
|
|
53191
|
+
}
|
|
53192
|
+
/** A session's queued messages as the browser sees them, next first. */
|
|
53193
|
+
list(sessionId) {
|
|
53194
|
+
this.deps.repo.get(sessionId);
|
|
53195
|
+
return this.deps.outbox.list(sessionId).map((m) => this.present(m));
|
|
53196
|
+
}
|
|
53197
|
+
edit(sessionId, messageId, text) {
|
|
53198
|
+
const message = this.require(sessionId, messageId);
|
|
53199
|
+
if (!text.trim() && message.input.attachments.length === 0) throw badRequest("A queued message needs text or an attachment", "empty_message");
|
|
53200
|
+
return this.present(this.deps.outbox.edit(message, text));
|
|
53201
|
+
}
|
|
53202
|
+
delete(sessionId, messageId) {
|
|
53203
|
+
this.deps.outbox.remove(this.require(sessionId, messageId), "deleted");
|
|
53204
|
+
}
|
|
53205
|
+
/**
|
|
53206
|
+
* Steer the running turn with a queued message, ahead of its place. Only a running turn
|
|
53207
|
+
* can take it (409 `no_turn` otherwise; the queue sends it next, in order), and only on a
|
|
53208
|
+
* provider that can steer. If the steer fails the message goes back in its place.
|
|
53209
|
+
*/
|
|
53210
|
+
async sendNow(sessionId, messageId) {
|
|
53211
|
+
const message = this.require(sessionId, messageId);
|
|
53212
|
+
const session = this.deps.repo.get(sessionId);
|
|
53213
|
+
if (!BUSY$1.has(session.status)) {
|
|
53214
|
+
this.deps.sessions.flushOutbox(sessionId).catch(() => void 0);
|
|
53215
|
+
throw conflict("No turn is running: the queue sends this message next", "no_turn");
|
|
53216
|
+
}
|
|
53217
|
+
if (!this.deps.providers().find((p) => p.id === session.provider)?.capabilities.steer) throw conflict("This provider cannot steer a running turn", "unsupported");
|
|
53218
|
+
this.deps.outbox.remove(message, "sent");
|
|
53219
|
+
try {
|
|
53220
|
+
await this.deps.sessions.steer(sessionId, message.input);
|
|
53221
|
+
return { status: "steered" };
|
|
53222
|
+
} catch (error) {
|
|
53223
|
+
this.deps.outbox.restore(message);
|
|
53224
|
+
this.deps.sessions.flushOutbox(sessionId).catch(() => void 0);
|
|
53225
|
+
throw error;
|
|
53226
|
+
}
|
|
53227
|
+
}
|
|
53228
|
+
/** The queued message, or 409 `already_sent` / 404 when it is no longer queued. */
|
|
53229
|
+
require(sessionId, messageId) {
|
|
53230
|
+
this.deps.repo.get(sessionId);
|
|
53231
|
+
const message = this.deps.outbox.get(sessionId, messageId);
|
|
53232
|
+
if (message) return message;
|
|
53233
|
+
const reason = this.deps.outbox.dequeuedReason(sessionId, messageId);
|
|
53234
|
+
if (reason === "sent") throw conflict("This message was already sent", "already_sent");
|
|
53235
|
+
if (reason) throw conflict("This message is no longer queued", "no_longer_queued");
|
|
53236
|
+
throw notFound("No such queued message", "queued_message_not_found");
|
|
53237
|
+
}
|
|
53238
|
+
present(message) {
|
|
53239
|
+
return {
|
|
53240
|
+
...message,
|
|
53241
|
+
input: clientInput(message.input, this.deps.artifacts)
|
|
53242
|
+
};
|
|
53243
|
+
}
|
|
53244
|
+
};
|
|
52818
53245
|
// ../server/src/services/sessions/recovery.ts
|
|
52819
53246
|
/** The message sent to an agent whose turn was cut off by a restart (§4.2). */
|
|
52820
53247
|
const RESTART_CONTINUE_MESSAGE = "You were interrupted by a CodeDen restart; continue where you left off.";
|
|
@@ -53023,6 +53450,8 @@ function projectDefaultProvider(project) {
|
|
|
53023
53450
|
return project.defaultProvider ?? readProjectConfig(project.path)?.defaultProvider ?? null;
|
|
53024
53451
|
}
|
|
53025
53452
|
// ../server/src/services/sessions/reconfigure.ts
|
|
53453
|
+
/** How long `PATCH` waits for the runner to take an autonomy change. */
|
|
53454
|
+
const AUTONOMY_TIMEOUT_MS = 1e4;
|
|
53026
53455
|
function fallbackMeta$1() {
|
|
53027
53456
|
return {
|
|
53028
53457
|
workspaceMode: "worktree",
|
|
@@ -53031,12 +53460,32 @@ function fallbackMeta$1() {
|
|
|
53031
53460
|
queuedBehind: []
|
|
53032
53461
|
};
|
|
53033
53462
|
}
|
|
53463
|
+
/** Merge `change` into the pending config (keys set to undefined are removed). */
|
|
53464
|
+
function setPending(repo, id, change) {
|
|
53465
|
+
repo.setStartMeta(id, (m) => {
|
|
53466
|
+
const { pendingConfig, ...meta } = m ?? fallbackMeta$1();
|
|
53467
|
+
const next = {
|
|
53468
|
+
...pendingConfig,
|
|
53469
|
+
...change
|
|
53470
|
+
};
|
|
53471
|
+
for (const key of Object.keys(next)) if (next[key] === void 0) delete next[key];
|
|
53472
|
+
return Object.keys(next).length > 0 ? {
|
|
53473
|
+
...meta,
|
|
53474
|
+
pendingConfig: next
|
|
53475
|
+
} : meta;
|
|
53476
|
+
});
|
|
53477
|
+
}
|
|
53034
53478
|
/**
|
|
53035
|
-
* `PATCH /api/sessions/:id`: update model, effort or title.
|
|
53036
|
-
*
|
|
53037
|
-
*
|
|
53479
|
+
* `PATCH /api/sessions/:id`: update model, effort, autonomy or title. Every change is saved
|
|
53480
|
+
* on the session (`session.updated`), so it survives a restart and later starts and resumes
|
|
53481
|
+
* use it.
|
|
53482
|
+
* - Model and effort are remembered as pending and applied right before the next turn
|
|
53483
|
+
* (`applyPendingConfig`), so a running turn keeps its settings.
|
|
53484
|
+
* - Autonomy is pushed to the live provider session at once: from the next tool call where
|
|
53485
|
+
* the provider supports it, otherwise from the next turn. If the runner cannot take it now,
|
|
53486
|
+
* it is retried before the next turn.
|
|
53038
53487
|
*/
|
|
53039
|
-
function updateSession(repo, id, req) {
|
|
53488
|
+
async function updateSession(repo, runner, id, req) {
|
|
53040
53489
|
const change = {};
|
|
53041
53490
|
if (req.model !== void 0) change.model = req.model;
|
|
53042
53491
|
if (req.effort !== void 0) change.effort = req.effort;
|
|
@@ -53044,24 +53493,34 @@ function updateSession(repo, id, req) {
|
|
|
53044
53493
|
...s,
|
|
53045
53494
|
...change.model !== void 0 ? { model: change.model } : {},
|
|
53046
53495
|
...change.effort !== void 0 ? { effort: change.effort } : {},
|
|
53496
|
+
...req.autonomy !== void 0 ? { autonomy: req.autonomy } : {},
|
|
53047
53497
|
...req.title !== void 0 ? { title: req.title } : {}
|
|
53048
53498
|
}));
|
|
53049
|
-
if (Object.keys(change).length > 0) repo
|
|
53050
|
-
|
|
53051
|
-
return {
|
|
53052
|
-
...meta,
|
|
53053
|
-
pendingConfig: {
|
|
53054
|
-
...meta.pendingConfig,
|
|
53055
|
-
...change
|
|
53056
|
-
}
|
|
53057
|
-
};
|
|
53058
|
-
});
|
|
53499
|
+
if (Object.keys(change).length > 0) setPending(repo, id, change);
|
|
53500
|
+
if (req.autonomy !== void 0) await applyAutonomy(repo, runner, session);
|
|
53059
53501
|
return session;
|
|
53060
53502
|
}
|
|
53061
53503
|
/**
|
|
53062
|
-
*
|
|
53063
|
-
*
|
|
53064
|
-
*
|
|
53504
|
+
* Tell the runner about the session's autonomy. When the runner does not host the session,
|
|
53505
|
+
* or cannot be reached, the level stays pending; a later start or resume reads it from the
|
|
53506
|
+
* session anyway.
|
|
53507
|
+
*/
|
|
53508
|
+
async function applyAutonomy(repo, runner, session) {
|
|
53509
|
+
try {
|
|
53510
|
+
await runner.request({
|
|
53511
|
+
name: "session.configure",
|
|
53512
|
+
sessionId: session.id,
|
|
53513
|
+
autonomy: session.autonomy
|
|
53514
|
+
}, AUTONOMY_TIMEOUT_MS);
|
|
53515
|
+
setPending(repo, session.id, { autonomy: void 0 });
|
|
53516
|
+
} catch {
|
|
53517
|
+
setPending(repo, session.id, { autonomy: session.autonomy });
|
|
53518
|
+
}
|
|
53519
|
+
}
|
|
53520
|
+
/**
|
|
53521
|
+
* Apply pending settings on the runner before the next turn. When the runner no longer hosts
|
|
53522
|
+
* the session, the resume that follows opens it with the new settings anyway. Returns a
|
|
53523
|
+
* warning when the provider cannot change them live (they apply on the next resume).
|
|
53065
53524
|
*/
|
|
53066
53525
|
async function applyPendingConfig(repo, runner, session) {
|
|
53067
53526
|
const pending = (repo.find(session.id)?.startMeta)?.pendingConfig;
|
|
@@ -53084,7 +53543,7 @@ async function applyPendingConfig(repo, runner, session) {
|
|
|
53084
53543
|
clear();
|
|
53085
53544
|
return;
|
|
53086
53545
|
}
|
|
53087
|
-
return `
|
|
53546
|
+
return `Session settings not applied live (${code ?? "error"}); they apply when the session is resumed.`;
|
|
53088
53547
|
}
|
|
53089
53548
|
}
|
|
53090
53549
|
// ../server/src/services/sessions/session-service.ts
|
|
@@ -53113,9 +53572,9 @@ var SessionService = class {
|
|
|
53113
53572
|
setInputDecorator(fn) {
|
|
53114
53573
|
this.inputDecorator = fn;
|
|
53115
53574
|
}
|
|
53116
|
-
/** Model
|
|
53575
|
+
/** Model and effort (applied on the next turn), autonomy (pushed to the runner) or title. */
|
|
53117
53576
|
update(id, req) {
|
|
53118
|
-
return updateSession(this.deps.repo, id, req);
|
|
53577
|
+
return updateSession(this.deps.repo, this.deps.runner, id, req);
|
|
53119
53578
|
}
|
|
53120
53579
|
detail(id) {
|
|
53121
53580
|
const found = this.deps.repo.find(id);
|
|
@@ -53163,7 +53622,7 @@ var SessionService = class {
|
|
|
53163
53622
|
if (req.input) await this.send(target.id, {
|
|
53164
53623
|
input: req.input,
|
|
53165
53624
|
whenBusy: "queue"
|
|
53166
|
-
});
|
|
53625
|
+
}, req.startedBy.kind);
|
|
53167
53626
|
return {
|
|
53168
53627
|
status: "joined",
|
|
53169
53628
|
sessionId: target.id
|
|
@@ -53220,16 +53679,17 @@ var SessionService = class {
|
|
|
53220
53679
|
} : s);
|
|
53221
53680
|
this.launchInBackground(session.id, pending.input, pending.workspace);
|
|
53222
53681
|
}
|
|
53223
|
-
|
|
53682
|
+
/** Send now, steer or queue (`whenBusy`); `from` labels the message if it is queued. */
|
|
53683
|
+
async send(id, req, from = "user") {
|
|
53224
53684
|
const session = this.deps.repo.get(id);
|
|
53225
|
-
if (NOT_STARTED.has(session.status)) return this.enqueue(id, req.input);
|
|
53685
|
+
if (NOT_STARTED.has(session.status)) return this.enqueue(id, req.input, from);
|
|
53226
53686
|
if (BUSY.has(session.status)) {
|
|
53227
53687
|
if (req.whenBusy === "reject") throw conflict("A turn is running", "turn_running");
|
|
53228
53688
|
if (req.whenBusy === "steer") {
|
|
53229
53689
|
await this.steer(id, req.input);
|
|
53230
53690
|
return { status: "steered" };
|
|
53231
53691
|
}
|
|
53232
|
-
return this.enqueue(id, req.input);
|
|
53692
|
+
return this.enqueue(id, req.input, from);
|
|
53233
53693
|
}
|
|
53234
53694
|
await this.sendNow(session, req.input);
|
|
53235
53695
|
return { status: "sent" };
|
|
@@ -53238,7 +53698,7 @@ var SessionService = class {
|
|
|
53238
53698
|
const session = this.deps.repo.get(id);
|
|
53239
53699
|
if (!BUSY.has(session.status)) throw conflict("No turn is running", "no_turn");
|
|
53240
53700
|
if (!this.provider(session.provider)?.capabilities.steer) throw conflict("This provider cannot steer a running turn", "unsupported");
|
|
53241
|
-
const input = this.
|
|
53701
|
+
const input = this.forRunner(session, raw);
|
|
53242
53702
|
await this.deps.runner.request({
|
|
53243
53703
|
name: "session.steer",
|
|
53244
53704
|
sessionId: id,
|
|
@@ -53324,15 +53784,20 @@ var SessionService = class {
|
|
|
53324
53784
|
return this.deps.outbox.count(id) > 0;
|
|
53325
53785
|
}
|
|
53326
53786
|
async sendNow(session, raw) {
|
|
53327
|
-
const input = this.
|
|
53787
|
+
const input = this.forRunner(session, this.inputDecorator?.(session, raw) ?? raw);
|
|
53328
53788
|
await applyPendingConfig(this.deps.repo, this.deps.runner, session);
|
|
53329
53789
|
const previous = session.status;
|
|
53330
53790
|
this.deps.repo.update(session.id, (s) => ({
|
|
53331
53791
|
...s,
|
|
53332
53792
|
status: "running",
|
|
53333
|
-
endedAt: null
|
|
53793
|
+
endedAt: null,
|
|
53794
|
+
archivedAt: null
|
|
53334
53795
|
}));
|
|
53335
53796
|
try {
|
|
53797
|
+
if (session.worktreeRemovedAt) {
|
|
53798
|
+
await this.deps.launcher.resume(this.deps.repo.get(session.id), input);
|
|
53799
|
+
return;
|
|
53800
|
+
}
|
|
53336
53801
|
try {
|
|
53337
53802
|
await this.deps.runner.request({
|
|
53338
53803
|
name: "session.send",
|
|
@@ -53351,20 +53816,25 @@ var SessionService = class {
|
|
|
53351
53816
|
throw toServiceError$1(error);
|
|
53352
53817
|
}
|
|
53353
53818
|
}
|
|
53354
|
-
enqueue(id, input) {
|
|
53819
|
+
enqueue(id, input, from) {
|
|
53355
53820
|
const session = this.deps.repo.get(id);
|
|
53356
|
-
this.deps.outbox.push(id, this.externalize(session, input));
|
|
53821
|
+
this.deps.outbox.push(id, this.externalize(session, input), from);
|
|
53357
53822
|
return { status: "queued" };
|
|
53358
53823
|
}
|
|
53824
|
+
/** Pasted images saved as artifacts: the form CodeDen stores (queued messages). */
|
|
53359
53825
|
externalize(session, input) {
|
|
53360
53826
|
return this.deps.artifacts.externalizeAttachments(input, {
|
|
53361
53827
|
sessionId: session.id,
|
|
53362
53828
|
projectId: session.projectId
|
|
53363
53829
|
});
|
|
53364
53830
|
}
|
|
53831
|
+
/** Input handed to the runner: artifacts resolved to the file paths providers read. */
|
|
53832
|
+
forRunner(session, input) {
|
|
53833
|
+
return this.deps.artifacts.resolveAttachments(this.externalize(session, input));
|
|
53834
|
+
}
|
|
53365
53835
|
launchInBackground(id, input, workspace) {
|
|
53366
53836
|
const session = this.deps.repo.get(id);
|
|
53367
|
-
const prepared = input ? this.
|
|
53837
|
+
const prepared = input ? this.forRunner(session, input) : void 0;
|
|
53368
53838
|
const promise = this.deps.launcher.launch(id, {
|
|
53369
53839
|
input: prepared,
|
|
53370
53840
|
workspace
|
|
@@ -53432,6 +53902,176 @@ function fallbackMeta() {
|
|
|
53432
53902
|
queuedBehind: []
|
|
53433
53903
|
};
|
|
53434
53904
|
}
|
|
53905
|
+
// ../server/src/services/sessions/worktree-cleanup.ts
|
|
53906
|
+
const DAY_MS = 864e5;
|
|
53907
|
+
const REMOVE_TIMEOUT_MS = 12e4;
|
|
53908
|
+
/** Sessions that may be using their worktree right now; never cleaned up. */
|
|
53909
|
+
const IN_USE = /* @__PURE__ */ new Set([
|
|
53910
|
+
"created",
|
|
53911
|
+
"queued",
|
|
53912
|
+
"starting",
|
|
53913
|
+
"running",
|
|
53914
|
+
"waiting"
|
|
53915
|
+
]);
|
|
53916
|
+
const outcome = (status, message = null) => ({
|
|
53917
|
+
status,
|
|
53918
|
+
message
|
|
53919
|
+
});
|
|
53920
|
+
/**
|
|
53921
|
+
* Whether the sweep should remove a session's worktree now: an agent session that owns a
|
|
53922
|
+
* worktree it is not using, and is either archived or has been inactive for `afterDays`
|
|
53923
|
+
* without an open pull request. Archived sessions are retried even with cleanup off.
|
|
53924
|
+
*/
|
|
53925
|
+
function cleanupDue(session, now, settings) {
|
|
53926
|
+
if (session.role !== "agent" || !session.projectId) return false;
|
|
53927
|
+
if (!session.worktreePath || session.worktreeRemovedAt) return false;
|
|
53928
|
+
if (IN_USE.has(session.status)) return false;
|
|
53929
|
+
if (session.archivedAt) return true;
|
|
53930
|
+
if (!settings.cleanup) return false;
|
|
53931
|
+
if (session.pr && (session.pr.state === "open" || session.pr.state === "draft")) return false;
|
|
53932
|
+
return now.getTime() - Date.parse(session.updatedAt) >= settings.afterDays * DAY_MS;
|
|
53933
|
+
}
|
|
53934
|
+
/**
|
|
53935
|
+
* Worktree cleanup (§9). Archiving a session removes its worktree right away; an hourly sweep
|
|
53936
|
+
* removes worktrees of sessions inactive for the configured number of days; archiving a project
|
|
53937
|
+
* cleans up its sessions. Everything goes through the runner's workspace commands, so a remote
|
|
53938
|
+
* runner cleans up its own storage. Uncommitted changes always keep a worktree unless forced.
|
|
53939
|
+
* The branch and checkpoints stay, and the next message recreates the worktree.
|
|
53940
|
+
*/
|
|
53941
|
+
var WorktreeCleanup = class {
|
|
53942
|
+
deps;
|
|
53943
|
+
timer;
|
|
53944
|
+
unsubscribe;
|
|
53945
|
+
sweeping = false;
|
|
53946
|
+
/** Sessions the sweep could not clean (dirty, locked), keyed to the `updatedAt` it saw. */
|
|
53947
|
+
kept = /* @__PURE__ */ new Map();
|
|
53948
|
+
constructor(deps) {
|
|
53949
|
+
this.deps = deps;
|
|
53950
|
+
}
|
|
53951
|
+
start() {
|
|
53952
|
+
this.unsubscribe = this.deps.store.subscribe((events) => {
|
|
53953
|
+
setImmediate(() => this.onEvents(events));
|
|
53954
|
+
});
|
|
53955
|
+
const interval = this.deps.intervalMs ?? 36e5;
|
|
53956
|
+
const first = setTimeout(() => {
|
|
53957
|
+
this.sweep();
|
|
53958
|
+
this.timer = setInterval(() => void this.sweep(), interval);
|
|
53959
|
+
this.timer.unref?.();
|
|
53960
|
+
}, this.deps.initialDelayMs ?? 6e4);
|
|
53961
|
+
first.unref?.();
|
|
53962
|
+
this.timer = first;
|
|
53963
|
+
}
|
|
53964
|
+
stop() {
|
|
53965
|
+
this.unsubscribe?.();
|
|
53966
|
+
clearTimeout(this.timer);
|
|
53967
|
+
clearInterval(this.timer);
|
|
53968
|
+
}
|
|
53969
|
+
/** Stop the session if needed, hide it from the sidebar and remove its worktree. */
|
|
53970
|
+
async archive(id, opts = {}) {
|
|
53971
|
+
const current = this.deps.repo.get(id);
|
|
53972
|
+
if (current.role !== "agent") throw badRequest("Only agent sessions can be archived", "not_archivable");
|
|
53973
|
+
if (IN_USE.has(current.status) || current.status === "idle") await this.deps.stopSession(id);
|
|
53974
|
+
this.deps.repo.update(id, (s) => s.archivedAt ? s : {
|
|
53975
|
+
...s,
|
|
53976
|
+
archivedAt: this.now().toISOString()
|
|
53977
|
+
});
|
|
53978
|
+
const worktree = await this.removeWorktree(id, { force: opts.force === true });
|
|
53979
|
+
return {
|
|
53980
|
+
session: this.deps.repo.get(id),
|
|
53981
|
+
worktree
|
|
53982
|
+
};
|
|
53983
|
+
}
|
|
53984
|
+
/** Show an archived session again. Its worktree comes back with the next message. */
|
|
53985
|
+
unarchive(id) {
|
|
53986
|
+
return this.deps.repo.update(id, (s) => s.archivedAt ? {
|
|
53987
|
+
...s,
|
|
53988
|
+
archivedAt: null
|
|
53989
|
+
} : s);
|
|
53990
|
+
}
|
|
53991
|
+
/**
|
|
53992
|
+
* Remove a session's worktree through the runner. Refuses while the session is in use, and
|
|
53993
|
+
* keeps a worktree with uncommitted changes unless `force`.
|
|
53994
|
+
*/
|
|
53995
|
+
async removeWorktree(id, opts) {
|
|
53996
|
+
const session = this.deps.repo.get(id);
|
|
53997
|
+
if (!session.worktreePath || session.worktreeRemovedAt || !session.projectId) return outcome("none");
|
|
53998
|
+
if (IN_USE.has(session.status)) return outcome("failed", "The session is still working; stop it first");
|
|
53999
|
+
let repoPath;
|
|
54000
|
+
try {
|
|
54001
|
+
repoPath = this.deps.projects.get(session.projectId).path;
|
|
54002
|
+
} catch {
|
|
54003
|
+
return outcome("none");
|
|
54004
|
+
}
|
|
54005
|
+
if (!this.deps.runner.isConnected()) return outcome("failed", "The runner is not connected; cleanup will retry later");
|
|
54006
|
+
await this.deps.runner.request({
|
|
54007
|
+
name: "session.stop",
|
|
54008
|
+
sessionId: id
|
|
54009
|
+
}, 15e3).catch(() => void 0);
|
|
54010
|
+
try {
|
|
54011
|
+
await this.deps.runner.request({
|
|
54012
|
+
name: "workspace.remove",
|
|
54013
|
+
repoPath,
|
|
54014
|
+
path: session.worktreePath,
|
|
54015
|
+
force: opts.force
|
|
54016
|
+
}, REMOVE_TIMEOUT_MS);
|
|
54017
|
+
} catch (error) {
|
|
54018
|
+
const message = errorMessage(error);
|
|
54019
|
+
if (runnerErrorCode(error) === "worktree_dirty" || /modified or untracked/.test(message)) return outcome("kept_changes", "The worktree has uncommitted changes, so it was kept");
|
|
54020
|
+
return outcome("failed", message);
|
|
54021
|
+
}
|
|
54022
|
+
this.deps.repo.update(id, (s) => ({
|
|
54023
|
+
...s,
|
|
54024
|
+
worktreeRemovedAt: this.now().toISOString()
|
|
54025
|
+
}));
|
|
54026
|
+
this.kept.delete(id);
|
|
54027
|
+
this.deps.log?.(`worktree cleanup: removed ${session.worktreePath}`);
|
|
54028
|
+
return outcome("removed");
|
|
54029
|
+
}
|
|
54030
|
+
/** Remove every worktree that is due (see `cleanupDue`). Returns the cleaned session ids. */
|
|
54031
|
+
async sweep() {
|
|
54032
|
+
if (this.sweeping || !this.deps.runner.isConnected()) return [];
|
|
54033
|
+
this.sweeping = true;
|
|
54034
|
+
const removed = [];
|
|
54035
|
+
try {
|
|
54036
|
+
const now = this.now();
|
|
54037
|
+
const settings = this.deps.settings().worktrees;
|
|
54038
|
+
for (const session of this.deps.repo.list({ role: "agent" })) {
|
|
54039
|
+
if (!cleanupDue(session, now, settings)) continue;
|
|
54040
|
+
if (this.kept.get(session.id) === session.updatedAt) continue;
|
|
54041
|
+
const result = await this.removeWorktree(session.id, { force: false });
|
|
54042
|
+
if (result.status === "removed") removed.push(session.id);
|
|
54043
|
+
else if (result.status !== "none") this.kept.set(session.id, this.deps.repo.get(session.id).updatedAt);
|
|
54044
|
+
}
|
|
54045
|
+
} catch (error) {
|
|
54046
|
+
this.deps.log?.(`worktree cleanup failed: ${errorMessage(error)}`);
|
|
54047
|
+
} finally {
|
|
54048
|
+
this.sweeping = false;
|
|
54049
|
+
}
|
|
54050
|
+
return removed;
|
|
54051
|
+
}
|
|
54052
|
+
/** Clean up the worktrees of an archived project's sessions that are not in use. */
|
|
54053
|
+
async cleanProject(projectId) {
|
|
54054
|
+
const removed = [];
|
|
54055
|
+
for (const session of this.deps.repo.list({
|
|
54056
|
+
projectId,
|
|
54057
|
+
role: "agent"
|
|
54058
|
+
})) {
|
|
54059
|
+
if (!session.worktreePath || session.worktreeRemovedAt || IN_USE.has(session.status)) continue;
|
|
54060
|
+
if ((await this.removeWorktree(session.id, { force: false })).status === "removed") removed.push(session.id);
|
|
54061
|
+
}
|
|
54062
|
+
return removed;
|
|
54063
|
+
}
|
|
54064
|
+
onEvents(events) {
|
|
54065
|
+
for (const e of events) {
|
|
54066
|
+
if (e.type !== "project.archived") continue;
|
|
54067
|
+
const projectId = e.payload.projectId;
|
|
54068
|
+
if (typeof projectId === "string") this.cleanProject(projectId).catch((error) => this.deps.log?.(`worktree cleanup for project ${projectId}: ${errorMessage(error)}`));
|
|
54069
|
+
}
|
|
54070
|
+
}
|
|
54071
|
+
now() {
|
|
54072
|
+
return this.deps.now?.() ?? /* @__PURE__ */ new Date();
|
|
54073
|
+
}
|
|
54074
|
+
};
|
|
53435
54075
|
// ../server/src/services/setup/claude-sdk.ts
|
|
53436
54076
|
/**
|
|
53437
54077
|
* Claude Code support (§14): reports and installs the Claude Agent SDK into `<home>/deps`.
|
|
@@ -53907,6 +54547,11 @@ async function checkNode({ nodeVersion, platform }) {
|
|
|
53907
54547
|
message: `Node.js 24+ is required`,
|
|
53908
54548
|
fix: installFix("node", platform)
|
|
53909
54549
|
};
|
|
54550
|
+
if (isElectronRuntime()) return {
|
|
54551
|
+
...check,
|
|
54552
|
+
state: "ok",
|
|
54553
|
+
message: `Node.js ${check.version} (built into the app)`
|
|
54554
|
+
};
|
|
53910
54555
|
return {
|
|
53911
54556
|
...check,
|
|
53912
54557
|
state: "ok",
|
|
@@ -53998,6 +54643,9 @@ var SetupService = class {
|
|
|
53998
54643
|
warnings
|
|
53999
54644
|
};
|
|
54000
54645
|
this.cached = status;
|
|
54646
|
+
try {
|
|
54647
|
+
this.deps.onChecked?.(status);
|
|
54648
|
+
} catch {}
|
|
54001
54649
|
return status;
|
|
54002
54650
|
}
|
|
54003
54651
|
regressed(previous, check) {
|
|
@@ -54119,6 +54767,178 @@ function changelogSection(markdown, version) {
|
|
|
54119
54767
|
const end = rest.findIndex((line) => /^##\s/.test(line));
|
|
54120
54768
|
return (end === -1 ? rest : rest.slice(0, end)).join("\n").trim() || null;
|
|
54121
54769
|
}
|
|
54770
|
+
// ../server/src/services/updates/desktop.ts
|
|
54771
|
+
const SIX_HOURS$1 = 216e5;
|
|
54772
|
+
const DESKTOP_RELEASES_URL = "https://github.com/leonvanzyl/codeden/releases/latest";
|
|
54773
|
+
/**
|
|
54774
|
+
* Update checks for the desktop app (§17). Same contract and policy as the npm self-update
|
|
54775
|
+
* (`UpdateService`): check on start and every 6 hours unless the mode is `off`, offer the
|
|
54776
|
+
* update in `notify` mode, download it in the background in `auto` mode (it installs when
|
|
54777
|
+
* the app quits). The work itself (GitHub Releases, download, installer) happens in the
|
|
54778
|
+
* desktop main process; this class drives it and maps its state onto `UpdateStatus`, so the
|
|
54779
|
+
* existing update banner and About page work unchanged.
|
|
54780
|
+
*/
|
|
54781
|
+
var DesktopUpdateService = class {
|
|
54782
|
+
deps;
|
|
54783
|
+
timer;
|
|
54784
|
+
restartWhenDownloaded = false;
|
|
54785
|
+
/** The last command that failed was a download or install (an "update failed", not a check). */
|
|
54786
|
+
installFailed = false;
|
|
54787
|
+
listeners = /* @__PURE__ */ new Set();
|
|
54788
|
+
notesCache = /* @__PURE__ */ new Map();
|
|
54789
|
+
stopDesktopEvents;
|
|
54790
|
+
constructor(deps) {
|
|
54791
|
+
this.deps = deps;
|
|
54792
|
+
this.stopDesktopEvents = deps.desktop.onChange((state) => this.onDesktopState(state));
|
|
54793
|
+
}
|
|
54794
|
+
get current() {
|
|
54795
|
+
return this.deps.current ?? CODEDEN_VERSION;
|
|
54796
|
+
}
|
|
54797
|
+
get enabled() {
|
|
54798
|
+
return this.deps.mode() !== "off" && !this.deps.checksDisabled;
|
|
54799
|
+
}
|
|
54800
|
+
start() {
|
|
54801
|
+
this.checkAndMaybeDownload();
|
|
54802
|
+
this.timer = setInterval(() => void this.checkAndMaybeDownload(), this.deps.intervalMs ?? SIX_HOURS$1);
|
|
54803
|
+
this.timer.unref();
|
|
54804
|
+
}
|
|
54805
|
+
stop() {
|
|
54806
|
+
clearInterval(this.timer);
|
|
54807
|
+
this.timer = void 0;
|
|
54808
|
+
this.stopDesktopEvents();
|
|
54809
|
+
}
|
|
54810
|
+
onChange(listener) {
|
|
54811
|
+
this.listeners.add(listener);
|
|
54812
|
+
return () => this.listeners.delete(listener);
|
|
54813
|
+
}
|
|
54814
|
+
async status(opts = {}) {
|
|
54815
|
+
if (this.enabled) {
|
|
54816
|
+
const { checkedAt, phase } = this.deps.desktop.state();
|
|
54817
|
+
const last = checkedAt ? Date.parse(checkedAt) : null;
|
|
54818
|
+
const stale = last === null || this.now() - last > SIX_HOURS$1;
|
|
54819
|
+
if ((opts.refresh || stale) && isQuiet(phase)) await this.check();
|
|
54820
|
+
}
|
|
54821
|
+
return this.snapshot();
|
|
54822
|
+
}
|
|
54823
|
+
snapshot() {
|
|
54824
|
+
const d = this.deps.desktop.state();
|
|
54825
|
+
const enabled = this.enabled;
|
|
54826
|
+
const latestIsNewer = d.latest !== null && isNewerVersion(d.latest, this.current);
|
|
54827
|
+
let state = "idle";
|
|
54828
|
+
let progress = null;
|
|
54829
|
+
if (d.phase === "downloading") {
|
|
54830
|
+
state = "installing";
|
|
54831
|
+
progress = d.percent === null ? "Downloading" : `Downloading ${Math.floor(d.percent)}%`;
|
|
54832
|
+
} else if (d.phase === "downloaded") state = "installed";
|
|
54833
|
+
else if (d.phase === "installing") state = "restarting";
|
|
54834
|
+
else if (d.phase === "error" && this.installFailed) state = "failed";
|
|
54835
|
+
const checkError = d.phase === "error" && !this.installFailed ? d.error : null;
|
|
54836
|
+
return {
|
|
54837
|
+
current: this.current,
|
|
54838
|
+
latest: d.latest,
|
|
54839
|
+
updateAvailable: enabled && latestIsNewer && d.unsupported === null,
|
|
54840
|
+
enabled,
|
|
54841
|
+
mode: this.deps.mode(),
|
|
54842
|
+
checkedAt: d.checkedAt,
|
|
54843
|
+
error: enabled ? d.unsupported ?? checkError : null,
|
|
54844
|
+
command: DESKTOP_RELEASES_URL,
|
|
54845
|
+
source: "desktop",
|
|
54846
|
+
state,
|
|
54847
|
+
target: state === "idle" ? null : d.latest,
|
|
54848
|
+
progress,
|
|
54849
|
+
installError: state === "failed" ? d.error ?? "The update could not be downloaded" : null,
|
|
54850
|
+
canRestart: d.unsupported === null,
|
|
54851
|
+
restartCommand: "Quit CodeDen from the tray and open it again",
|
|
54852
|
+
installed: [],
|
|
54853
|
+
rollbackTo: null,
|
|
54854
|
+
canRollback: false,
|
|
54855
|
+
warning: null
|
|
54856
|
+
};
|
|
54857
|
+
}
|
|
54858
|
+
/**
|
|
54859
|
+
* Download the latest release and, unless `restart` is false, install it and restart the
|
|
54860
|
+
* app once downloaded. Returns at once; progress follows through `onChange`.
|
|
54861
|
+
*/
|
|
54862
|
+
install(opts = {}) {
|
|
54863
|
+
const d = this.deps.desktop.state();
|
|
54864
|
+
if (d.unsupported) throw conflict(d.unsupported, "updates_unsupported");
|
|
54865
|
+
if (opts.version && d.latest && opts.version !== d.latest) throw conflict(`The desktop app installs the latest release (${d.latest}); download older versions from GitHub.`, "version_unavailable");
|
|
54866
|
+
if (d.phase === "downloading" || d.phase === "installing") return this.snapshot();
|
|
54867
|
+
const restart = opts.restart !== false;
|
|
54868
|
+
if (d.phase === "downloaded") {
|
|
54869
|
+
if (restart) this.runCommand("install");
|
|
54870
|
+
return this.snapshot();
|
|
54871
|
+
}
|
|
54872
|
+
this.restartWhenDownloaded = restart;
|
|
54873
|
+
this.runCommand("download");
|
|
54874
|
+
return this.snapshot();
|
|
54875
|
+
}
|
|
54876
|
+
/** Install the downloaded update and restart the app. */
|
|
54877
|
+
restartNow() {
|
|
54878
|
+
if (this.deps.desktop.state().phase !== "downloaded") throw conflict("No downloaded update is waiting for a restart", "nothing_to_restart");
|
|
54879
|
+
this.runCommand("install");
|
|
54880
|
+
return this.snapshot();
|
|
54881
|
+
}
|
|
54882
|
+
rollback() {
|
|
54883
|
+
throw conflict("The desktop app can't roll back by itself; install an older release from GitHub.", "nothing_to_roll_back");
|
|
54884
|
+
}
|
|
54885
|
+
notes(version) {
|
|
54886
|
+
let cached = this.notesCache.get(version);
|
|
54887
|
+
if (!cached) {
|
|
54888
|
+
cached = fetchReleaseNotes(version, this.deps.notes, this.deps.fetch);
|
|
54889
|
+
cached.catch(() => this.notesCache.delete(version));
|
|
54890
|
+
this.notesCache.set(version, cached);
|
|
54891
|
+
}
|
|
54892
|
+
return cached;
|
|
54893
|
+
}
|
|
54894
|
+
async checkAndMaybeDownload() {
|
|
54895
|
+
if (!this.enabled || !isQuiet(this.deps.desktop.state().phase)) return;
|
|
54896
|
+
await this.check();
|
|
54897
|
+
const d = this.deps.desktop.state();
|
|
54898
|
+
if (this.deps.mode() === "auto" && d.phase === "available" && d.unsupported === null) {
|
|
54899
|
+
this.deps.log?.(`updates: downloading ${d.latest} in the background (auto mode)`);
|
|
54900
|
+
this.restartWhenDownloaded = false;
|
|
54901
|
+
this.runCommand("download");
|
|
54902
|
+
}
|
|
54903
|
+
}
|
|
54904
|
+
async check() {
|
|
54905
|
+
try {
|
|
54906
|
+
await this.deps.desktop.run("check");
|
|
54907
|
+
this.installFailed = false;
|
|
54908
|
+
} catch (error) {
|
|
54909
|
+
this.deps.log?.(`updates: check failed: ${errorMessage(error)}`);
|
|
54910
|
+
}
|
|
54911
|
+
this.emit();
|
|
54912
|
+
}
|
|
54913
|
+
runCommand(command) {
|
|
54914
|
+
this.installFailed = false;
|
|
54915
|
+
this.deps.desktop.run(command).catch((error) => {
|
|
54916
|
+
this.installFailed = true;
|
|
54917
|
+
this.restartWhenDownloaded = false;
|
|
54918
|
+
this.deps.log?.(`updates: ${command} failed: ${errorMessage(error)}`);
|
|
54919
|
+
this.emit();
|
|
54920
|
+
});
|
|
54921
|
+
}
|
|
54922
|
+
onDesktopState(state) {
|
|
54923
|
+
if (state.phase === "downloaded" && this.restartWhenDownloaded) {
|
|
54924
|
+
this.restartWhenDownloaded = false;
|
|
54925
|
+
this.runCommand("install");
|
|
54926
|
+
}
|
|
54927
|
+
this.emit();
|
|
54928
|
+
}
|
|
54929
|
+
emit() {
|
|
54930
|
+
if (this.listeners.size === 0) return;
|
|
54931
|
+
const status = this.snapshot();
|
|
54932
|
+
for (const listener of this.listeners) listener(status);
|
|
54933
|
+
}
|
|
54934
|
+
now() {
|
|
54935
|
+
return this.deps.now?.() ?? Date.now();
|
|
54936
|
+
}
|
|
54937
|
+
};
|
|
54938
|
+
/** No check, download or install is in flight. */
|
|
54939
|
+
function isQuiet(phase) {
|
|
54940
|
+
return phase === "idle" || phase === "available" || phase === "error";
|
|
54941
|
+
}
|
|
54122
54942
|
// ../server/src/services/updates/installer.ts
|
|
54123
54943
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
54124
54944
|
/**
|
|
@@ -54197,10 +55017,10 @@ async function installAppVersion(opts) {
|
|
|
54197
55017
|
function reportedVersion(entry) {
|
|
54198
55018
|
return new Promise((resolve, reject) => {
|
|
54199
55019
|
execFile(process.execPath, [entry, "--version"], {
|
|
54200
|
-
env: {
|
|
55020
|
+
env: ownNodeEnv({
|
|
54201
55021
|
...process.env,
|
|
54202
55022
|
CODEDEN_NO_SWITCH: "1"
|
|
54203
|
-
},
|
|
55023
|
+
}),
|
|
54204
55024
|
timeout: 6e4,
|
|
54205
55025
|
windowsHide: true
|
|
54206
55026
|
}, (error, stdout, stderr) => {
|
|
@@ -54357,7 +55177,7 @@ var UpdateService = class {
|
|
|
54357
55177
|
* Returns at once with the `installing` status; progress follows through `onChange`.
|
|
54358
55178
|
*/
|
|
54359
55179
|
install(opts = {}) {
|
|
54360
|
-
if (this.host.source
|
|
55180
|
+
if (this.host.source !== "packaged") throw conflict("You're running CodeDen from source: pull and rebuild to update.", "running_from_source");
|
|
54361
55181
|
if (this.state === "installing" || this.state === "restarting") return this.snapshot();
|
|
54362
55182
|
const restart = opts.restart !== false;
|
|
54363
55183
|
this.state = "installing";
|
|
@@ -55783,6 +56603,532 @@ var ProviderUtilityService = class {
|
|
|
55783
56603
|
function clip(text, max) {
|
|
55784
56604
|
return text.length > max ? `${text.slice(0, max)}...` : text;
|
|
55785
56605
|
}
|
|
56606
|
+
// ../server/src/services/voice/local/catalog.ts
|
|
56607
|
+
const WHISPER_RUNTIME_VERSION = "1.1.3";
|
|
56608
|
+
const runtime = (target, sha256, bytes) => ({
|
|
56609
|
+
version: WHISPER_RUNTIME_VERSION,
|
|
56610
|
+
url: `https://registry.npmjs.org/@fugood/node-whisper-${target}/-/node-whisper-${target}-${WHISPER_RUNTIME_VERSION}.tgz`,
|
|
56611
|
+
sha256,
|
|
56612
|
+
bytes,
|
|
56613
|
+
entry: "package/index.node"
|
|
56614
|
+
});
|
|
56615
|
+
/** Prebuilt runtimes by `process.platform-process.arch`. */
|
|
56616
|
+
const RUNTIME_PINS = {
|
|
56617
|
+
"win32-x64": runtime("win32-x64", "25d54aff191d51833a4941ed77d54beeef034fad585c485938fb34e59f32cb4a", 1205460),
|
|
56618
|
+
"win32-arm64": runtime("win32-arm64", "c6e36608d32aaec62e6f07bdfd7a29576c12b0eb2cf396bed6f6b5ee236a1342", 1226903),
|
|
56619
|
+
"darwin-x64": runtime("darwin-x64", "8f9af3930e94f7b4123cc39ae8fea2554477404e65d4d5ad6a0ca142a6add591", 1642873),
|
|
56620
|
+
"darwin-arm64": runtime("darwin-arm64", "cb54d7e9050a896d794193367769b0c93e77c51e34861d521140c2dd8cc501dc", 1516419),
|
|
56621
|
+
"linux-x64": runtime("linux-x64", "d847231487492e4bcaef2d6d47e06e4b4ea872ee621fc988d01b7f0733006224", 1279405),
|
|
56622
|
+
"linux-arm64": runtime("linux-arm64", "f392eacef6c773442e5c427a6dca5ab6cd3f4f104ca8d609c27c7cf97e6e2a4a", 1199206)
|
|
56623
|
+
};
|
|
56624
|
+
/** Hugging Face revision of `ggerganov/whisper.cpp` the checksums below belong to. */
|
|
56625
|
+
const MODEL_REVISION = "5359861c739e955e79d9a303bcbc70fb988958b1";
|
|
56626
|
+
const model = (id, sha256, bytes, info) => {
|
|
56627
|
+
const file = `ggml-${id}-q5_1.bin`;
|
|
56628
|
+
return {
|
|
56629
|
+
id,
|
|
56630
|
+
file,
|
|
56631
|
+
url: `https://huggingface.co/ggerganov/whisper.cpp/resolve/${MODEL_REVISION}/${file}`,
|
|
56632
|
+
sha256,
|
|
56633
|
+
bytes,
|
|
56634
|
+
englishOnly: id.endsWith(".en"),
|
|
56635
|
+
...info
|
|
56636
|
+
};
|
|
56637
|
+
};
|
|
56638
|
+
/** The models offered in Settings, smallest first. */
|
|
56639
|
+
const MODEL_PINS = [
|
|
56640
|
+
model("tiny.en", "c77c5766f1cef09b6b7d47f21b546cbddd4157886b3b5d6d4f709e91e66c7c2b", 32166155, {
|
|
56641
|
+
label: "Tiny (English)",
|
|
56642
|
+
hint: "Fastest, lower accuracy",
|
|
56643
|
+
recommended: false
|
|
56644
|
+
}),
|
|
56645
|
+
model("tiny", "818710568da3ca15689e31a743197b520007872ff9576237bda97bd1b469c3d7", 32152673, {
|
|
56646
|
+
label: "Tiny",
|
|
56647
|
+
hint: "Fastest, lower accuracy, many languages",
|
|
56648
|
+
recommended: false
|
|
56649
|
+
}),
|
|
56650
|
+
model("base.en", "4baf70dd0d7c4247ba2b81fafd9c01005ac77c2f9ef064e00dcf195d0e2fdd2f", 59721011, {
|
|
56651
|
+
label: "Base (English)",
|
|
56652
|
+
hint: "Fast, good accuracy",
|
|
56653
|
+
recommended: true
|
|
56654
|
+
}),
|
|
56655
|
+
model("base", "422f1ae452ade6f30a004d7e5c6a43195e4433bc370bf23fac9cc591f01a8898", 59707625, {
|
|
56656
|
+
label: "Base",
|
|
56657
|
+
hint: "Fast, good accuracy, many languages",
|
|
56658
|
+
recommended: false
|
|
56659
|
+
}),
|
|
56660
|
+
model("small.en", "bfdff4894dcb76bbf647d56263ea2a96645423f1669176f4844a1bf8e478ad30", 190098681, {
|
|
56661
|
+
label: "Small (English)",
|
|
56662
|
+
hint: "Slower, best accuracy",
|
|
56663
|
+
recommended: false
|
|
56664
|
+
}),
|
|
56665
|
+
model("small", "ae85e4a935d7a567bd102fe55afc16bb595bdb618e11b2fc7591bc08120411bb", 190085487, {
|
|
56666
|
+
label: "Small",
|
|
56667
|
+
hint: "Slower, best accuracy, many languages",
|
|
56668
|
+
recommended: false
|
|
56669
|
+
})
|
|
56670
|
+
];
|
|
56671
|
+
/** `win32-x64` style key for the current process. */
|
|
56672
|
+
function currentPlatform() {
|
|
56673
|
+
return `${process.platform}-${process.arch}`;
|
|
56674
|
+
}
|
|
56675
|
+
// ../server/src/services/voice/local/download.ts
|
|
56676
|
+
/** Why a download failed; `incomplete` leaves the partial file so the next try resumes. */
|
|
56677
|
+
var DownloadError = class extends Error {
|
|
56678
|
+
code;
|
|
56679
|
+
constructor(code, message) {
|
|
56680
|
+
super(message);
|
|
56681
|
+
this.code = code;
|
|
56682
|
+
this.name = "DownloadError";
|
|
56683
|
+
}
|
|
56684
|
+
};
|
|
56685
|
+
/** The partial file a download writes to. */
|
|
56686
|
+
const partialPath = (dest) => `${dest}.partial`;
|
|
56687
|
+
async function hashExisting(path, hash) {
|
|
56688
|
+
for await (const chunk of createReadStream(path)) hash.update(chunk);
|
|
56689
|
+
}
|
|
56690
|
+
/**
|
|
56691
|
+
* Download a pinned file to `dest`, resuming a previous partial download with an HTTP range
|
|
56692
|
+
* request when the server allows it (and starting over when it does not). The file only
|
|
56693
|
+
* becomes `dest` once its size and SHA-256 match the pin; a mismatch deletes it.
|
|
56694
|
+
*/
|
|
56695
|
+
async function downloadVerified(opts) {
|
|
56696
|
+
const { file, dest } = opts;
|
|
56697
|
+
const fetchImpl = opts.fetch ?? ((input, init) => fetch(input, init));
|
|
56698
|
+
const partial = partialPath(dest);
|
|
56699
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
56700
|
+
let hash = createHash("sha256");
|
|
56701
|
+
let received = 0;
|
|
56702
|
+
if (existsSync(partial)) {
|
|
56703
|
+
const size = statSync(partial).size;
|
|
56704
|
+
if (size > file.bytes) rmSync(partial, { force: true });
|
|
56705
|
+
else if (size > 0) {
|
|
56706
|
+
await hashExisting(partial, hash);
|
|
56707
|
+
received = size;
|
|
56708
|
+
}
|
|
56709
|
+
}
|
|
56710
|
+
if (received < file.bytes) {
|
|
56711
|
+
const res = await fetchImpl(file.url, {
|
|
56712
|
+
headers: received > 0 ? { range: `bytes=${received}-` } : {},
|
|
56713
|
+
signal: opts.signal
|
|
56714
|
+
});
|
|
56715
|
+
if (!(res.status === 206 && (res.headers.get("content-range") ?? "").startsWith(`bytes ${received}-`))) {
|
|
56716
|
+
if (res.status === 206 || res.status === 416) {
|
|
56717
|
+
await res.body?.cancel();
|
|
56718
|
+
rmSync(partial, { force: true });
|
|
56719
|
+
return downloadVerified(opts);
|
|
56720
|
+
}
|
|
56721
|
+
if (!res.ok) {
|
|
56722
|
+
await res.body?.cancel();
|
|
56723
|
+
throw new DownloadError("http", `Download failed: ${res.status} ${res.statusText}`);
|
|
56724
|
+
}
|
|
56725
|
+
hash = createHash("sha256");
|
|
56726
|
+
received = 0;
|
|
56727
|
+
}
|
|
56728
|
+
if (!res.body) throw new DownloadError("http", "Download failed: empty response");
|
|
56729
|
+
const out = await open(partial, received > 0 ? "a" : "w");
|
|
56730
|
+
let failure;
|
|
56731
|
+
try {
|
|
56732
|
+
opts.onProgress?.(received, file.bytes);
|
|
56733
|
+
for await (const chunk of res.body) {
|
|
56734
|
+
received += chunk.length;
|
|
56735
|
+
if (received > file.bytes) throw new DownloadError("size_mismatch", "The download is larger than expected");
|
|
56736
|
+
hash.update(chunk);
|
|
56737
|
+
await out.write(chunk);
|
|
56738
|
+
opts.onProgress?.(received, file.bytes);
|
|
56739
|
+
}
|
|
56740
|
+
} catch (error) {
|
|
56741
|
+
failure = error;
|
|
56742
|
+
} finally {
|
|
56743
|
+
await out.close();
|
|
56744
|
+
}
|
|
56745
|
+
if (failure) {
|
|
56746
|
+
if (failure instanceof DownloadError) rmSync(partial, { force: true });
|
|
56747
|
+
throw failure;
|
|
56748
|
+
}
|
|
56749
|
+
}
|
|
56750
|
+
if (received !== file.bytes) throw new DownloadError("incomplete", `The download stopped at ${received} of ${file.bytes} bytes; try again to resume`);
|
|
56751
|
+
if (hash.digest("hex") !== file.sha256) {
|
|
56752
|
+
rmSync(partial, { force: true });
|
|
56753
|
+
throw new DownloadError("checksum_mismatch", "The download did not match its pinned SHA-256 checksum and was deleted");
|
|
56754
|
+
}
|
|
56755
|
+
await rename(partial, dest);
|
|
56756
|
+
}
|
|
56757
|
+
// ../server/src/services/voice/local/runtime.ts
|
|
56758
|
+
/** An addon is loaded once per process; loading the same file twice is not allowed. */
|
|
56759
|
+
const loaded = /* @__PURE__ */ new Map();
|
|
56760
|
+
function loadAddon(addonPath) {
|
|
56761
|
+
const cached = loaded.get(addonPath);
|
|
56762
|
+
if (cached) return cached;
|
|
56763
|
+
const mod = { exports: {} };
|
|
56764
|
+
process.dlopen(mod, addonPath);
|
|
56765
|
+
const ctor = mod.exports.WhisperContext;
|
|
56766
|
+
if (typeof ctor !== "function") throw new Error(`${addonPath} is not a Whisper runtime`);
|
|
56767
|
+
loaded.set(addonPath, ctor);
|
|
56768
|
+
return ctor;
|
|
56769
|
+
}
|
|
56770
|
+
/** The real runtime: whisper.cpp's Node-API addon, loaded in the server process. */
|
|
56771
|
+
const loadNativeRuntime = async (addonPath) => {
|
|
56772
|
+
const Context = loadAddon(addonPath);
|
|
56773
|
+
return { async loadModel(modelPath) {
|
|
56774
|
+
const ctx = new Context({
|
|
56775
|
+
filePath: modelPath,
|
|
56776
|
+
useGpu: true
|
|
56777
|
+
});
|
|
56778
|
+
return {
|
|
56779
|
+
async transcribe(pcm, { language, signal }) {
|
|
56780
|
+
const job = ctx.transcribeData(pcm, { language });
|
|
56781
|
+
const stop = () => void job.stop();
|
|
56782
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
56783
|
+
try {
|
|
56784
|
+
const result = await job.promise;
|
|
56785
|
+
if (result.isAborted) throw new Error("Transcription was cancelled");
|
|
56786
|
+
return {
|
|
56787
|
+
text: result.result,
|
|
56788
|
+
language: result.language || null
|
|
56789
|
+
};
|
|
56790
|
+
} finally {
|
|
56791
|
+
signal?.removeEventListener("abort", stop);
|
|
56792
|
+
}
|
|
56793
|
+
},
|
|
56794
|
+
release: () => ctx.release()
|
|
56795
|
+
};
|
|
56796
|
+
} };
|
|
56797
|
+
};
|
|
56798
|
+
// ../server/src/services/voice/local/tgz.ts
|
|
56799
|
+
const BLOCK = 512;
|
|
56800
|
+
function field(header, start, length) {
|
|
56801
|
+
const raw = header.subarray(start, start + length).toString("utf8");
|
|
56802
|
+
const nul = raw.indexOf("\0");
|
|
56803
|
+
return nul === -1 ? raw : raw.slice(0, nul);
|
|
56804
|
+
}
|
|
56805
|
+
/**
|
|
56806
|
+
* Read one regular file out of a gzipped tar archive (an npm package tarball). Only the
|
|
56807
|
+
* named entry is returned, so nothing in the archive decides where files are written.
|
|
56808
|
+
*/
|
|
56809
|
+
function extractTgzEntry(archive, entry) {
|
|
56810
|
+
const tar = gunzipSync(archive);
|
|
56811
|
+
let offset = 0;
|
|
56812
|
+
while (offset + BLOCK <= tar.length) {
|
|
56813
|
+
const header = tar.subarray(offset, offset + BLOCK);
|
|
56814
|
+
const name = field(header, 0, 100);
|
|
56815
|
+
if (!name) break;
|
|
56816
|
+
const size = Number.parseInt(field(header, 124, 12).trim() || "0", 8);
|
|
56817
|
+
if (!Number.isFinite(size) || size < 0) throw new Error("Corrupt archive header");
|
|
56818
|
+
const type = String.fromCharCode(header[156] ?? 0);
|
|
56819
|
+
const prefix = field(header, 257, 6).startsWith("ustar") ? field(header, 345, 155) : "";
|
|
56820
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
56821
|
+
const start = offset + BLOCK;
|
|
56822
|
+
if ((type === "0" || type === "\0") && path === entry) {
|
|
56823
|
+
if (start + size > tar.length) throw new Error("Archive is truncated");
|
|
56824
|
+
return Buffer.from(tar.subarray(start, start + size));
|
|
56825
|
+
}
|
|
56826
|
+
offset = start + Math.ceil(size / BLOCK) * BLOCK;
|
|
56827
|
+
}
|
|
56828
|
+
throw new Error(`${entry} is not in the archive`);
|
|
56829
|
+
}
|
|
56830
|
+
// ../server/src/services/voice/local/wav.ts
|
|
56831
|
+
/** Shortest clip worth transcribing. */
|
|
56832
|
+
const MIN_SECONDS = .1;
|
|
56833
|
+
const BYTES_PER_SECOND = LOCAL_STT_SAMPLE_RATE * 2;
|
|
56834
|
+
const FORMAT_HELP = `The local engine takes WAV audio: 16-bit PCM, mono, ${LOCAL_STT_SAMPLE_RATE / 1e3} kHz`;
|
|
56835
|
+
const unsupported = (detail) => new ServiceError(415, "unsupported_audio", `${FORMAT_HELP} (${detail}).`);
|
|
56836
|
+
/**
|
|
56837
|
+
* Validate an uploaded WAV file for the local engine and return its samples. The browser
|
|
56838
|
+
* converts recordings to this format, so the server needs no audio decoder.
|
|
56839
|
+
*/
|
|
56840
|
+
function parseWav(bytes) {
|
|
56841
|
+
if (bytes.length === 0) throw new ServiceError(400, "empty_audio", "No audio was sent");
|
|
56842
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
56843
|
+
const ascii = (at) => at + 4 <= bytes.length ? String.fromCharCode(...bytes.subarray(at, at + 4)) : "";
|
|
56844
|
+
if (ascii(0) !== "RIFF" || ascii(8) !== "WAVE") throw unsupported("not a WAV file");
|
|
56845
|
+
let format = null;
|
|
56846
|
+
let offset = 12;
|
|
56847
|
+
while (offset + 8 <= bytes.length) {
|
|
56848
|
+
const id = ascii(offset);
|
|
56849
|
+
const declared = view.getUint32(offset + 4, true);
|
|
56850
|
+
const body = offset + 8;
|
|
56851
|
+
if (id === "fmt " && body + 16 <= bytes.length) format = {
|
|
56852
|
+
tag: view.getUint16(body, true),
|
|
56853
|
+
channels: view.getUint16(body + 2, true),
|
|
56854
|
+
rate: view.getUint32(body + 4, true),
|
|
56855
|
+
bits: view.getUint16(body + 14, true)
|
|
56856
|
+
};
|
|
56857
|
+
else if (id === "data") {
|
|
56858
|
+
if (!format) throw unsupported("the format chunk is missing");
|
|
56859
|
+
if (format.tag !== 1 && format.tag !== 65534) throw unsupported("not PCM");
|
|
56860
|
+
if (format.bits !== 16) throw unsupported(`${format.bits}-bit samples`);
|
|
56861
|
+
if (format.channels !== 1) throw unsupported(`${format.channels} channels`);
|
|
56862
|
+
if (format.rate !== 16e3) throw unsupported(`${format.rate} Hz`);
|
|
56863
|
+
let length = Math.min(declared, bytes.length - body);
|
|
56864
|
+
length -= length % 2;
|
|
56865
|
+
const durationSec = length / BYTES_PER_SECOND;
|
|
56866
|
+
if (durationSec > 120) throw new ServiceError(413, "audio_too_long", `Audio is longer than 120 seconds`);
|
|
56867
|
+
if (durationSec < MIN_SECONDS) throw new ServiceError(400, "empty_audio", "The recording is too short to transcribe");
|
|
56868
|
+
const pcm = new Uint8Array(length);
|
|
56869
|
+
pcm.set(bytes.subarray(body, body + length));
|
|
56870
|
+
return {
|
|
56871
|
+
pcm: pcm.buffer,
|
|
56872
|
+
durationSec
|
|
56873
|
+
};
|
|
56874
|
+
}
|
|
56875
|
+
offset = body + declared + declared % 2;
|
|
56876
|
+
}
|
|
56877
|
+
throw unsupported("no audio data");
|
|
56878
|
+
}
|
|
56879
|
+
// ../server/src/services/voice/local/local-stt-service.ts
|
|
56880
|
+
const RUNTIME = "runtime";
|
|
56881
|
+
/** Strip Whisper's non-speech markers such as `[BLANK_AUDIO]` or `(silence)`. */
|
|
56882
|
+
function cleanTranscript(text) {
|
|
56883
|
+
return text.replace(/\[[^\]]*\]|\((?:silence|blank[ _]audio|music|inaudible|noise)[^)]*\)/gi, " ").replace(/\s+/g, " ").trim();
|
|
56884
|
+
}
|
|
56885
|
+
/** Stop waiting for `promise` when `signal` aborts (the work itself carries on). */
|
|
56886
|
+
function abortable(promise, signal) {
|
|
56887
|
+
if (signal.aborted) return Promise.reject(signal.reason);
|
|
56888
|
+
return new Promise((resolve, reject) => {
|
|
56889
|
+
const onAbort = () => reject(signal.reason);
|
|
56890
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
56891
|
+
promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
56892
|
+
});
|
|
56893
|
+
}
|
|
56894
|
+
/** `en-US` -> `en`. */
|
|
56895
|
+
const isoLanguage = (language) => language?.split("-")[0]?.toLowerCase() || null;
|
|
56896
|
+
function fileSize(path) {
|
|
56897
|
+
try {
|
|
56898
|
+
return statSync(path).size;
|
|
56899
|
+
} catch {
|
|
56900
|
+
return null;
|
|
56901
|
+
}
|
|
56902
|
+
}
|
|
56903
|
+
/** Why the prebuilt runtime cannot load on this Linux (it needs glibc 2.34 or newer). */
|
|
56904
|
+
function linuxProblem() {
|
|
56905
|
+
const glibc = (process.report?.getReport())?.header?.glibcVersionRuntime;
|
|
56906
|
+
if (!glibc) return "Local Whisper needs a glibc-based Linux (musl is not supported)";
|
|
56907
|
+
const [major = 0, minor = 0] = glibc.split(".").map(Number);
|
|
56908
|
+
return major > 2 || major === 2 && minor >= 34 ? null : `Local Whisper needs glibc 2.34 or newer (this system has ${glibc})`;
|
|
56909
|
+
}
|
|
56910
|
+
/**
|
|
56911
|
+
* Local Whisper (§11.3): downloads the runtime and models into the CodeDen home on demand,
|
|
56912
|
+
* with progress, resume and checksum checks, and transcribes uploaded WAV audio in the
|
|
56913
|
+
* server process. Downloads run in the background; the UI polls `status()`.
|
|
56914
|
+
*/
|
|
56915
|
+
var LocalSttService = class {
|
|
56916
|
+
deps;
|
|
56917
|
+
platform;
|
|
56918
|
+
runtimePin;
|
|
56919
|
+
models;
|
|
56920
|
+
problem;
|
|
56921
|
+
jobs = /* @__PURE__ */ new Map();
|
|
56922
|
+
errors = /* @__PURE__ */ new Map();
|
|
56923
|
+
loaded = null;
|
|
56924
|
+
queue = Promise.resolve();
|
|
56925
|
+
idleTimer;
|
|
56926
|
+
constructor(deps) {
|
|
56927
|
+
this.deps = deps;
|
|
56928
|
+
this.platform = deps.platform ?? currentPlatform();
|
|
56929
|
+
this.runtimePin = (deps.runtimePins ?? RUNTIME_PINS)[this.platform];
|
|
56930
|
+
this.models = deps.models ?? MODEL_PINS;
|
|
56931
|
+
this.problem = !this.runtimePin ? `No prebuilt Whisper runtime for ${this.platform}` : !deps.platform && process.platform === "linux" ? linuxProblem() : null;
|
|
56932
|
+
}
|
|
56933
|
+
get addonPath() {
|
|
56934
|
+
const version = this.runtimePin?.version ?? "1.1.3";
|
|
56935
|
+
return join(this.deps.runtimeDir, `${version}-${this.platform}`, "whisper.node");
|
|
56936
|
+
}
|
|
56937
|
+
modelPath(pin) {
|
|
56938
|
+
return join(this.deps.modelsDir, pin.file);
|
|
56939
|
+
}
|
|
56940
|
+
pin(id) {
|
|
56941
|
+
const pin = this.models.find((m) => m.id === id);
|
|
56942
|
+
if (!pin) throw new ServiceError(404, "unknown_model", `Unknown Whisper model: ${id}`);
|
|
56943
|
+
return pin;
|
|
56944
|
+
}
|
|
56945
|
+
runtimeInstalled() {
|
|
56946
|
+
return existsSync(this.addonPath);
|
|
56947
|
+
}
|
|
56948
|
+
modelInstalled(pin) {
|
|
56949
|
+
return fileSize(this.modelPath(pin)) === pin.bytes;
|
|
56950
|
+
}
|
|
56951
|
+
status() {
|
|
56952
|
+
const entry = (key, total, installedPath, partial) => {
|
|
56953
|
+
const job = this.jobs.get(key);
|
|
56954
|
+
const sizeOnDisk = installedPath ? fileSize(installedPath) : null;
|
|
56955
|
+
const state = job ? "downloading" : sizeOnDisk !== null ? "installed" : this.errors.has(key) ? "failed" : "missing";
|
|
56956
|
+
return {
|
|
56957
|
+
state,
|
|
56958
|
+
receivedBytes: job?.received ?? (state === "installed" ? total : fileSize(partial) ?? 0),
|
|
56959
|
+
totalBytes: total,
|
|
56960
|
+
sizeOnDisk,
|
|
56961
|
+
error: state === "failed" ? this.errors.get(key) ?? null : null
|
|
56962
|
+
};
|
|
56963
|
+
};
|
|
56964
|
+
const runtime = this.runtimePin;
|
|
56965
|
+
return {
|
|
56966
|
+
supported: this.problem === null,
|
|
56967
|
+
platform: this.platform,
|
|
56968
|
+
unsupportedReason: this.problem,
|
|
56969
|
+
runtime: {
|
|
56970
|
+
version: runtime?.version ?? "1.1.3",
|
|
56971
|
+
...entry(RUNTIME, runtime?.bytes ?? 0, this.runtimeInstalled() ? this.addonPath : null, partialPath(this.archivePath()))
|
|
56972
|
+
},
|
|
56973
|
+
models: this.models.map((m) => ({
|
|
56974
|
+
id: m.id,
|
|
56975
|
+
label: m.label,
|
|
56976
|
+
hint: m.hint,
|
|
56977
|
+
englishOnly: m.englishOnly,
|
|
56978
|
+
recommended: m.recommended,
|
|
56979
|
+
...entry(m.id, m.bytes, this.modelInstalled(m) ? this.modelPath(m) : null, partialPath(this.modelPath(m)))
|
|
56980
|
+
})),
|
|
56981
|
+
location: this.deps.modelsDir
|
|
56982
|
+
};
|
|
56983
|
+
}
|
|
56984
|
+
/** Start (or join) downloading the runtime and a model; returns right away. */
|
|
56985
|
+
download(id) {
|
|
56986
|
+
if (this.problem) throw new ServiceError(409, "unsupported_platform", this.problem);
|
|
56987
|
+
const pin = this.pin(id);
|
|
56988
|
+
if (!this.jobs.has(id) && !(this.runtimeInstalled() && this.modelInstalled(pin))) this.track(id, async (signal, progress) => {
|
|
56989
|
+
await abortable(this.ensureRuntime(), signal);
|
|
56990
|
+
if (this.modelInstalled(pin)) return;
|
|
56991
|
+
await downloadVerified({
|
|
56992
|
+
file: pin,
|
|
56993
|
+
dest: this.modelPath(pin),
|
|
56994
|
+
fetch: this.deps.fetch,
|
|
56995
|
+
signal,
|
|
56996
|
+
onProgress: progress
|
|
56997
|
+
});
|
|
56998
|
+
this.deps.log?.(`Whisper model ${pin.id} downloaded`);
|
|
56999
|
+
}).catch(() => {});
|
|
57000
|
+
return this.status();
|
|
57001
|
+
}
|
|
57002
|
+
/** Cancel a running download or delete a downloaded model (and its partial file). */
|
|
57003
|
+
async remove(id) {
|
|
57004
|
+
const pin = this.pin(id);
|
|
57005
|
+
const job = this.jobs.get(id);
|
|
57006
|
+
if (job) {
|
|
57007
|
+
job.abort.abort();
|
|
57008
|
+
await job.promise.catch(() => {});
|
|
57009
|
+
}
|
|
57010
|
+
if (this.loaded?.id === id) await this.serially(() => this.unload());
|
|
57011
|
+
rmSync(this.modelPath(pin), { force: true });
|
|
57012
|
+
rmSync(partialPath(this.modelPath(pin)), { force: true });
|
|
57013
|
+
this.errors.delete(id);
|
|
57014
|
+
return this.status();
|
|
57015
|
+
}
|
|
57016
|
+
/** Transcribe a WAV upload with the model chosen in Settings. */
|
|
57017
|
+
async transcribe(audio, opts = {}) {
|
|
57018
|
+
if (this.problem) throw new ServiceError(409, "unsupported_platform", this.problem);
|
|
57019
|
+
const { pcm } = parseWav(audio);
|
|
57020
|
+
const pin = this.pin(this.deps.settings.get().voice.dictation.local.model);
|
|
57021
|
+
if (!this.runtimeInstalled() || !this.modelInstalled(pin)) throw new ServiceError(409, "model_not_installed", `The ${pin.label} Whisper model isn't downloaded yet. Download it in Settings > Voice.`);
|
|
57022
|
+
const language = pin.englishOnly ? "en" : isoLanguage(opts.language) ?? "auto";
|
|
57023
|
+
return this.serially(async () => {
|
|
57024
|
+
const model = await this.model(pin);
|
|
57025
|
+
const started = performance.now();
|
|
57026
|
+
try {
|
|
57027
|
+
const result = await model.transcribe(pcm, {
|
|
57028
|
+
language,
|
|
57029
|
+
signal: opts.signal
|
|
57030
|
+
});
|
|
57031
|
+
return {
|
|
57032
|
+
text: cleanTranscript(result.text),
|
|
57033
|
+
language: result.language ?? (language === "auto" ? null : language),
|
|
57034
|
+
engine: "local",
|
|
57035
|
+
elapsedMs: Math.round(performance.now() - started)
|
|
57036
|
+
};
|
|
57037
|
+
} catch (error) {
|
|
57038
|
+
if (opts.signal?.aborted) throw new ServiceError(400, "aborted", "Cancelled");
|
|
57039
|
+
throw new ServiceError(500, "transcription_failed", errorMessage(error));
|
|
57040
|
+
} finally {
|
|
57041
|
+
this.armIdleUnload();
|
|
57042
|
+
}
|
|
57043
|
+
});
|
|
57044
|
+
}
|
|
57045
|
+
/** Resolves when no download is running (tests). */
|
|
57046
|
+
async settled() {
|
|
57047
|
+
while (this.jobs.size > 0) await Promise.allSettled([...this.jobs.values()].map((j) => j.promise));
|
|
57048
|
+
}
|
|
57049
|
+
/** Cancel downloads and free the model (server shutdown). */
|
|
57050
|
+
async stop() {
|
|
57051
|
+
for (const job of this.jobs.values()) job.abort.abort();
|
|
57052
|
+
await this.settled();
|
|
57053
|
+
await this.unload();
|
|
57054
|
+
}
|
|
57055
|
+
archivePath() {
|
|
57056
|
+
const name = this.runtimePin?.url.split("/").pop() ?? "runtime.tgz";
|
|
57057
|
+
return join(this.deps.runtimeDir, ".downloads", name);
|
|
57058
|
+
}
|
|
57059
|
+
ensureRuntime() {
|
|
57060
|
+
const pin = this.runtimePin;
|
|
57061
|
+
if (!pin) return Promise.reject(new Error(this.problem ?? "No runtime"));
|
|
57062
|
+
if (this.runtimeInstalled()) return Promise.resolve();
|
|
57063
|
+
return this.track(RUNTIME, async (signal, progress) => {
|
|
57064
|
+
const archive = this.archivePath();
|
|
57065
|
+
await downloadVerified({
|
|
57066
|
+
file: pin,
|
|
57067
|
+
dest: archive,
|
|
57068
|
+
fetch: this.deps.fetch,
|
|
57069
|
+
signal,
|
|
57070
|
+
onProgress: progress
|
|
57071
|
+
});
|
|
57072
|
+
const addon = extractTgzEntry(readFileSync(archive), pin.entry);
|
|
57073
|
+
mkdirSync(join(this.addonPath, ".."), { recursive: true });
|
|
57074
|
+
const staging = `${this.addonPath}.tmp`;
|
|
57075
|
+
writeFileSync(staging, addon, { mode: 493 });
|
|
57076
|
+
renameSync(staging, this.addonPath);
|
|
57077
|
+
rmSync(archive, { force: true });
|
|
57078
|
+
this.deps.log?.(`Whisper runtime ${pin.version} installed for ${this.platform}`);
|
|
57079
|
+
});
|
|
57080
|
+
}
|
|
57081
|
+
/** Run one download at a time per key; callers join a running one. */
|
|
57082
|
+
track(key, work) {
|
|
57083
|
+
const running = this.jobs.get(key);
|
|
57084
|
+
if (running) return running.promise;
|
|
57085
|
+
const abort = new AbortController();
|
|
57086
|
+
const job = {
|
|
57087
|
+
received: 0,
|
|
57088
|
+
abort,
|
|
57089
|
+
promise: Promise.resolve()
|
|
57090
|
+
};
|
|
57091
|
+
this.errors.delete(key);
|
|
57092
|
+
job.promise = work(abort.signal, (received) => {
|
|
57093
|
+
job.received = received;
|
|
57094
|
+
}).catch((error) => {
|
|
57095
|
+
if (!abort.signal.aborted) {
|
|
57096
|
+
this.errors.set(key, errorMessage(error));
|
|
57097
|
+
this.deps.log?.(`Whisper download (${key}) failed: ${errorMessage(error)}`);
|
|
57098
|
+
}
|
|
57099
|
+
throw error;
|
|
57100
|
+
}).finally(() => this.jobs.delete(key));
|
|
57101
|
+
this.jobs.set(key, job);
|
|
57102
|
+
return job.promise;
|
|
57103
|
+
}
|
|
57104
|
+
serially(fn) {
|
|
57105
|
+
const next = this.queue.then(fn, fn);
|
|
57106
|
+
this.queue = next.catch(() => {});
|
|
57107
|
+
return next;
|
|
57108
|
+
}
|
|
57109
|
+
async model(pin) {
|
|
57110
|
+
clearTimeout(this.idleTimer);
|
|
57111
|
+
if (this.loaded?.id === pin.id) return this.loaded.model;
|
|
57112
|
+
await this.unload();
|
|
57113
|
+
const model = await (await (this.deps.loadRuntime ?? loadNativeRuntime)(this.addonPath)).loadModel(this.modelPath(pin));
|
|
57114
|
+
this.loaded = {
|
|
57115
|
+
id: pin.id,
|
|
57116
|
+
model
|
|
57117
|
+
};
|
|
57118
|
+
return model;
|
|
57119
|
+
}
|
|
57120
|
+
armIdleUnload() {
|
|
57121
|
+
clearTimeout(this.idleTimer);
|
|
57122
|
+
this.idleTimer = setTimeout(() => void this.serially(() => this.unload()), this.deps.idleUnloadMs ?? 6e5);
|
|
57123
|
+
this.idleTimer.unref();
|
|
57124
|
+
}
|
|
57125
|
+
async unload() {
|
|
57126
|
+
clearTimeout(this.idleTimer);
|
|
57127
|
+
const current = this.loaded;
|
|
57128
|
+
this.loaded = null;
|
|
57129
|
+
await current?.model.release().catch(() => {});
|
|
57130
|
+
}
|
|
57131
|
+
};
|
|
55786
57132
|
// ../server/src/services/container.ts
|
|
55787
57133
|
function createServices(opts) {
|
|
55788
57134
|
const exec = opts.exec ?? runTool;
|
|
@@ -55845,6 +57191,13 @@ function createServices(opts) {
|
|
|
55845
57191
|
outbox,
|
|
55846
57192
|
questions
|
|
55847
57193
|
});
|
|
57194
|
+
const queue = new QueueService({
|
|
57195
|
+
outbox,
|
|
57196
|
+
repo: sessionRepo,
|
|
57197
|
+
sessions,
|
|
57198
|
+
artifacts,
|
|
57199
|
+
providers: () => opts.runner.providers()
|
|
57200
|
+
});
|
|
55848
57201
|
const lifecycle = new SessionLifecycle({
|
|
55849
57202
|
db: opts.db,
|
|
55850
57203
|
store: opts.store,
|
|
@@ -55870,12 +57223,20 @@ function createServices(opts) {
|
|
|
55870
57223
|
});
|
|
55871
57224
|
const setup = new SetupService({
|
|
55872
57225
|
exec,
|
|
55873
|
-
settings: opts.settings
|
|
57226
|
+
settings: opts.settings,
|
|
57227
|
+
onChecked: () => redetectProviders(opts.runner, opts.log)
|
|
55874
57228
|
});
|
|
55875
57229
|
const claudeSdk = new ClaudeSdkService({
|
|
55876
57230
|
depsDir: opts.paths.deps,
|
|
55877
57231
|
log: opts.log
|
|
55878
57232
|
});
|
|
57233
|
+
const localStt = new LocalSttService({
|
|
57234
|
+
runtimeDir: join(opts.paths.deps, "whisper"),
|
|
57235
|
+
modelsDir: join(opts.paths.models, "whisper"),
|
|
57236
|
+
settings: opts.settings,
|
|
57237
|
+
...opts.localStt,
|
|
57238
|
+
log: opts.log
|
|
57239
|
+
});
|
|
55879
57240
|
const automations = new AutomationService({
|
|
55880
57241
|
db: opts.db,
|
|
55881
57242
|
store: opts.store,
|
|
@@ -55924,6 +57285,16 @@ function createServices(opts) {
|
|
|
55924
57285
|
options: opts.factory,
|
|
55925
57286
|
log: opts.log
|
|
55926
57287
|
});
|
|
57288
|
+
const worktreeCleanup = new WorktreeCleanup({
|
|
57289
|
+
repo: sessionRepo,
|
|
57290
|
+
projects,
|
|
57291
|
+
runner: opts.runner,
|
|
57292
|
+
store: opts.store,
|
|
57293
|
+
settings: () => opts.settings.get(),
|
|
57294
|
+
stopSession: (id) => sessions.stop(id),
|
|
57295
|
+
...opts.worktreeCleanup,
|
|
57296
|
+
log: opts.log
|
|
57297
|
+
});
|
|
55927
57298
|
const services = {
|
|
55928
57299
|
db: opts.db,
|
|
55929
57300
|
store: opts.store,
|
|
@@ -55935,6 +57306,7 @@ function createServices(opts) {
|
|
|
55935
57306
|
projects,
|
|
55936
57307
|
sessionRepo,
|
|
55937
57308
|
sessions,
|
|
57309
|
+
queue,
|
|
55938
57310
|
lifecycle,
|
|
55939
57311
|
recovery: new SessionRecovery({
|
|
55940
57312
|
repo: sessionRepo,
|
|
@@ -55949,17 +57321,7 @@ function createServices(opts) {
|
|
|
55949
57321
|
runner: opts.runner,
|
|
55950
57322
|
repo: sessionRepo
|
|
55951
57323
|
}),
|
|
55952
|
-
updates:
|
|
55953
|
-
home: opts.paths.home,
|
|
55954
|
-
mode: () => opts.settings.get().updates.mode,
|
|
55955
|
-
checksDisabled: process.env.CODEDEN_UPDATE_CHECK === "0",
|
|
55956
|
-
host: opts.selfUpdate,
|
|
55957
|
-
registry: updateRegistry(),
|
|
55958
|
-
notes: notesSources(),
|
|
55959
|
-
fetch: opts.updates?.fetch,
|
|
55960
|
-
intervalMs: opts.updates?.intervalMs,
|
|
55961
|
-
log: opts.log
|
|
55962
|
-
}),
|
|
57324
|
+
updates: createUpdates(opts),
|
|
55963
57325
|
images: new ImageService({
|
|
55964
57326
|
registry: opts.imageEngines ?? defaultImageEngines(opts.secrets),
|
|
55965
57327
|
settings: opts.settings,
|
|
@@ -55972,12 +57334,15 @@ function createServices(opts) {
|
|
|
55972
57334
|
voice: new VoiceService({
|
|
55973
57335
|
secrets: opts.secrets,
|
|
55974
57336
|
settings: opts.settings,
|
|
57337
|
+
local: localStt,
|
|
55975
57338
|
...opts.voice
|
|
55976
57339
|
}),
|
|
57340
|
+
localStt,
|
|
55977
57341
|
setup,
|
|
55978
57342
|
claudeSdk,
|
|
55979
57343
|
usage,
|
|
55980
57344
|
factory,
|
|
57345
|
+
worktreeCleanup,
|
|
55981
57346
|
configFiles: new ConfigFiles({
|
|
55982
57347
|
projects,
|
|
55983
57348
|
home: userHome,
|
|
@@ -56014,6 +57379,28 @@ function defaultImageEngines(secrets) {
|
|
|
56014
57379
|
openai: { apiKey: key("openai.apiKey") }
|
|
56015
57380
|
});
|
|
56016
57381
|
}
|
|
57382
|
+
/** npm self-update for `codeden`, or the desktop app's own updater when it hosts the server. */
|
|
57383
|
+
function createUpdates(opts) {
|
|
57384
|
+
const common = {
|
|
57385
|
+
mode: () => opts.settings.get().updates.mode,
|
|
57386
|
+
checksDisabled: process.env.CODEDEN_UPDATE_CHECK === "0",
|
|
57387
|
+
notes: notesSources(),
|
|
57388
|
+
fetch: opts.updates?.fetch,
|
|
57389
|
+
intervalMs: opts.updates?.intervalMs,
|
|
57390
|
+
log: opts.log
|
|
57391
|
+
};
|
|
57392
|
+
const desktop = opts.selfUpdate?.desktop;
|
|
57393
|
+
if (desktop) return new DesktopUpdateService({
|
|
57394
|
+
...common,
|
|
57395
|
+
desktop
|
|
57396
|
+
});
|
|
57397
|
+
return new UpdateService({
|
|
57398
|
+
...common,
|
|
57399
|
+
home: opts.paths.home,
|
|
57400
|
+
host: opts.selfUpdate,
|
|
57401
|
+
registry: updateRegistry()
|
|
57402
|
+
});
|
|
57403
|
+
}
|
|
56017
57404
|
// ../server/src/services/secrets.ts
|
|
56018
57405
|
/** In-memory store for tests and machines without a usable keychain. */
|
|
56019
57406
|
var MemorySecretStore = class {
|
|
@@ -56140,7 +57527,8 @@ async function startServer(opts) {
|
|
|
56140
57527
|
const runnerStatus = () => runner?.status ?? disabledStatus;
|
|
56141
57528
|
const hub = new WsHub(store, database.db, runnerStatus, {
|
|
56142
57529
|
flushMs: opts.realtimeFlushMs,
|
|
56143
|
-
updateStatus: () => services.updates.snapshot()
|
|
57530
|
+
updateStatus: () => services.updates.snapshot(),
|
|
57531
|
+
artifacts: { artifactForPath: (path) => services.artifacts.artifactForPath(path) }
|
|
56144
57532
|
});
|
|
56145
57533
|
const secrets = opts.secrets === "memory" ? new MemorySecretStore() : opts.secrets ?? await createSecretStore(log);
|
|
56146
57534
|
const builtin = opts.builtinProviders ?? process.env.CODEDEN_BUILTIN_PROVIDERS !== "0";
|
|
@@ -56158,12 +57546,14 @@ async function startServer(opts) {
|
|
|
56158
57546
|
askUserWaitMs: opts.askUserWaitMs,
|
|
56159
57547
|
imageEngines: opts.imageEngines ?? (builtin ? void 0 : new ImageEngineRegistry([])),
|
|
56160
57548
|
voice: opts.voice,
|
|
57549
|
+
localStt: opts.localStt,
|
|
56161
57550
|
orchestration: opts.orchestration,
|
|
56162
57551
|
github: opts.github,
|
|
56163
57552
|
automations: opts.automations,
|
|
56164
57553
|
factory: opts.factory,
|
|
56165
57554
|
selfUpdate: opts.selfUpdate,
|
|
56166
57555
|
updates: opts.updates,
|
|
57556
|
+
worktreeCleanup: opts.worktreeCleanup,
|
|
56167
57557
|
log
|
|
56168
57558
|
});
|
|
56169
57559
|
services.images.refresh();
|
|
@@ -56173,6 +57563,7 @@ async function startServer(opts) {
|
|
|
56173
57563
|
services.usage.start();
|
|
56174
57564
|
services.automations.start();
|
|
56175
57565
|
services.factory.start();
|
|
57566
|
+
services.worktreeCleanup.start();
|
|
56176
57567
|
const stopUpdateEvents = services.updates.onChange((status) => hub.broadcastUpdate(status));
|
|
56177
57568
|
services.updates.start();
|
|
56178
57569
|
let origins = /* @__PURE__ */ new Set();
|
|
@@ -56206,7 +57597,7 @@ async function startServer(opts) {
|
|
|
56206
57597
|
runner: services.runner,
|
|
56207
57598
|
cwdFor: (sessionId) => {
|
|
56208
57599
|
const session = sessionId ? services.sessionRepo.find(sessionId)?.session : void 0;
|
|
56209
|
-
return session ? session
|
|
57600
|
+
return session ? sessionWorkdir(session) : paths.home;
|
|
56210
57601
|
},
|
|
56211
57602
|
log
|
|
56212
57603
|
});
|
|
@@ -56288,7 +57679,9 @@ async function startServer(opts) {
|
|
|
56288
57679
|
services.usage.stop();
|
|
56289
57680
|
services.automations.stop();
|
|
56290
57681
|
services.factory.stop();
|
|
57682
|
+
services.worktreeCleanup.stop();
|
|
56291
57683
|
services.updates.stop();
|
|
57684
|
+
await services.localStt.stop();
|
|
56292
57685
|
stopUpdateEvents();
|
|
56293
57686
|
stopTerminalEvents();
|
|
56294
57687
|
if (runner) {
|
|
@@ -56706,6 +58099,8 @@ async function stopRunner(lockFile, timeoutMs = 1e4) {
|
|
|
56706
58099
|
return lock.pid;
|
|
56707
58100
|
}
|
|
56708
58101
|
// src/stop.ts
|
|
58102
|
+
/** How long to keep watching for a runner the server was starting as it shut down. */
|
|
58103
|
+
const LATE_RUNNER_MS = 2e3;
|
|
56709
58104
|
async function waitForExit(pid, timeoutMs) {
|
|
56710
58105
|
const deadline = Date.now() + timeoutMs;
|
|
56711
58106
|
while (Date.now() < deadline) {
|
|
@@ -56740,6 +58135,17 @@ async function stop(home) {
|
|
|
56740
58135
|
if (!await waitForExit(runner.pid, 5e3)) await stopRunner(paths.runnerLock);
|
|
56741
58136
|
console.log(`Stopped runner (pid ${runner.pid}).`);
|
|
56742
58137
|
}
|
|
58138
|
+
const deadline = Date.now() + LATE_RUNNER_MS;
|
|
58139
|
+
while (Date.now() < deadline) {
|
|
58140
|
+
const late = readLiveRunnerLock(paths.runnerLock);
|
|
58141
|
+
if (late && late.pid !== runner?.pid) {
|
|
58142
|
+
await stopRunner(paths.runnerLock);
|
|
58143
|
+
console.log(`Stopped runner (pid ${late.pid}).`);
|
|
58144
|
+
break;
|
|
58145
|
+
}
|
|
58146
|
+
if (!late && !server) break;
|
|
58147
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
58148
|
+
}
|
|
56743
58149
|
return 0;
|
|
56744
58150
|
}
|
|
56745
58151
|
// src/update-command.ts
|
|
@@ -57009,4 +58415,4 @@ main().then((code) => process.exit(code), (error) => {
|
|
|
57009
58415
|
});
|
|
57010
58416
|
export {};
|
|
57011
58417
|
|
|
57012
|
-
//# sourceMappingURL=main-
|
|
58418
|
+
//# sourceMappingURL=main-pXwL7GDQ.js.map
|