codeden 0.4.4 → 0.5.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 +3 -3
- package/dist/cli.js +1 -1
- package/dist/drizzle/20261110154932_notice_not_before/migration.sql +1 -0
- package/dist/drizzle/20261110154932_notice_not_before/snapshot.json +3484 -0
- package/dist/{main-CKLHi4sz.js → main-BQwSt6Az.js} +2783 -691
- package/dist/main-BQwSt6Az.js.map +1 -0
- package/dist/{main-D335_24e.js → main-Drh58zq6.js} +2 -2
- package/dist/{main-D335_24e.js.map → main-Drh58zq6.js.map} +1 -1
- package/dist/{runner-DjavcMnG.js → runner-WAbCC8k2.js} +3321 -513
- package/dist/runner-WAbCC8k2.js.map +1 -0
- package/dist/runner.js +1 -1
- package/dist/templates/automations/issue-triage.md +1 -1
- package/dist/web/assets/{core-BFbrQR07.js → core-aeJ3srYS.js} +1 -1
- package/dist/web/assets/engine-oniguruma-CyGcXvAw.js +1 -0
- package/dist/web/assets/{highlighted-body-JLMTFACH-C9Ii0196.js → highlighted-body-JLMTFACH-NhDHKomc.js} +1 -1
- package/dist/web/assets/{highlights-C4aNCgJw.js → highlights-af6Fohu4.js} +1 -1
- package/dist/web/assets/index-BlGTOYYh.css +1 -0
- package/dist/web/assets/{index-WLihS360.js → index-iad71iug.js} +194 -194
- package/dist/web/assets/langs-DEIJuibn.js +1 -0
- package/dist/web/assets/mermaid-WFAXDIZX-RdnNjs5C.js +1 -0
- package/dist/web/assets/{shiki-MNiRnTih.js → shiki-Do52T6bJ.js} +2 -2
- package/dist/web/assets/{stream-tokenizer-BOiamYv-.js → stream-tokenizer-DFPcXHG7.js} +1 -1
- package/dist/web/assets/{themes-DPD-gG-5.js → themes-B76yFk4c.js} +2 -2
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/main-CKLHi4sz.js.map +0 -1
- package/dist/runner-DjavcMnG.js.map +0 -1
- package/dist/web/assets/engine-oniguruma-Dod-24Yp.js +0 -1
- package/dist/web/assets/index-BIBpPpjX.css +0 -1
- package/dist/web/assets/langs-X885YKmd.js +0 -1
- package/dist/web/assets/mermaid-WFAXDIZX-Dh9Bmb5R.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 SetNetworkPasswordResponse, $n as SessionOptions, $t as speechExcerpt, A as findExecutable, An as PutVisionRequest, Ar as intersection, At as UpdateAutomationProjectSettingsRequest, B as RunnerToServerMessage, Bn as TriagePolicy, Br as unknown, Bt as SecretKey, C as importClaudeSdkFrom, Cn as FactoryAllowLargeChangeRequest, Cr as _enum, Ct as ListAutomationsQuery, D as resolveNpm, Dn as FactoryTuning, Dr as custom, Dt as TriageDecisionRequest, E as runNpm, En as FactoryTriageRequest, Er as boolean, Et as StarterSetRequest, F as CODEDEN_VERSION, Fn as Automation, Fr as optional, Ft as ListInboxQuery, G as ResolveConflictRequest, Gn as GitCommitRequest, Gt as SteerRequest, H as ArchiveSessionRequest, Hn as SessionStatus, Hr as toJSONSchema, Ht as SessionStartMeta, I as UsageQuery, In as AutomationAction, Ir as preprocess, It as ListSessionsQuery, J as NetworkStatus, Jn as SessionResult, Jt as UpdateProjectRequest, K as RoleMessageRequest, Kn as GitPushRequest, Kt as ThreadItemsQuery, L as UsageSettings, Ln as AutomationGuards, Lr as record, Lt as ReorderProjectsRequest, M as ownNodeEnv, Mn as SetAutonomyModeRequest, Mr as looseObject, Mt as CreateProjectRequest, N as import_websocket_server, Nn as isFactoryMode, Nr as number, Nt as FileReadQuery, O as acquireInstallLock, On as KillSwitchRequest, Or as discriminatedUnion, Ot as TriageReport, P as wrapper_default, Pn as Artifact, Pr as object, Pt as FileWriteRequest, Q as SetNetworkPasswordRequest, Qn as RunnerCommandResults, Qt as Settings, R as SessionFileQuery, Rn as AutomationRun, Rr as string, Rt as ResolveInboxRequest, S as CLAUDE_SDK_VERSION, Sn as DiscoveryArea, Sr as datetime, St as ListAutomationRunsQuery, T as isClaudeSdkInstalled, Tn as FactoryDecisionRequest, Tr as array, Tt as SetAutomationEnabledRequest, U as ListConflictsQuery, Un as DiffScope, Ur as safeParse$1, Ut as SetSecretRequest, V as ServerLock, Vn as Session, Vr as ZodError, Vt as SendMessageRequest, W as ListNoticesQuery, Wn as EditQueuedMessageRequest, Wr as prettifyError, Wt as StartSessionRequest, X as RemoteAccessFile, Xn as TerminalClientMessage, Xt as agentEventToNewEvent, Y as PASSWORD_RULE_TEXT, Yn as SpeechSummaryRequest, Yt as HealthResponse, Z as SetNetworkAccessRequest, Zn as TerminalQuery, Zt as READ_ALOUD_FULL_MAX_CHARS, _ as ImageGenerationError, _n as GithubPr, _r as McpServerTarget, _t as ImportAutomationsRequest, a as Hono, an as UpdateInstallRequest, ar as sortProjects, at as BrowserClientMessage, b as mainRepoRoot, bn as githubPrId, br as ProviderId, bt as AutomationProjectRequest, c as createAdaptorServer, cn as isNewerVersion, cr as InboxItem, ct as BrowserNavigateRequest, d as defaultShell, dn as NoticeBatchData, dr as AgentConfigDeleteQuery, dt as BrowserViewportRequest, en as LOCAL_STT_SAMPLE_RATE, er as ProjectConfigFile, et as SignInRequest, f as shellCommandLine, fn as CreateGithubIssueRequest, fr as AgentConfigDiffRequest, ft as BrowserWsQuery, g as resolveImageOutput, gn as GithubListQuery, gr as McpServerDiffRequest, gt as ExportAutomationsQuery, h as IMAGE_ASPECT_RATIOS, hn as GithubLabelRequest, hr as AgentConfigWriteRequest, ht as DuplicateAutomationRequest, i as killTree$1, in as AppPointer, ir as Project, it as summarizeActivity, j as isElectronRuntime, jn as ReviewVerdict, jr as literal, jt as UpdateAutomationRequest, k as brokenInstallMessage, kn as LEGACY_SIZE_LIMITS, kr as int, kt as UpcomingRunsQuery, l as upgradeWebSocket, ln as ConflictRecord, lr as InboxItemKind, lt as BrowserNewTabRequest, m as createImageEngineRegistry, mn as GithubIssue, mr as AgentConfigLocator, mt as AutomationImportSource, n as readLock, nn as SttQuery, nr as StartedBy, nt as ImageTestRequest, o as tryDecodeURIComponent, on as UpdateStatus, or as DEFAULT_VISION_PATH, ot as BrowserDevServerRequest, p as ImageEngineRegistry, pn as GithubCommentRequest, pr as AgentConfigListQuery, pt as AUTOMATION_IMPORT_LIMITS, q as UpdateSessionRequest, qn as RestoreFilesRequest, qt as UiClientMessage, r as isAlive, rn as TtsRequest, rr as WorkKey, rt as BrowserHostClientMessage, s as bufferToFormData, sn as compareVersions, sr as FactoryLimits, st as BrowserHistoryRequest, tn as LocalSttModelParams, tr as ConflictDecision, tt as passwordProblem, u as refreshProcessPath, un as NOTICE_BATCH_CODE, ur as messageFrom, ut as BrowserOpenRequest, v as claudeMemoryDir, vn as MergePrRequest, vr as McpServerUpsertRequest, vt as AutomationFileFrontmatter, w as installClaudeSdk, wn as FactoryCandidate, wr as _null, wt as RunAutomationRequest, x as claudeSdkSource, xn as parseGithubPrId, xr as ReasoningEffort, xt as CreateAutomationRequest, y as findGitRoot, yn as TriageIssueRequest, yr as AutonomyLevel, yt as AutomationGuardsPatch, z as RunnerLock, zn as AutomationTrigger, zr as union$1, zt as RespondRequest } from "./runner-WAbCC8k2.js";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import fs, { accessSync, appendFileSync, closeSync, constants, copyFileSync, cpSync, createReadStream, existsSync, mkdirSync, openSync, readFileSync, readSync, readdirSync, realpathSync, renameSync, rmSync, statSync, watch, writeFileSync } from "node:fs";
|
|
5
5
|
import { homedir, networkInterfaces, tmpdir } from "node:os";
|
|
@@ -7856,7 +7856,9 @@ const notices = sqliteTable("notices", {
|
|
|
7856
7856
|
sessionId: text("session_id"),
|
|
7857
7857
|
notice: json("notice").notNull(),
|
|
7858
7858
|
createdAt: text("created_at").notNull(),
|
|
7859
|
-
deliveredAt: text("delivered_at")
|
|
7859
|
+
deliveredAt: text("delivered_at"),
|
|
7860
|
+
/** Not delivered before this time (a scheduled wake-up); null: at once. */
|
|
7861
|
+
notBefore: text("not_before")
|
|
7860
7862
|
}, (t) => [index("notices_target").on(t.target, t.projectId, t.deliveredAt), index("notices_created").on(t.createdAt)]);
|
|
7861
7863
|
/** Conflict details; the status lives on the `conflict` inbox item with the same id. */
|
|
7862
7864
|
const conflicts = sqliteTable("conflicts", {
|
|
@@ -8194,7 +8196,8 @@ function requestResolution(item) {
|
|
|
8194
8196
|
};
|
|
8195
8197
|
return {
|
|
8196
8198
|
status: item.status,
|
|
8197
|
-
...item.answers ? { answers: item.answers } : {}
|
|
8199
|
+
...item.answers ? { answers: item.answers } : {},
|
|
8200
|
+
...item.decidedBy ? { decidedBy: item.decidedBy } : {}
|
|
8198
8201
|
};
|
|
8199
8202
|
}
|
|
8200
8203
|
/** Close an open inbox item; `completed`/`declined` resolve it, anything else dismisses it. */
|
|
@@ -9539,7 +9542,8 @@ function applyOrchestrationProjection(tx, e) {
|
|
|
9539
9542
|
sessionId: notice.sessionId,
|
|
9540
9543
|
notice,
|
|
9541
9544
|
createdAt: notice.createdAt,
|
|
9542
|
-
deliveredAt: notice.deliveredAt
|
|
9545
|
+
deliveredAt: notice.deliveredAt,
|
|
9546
|
+
notBefore: notice.notBefore ?? null
|
|
9543
9547
|
};
|
|
9544
9548
|
tx.insert(notices).values(row).onConflictDoUpdate({
|
|
9545
9549
|
target: notices.id,
|
|
@@ -9562,6 +9566,21 @@ function applyOrchestrationProjection(tx, e) {
|
|
|
9562
9566
|
}
|
|
9563
9567
|
return;
|
|
9564
9568
|
}
|
|
9569
|
+
case "notice.returned": {
|
|
9570
|
+
if (e.payload.noticeIds.length === 0) return;
|
|
9571
|
+
const rows = tx.select().from(notices).where(inArray(notices.id, e.payload.noticeIds)).all();
|
|
9572
|
+
for (const row of rows) {
|
|
9573
|
+
if (!row.deliveredAt) continue;
|
|
9574
|
+
tx.update(notices).set({
|
|
9575
|
+
deliveredAt: null,
|
|
9576
|
+
notice: {
|
|
9577
|
+
...row.notice,
|
|
9578
|
+
deliveredAt: null
|
|
9579
|
+
}
|
|
9580
|
+
}).where(eq(notices.id, row.id)).run();
|
|
9581
|
+
}
|
|
9582
|
+
return;
|
|
9583
|
+
}
|
|
9565
9584
|
case "conflict.created": {
|
|
9566
9585
|
const { conflict } = e.payload;
|
|
9567
9586
|
tx.insert(conflicts).values({
|
|
@@ -9669,6 +9688,17 @@ function applyUsageReport(tx, report) {
|
|
|
9669
9688
|
}
|
|
9670
9689
|
}
|
|
9671
9690
|
}
|
|
9691
|
+
// ../server/src/events/reducer/retry.ts
|
|
9692
|
+
/**
|
|
9693
|
+
* Whether a failed root turn is left for automatic retry to decide (`AutoRetry`): an agent
|
|
9694
|
+
* session's turn that failed for a passing reason (`transient`, or a `usage_limit`). The
|
|
9695
|
+
* reducer leaves such a session idle with no inbox item; the auto-retry service then either
|
|
9696
|
+
* schedules the retry or fails the session the usual way. Orchestrator and PM sessions, and
|
|
9697
|
+
* every other failure, fail at once. Pure.
|
|
9698
|
+
*/
|
|
9699
|
+
function awaitsRetryDecision(session, error) {
|
|
9700
|
+
return session.role === "agent" && (error.kind === "transient" || error.kind === "usage_limit");
|
|
9701
|
+
}
|
|
9672
9702
|
// ../server/src/events/reducer/usage.ts
|
|
9673
9703
|
/** Fields that add up across turns. */
|
|
9674
9704
|
const ADDITIVE$1 = [
|
|
@@ -9824,6 +9854,11 @@ function onTurnEnded(d, sessionId, event, ts) {
|
|
|
9824
9854
|
if (event.type === "turn.completed") setStatus(d, sessionId, "idle", ts);
|
|
9825
9855
|
else if (event.type === "turn.interrupted") setStatus(d, sessionId, event.reason === "runner_restart" ? "interrupted" : "idle", ts);
|
|
9826
9856
|
else {
|
|
9857
|
+
const current = d.getSession(sessionId)?.session;
|
|
9858
|
+
if (current && awaitsRetryDecision(current, event.error)) {
|
|
9859
|
+
setStatus(d, sessionId, "idle", ts);
|
|
9860
|
+
return;
|
|
9861
|
+
}
|
|
9827
9862
|
setStatus(d, sessionId, "failed", ts);
|
|
9828
9863
|
const session = d.getSession(sessionId)?.session;
|
|
9829
9864
|
if (session) {
|
|
@@ -10088,6 +10123,7 @@ function applyProjection(tx, e) {
|
|
|
10088
10123
|
return [];
|
|
10089
10124
|
case "notice.created":
|
|
10090
10125
|
case "notice.delivered":
|
|
10126
|
+
case "notice.returned":
|
|
10091
10127
|
case "conflict.created":
|
|
10092
10128
|
applyOrchestrationProjection(tx, e);
|
|
10093
10129
|
return [];
|
|
@@ -21265,7 +21301,7 @@ function createMcpHandler(deps) {
|
|
|
21265
21301
|
const FREE_FAILURES = 3;
|
|
21266
21302
|
/** First wait once the free failures are used up; it doubles with each failure. */
|
|
21267
21303
|
const BASE_DELAY_MS = 1e3;
|
|
21268
|
-
const MAX_DELAY_MS = 3e4;
|
|
21304
|
+
const MAX_DELAY_MS$1 = 3e4;
|
|
21269
21305
|
/** This many failures inside the window lock the address out. */
|
|
21270
21306
|
const LOCKOUT_FAILURES = 10;
|
|
21271
21307
|
const WINDOW_MS = 9e5;
|
|
@@ -21285,7 +21321,7 @@ var SignInLimiter = class SignInLimiter {
|
|
|
21285
21321
|
/** Wait required before the `failures`-th failure may be followed by another attempt. */
|
|
21286
21322
|
static delayAfter(failures) {
|
|
21287
21323
|
if (failures < FREE_FAILURES) return 0;
|
|
21288
|
-
return Math.min(MAX_DELAY_MS, BASE_DELAY_MS * 2 ** (failures - FREE_FAILURES));
|
|
21324
|
+
return Math.min(MAX_DELAY_MS$1, BASE_DELAY_MS * 2 ** (failures - FREE_FAILURES));
|
|
21289
21325
|
}
|
|
21290
21326
|
/** May `address` try a password now? */
|
|
21291
21327
|
check(address) {
|
|
@@ -21593,6 +21629,26 @@ function redetectProviders(runner, log) {
|
|
|
21593
21629
|
log?.(`runner: provider re-detect failed: ${error instanceof Error ? error.message : error}`);
|
|
21594
21630
|
});
|
|
21595
21631
|
}
|
|
21632
|
+
/** Model discovery starts a provider CLI (up to 30 s); detection runs first. */
|
|
21633
|
+
const REFRESH_MODELS_TIMEOUT_MS = 9e4;
|
|
21634
|
+
/**
|
|
21635
|
+
* "Refresh models": re-detect one provider and wait until it has listed its models again.
|
|
21636
|
+
* Resolves with every provider's list after the refresh (the runner also broadcasts it, so
|
|
21637
|
+
* other clients update as well).
|
|
21638
|
+
*/
|
|
21639
|
+
async function refreshProviderModels(runner, providerId) {
|
|
21640
|
+
if (!runner.isConnected()) throw new ServiceError(503, "runner_unavailable", "The runner is not connected");
|
|
21641
|
+
if (!runner.providers().some((p) => p.id === providerId)) throw new ServiceError(404, "unknown_provider", `No provider "${providerId}" is loaded`);
|
|
21642
|
+
try {
|
|
21643
|
+
return { providers: (await runner.request({
|
|
21644
|
+
name: "provider.detect",
|
|
21645
|
+
providerId,
|
|
21646
|
+
waitForModels: true
|
|
21647
|
+
}, REFRESH_MODELS_TIMEOUT_MS)).providers ?? runner.providers() };
|
|
21648
|
+
} catch (error) {
|
|
21649
|
+
throw toServiceError$1(error);
|
|
21650
|
+
}
|
|
21651
|
+
}
|
|
21596
21652
|
/** `RunnerPort` over the server's (possibly absent) `RunnerClient`. */
|
|
21597
21653
|
function runnerPort(get) {
|
|
21598
21654
|
return {
|
|
@@ -21619,6 +21675,175 @@ function runnerPort(get) {
|
|
|
21619
21675
|
}
|
|
21620
21676
|
};
|
|
21621
21677
|
}
|
|
21678
|
+
// ../server/src/services/browser/agent-activity.ts
|
|
21679
|
+
/** How long the "Agent is acting" signal lingers after the last agent browser command. */
|
|
21680
|
+
const AGENT_ACTIVITY_DECAY_MS = 2500;
|
|
21681
|
+
/**
|
|
21682
|
+
* Tracks when agents drive a session's browser (§12.2): a session is "acting" while an agent
|
|
21683
|
+
* `browser.*` command runs and for `decayMs` after the last one ends, so a burst of tool calls
|
|
21684
|
+
* reads as one stretch of activity. Listeners hear only transitions.
|
|
21685
|
+
*/
|
|
21686
|
+
var AgentActivity = class {
|
|
21687
|
+
decayMs;
|
|
21688
|
+
sessions = /* @__PURE__ */ new Map();
|
|
21689
|
+
listeners = /* @__PURE__ */ new Set();
|
|
21690
|
+
constructor(decayMs = AGENT_ACTIVITY_DECAY_MS) {
|
|
21691
|
+
this.decayMs = decayMs;
|
|
21692
|
+
}
|
|
21693
|
+
onChange(listener) {
|
|
21694
|
+
this.listeners.add(listener);
|
|
21695
|
+
return () => this.listeners.delete(listener);
|
|
21696
|
+
}
|
|
21697
|
+
isActing(sessionId) {
|
|
21698
|
+
return this.sessions.get(sessionId)?.acting ?? false;
|
|
21699
|
+
}
|
|
21700
|
+
/** Run an agent command, marking the session acting around it. */
|
|
21701
|
+
async track(sessionId, run) {
|
|
21702
|
+
this.begin(sessionId);
|
|
21703
|
+
try {
|
|
21704
|
+
return await run();
|
|
21705
|
+
} finally {
|
|
21706
|
+
this.end(sessionId);
|
|
21707
|
+
}
|
|
21708
|
+
}
|
|
21709
|
+
begin(sessionId) {
|
|
21710
|
+
const s = this.sessions.get(sessionId) ?? {
|
|
21711
|
+
inFlight: 0,
|
|
21712
|
+
acting: false
|
|
21713
|
+
};
|
|
21714
|
+
this.sessions.set(sessionId, s);
|
|
21715
|
+
s.inFlight += 1;
|
|
21716
|
+
clearTimeout(s.timer);
|
|
21717
|
+
s.timer = void 0;
|
|
21718
|
+
this.set(sessionId, s, true);
|
|
21719
|
+
}
|
|
21720
|
+
end(sessionId) {
|
|
21721
|
+
const s = this.sessions.get(sessionId);
|
|
21722
|
+
if (!s) return;
|
|
21723
|
+
s.inFlight = Math.max(0, s.inFlight - 1);
|
|
21724
|
+
if (s.inFlight > 0) return;
|
|
21725
|
+
clearTimeout(s.timer);
|
|
21726
|
+
s.timer = setTimeout(() => {
|
|
21727
|
+
s.timer = void 0;
|
|
21728
|
+
this.set(sessionId, s, false);
|
|
21729
|
+
this.sessions.delete(sessionId);
|
|
21730
|
+
}, this.decayMs);
|
|
21731
|
+
s.timer.unref?.();
|
|
21732
|
+
}
|
|
21733
|
+
set(sessionId, s, acting) {
|
|
21734
|
+
if (s.acting === acting) return;
|
|
21735
|
+
s.acting = acting;
|
|
21736
|
+
for (const l of this.listeners) l(sessionId, acting);
|
|
21737
|
+
}
|
|
21738
|
+
};
|
|
21739
|
+
// ../server/src/services/browser/browser-host.ts
|
|
21740
|
+
const ATTACH_TIMEOUT_MS = 1e4;
|
|
21741
|
+
/**
|
|
21742
|
+
* The desktop app's offer of native browser views (§17 Phase 2), over `/ws/browser-host`
|
|
21743
|
+
* (loopback only, install token). One app runs per home, so the latest app to connect is the
|
|
21744
|
+
* host. The server asks it to `attach` a session and gets that session's relay back; when the
|
|
21745
|
+
* app quits it hands its open sessions to the runner through here.
|
|
21746
|
+
*/
|
|
21747
|
+
var BrowserHostLink = class {
|
|
21748
|
+
deps;
|
|
21749
|
+
host = null;
|
|
21750
|
+
waiting = /* @__PURE__ */ new Map();
|
|
21751
|
+
constructor(deps) {
|
|
21752
|
+
this.deps = deps;
|
|
21753
|
+
}
|
|
21754
|
+
/** A desktop app is connected and can host native views. */
|
|
21755
|
+
get available() {
|
|
21756
|
+
return this.host !== null && this.host.ws.readyState === 1;
|
|
21757
|
+
}
|
|
21758
|
+
/** Ask the app for a session's native browser; rejects when no app answers in time. */
|
|
21759
|
+
attach(sessionId, timeoutMs = ATTACH_TIMEOUT_MS) {
|
|
21760
|
+
const ws = this.host?.ws;
|
|
21761
|
+
if (ws?.readyState !== 1) return Promise.reject(/* @__PURE__ */ new Error("No desktop app is connected"));
|
|
21762
|
+
const id = randomUUID();
|
|
21763
|
+
return new Promise((resolve, reject) => {
|
|
21764
|
+
const timer = setTimeout(() => {
|
|
21765
|
+
this.waiting.delete(id);
|
|
21766
|
+
reject(/* @__PURE__ */ new Error("The desktop app did not answer"));
|
|
21767
|
+
}, timeoutMs);
|
|
21768
|
+
timer.unref?.();
|
|
21769
|
+
this.waiting.set(id, {
|
|
21770
|
+
resolve,
|
|
21771
|
+
reject,
|
|
21772
|
+
timer
|
|
21773
|
+
});
|
|
21774
|
+
send$2(ws, {
|
|
21775
|
+
type: "attach",
|
|
21776
|
+
id,
|
|
21777
|
+
sessionId
|
|
21778
|
+
});
|
|
21779
|
+
});
|
|
21780
|
+
}
|
|
21781
|
+
handlers() {
|
|
21782
|
+
let ws;
|
|
21783
|
+
return {
|
|
21784
|
+
onOpen: (_event, socket) => {
|
|
21785
|
+
ws = socket;
|
|
21786
|
+
},
|
|
21787
|
+
onMessage: (event) => {
|
|
21788
|
+
if (!ws) return;
|
|
21789
|
+
let msg;
|
|
21790
|
+
try {
|
|
21791
|
+
msg = BrowserHostClientMessage.parse(JSON.parse(String(event.data)));
|
|
21792
|
+
} catch {
|
|
21793
|
+
return;
|
|
21794
|
+
}
|
|
21795
|
+
this.handle(ws, msg);
|
|
21796
|
+
},
|
|
21797
|
+
onClose: () => {
|
|
21798
|
+
if (ws && this.host?.ws === ws) {
|
|
21799
|
+
this.host = null;
|
|
21800
|
+
this.deps.log?.("browser host: the desktop app disconnected");
|
|
21801
|
+
for (const [id, w] of this.waiting) {
|
|
21802
|
+
clearTimeout(w.timer);
|
|
21803
|
+
w.reject(/* @__PURE__ */ new Error("The desktop app disconnected"));
|
|
21804
|
+
this.waiting.delete(id);
|
|
21805
|
+
}
|
|
21806
|
+
}
|
|
21807
|
+
}
|
|
21808
|
+
};
|
|
21809
|
+
}
|
|
21810
|
+
async handle(ws, msg) {
|
|
21811
|
+
switch (msg.type) {
|
|
21812
|
+
case "hello":
|
|
21813
|
+
if (this.host && this.host.ws !== ws) this.host.ws.close(1e3, "replaced");
|
|
21814
|
+
this.host = {
|
|
21815
|
+
ws,
|
|
21816
|
+
version: msg.version
|
|
21817
|
+
};
|
|
21818
|
+
this.deps.log?.(`browser host: desktop app ${msg.version} offers native views`);
|
|
21819
|
+
return;
|
|
21820
|
+
case "attached":
|
|
21821
|
+
case "failed": {
|
|
21822
|
+
const w = this.waiting.get(msg.id);
|
|
21823
|
+
if (!w) return;
|
|
21824
|
+
this.waiting.delete(msg.id);
|
|
21825
|
+
clearTimeout(w.timer);
|
|
21826
|
+
if (msg.type === "attached") w.resolve(msg.relay);
|
|
21827
|
+
else w.reject(new Error(msg.message));
|
|
21828
|
+
return;
|
|
21829
|
+
}
|
|
21830
|
+
case "handoff": {
|
|
21831
|
+
const failed = await this.deps.handoff(msg.sessions).catch((error) => {
|
|
21832
|
+
this.deps.log?.(`browser host: hand-off failed: ${String(error)}`);
|
|
21833
|
+
return msg.sessions.map((s) => s.sessionId);
|
|
21834
|
+
});
|
|
21835
|
+
send$2(ws, {
|
|
21836
|
+
type: "handoff.done",
|
|
21837
|
+
id: msg.id,
|
|
21838
|
+
failed
|
|
21839
|
+
});
|
|
21840
|
+
}
|
|
21841
|
+
}
|
|
21842
|
+
}
|
|
21843
|
+
};
|
|
21844
|
+
function send$2(ws, msg) {
|
|
21845
|
+
if (ws.readyState === 1) ws.send(JSON.stringify(msg));
|
|
21846
|
+
}
|
|
21622
21847
|
// ../server/src/services/browser/browser-service.ts
|
|
21623
21848
|
/** Navigation and agent actions can wait on slow pages; leave headroom over the runner's 30s. */
|
|
21624
21849
|
const BROWSER_TIMEOUT_MS = 6e4;
|
|
@@ -21630,11 +21855,49 @@ function isBrowserNotOpen(error) {
|
|
|
21630
21855
|
* The session browser (§12.2) for the panel routes, the `/ws/browser` channel and the agent
|
|
21631
21856
|
* tools. Every call names the session it acts for, so nobody can reach another session's
|
|
21632
21857
|
* context; the runner enforces the same isolation.
|
|
21858
|
+
*
|
|
21859
|
+
* Engines: a session's browser opens on native views in the desktop app when the app is
|
|
21860
|
+
* connected, the setting is on and the desktop window or an agent opens it; otherwise on the
|
|
21861
|
+
* runner's Chromium. An open browser keeps its engine until it closes, or until the app quits
|
|
21862
|
+
* and hands it to the runner.
|
|
21633
21863
|
*/
|
|
21634
21864
|
var BrowserService = class {
|
|
21635
21865
|
deps;
|
|
21866
|
+
/** When agents drive each session's browser, for the panel's "Agent is acting" chip. */
|
|
21867
|
+
agentActivity = new AgentActivity();
|
|
21868
|
+
/** The desktop app, when one offers native views. */
|
|
21869
|
+
host;
|
|
21636
21870
|
constructor(deps) {
|
|
21637
21871
|
this.deps = deps;
|
|
21872
|
+
this.host = new BrowserHostLink({
|
|
21873
|
+
handoff: (s) => this.handoff(s),
|
|
21874
|
+
log: deps.log
|
|
21875
|
+
});
|
|
21876
|
+
}
|
|
21877
|
+
/** A `browser.*` command an agent tool sends: like `command`, and marks the agent acting. */
|
|
21878
|
+
agentCommand(command, timeoutMs = BROWSER_TIMEOUT_MS) {
|
|
21879
|
+
const { sessionId } = command;
|
|
21880
|
+
return this.agentActivity.track(sessionId, () => this.command(command, timeoutMs));
|
|
21881
|
+
}
|
|
21882
|
+
/** `navigate` on behalf of an agent tool (marks the agent acting). */
|
|
21883
|
+
agentNavigate(sessionId, url, tabId) {
|
|
21884
|
+
return this.agentActivity.track(sessionId, () => this.navigate(sessionId, url, tabId, { native: true }));
|
|
21885
|
+
}
|
|
21886
|
+
/** Open a dev server for an agent: a new tab, or the first tab of a browser it opens. */
|
|
21887
|
+
agentOpenDevServer(sessionId, port, path) {
|
|
21888
|
+
return this.agentActivity.track(sessionId, async () => {
|
|
21889
|
+
if ((await this.tabs(sessionId)).open) return this.command({
|
|
21890
|
+
name: "browser.devServer",
|
|
21891
|
+
sessionId,
|
|
21892
|
+
port,
|
|
21893
|
+
path
|
|
21894
|
+
});
|
|
21895
|
+
const url = `http://localhost:${port}${path ?? "/"}`;
|
|
21896
|
+
const { tabs } = await this.open(sessionId, url, { native: true });
|
|
21897
|
+
const tab = tabs.find((t) => t.active) ?? tabs[0];
|
|
21898
|
+
if (!tab) throw new ServiceError(500, "browser_error", "The browser opened without a tab");
|
|
21899
|
+
return { tab };
|
|
21900
|
+
});
|
|
21638
21901
|
}
|
|
21639
21902
|
/** Send a `browser.*` command for an existing session, mapping runner failures. */
|
|
21640
21903
|
async command(command, timeoutMs = BROWSER_TIMEOUT_MS) {
|
|
@@ -21646,27 +21909,31 @@ var BrowserService = class {
|
|
|
21646
21909
|
throw toServiceError$1(error);
|
|
21647
21910
|
}
|
|
21648
21911
|
}
|
|
21649
|
-
async open(sessionId, url) {
|
|
21912
|
+
async open(sessionId, url, opts = {}) {
|
|
21913
|
+
const relay = opts.native ? await this.nativeRelay(sessionId) : void 0;
|
|
21650
21914
|
const result = await this.command({
|
|
21651
21915
|
name: "browser.open",
|
|
21652
21916
|
sessionId,
|
|
21653
|
-
url
|
|
21917
|
+
url,
|
|
21918
|
+
relay
|
|
21654
21919
|
});
|
|
21655
21920
|
return {
|
|
21656
21921
|
cdpUrl: result.cdpUrl,
|
|
21657
|
-
tabs: result.tabs ?? []
|
|
21922
|
+
tabs: result.tabs ?? [],
|
|
21923
|
+
engine: result.engine ?? "runner"
|
|
21658
21924
|
};
|
|
21659
21925
|
}
|
|
21660
21926
|
/** The session's tabs; `open: false` when it has no browser yet (never throws for that). */
|
|
21661
21927
|
async tabs(sessionId) {
|
|
21662
21928
|
try {
|
|
21663
|
-
const { tabs } = await this.command({
|
|
21929
|
+
const { tabs, engine } = await this.command({
|
|
21664
21930
|
name: "browser.tabs.list",
|
|
21665
21931
|
sessionId
|
|
21666
21932
|
});
|
|
21667
21933
|
return {
|
|
21668
21934
|
open: true,
|
|
21669
|
-
tabs
|
|
21935
|
+
tabs,
|
|
21936
|
+
engine: engine ?? "runner"
|
|
21670
21937
|
};
|
|
21671
21938
|
} catch (error) {
|
|
21672
21939
|
if (isBrowserNotOpen(error)) return {
|
|
@@ -21677,8 +21944,8 @@ var BrowserService = class {
|
|
|
21677
21944
|
}
|
|
21678
21945
|
}
|
|
21679
21946
|
/** Open the browser if needed, then navigate the active tab. */
|
|
21680
|
-
async navigate(sessionId, url, tabId) {
|
|
21681
|
-
if (!(await this.tabs(sessionId)).open) await this.open(sessionId);
|
|
21947
|
+
async navigate(sessionId, url, tabId, opts = {}) {
|
|
21948
|
+
if (!(await this.tabs(sessionId)).open) await this.open(sessionId, void 0, opts);
|
|
21682
21949
|
return this.command({
|
|
21683
21950
|
name: "browser.navigate",
|
|
21684
21951
|
sessionId,
|
|
@@ -21698,7 +21965,92 @@ var BrowserService = class {
|
|
|
21698
21965
|
throw toServiceError$1(error);
|
|
21699
21966
|
}
|
|
21700
21967
|
}
|
|
21968
|
+
/**
|
|
21969
|
+
* The desktop app's relay for a session that has no browser yet, when native views are on
|
|
21970
|
+
* offer; undefined means the runner's Chromium (an open browser keeps its engine).
|
|
21971
|
+
*/
|
|
21972
|
+
async nativeRelay(sessionId) {
|
|
21973
|
+
if (!this.host.available) return void 0;
|
|
21974
|
+
if (this.deps.settings && !this.deps.settings().desktop.nativeBrowser) return void 0;
|
|
21975
|
+
if ((await this.tabs(sessionId)).open) return void 0;
|
|
21976
|
+
try {
|
|
21977
|
+
return await this.host.attach(sessionId);
|
|
21978
|
+
} catch (error) {
|
|
21979
|
+
this.deps.log?.(`browser: native views unavailable, using the runner: ${errorMessage$1(error)}`);
|
|
21980
|
+
return;
|
|
21981
|
+
}
|
|
21982
|
+
}
|
|
21983
|
+
/** The desktop app quits: its open sessions move to the runner's Chromium. */
|
|
21984
|
+
async handoff(sessions) {
|
|
21985
|
+
const failed = [];
|
|
21986
|
+
await Promise.all(sessions.map(async ({ sessionId, tabs, cookies }) => {
|
|
21987
|
+
try {
|
|
21988
|
+
if (!this.deps.repo.find(sessionId)) throw new Error("no such session");
|
|
21989
|
+
await this.deps.runner.request({
|
|
21990
|
+
name: "browser.handoff",
|
|
21991
|
+
sessionId,
|
|
21992
|
+
tabs,
|
|
21993
|
+
cookies
|
|
21994
|
+
}, BROWSER_TIMEOUT_MS);
|
|
21995
|
+
} catch (error) {
|
|
21996
|
+
this.deps.log?.(`browser: hand-off of ${sessionId}: ${errorMessage$1(error)}`);
|
|
21997
|
+
failed.push(sessionId);
|
|
21998
|
+
}
|
|
21999
|
+
}));
|
|
22000
|
+
return failed;
|
|
22001
|
+
}
|
|
21701
22002
|
};
|
|
22003
|
+
// ../server/src/realtime/browser-viewport.ts
|
|
22004
|
+
/**
|
|
22005
|
+
* Decides which viewer's panel size a session's page follows (§12.2), so a phone opening the
|
|
22006
|
+
* panel doesn't resize the page under a desktop viewer:
|
|
22007
|
+
*
|
|
22008
|
+
* - The first viewer to report a size owns the viewport.
|
|
22009
|
+
* - A viewer that claims (it was focused, clicked, or picked a size) becomes the owner.
|
|
22010
|
+
* - The page follows the owner's reports; other viewers' reports are only remembered.
|
|
22011
|
+
* - When the owner leaves, the remaining viewer that claimed most recently takes over
|
|
22012
|
+
* (viewers that never claimed rank by when they first reported).
|
|
22013
|
+
*/
|
|
22014
|
+
var ViewportArbiter = class {
|
|
22015
|
+
reports = /* @__PURE__ */ new Map();
|
|
22016
|
+
owner;
|
|
22017
|
+
clock = 0;
|
|
22018
|
+
get ownerId() {
|
|
22019
|
+
return this.owner;
|
|
22020
|
+
}
|
|
22021
|
+
/** The size the page should have, or undefined when no viewer reported one. */
|
|
22022
|
+
get size() {
|
|
22023
|
+
return this.owner ? this.reports.get(this.owner)?.size : void 0;
|
|
22024
|
+
}
|
|
22025
|
+
get viewers() {
|
|
22026
|
+
return this.reports.size;
|
|
22027
|
+
}
|
|
22028
|
+
/** Record a viewer's size. Returns true when the owner's size (what the page follows) changed. */
|
|
22029
|
+
report(viewerId, size, claim = false) {
|
|
22030
|
+
const before = this.size;
|
|
22031
|
+
const previous = this.reports.get(viewerId);
|
|
22032
|
+
const rank = claim || !previous ? ++this.clock : previous.rank;
|
|
22033
|
+
this.reports.set(viewerId, {
|
|
22034
|
+
size: { ...size },
|
|
22035
|
+
rank
|
|
22036
|
+
});
|
|
22037
|
+
if (claim || this.owner === void 0) this.owner = viewerId;
|
|
22038
|
+
return !sameSize(before, this.size);
|
|
22039
|
+
}
|
|
22040
|
+
/** Forget a viewer. Returns true when the owner's size changed (ownership passed on). */
|
|
22041
|
+
remove(viewerId) {
|
|
22042
|
+
const before = this.size;
|
|
22043
|
+
if (!this.reports.delete(viewerId)) return false;
|
|
22044
|
+
if (this.owner !== viewerId) return false;
|
|
22045
|
+
let next;
|
|
22046
|
+
for (const entry of this.reports) if (!next || entry[1].rank > next[1].rank) next = entry;
|
|
22047
|
+
this.owner = next?.[0];
|
|
22048
|
+
return next !== void 0 && !sameSize(before, this.size);
|
|
22049
|
+
}
|
|
22050
|
+
};
|
|
22051
|
+
function sameSize(a, b) {
|
|
22052
|
+
return a?.width === b?.width && a?.height === b?.height && a?.deviceScaleFactor === b?.deviceScaleFactor;
|
|
22053
|
+
}
|
|
21702
22054
|
// ../server/src/realtime/browser.ts
|
|
21703
22055
|
/** Skip frames for a socket that has this much unsent data (slow client). */
|
|
21704
22056
|
const MAX_BUFFERED_BYTES = 4194304;
|
|
@@ -21711,19 +22063,34 @@ const MAX_BUFFERED_BYTES = 4194304;
|
|
|
21711
22063
|
var BrowserBridge = class {
|
|
21712
22064
|
deps;
|
|
21713
22065
|
viewers = /* @__PURE__ */ new Map();
|
|
22066
|
+
/** Per session: whose panel size the page follows. */
|
|
22067
|
+
arbiters = /* @__PURE__ */ new Map();
|
|
22068
|
+
/** Per session: the viewport the runner last applied (cleared when the browser closes). */
|
|
22069
|
+
applied = /* @__PURE__ */ new Map();
|
|
22070
|
+
applying = /* @__PURE__ */ new Map();
|
|
22071
|
+
/** Per session: what backs its open browser. */
|
|
22072
|
+
engines = /* @__PURE__ */ new Map();
|
|
21714
22073
|
constructor(deps) {
|
|
21715
22074
|
this.deps = deps;
|
|
22075
|
+
deps.browser.agentActivity?.onChange((sessionId, acting) => {
|
|
22076
|
+
for (const v of this.viewers.get(sessionId) ?? []) send$1(v.ws, {
|
|
22077
|
+
type: "agent",
|
|
22078
|
+
acting
|
|
22079
|
+
});
|
|
22080
|
+
});
|
|
21716
22081
|
}
|
|
21717
22082
|
handlers(query) {
|
|
21718
22083
|
let viewer;
|
|
21719
22084
|
return {
|
|
21720
22085
|
onOpen: (_event, ws) => {
|
|
21721
|
-
const { sessionId, open: _open, ...settings } = query;
|
|
22086
|
+
const { sessionId, open: _open, native: nativeQuery, ...settings } = query;
|
|
22087
|
+
const native = nativeQuery === "1";
|
|
21722
22088
|
viewer = {
|
|
21723
22089
|
id: `ui-${randomUUID()}`,
|
|
21724
22090
|
ws,
|
|
21725
22091
|
sessionId,
|
|
21726
|
-
wantsWatch:
|
|
22092
|
+
wantsWatch: !native,
|
|
22093
|
+
native,
|
|
21727
22094
|
watching: false,
|
|
21728
22095
|
open: false,
|
|
21729
22096
|
settings,
|
|
@@ -21753,6 +22120,14 @@ var BrowserBridge = class {
|
|
|
21753
22120
|
viewer.closed = true;
|
|
21754
22121
|
this.remove(viewer);
|
|
21755
22122
|
if (viewer.watching) this.setWatching(viewer, false).catch(() => void 0);
|
|
22123
|
+
const arbiter = this.arbiters.get(viewer.sessionId);
|
|
22124
|
+
if (arbiter) {
|
|
22125
|
+
const owner = arbiter.ownerId;
|
|
22126
|
+
const resized = arbiter.remove(viewer.id);
|
|
22127
|
+
if (arbiter.viewers === 0) this.arbiters.delete(viewer.sessionId);
|
|
22128
|
+
else if (resized) this.applyViewport(viewer.sessionId);
|
|
22129
|
+
else if (owner !== arbiter.ownerId) this.announceViewport(viewer.sessionId);
|
|
22130
|
+
}
|
|
21756
22131
|
}
|
|
21757
22132
|
};
|
|
21758
22133
|
}
|
|
@@ -21769,18 +22144,27 @@ var BrowserBridge = class {
|
|
|
21769
22144
|
}
|
|
21770
22145
|
return;
|
|
21771
22146
|
}
|
|
21772
|
-
if (stream.kind === "browser.tabs")
|
|
21773
|
-
|
|
21774
|
-
|
|
21775
|
-
|
|
22147
|
+
if (stream.kind === "browser.tabs") {
|
|
22148
|
+
const engineChanged = stream.engine !== void 0 && this.engines.get(stream.sessionId) !== stream.engine;
|
|
22149
|
+
if (stream.engine) this.engines.set(stream.sessionId, stream.engine);
|
|
22150
|
+
if (stream.tabs.length === 0 || engineChanged) this.applied.delete(stream.sessionId);
|
|
22151
|
+
for (const v of this.viewers.get(stream.sessionId) ?? []) {
|
|
22152
|
+
if (stream.tabs.length === 0) {
|
|
22153
|
+
if (v.open) this.detach(v);
|
|
22154
|
+
continue;
|
|
22155
|
+
}
|
|
22156
|
+
if (!v.open) {
|
|
22157
|
+
v.open = true;
|
|
22158
|
+
this.watchIfWanted(v, stream.tabs).catch((error) => this.fail(v, error));
|
|
22159
|
+
} else {
|
|
22160
|
+
send$1(v.ws, {
|
|
22161
|
+
type: "tabs",
|
|
22162
|
+
tabs: stream.tabs,
|
|
22163
|
+
engine: this.engines.get(v.sessionId)
|
|
22164
|
+
});
|
|
22165
|
+
if (engineChanged && stream.engine === "runner") this.applyViewport(v.sessionId);
|
|
22166
|
+
}
|
|
21776
22167
|
}
|
|
21777
|
-
if (!v.open) {
|
|
21778
|
-
v.open = true;
|
|
21779
|
-
this.watchIfWanted(v, stream.tabs).catch((error) => this.fail(v, error));
|
|
21780
|
-
} else send$1(v.ws, {
|
|
21781
|
-
type: "tabs",
|
|
21782
|
-
tabs: stream.tabs
|
|
21783
|
-
});
|
|
21784
22168
|
}
|
|
21785
22169
|
}
|
|
21786
22170
|
/** Number of viewers per session (diagnostics and tests). */
|
|
@@ -21789,11 +22173,13 @@ var BrowserBridge = class {
|
|
|
21789
22173
|
}
|
|
21790
22174
|
async start(v, openNow) {
|
|
21791
22175
|
if (openNow) {
|
|
21792
|
-
const opened = await this.deps.browser.open(v.sessionId);
|
|
22176
|
+
const opened = await this.deps.browser.open(v.sessionId, void 0, { native: v.native });
|
|
22177
|
+
this.noteEngine(v.sessionId, opened.engine);
|
|
21793
22178
|
v.open = true;
|
|
21794
22179
|
return this.watchIfWanted(v, opened.tabs);
|
|
21795
22180
|
}
|
|
21796
|
-
const { open, tabs } = await this.deps.browser.tabs(v.sessionId);
|
|
22181
|
+
const { open, tabs, engine } = await this.deps.browser.tabs(v.sessionId);
|
|
22182
|
+
this.noteEngine(v.sessionId, engine);
|
|
21797
22183
|
v.open = v.open || open;
|
|
21798
22184
|
if (!v.open) return this.sendState(v, []);
|
|
21799
22185
|
return this.watchIfWanted(v, tabs);
|
|
@@ -21821,17 +22207,89 @@ var BrowserBridge = class {
|
|
|
21821
22207
|
if (v.watching) await this.setWatching(v, false);
|
|
21822
22208
|
return this.sendState(v, (await browser.tabs(v.sessionId)).tabs);
|
|
21823
22209
|
case "open": {
|
|
21824
|
-
const opened = await browser.open(v.sessionId, msg.url);
|
|
22210
|
+
const opened = await browser.open(v.sessionId, msg.url, { native: v.native });
|
|
22211
|
+
this.noteEngine(v.sessionId, opened.engine);
|
|
21825
22212
|
v.open = true;
|
|
21826
22213
|
return this.watchIfWanted(v, opened.tabs);
|
|
21827
22214
|
}
|
|
22215
|
+
case "viewport": {
|
|
22216
|
+
const { type: _t, claim, ...size } = msg;
|
|
22217
|
+
let arbiter = this.arbiters.get(v.sessionId);
|
|
22218
|
+
if (!arbiter) {
|
|
22219
|
+
arbiter = new ViewportArbiter();
|
|
22220
|
+
this.arbiters.set(v.sessionId, arbiter);
|
|
22221
|
+
}
|
|
22222
|
+
const owner = arbiter.ownerId;
|
|
22223
|
+
if (arbiter.report(v.id, size, claim)) this.applyViewport(v.sessionId);
|
|
22224
|
+
else if (owner !== arbiter.ownerId) this.announceViewport(v.sessionId);
|
|
22225
|
+
return;
|
|
22226
|
+
}
|
|
21828
22227
|
}
|
|
21829
22228
|
}
|
|
21830
22229
|
async watchIfWanted(v, tabs) {
|
|
21831
22230
|
if (v.wantsWatch && !v.watching) await this.setWatching(v, true);
|
|
21832
22231
|
this.sendState(v, tabs);
|
|
22232
|
+
if (v.open) {
|
|
22233
|
+
if (this.applied.has(v.sessionId)) this.announceViewport(v.sessionId, v);
|
|
22234
|
+
else this.applyViewport(v.sessionId);
|
|
22235
|
+
}
|
|
22236
|
+
if (this.deps.browser.agentActivity?.isActing(v.sessionId)) send$1(v.ws, {
|
|
22237
|
+
type: "agent",
|
|
22238
|
+
acting: true
|
|
22239
|
+
});
|
|
22240
|
+
}
|
|
22241
|
+
/**
|
|
22242
|
+
* Bring the runner's viewport in line with the owning viewer's panel. Serialized per session
|
|
22243
|
+
* and always applies the latest size, so a burst of resizes ends at the final one.
|
|
22244
|
+
*/
|
|
22245
|
+
applyViewport(sessionId) {
|
|
22246
|
+
const run = async () => {
|
|
22247
|
+
const size = this.arbiters.get(sessionId)?.size;
|
|
22248
|
+
if (!size) return;
|
|
22249
|
+
const key = `${size.width}x${size.height}@${size.deviceScaleFactor}`;
|
|
22250
|
+
const known = this.applied.get(sessionId);
|
|
22251
|
+
if (known?.key === key && !known.fixed) return this.announceViewport(sessionId);
|
|
22252
|
+
try {
|
|
22253
|
+
const result = await this.deps.browser.command({
|
|
22254
|
+
name: "browser.viewport",
|
|
22255
|
+
sessionId,
|
|
22256
|
+
width: Math.min(size.width, 7680),
|
|
22257
|
+
height: Math.min(size.height, 4320),
|
|
22258
|
+
deviceScaleFactor: size.deviceScaleFactor
|
|
22259
|
+
});
|
|
22260
|
+
this.applied.set(sessionId, {
|
|
22261
|
+
key,
|
|
22262
|
+
width: result.width ?? size.width,
|
|
22263
|
+
height: result.height ?? size.height,
|
|
22264
|
+
deviceScaleFactor: result.deviceScaleFactor ?? size.deviceScaleFactor,
|
|
22265
|
+
fixed: result.fixed === true
|
|
22266
|
+
});
|
|
22267
|
+
this.announceViewport(sessionId);
|
|
22268
|
+
} catch (error) {
|
|
22269
|
+
if (!isBrowserNotOpen(error)) this.deps.log?.(`browser viewport: ${errorMessage$1(error)}`);
|
|
22270
|
+
}
|
|
22271
|
+
};
|
|
22272
|
+
const next = (this.applying.get(sessionId) ?? Promise.resolve()).then(run, run);
|
|
22273
|
+
this.applying.set(sessionId, next);
|
|
22274
|
+
next.finally(() => {
|
|
22275
|
+
if (this.applying.get(sessionId) === next) this.applying.delete(sessionId);
|
|
22276
|
+
});
|
|
22277
|
+
}
|
|
22278
|
+
/** Tell viewers (or one) the applied viewport and whether it follows their panel. */
|
|
22279
|
+
announceViewport(sessionId, only) {
|
|
22280
|
+
const applied = this.applied.get(sessionId);
|
|
22281
|
+
if (!applied) return;
|
|
22282
|
+
const owner = this.arbiters.get(sessionId)?.ownerId;
|
|
22283
|
+
const { key: _k, fixed, ...viewport } = applied;
|
|
22284
|
+
for (const v of only ? [only] : this.viewers.get(sessionId) ?? []) if (v.open) send$1(v.ws, {
|
|
22285
|
+
type: "viewport",
|
|
22286
|
+
...viewport,
|
|
22287
|
+
mine: !fixed && v.id === owner
|
|
22288
|
+
});
|
|
21833
22289
|
}
|
|
21834
22290
|
async setWatching(v, watching) {
|
|
22291
|
+
const before = v.watching;
|
|
22292
|
+
v.watching = watching && !v.closed;
|
|
21835
22293
|
try {
|
|
21836
22294
|
await this.deps.browser.command({
|
|
21837
22295
|
name: "browser.screencast",
|
|
@@ -21840,8 +22298,8 @@ var BrowserBridge = class {
|
|
|
21840
22298
|
watching: watching && !v.closed,
|
|
21841
22299
|
...v.settings
|
|
21842
22300
|
});
|
|
21843
|
-
v.watching = watching && !v.closed;
|
|
21844
22301
|
} catch (error) {
|
|
22302
|
+
v.watching = before;
|
|
21845
22303
|
if (!isBrowserNotOpen(error)) throw error;
|
|
21846
22304
|
v.open = false;
|
|
21847
22305
|
v.watching = false;
|
|
@@ -21859,14 +22317,19 @@ var BrowserBridge = class {
|
|
|
21859
22317
|
this.sendState(v, []);
|
|
21860
22318
|
}
|
|
21861
22319
|
sendState(v, tabs) {
|
|
22320
|
+
const engine = v.open ? this.engines.get(v.sessionId) : void 0;
|
|
21862
22321
|
send$1(v.ws, {
|
|
21863
22322
|
type: "state",
|
|
21864
22323
|
viewerId: v.id,
|
|
21865
22324
|
open: v.open,
|
|
21866
22325
|
watching: v.watching,
|
|
21867
|
-
tabs
|
|
22326
|
+
tabs,
|
|
22327
|
+
engine
|
|
21868
22328
|
});
|
|
21869
22329
|
}
|
|
22330
|
+
noteEngine(sessionId, engine) {
|
|
22331
|
+
if (engine) this.engines.set(sessionId, engine);
|
|
22332
|
+
}
|
|
21870
22333
|
fail(v, error) {
|
|
21871
22334
|
const code = error instanceof ServiceError ? error.code : "browser_error";
|
|
21872
22335
|
send$1(v.ws, {
|
|
@@ -21892,6 +22355,35 @@ function buffered(ws) {
|
|
|
21892
22355
|
function send$1(ws, msg) {
|
|
21893
22356
|
if (ws.readyState === 1) ws.send(JSON.stringify(msg));
|
|
21894
22357
|
}
|
|
22358
|
+
// ../server/src/realtime/browser-routes.ts
|
|
22359
|
+
/** Paths only the machine itself may use, even with network access on (§15.1). */
|
|
22360
|
+
const BROWSER_LOOPBACK_ONLY_PATHS = ["/ws/browser-host"];
|
|
22361
|
+
/**
|
|
22362
|
+
* The browser sockets (§12.2, §17 Phase 2):
|
|
22363
|
+
* - `/ws/browser`: one viewer of one session's browser (frames, tabs, input, viewport);
|
|
22364
|
+
* - `/ws/browser-host`: the desktop app offering native views (loopback only).
|
|
22365
|
+
* Returns the bridge, which also takes the runner's browser stream data.
|
|
22366
|
+
*/
|
|
22367
|
+
function mountBrowserSockets(app, services, log) {
|
|
22368
|
+
const bridge = new BrowserBridge({
|
|
22369
|
+
browser: services.browser,
|
|
22370
|
+
log
|
|
22371
|
+
});
|
|
22372
|
+
app.get("/ws/browser", async (c, next) => {
|
|
22373
|
+
const query = BrowserWsQuery.safeParse(c.req.query());
|
|
22374
|
+
if (!query.success) return c.json({ error: {
|
|
22375
|
+
code: "bad_request",
|
|
22376
|
+
message: "Invalid browser query"
|
|
22377
|
+
} }, 400);
|
|
22378
|
+
if (!services.sessionRepo.find(query.data.sessionId)) return c.json({ error: {
|
|
22379
|
+
code: "session_not_found",
|
|
22380
|
+
message: "No such session"
|
|
22381
|
+
} }, 404);
|
|
22382
|
+
return upgradeWebSocket(() => bridge.handlers(query.data))(c, next);
|
|
22383
|
+
});
|
|
22384
|
+
app.get("/ws/browser-host", upgradeWebSocket(() => services.browser.host.handlers()));
|
|
22385
|
+
return bridge;
|
|
22386
|
+
}
|
|
21895
22387
|
// ../server/src/services/sessions/preview.ts
|
|
21896
22388
|
/** Tool output kept in transcript pages and live updates; the rest is fetched on demand (§6). */
|
|
21897
22389
|
const OUTPUT_PREVIEW_CHARS = 16e3;
|
|
@@ -31819,7 +32311,10 @@ function projectAutomationRoutes(s) {
|
|
|
31819
32311
|
*/
|
|
31820
32312
|
function sessionBrowserRoutes(s) {
|
|
31821
32313
|
const b = s.browser;
|
|
31822
|
-
return new Hono().post("/:id/browser/open", jsonBody(BrowserOpenRequest), async (c) =>
|
|
32314
|
+
return new Hono().post("/:id/browser/open", jsonBody(BrowserOpenRequest), async (c) => {
|
|
32315
|
+
const { url, native } = c.req.valid("json");
|
|
32316
|
+
return c.json(await b.open(c.req.param("id"), url, { native }));
|
|
32317
|
+
}).post("/:id/browser/close", async (c) => {
|
|
31823
32318
|
await b.command({
|
|
31824
32319
|
name: "browser.close",
|
|
31825
32320
|
sessionId: c.req.param("id")
|
|
@@ -32712,6 +33207,13 @@ function setupRoutes(s) {
|
|
|
32712
33207
|
return c.json({ setupCompleted });
|
|
32713
33208
|
});
|
|
32714
33209
|
}
|
|
33210
|
+
/** `/api/providers`: the runner's providers. Their model lists arrive with the runner status. */
|
|
33211
|
+
function providerRoutes(s) {
|
|
33212
|
+
return new Hono().post("/:id/refresh-models", async (c) => {
|
|
33213
|
+
const body = await refreshProviderModels(s.runner, c.req.param("id"));
|
|
33214
|
+
return c.json(body);
|
|
33215
|
+
});
|
|
33216
|
+
}
|
|
32715
33217
|
/** `/api/fs` (folder picker) and `/api/files` (config editors). */
|
|
32716
33218
|
function fileRoutes(s) {
|
|
32717
33219
|
return new Hono().get("/fs/list", queryOf(object({
|
|
@@ -33166,7 +33668,7 @@ function createApi(ctx) {
|
|
|
33166
33668
|
uptimeSec: (Date.now() - ctx.startedAt) / 1e3
|
|
33167
33669
|
};
|
|
33168
33670
|
return c.json(body);
|
|
33169
|
-
}).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("/network", networkRoutes(ctx.network)).route("/auth", authRoutes(ctx.network)).route("/", fileRoutes(s)).get("/settings", (c) => c.json(s.settings.get())).patch("/settings", async (c) => {
|
|
33671
|
+
}).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("/providers", providerRoutes(s)).route("/updates", updateRoutes(s)).route("/artifacts", artifactRoutes(s)).route("/secrets", secretRoutes(s)).route("/network", networkRoutes(ctx.network)).route("/auth", authRoutes(ctx.network)).route("/", fileRoutes(s)).get("/settings", (c) => c.json(s.settings.get())).patch("/settings", async (c) => {
|
|
33170
33672
|
const patch = await c.req.json().catch(() => void 0);
|
|
33171
33673
|
if (!patch || typeof patch !== "object" || Array.isArray(patch)) throw badRequest("Expected a JSON object");
|
|
33172
33674
|
let settings;
|
|
@@ -34201,6 +34703,15 @@ const UNATTENDED_ANSWER = [
|
|
|
34201
34703
|
"If the user is talking to you directly in this conversation, ask them in your reply instead."
|
|
34202
34704
|
].join("\n");
|
|
34203
34705
|
/**
|
|
34706
|
+
* The denial an unattended factory session gets instead of waiting for an approval. Nothing is
|
|
34707
|
+
* ever approved on the user's behalf: the agent finds a way that needs no approval, or flags it.
|
|
34708
|
+
*/
|
|
34709
|
+
const UNATTENDED_APPROVAL = [
|
|
34710
|
+
"Not approved: this project runs as a dark factory, so nobody is watching to approve it. Don't wait for it or try the same thing again.",
|
|
34711
|
+
"- Find a way that stays inside your worktree and the factory's limits. To publish your work, commit it and call `create_pull_request`: it pushes your own branch (with a lease after a rebase).",
|
|
34712
|
+
"- If the work can't be done without this, call `factory_record_decision` with `needsHuman: true`, saying what you needed and why, and carry on with whatever doesn't depend on it."
|
|
34713
|
+
].join("\n");
|
|
34714
|
+
/**
|
|
34204
34715
|
* True when nobody is expected to answer this session's questions: its project is a dark factory
|
|
34205
34716
|
* and the session is the PM or one of the factory's own agent sessions. Sessions the user
|
|
34206
34717
|
* started themselves still ask as usual.
|
|
@@ -34329,7 +34840,7 @@ function postSharedArtifact(services, sessionId, artifact, caption) {
|
|
|
34329
34840
|
*/
|
|
34330
34841
|
const askUser = defineTool({
|
|
34331
34842
|
name: "ask_user",
|
|
34332
|
-
description: "Ask the user a
|
|
34843
|
+
description: "Ask the user for a decision only they can make: destructive or irreversible steps, spending money or creating accounts or credentials, outward-facing actions nobody asked for (publishing, releases, deploys, messages to other people), protected areas, or a request with two readings that lead to very different work. For anything else, pick a sensible default, say what you assumed, and carry on. Put your recommended option first. The call waits a few minutes for an answer. If it returns 'pending', do not wait or ask again: finish whatever does not depend on the answer, then end your turn; the answer will arrive as your next message.",
|
|
34333
34844
|
roles: ["agent", "pm"],
|
|
34334
34845
|
scope: "session",
|
|
34335
34846
|
alwaysLoad: true,
|
|
@@ -34352,7 +34863,7 @@ const askUser = defineTool({
|
|
|
34352
34863
|
multiSelect: input.multiSelect ?? false,
|
|
34353
34864
|
allowFreeText: true
|
|
34354
34865
|
}, signal);
|
|
34355
|
-
if (outcome.status === "answered") return
|
|
34866
|
+
if (outcome.status === "answered") return `${outcome.by === "pm" ? "Your project manager" : "The user"} answered:\n${outcome.text}`;
|
|
34356
34867
|
if (outcome.status === "dismissed") return "The user dismissed the question without answering.";
|
|
34357
34868
|
return "pending: the question is waiting in the user's inbox. Do not wait for it. Finish what you can without the answer, then end your turn; the answer will arrive as your next message.";
|
|
34358
34869
|
}
|
|
@@ -34378,7 +34889,7 @@ const browserNavigate = defineTool({
|
|
|
34378
34889
|
tabId
|
|
34379
34890
|
}),
|
|
34380
34891
|
handler: async (input, { services, sessionId }) => {
|
|
34381
|
-
const result = await services.browser.
|
|
34892
|
+
const result = await services.browser.agentNavigate(sessionId, input.url, input.tabId);
|
|
34382
34893
|
const { tabs } = await services.browser.tabs(sessionId);
|
|
34383
34894
|
const status = result.status ? ` (HTTP ${result.status})` : "";
|
|
34384
34895
|
return `Navigated to ${result.url ?? input.url}${status}.\nTabs:\n${tabLine(tabs)}`;
|
|
@@ -34391,7 +34902,7 @@ const browserSnapshot = defineTool({
|
|
|
34391
34902
|
scope: "session",
|
|
34392
34903
|
input: object({ tabId }),
|
|
34393
34904
|
handler: async (input, { services, sessionId }) => {
|
|
34394
|
-
const snap = await services.browser.
|
|
34905
|
+
const snap = await services.browser.agentCommand({
|
|
34395
34906
|
name: "browser.snapshot",
|
|
34396
34907
|
sessionId,
|
|
34397
34908
|
tabId: input.tabId
|
|
@@ -34416,7 +34927,7 @@ const browserClick = defineTool({
|
|
|
34416
34927
|
tabId
|
|
34417
34928
|
}),
|
|
34418
34929
|
handler: async (input, { services, sessionId }) => {
|
|
34419
|
-
await services.browser.
|
|
34930
|
+
await services.browser.agentCommand({
|
|
34420
34931
|
name: "browser.click",
|
|
34421
34932
|
sessionId,
|
|
34422
34933
|
...input
|
|
@@ -34436,7 +34947,7 @@ const browserType = defineTool({
|
|
|
34436
34947
|
tabId
|
|
34437
34948
|
}),
|
|
34438
34949
|
handler: async (input, { services, sessionId }) => {
|
|
34439
|
-
await services.browser.
|
|
34950
|
+
await services.browser.agentCommand({
|
|
34440
34951
|
name: "browser.type",
|
|
34441
34952
|
sessionId,
|
|
34442
34953
|
...input
|
|
@@ -34454,7 +34965,7 @@ const browserPress = defineTool({
|
|
|
34454
34965
|
tabId
|
|
34455
34966
|
}),
|
|
34456
34967
|
handler: async (input, { services, sessionId }) => {
|
|
34457
|
-
await services.browser.
|
|
34968
|
+
await services.browser.agentCommand({
|
|
34458
34969
|
name: "browser.press",
|
|
34459
34970
|
sessionId,
|
|
34460
34971
|
...input
|
|
@@ -34519,7 +35030,7 @@ const browserWait = defineTool({
|
|
|
34519
35030
|
v.networkIdle || void 0
|
|
34520
35031
|
].filter(Boolean).length === 1, { message: "Pass exactly one of selector, text or networkIdle" }),
|
|
34521
35032
|
handler: async (input, { services, sessionId }) => {
|
|
34522
|
-
const { matched } = await services.browser.
|
|
35033
|
+
const { matched } = await services.browser.agentCommand({
|
|
34523
35034
|
name: "browser.wait",
|
|
34524
35035
|
sessionId,
|
|
34525
35036
|
...input
|
|
@@ -34537,7 +35048,7 @@ const browserEvaluate = defineTool({
|
|
|
34537
35048
|
tabId
|
|
34538
35049
|
}),
|
|
34539
35050
|
handler: async (input, { services, sessionId }) => {
|
|
34540
|
-
const result = await services.browser.
|
|
35051
|
+
const result = await services.browser.agentCommand({
|
|
34541
35052
|
name: "browser.evaluate",
|
|
34542
35053
|
sessionId,
|
|
34543
35054
|
...input
|
|
@@ -34562,12 +35073,7 @@ const browserOpenDevServer = defineTool({
|
|
|
34562
35073
|
if (servers.length > 1) return `Several dev servers are running; call again with a port:\n${servers.map((s) => `- ${s.url}${s.pid ? ` (pid ${s.pid})` : ""}`).join("\n")}`;
|
|
34563
35074
|
port = servers[0]?.port;
|
|
34564
35075
|
}
|
|
34565
|
-
const { tab } = await services.browser.
|
|
34566
|
-
name: "browser.devServer",
|
|
34567
|
-
sessionId,
|
|
34568
|
-
port,
|
|
34569
|
-
path: input.path
|
|
34570
|
-
});
|
|
35076
|
+
const { tab } = await services.browser.agentOpenDevServer(sessionId, port, input.path);
|
|
34571
35077
|
return `Opened ${tab.url} in tab ${tab.tabId}. Use browser_snapshot to read it.`;
|
|
34572
35078
|
}
|
|
34573
35079
|
});
|
|
@@ -34598,7 +35104,7 @@ const browserScreenshot = defineTool({
|
|
|
34598
35104
|
mkdirSync(incoming, { recursive: true });
|
|
34599
35105
|
const tmp = join(incoming, `${randomUUID()}.png`);
|
|
34600
35106
|
try {
|
|
34601
|
-
const shot = await services.browser.
|
|
35107
|
+
const shot = await services.browser.agentCommand({
|
|
34602
35108
|
name: "browser.screenshot",
|
|
34603
35109
|
sessionId,
|
|
34604
35110
|
path: tmp,
|
|
@@ -35011,7 +35517,8 @@ function buildProjectStatus(input) {
|
|
|
35011
35517
|
project: {
|
|
35012
35518
|
id: input.project.id,
|
|
35013
35519
|
name: input.project.name,
|
|
35014
|
-
kind: input.project.kind
|
|
35520
|
+
kind: input.project.kind,
|
|
35521
|
+
mode: input.project.autonomyMode
|
|
35015
35522
|
},
|
|
35016
35523
|
running: input.work.filter((w) => w.status !== "queued"),
|
|
35017
35524
|
queued: input.work.filter((w) => w.status === "queued"),
|
|
@@ -35049,6 +35556,22 @@ function age(from, now) {
|
|
|
35049
35556
|
if (sec < 86400) return `${Math.round(sec / 3600)}h`;
|
|
35050
35557
|
return `${Math.round(sec / 86400)}d`;
|
|
35051
35558
|
}
|
|
35559
|
+
/**
|
|
35560
|
+
* Shorten `text` to at most `max` characters, keeping its start and its end: agents put the
|
|
35561
|
+
* question or the next step they need in their last paragraph, so cutting only the end would
|
|
35562
|
+
* hide it. Keeps the whole last paragraph when it fits in about half of `max`.
|
|
35563
|
+
*/
|
|
35564
|
+
function clipKeepingEnd(text, max = 500) {
|
|
35565
|
+
const trimmed = text.trim();
|
|
35566
|
+
if (trimmed.length <= max) return trimmed;
|
|
35567
|
+
const gap = " [...] ";
|
|
35568
|
+
const room = max - 7;
|
|
35569
|
+
const paragraphs = trimmed.split(/\n\s*\n/);
|
|
35570
|
+
const last = paragraphs.length > 1 ? (paragraphs.at(-1) ?? "").trim() : "";
|
|
35571
|
+
const tailMax = Math.floor(room / 2);
|
|
35572
|
+
const tail = last && last.length <= tailMax ? last : trimmed.slice(-tailMax).trimStart();
|
|
35573
|
+
return `${trimmed.slice(0, room - tail.length).trimEnd()}${gap}${tail}`;
|
|
35574
|
+
}
|
|
35052
35575
|
const short = (id) => id.slice(0, 8);
|
|
35053
35576
|
const quote = (text, max = 100) => `"${text.length > max ? `${text.slice(0, max - 3)}...` : text}"`;
|
|
35054
35577
|
function workLine(w, now) {
|
|
@@ -35099,7 +35622,7 @@ function block(open, close, lines) {
|
|
|
35099
35622
|
/** The PM's digest for its project. */
|
|
35100
35623
|
function renderProjectDigest(s, now = Date.now()) {
|
|
35101
35624
|
const at = new Date(now).toISOString();
|
|
35102
|
-
return block(`<codeden_status project=${quote(neutralizeTags(s.project.name))} at="${at}">`, "</codeden_status>", projectLines(s, now));
|
|
35625
|
+
return block(`<codeden_status project=${quote(neutralizeTags(s.project.name))} mode="${s.project.mode}" at="${at}">`, "</codeden_status>", projectLines(s, now));
|
|
35103
35626
|
}
|
|
35104
35627
|
/** The orchestrator's digest: every project rolled up. */
|
|
35105
35628
|
function renderOverviewDigest(o, now = Date.now()) {
|
|
@@ -35131,8 +35654,8 @@ function renderNotices(list, projectName) {
|
|
|
35131
35654
|
}
|
|
35132
35655
|
function noticeDetail(n) {
|
|
35133
35656
|
switch (n.type) {
|
|
35134
|
-
case "session.finished": return ` · ${n.data.workKey} · session ${short(n.data.sessionId)}${n.data.summary ? ` · ${
|
|
35135
|
-
case "session.needs_input": return ` · ${n.data.kind}: ${quote(n.data.question, 200)} (waiting
|
|
35657
|
+
case "session.finished": return ` · ${n.data.workKey} · session ${short(n.data.sessionId)}${n.data.summary ? ` · "${clipKeepingEnd(n.data.summary)}"` : ""}`;
|
|
35658
|
+
case "session.needs_input": return ` · ${n.data.kind}: ${quote(n.data.question, 200)} (${n.data.kind !== "question" ? "waiting for the user's approval" : n.data.startedBy.kind === "user" ? `inbox item ${n.data.inboxItemId}: the user started this session and answers its questions` : `inbox item ${n.data.inboxItemId}: answer it with answer_question when the brief, the notes or the vision settle it`})`;
|
|
35136
35659
|
case "session.failed": return ` · ${n.data.workKey} · ${quote(n.data.error, 200)}`;
|
|
35137
35660
|
case "pr.opened": return ` · ${n.data.pr.url}`;
|
|
35138
35661
|
case "pr.checks_failed": return ` · ${n.data.pr.url} · failed: ${n.data.failedChecks.join(", ") || "unknown"}`;
|
|
@@ -35143,6 +35666,8 @@ function noticeDetail(n) {
|
|
|
35143
35666
|
case "automation.skipped": return ` · ${n.data.workKey} · reason: ${n.data.reason}`;
|
|
35144
35667
|
case "conflict": return ` · ${n.data.workKey} requested by ${n.data.requestedBy.kind}, matches ${n.data.existing.map((w) => w.workKey).join(", ")} (conflict ${short(n.data.conflictId)} waits in the user's inbox)`;
|
|
35145
35668
|
case "pm.message": return ` · ${quote(n.data.text, 2e3)}`;
|
|
35669
|
+
case "orchestrator.message": return ` · ${quote(n.data.text, Number.POSITIVE_INFINITY)}`;
|
|
35670
|
+
case "reminder": return ` · ${quote(n.data.note, 2e3)} (scheduled for ${n.notBefore ?? n.createdAt})`;
|
|
35146
35671
|
case "factory.update": return ` · ${n.data.event}\n${quote(n.data.text, 4e3)}`;
|
|
35147
35672
|
}
|
|
35148
35673
|
}
|
|
@@ -35168,12 +35693,23 @@ function githubProject(services, projectId) {
|
|
|
35168
35693
|
if (project.kind !== "github") throw new ToolError(`${project.name} is a local project without a GitHub remote: no issues or PRs.`);
|
|
35169
35694
|
return project;
|
|
35170
35695
|
}
|
|
35171
|
-
/**
|
|
35696
|
+
/**
|
|
35697
|
+
* An agent session of the caller's project, or a tool error. Takes the full id or a unique
|
|
35698
|
+
* prefix of at least six characters, as digests and updates show ids.
|
|
35699
|
+
*/
|
|
35172
35700
|
function agentSessionOf(services, sessionId, projectId) {
|
|
35173
|
-
const
|
|
35701
|
+
const id = services.sessionRepo.find(sessionId) ? sessionId : byPrefix(services, sessionId, projectId);
|
|
35702
|
+
const found = id ? services.sessionRepo.find(id)?.session : void 0;
|
|
35174
35703
|
if (found?.role !== "agent" || found.projectId !== projectId) throw new ToolError(`No agent session ${sessionId} in this project. Call list_sessions for the ids.`);
|
|
35175
35704
|
return found;
|
|
35176
35705
|
}
|
|
35706
|
+
/** The one agent session of a project whose id starts with `prefix`, if exactly one does. */
|
|
35707
|
+
function byPrefix(services, prefix, projectId) {
|
|
35708
|
+
if (!/^[0-9a-f-]{6,}$/i.test(prefix)) return void 0;
|
|
35709
|
+
const rows = services.db.select({ id: sessions.id }).from(sessions).where(and(eq(sessions.projectId, projectId), eq(sessions.role, "agent"), like(sessions.id, `${prefix.toLowerCase()}%`))).limit(5).all();
|
|
35710
|
+
if (rows.length > 1) throw new ToolError(`More than one session starts with ${prefix}: ${rows.map((r) => r.id).join(", ")}. Use the full id.`);
|
|
35711
|
+
return rows[0]?.id;
|
|
35712
|
+
}
|
|
35177
35713
|
/** A session as tools report it: enough to identify and judge the work. */
|
|
35178
35714
|
function compactSession(s) {
|
|
35179
35715
|
return {
|
|
@@ -35268,12 +35804,32 @@ const messagePm = defineTool({
|
|
|
35268
35804
|
projectId: string().min(1),
|
|
35269
35805
|
text: string().min(1).max(2e4)
|
|
35270
35806
|
}),
|
|
35271
|
-
handler: async ({ projectId, text }, { services }) => {
|
|
35807
|
+
handler: async ({ projectId, text }, { services, caller, sessionId }) => {
|
|
35272
35808
|
const project = projectFor(services, projectId);
|
|
35273
|
-
const res = await services.orchestration.messagePm(projectId, text
|
|
35809
|
+
const res = await services.orchestration.messagePm(projectId, text, {
|
|
35810
|
+
caller,
|
|
35811
|
+
sessionId
|
|
35812
|
+
});
|
|
35813
|
+
if (res.status === "held") return `${heldNote(project.name, res.until)} Its reply arrives as a later message.`;
|
|
35274
35814
|
return `Message ${res.status} to the PM of ${project.name} (session ${res.sessionId}). Its reply arrives as a later message.`;
|
|
35275
35815
|
}
|
|
35276
35816
|
});
|
|
35817
|
+
/**
|
|
35818
|
+
* What the orchestrator is told about a message the loop guard held back (§8.2): it is
|
|
35819
|
+
* queued and delivered automatically, so there is nothing to wait for or retry.
|
|
35820
|
+
*/
|
|
35821
|
+
function heldNote(projectName, until, now = Date.now()) {
|
|
35822
|
+
return [
|
|
35823
|
+
`Queued: CodeDen delivers this to the PM of ${projectName} automatically ${until - now <= 6e4 ? "within a minute" : `at about ${clock(until)} (local time)`}, or sooner if the PM wakes up for something else.`,
|
|
35824
|
+
"Nothing else to do: do not send it again or plan a retry.",
|
|
35825
|
+
"(CodeDen paces automatic back-and-forth between you and a PM while the user has not stepped in.)"
|
|
35826
|
+
].join(" ");
|
|
35827
|
+
}
|
|
35828
|
+
/** `HH:MM` in the server's (the user's) local time. */
|
|
35829
|
+
const clock = (at) => {
|
|
35830
|
+
const d = new Date(at);
|
|
35831
|
+
return `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
35832
|
+
};
|
|
35277
35833
|
/** `ask_pm` (§8.3): ask a PM and wait for its answer. */
|
|
35278
35834
|
const askPm = defineTool({
|
|
35279
35835
|
name: "ask_pm",
|
|
@@ -35286,13 +35842,17 @@ const askPm = defineTool({
|
|
|
35286
35842
|
question: string().min(1).max(2e4),
|
|
35287
35843
|
timeoutSec: int().min(10).max(540).optional()
|
|
35288
35844
|
}),
|
|
35289
|
-
handler: async ({ projectId, question, timeoutSec }, { services, sessionId, signal }) => {
|
|
35845
|
+
handler: async ({ projectId, question, timeoutSec }, { services, caller, sessionId, signal }) => {
|
|
35290
35846
|
const project = projectFor(services, projectId);
|
|
35291
35847
|
const outcome = await services.orchestration.askPm(projectId, question, {
|
|
35292
|
-
|
|
35848
|
+
origin: {
|
|
35849
|
+
caller,
|
|
35850
|
+
sessionId
|
|
35851
|
+
},
|
|
35293
35852
|
timeoutMs: timeoutSec ? timeoutSec * 1e3 : void 0,
|
|
35294
35853
|
signal
|
|
35295
35854
|
});
|
|
35855
|
+
if (outcome.status === "held") return `pending: ${heldNote(project.name, outcome.until)} Do not ask again; the PM's answer arrives as a message.`;
|
|
35296
35856
|
if (outcome.status === "answered") return `The PM of ${project.name} answered:\n${outcome.text}`;
|
|
35297
35857
|
return `pending: the PM of ${project.name} is still working on it. Do not ask again; its answer will arrive as a message.`;
|
|
35298
35858
|
}
|
|
@@ -35479,7 +36039,7 @@ const listAutomations = defineTool({
|
|
|
35479
36039
|
/** `run_automation` (§8.3, §8.4): run now, with the conflict check. */
|
|
35480
36040
|
const runAutomation = defineTool({
|
|
35481
36041
|
name: "run_automation",
|
|
35482
|
-
description: "Run one of this project's automations now (even if it is disabled or paused). If matching work is already running or queued, nothing starts and the result is 'conflict' with the existing work: call again with conflict = {resolution: 'wait'}, {resolution: 'skip'}, {resolution: 'join', sessionId} or {resolution: 'force', reason}.",
|
|
36042
|
+
description: "Run one of this project's automations now (even if it is disabled or paused). If matching work is already running or queued, nothing starts and the result is 'conflict' with the existing work: choose the resolution yourself and call again with conflict = {resolution: 'wait'}, {resolution: 'skip'}, {resolution: 'join', sessionId} or {resolution: 'force', reason}.",
|
|
35483
36043
|
roles: ["pm"],
|
|
35484
36044
|
scope: "project",
|
|
35485
36045
|
input: object({
|
|
@@ -35539,7 +36099,7 @@ const getIssue = defineTool({
|
|
|
35539
36099
|
/** `triage_issue` (§8.3, §10 issue triage): labels, comment, accept or decline. */
|
|
35540
36100
|
const triageIssue$1 = defineTool({
|
|
35541
36101
|
name: "triage_issue",
|
|
35542
|
-
description: "Record a triage decision on an issue: ready, needs_info, unsafe, out_of_scope or duplicate, with labels to add or remove and an optional comment (close: true for out of scope or duplicates). Follow the project's policy
|
|
36102
|
+
description: "Record a triage decision on an issue: ready, needs_info, unsafe, out_of_scope or duplicate, with labels to add or remove and an optional comment (close: true for out of scope or duplicates). Follow the project's policy. When you can't decide, choose needs_info (ask the author the specific questions in the comment) or unsafe (it needs a human decision); don't wait on the user.",
|
|
35543
36103
|
roles: ["pm"],
|
|
35544
36104
|
scope: "project",
|
|
35545
36105
|
isAvailable: inGithubProject,
|
|
@@ -35659,6 +36219,319 @@ const messageOrchestrator = defineTool({
|
|
|
35659
36219
|
return "Sent: the orchestrator will be woken with it shortly.";
|
|
35660
36220
|
}
|
|
35661
36221
|
});
|
|
36222
|
+
// ../server/src/services/orchestration/notices.ts
|
|
36223
|
+
const clip$4 = (text, max = 300) => text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
36224
|
+
/**
|
|
36225
|
+
* Work started by the PM, the orchestrator or an automation wakes the PM; sessions the user
|
|
36226
|
+
* started (and is watching) only produce notices that show up in the PM's next digest.
|
|
36227
|
+
*/
|
|
36228
|
+
function wakesPm(session) {
|
|
36229
|
+
return session.startedBy.kind !== "user" || session.parentSessionId !== null;
|
|
36230
|
+
}
|
|
36231
|
+
/** Work a PM started for a request the orchestrator passed on (`startedBy.reportTo`). */
|
|
36232
|
+
function reportsToOrchestrator(session) {
|
|
36233
|
+
return session.startedBy.reportTo === "orchestrator";
|
|
36234
|
+
}
|
|
36235
|
+
/** A session notice, copied for the orchestrator so the result reaches whoever asked (§8.2). */
|
|
36236
|
+
function forOrchestrator(notice, id) {
|
|
36237
|
+
return {
|
|
36238
|
+
wake: true,
|
|
36239
|
+
notice: {
|
|
36240
|
+
...notice,
|
|
36241
|
+
id,
|
|
36242
|
+
target: "orchestrator"
|
|
36243
|
+
}
|
|
36244
|
+
};
|
|
36245
|
+
}
|
|
36246
|
+
/** The id of an opened PR's notice: the same whether it came from the session or GitHub sync. */
|
|
36247
|
+
function prOpenedNoticeId(projectId, number) {
|
|
36248
|
+
return `pr.opened:${projectId}:${number}`;
|
|
36249
|
+
}
|
|
36250
|
+
function workRef(s) {
|
|
36251
|
+
return {
|
|
36252
|
+
sessionId: s.id,
|
|
36253
|
+
workKey: s.workKey,
|
|
36254
|
+
purpose: s.purpose,
|
|
36255
|
+
startedBy: s.startedBy
|
|
36256
|
+
};
|
|
36257
|
+
}
|
|
36258
|
+
/**
|
|
36259
|
+
* Typed notices for a batch of committed events (§8.2 step 4). Pure apart from the lookups.
|
|
36260
|
+
* Only agent sessions inside a project produce notices; ids are derived from the source so
|
|
36261
|
+
* replays and repeats never create duplicates.
|
|
36262
|
+
*/
|
|
36263
|
+
function deriveNotices(events, lookup) {
|
|
36264
|
+
const out = [];
|
|
36265
|
+
const agentSession = (id) => {
|
|
36266
|
+
const s = id ? lookup.session(id) : void 0;
|
|
36267
|
+
return s && s.role === "agent" && s.projectId ? s : void 0;
|
|
36268
|
+
};
|
|
36269
|
+
for (const e of events) if (e.type === "agent.turn.completed") {
|
|
36270
|
+
const event = e.payload;
|
|
36271
|
+
const s = agentSession(e.sessionId);
|
|
36272
|
+
if (!s || s.rootThreadId !== event.threadId) continue;
|
|
36273
|
+
const final = lookup.finalMessage(s.id, event.turnId);
|
|
36274
|
+
const finished = {
|
|
36275
|
+
id: `session.finished:${s.id}:${event.turnId}`,
|
|
36276
|
+
type: "session.finished",
|
|
36277
|
+
projectId: s.projectId,
|
|
36278
|
+
target: "pm",
|
|
36279
|
+
sessionId: s.id,
|
|
36280
|
+
title: `Finished: ${s.title ?? s.purpose}`,
|
|
36281
|
+
data: {
|
|
36282
|
+
...workRef(s),
|
|
36283
|
+
summary: s.result?.summary ?? (final ? clipKeepingEnd(final) : null),
|
|
36284
|
+
outcome: s.result?.outcome ?? null
|
|
36285
|
+
}
|
|
36286
|
+
};
|
|
36287
|
+
out.push({
|
|
36288
|
+
wake: wakesPm(s),
|
|
36289
|
+
notice: finished
|
|
36290
|
+
});
|
|
36291
|
+
if (reportsToOrchestrator(s) && s.result) out.push(forOrchestrator(finished, `session.finished:orchestrator:${s.id}:${s.result.at}`));
|
|
36292
|
+
} else if (e.type === "inbox.created") {
|
|
36293
|
+
const { item } = e.payload;
|
|
36294
|
+
const s = agentSession(item.sessionId);
|
|
36295
|
+
if (!s) continue;
|
|
36296
|
+
if (item.kind === "question" || item.kind === "approval") out.push({
|
|
36297
|
+
wake: wakesPm(s),
|
|
36298
|
+
notice: {
|
|
36299
|
+
id: `session.needs_input:${item.id}`,
|
|
36300
|
+
type: "session.needs_input",
|
|
36301
|
+
projectId: s.projectId,
|
|
36302
|
+
target: "pm",
|
|
36303
|
+
sessionId: s.id,
|
|
36304
|
+
title: `Needs input: ${s.title ?? s.purpose}`,
|
|
36305
|
+
data: {
|
|
36306
|
+
...workRef(s),
|
|
36307
|
+
inboxItemId: item.id,
|
|
36308
|
+
kind: item.kind,
|
|
36309
|
+
question: item.title
|
|
36310
|
+
}
|
|
36311
|
+
}
|
|
36312
|
+
});
|
|
36313
|
+
else if (item.kind === "run_failed") {
|
|
36314
|
+
const failed = {
|
|
36315
|
+
id: `session.failed:${item.id}`,
|
|
36316
|
+
type: "session.failed",
|
|
36317
|
+
projectId: s.projectId,
|
|
36318
|
+
target: "pm",
|
|
36319
|
+
sessionId: s.id,
|
|
36320
|
+
title: `Failed: ${s.title ?? s.purpose}`,
|
|
36321
|
+
data: {
|
|
36322
|
+
...workRef(s),
|
|
36323
|
+
error: clip$4(item.body ?? item.title)
|
|
36324
|
+
}
|
|
36325
|
+
};
|
|
36326
|
+
out.push({
|
|
36327
|
+
wake: wakesPm(s),
|
|
36328
|
+
notice: failed
|
|
36329
|
+
});
|
|
36330
|
+
if (reportsToOrchestrator(s)) out.push(forOrchestrator(failed, `session.failed:orchestrator:${item.id}`));
|
|
36331
|
+
}
|
|
36332
|
+
} else if (e.type === "session.updated") {
|
|
36333
|
+
const { session } = e.payload;
|
|
36334
|
+
if (session.role !== "agent" || !session.projectId || !session.pr) continue;
|
|
36335
|
+
out.push({
|
|
36336
|
+
wake: wakesPm(session),
|
|
36337
|
+
notice: {
|
|
36338
|
+
id: prOpenedNoticeId(session.projectId, session.pr.number),
|
|
36339
|
+
type: "pr.opened",
|
|
36340
|
+
projectId: session.projectId,
|
|
36341
|
+
target: "pm",
|
|
36342
|
+
sessionId: session.id,
|
|
36343
|
+
title: `PR #${session.pr.number} opened: ${session.pr.title ?? session.purpose}`,
|
|
36344
|
+
data: {
|
|
36345
|
+
sessionId: session.id,
|
|
36346
|
+
pr: session.pr
|
|
36347
|
+
}
|
|
36348
|
+
}
|
|
36349
|
+
});
|
|
36350
|
+
} else if (e.type === "automation_run.updated") {
|
|
36351
|
+
const { run } = e.payload;
|
|
36352
|
+
if (run.status !== "skipped" || !run.projectId) continue;
|
|
36353
|
+
out.push({
|
|
36354
|
+
wake: true,
|
|
36355
|
+
notice: {
|
|
36356
|
+
id: `automation.skipped:${run.id}`,
|
|
36357
|
+
type: "automation.skipped",
|
|
36358
|
+
projectId: run.projectId,
|
|
36359
|
+
target: "pm",
|
|
36360
|
+
sessionId: run.sessionId,
|
|
36361
|
+
title: `Automation skipped: ${run.purpose}`,
|
|
36362
|
+
data: {
|
|
36363
|
+
automationRunId: run.id,
|
|
36364
|
+
automationId: run.automationId,
|
|
36365
|
+
workKey: run.workKey,
|
|
36366
|
+
purpose: run.purpose,
|
|
36367
|
+
reason: run.skipReason ?? "skipped"
|
|
36368
|
+
}
|
|
36369
|
+
}
|
|
36370
|
+
});
|
|
36371
|
+
}
|
|
36372
|
+
return out;
|
|
36373
|
+
}
|
|
36374
|
+
/** Stored notices (§8.2): created through `notice.created`, delivered through `notice.delivered`. */
|
|
36375
|
+
var NoticeStore = class {
|
|
36376
|
+
db;
|
|
36377
|
+
store;
|
|
36378
|
+
constructor(db, store) {
|
|
36379
|
+
this.db = db;
|
|
36380
|
+
this.store = store;
|
|
36381
|
+
}
|
|
36382
|
+
/** Store a notice unless one with the same id exists. Returns the stored notice, if new. */
|
|
36383
|
+
create(draft) {
|
|
36384
|
+
if (this.db.select({ id: notices.id }).from(notices).where(eq(notices.id, draft.id)).get()) return;
|
|
36385
|
+
const notice = {
|
|
36386
|
+
...draft,
|
|
36387
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
36388
|
+
deliveredAt: null
|
|
36389
|
+
};
|
|
36390
|
+
this.store.append([{
|
|
36391
|
+
type: "notice.created",
|
|
36392
|
+
sessionId: null,
|
|
36393
|
+
payload: { notice }
|
|
36394
|
+
}]);
|
|
36395
|
+
return notice;
|
|
36396
|
+
}
|
|
36397
|
+
list(query = {}) {
|
|
36398
|
+
const conditions = [];
|
|
36399
|
+
if (query.projectId) conditions.push(eq(notices.projectId, query.projectId));
|
|
36400
|
+
if (query.target) conditions.push(eq(notices.target, query.target));
|
|
36401
|
+
return this.db.select().from(notices).where(and(...conditions)).orderBy(desc(notices.createdAt)).limit(query.limit ?? 100).all().map((r) => r.notice);
|
|
36402
|
+
}
|
|
36403
|
+
/**
|
|
36404
|
+
* Undelivered notices for a target, oldest first. `projectId` null = the orchestrator. A
|
|
36405
|
+
* scheduled wake-up counts once it is due.
|
|
36406
|
+
*/
|
|
36407
|
+
undelivered(target, projectId) {
|
|
36408
|
+
return this.db.select().from(notices).where(and(eq(notices.target, target), isNull(notices.deliveredAt), projectId === null ? void 0 : eq(notices.projectId, projectId), or$1(isNull(notices.notBefore), lte(notices.notBefore, (/* @__PURE__ */ new Date()).toISOString())))).orderBy(notices.createdAt).limit(50).all().map((r) => r.notice);
|
|
36409
|
+
}
|
|
36410
|
+
/** A target's scheduled wake-ups still to deliver (due or not), soonest first. */
|
|
36411
|
+
scheduled(target, projectId) {
|
|
36412
|
+
return this.db.select().from(notices).where(and(eq(notices.type, "reminder"), eq(notices.target, target), isNull(notices.deliveredAt), projectId === null ? isNull(notices.projectId) : eq(notices.projectId, projectId))).orderBy(asc(notices.notBefore)).all().map((r) => r.notice);
|
|
36413
|
+
}
|
|
36414
|
+
/** Undelivered notices held back until a time: the due ones, or the soonest still ahead. */
|
|
36415
|
+
heldBack(due) {
|
|
36416
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
36417
|
+
return this.db.select().from(notices).where(and(isNull(notices.deliveredAt), due ? lte(notices.notBefore, now) : gt$1(notices.notBefore, now))).orderBy(asc(notices.notBefore)).limit(due ? 200 : 1).all().map((r) => r.notice);
|
|
36418
|
+
}
|
|
36419
|
+
/** Notices of the given types for a project since `since`, newest first. */
|
|
36420
|
+
recent(projectId, types, since, limit = 5) {
|
|
36421
|
+
return this.db.select().from(notices).where(and(eq(notices.projectId, projectId), inArray(notices.type, types), gte(notices.createdAt, since))).orderBy(desc(notices.createdAt)).limit(limit).all().map((r) => r.notice);
|
|
36422
|
+
}
|
|
36423
|
+
markDelivered(ids) {
|
|
36424
|
+
if (ids.length === 0) return;
|
|
36425
|
+
this.store.append([{
|
|
36426
|
+
type: "notice.delivered",
|
|
36427
|
+
sessionId: null,
|
|
36428
|
+
payload: {
|
|
36429
|
+
noticeIds: ids,
|
|
36430
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
36431
|
+
}
|
|
36432
|
+
}]);
|
|
36433
|
+
}
|
|
36434
|
+
/** Back to undelivered: the turn that carried them failed, so the next wake carries them. */
|
|
36435
|
+
markReturned(ids) {
|
|
36436
|
+
if (ids.length === 0) return;
|
|
36437
|
+
this.store.append([{
|
|
36438
|
+
type: "notice.returned",
|
|
36439
|
+
sessionId: null,
|
|
36440
|
+
payload: {
|
|
36441
|
+
noticeIds: ids,
|
|
36442
|
+
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
36443
|
+
}
|
|
36444
|
+
}]);
|
|
36445
|
+
}
|
|
36446
|
+
};
|
|
36447
|
+
// ../server/src/mcp/tools/shared/session-items.ts
|
|
36448
|
+
/** A session's question item (`ask_user` or the provider's own) by its request id. */
|
|
36449
|
+
function questionItem(db, sessionId, requestId) {
|
|
36450
|
+
const rows = db.select().from(items).where(and(eq(items.sessionId, sessionId), eq(items.kind, "user_question"))).all();
|
|
36451
|
+
for (const row of rows) if (row.data.kind === "user_question" && row.data.requestId === requestId) return row.data;
|
|
36452
|
+
}
|
|
36453
|
+
/** The latest non-empty assistant message on a session's root thread. */
|
|
36454
|
+
function lastAssistantMessage(db, session) {
|
|
36455
|
+
if (!session.rootThreadId) return null;
|
|
36456
|
+
const rows = db.select().from(items).where(and(eq(items.sessionId, session.id), eq(items.threadId, session.rootThreadId), eq(items.kind, "assistant_message"))).orderBy(desc(items.position)).limit(5).all();
|
|
36457
|
+
for (const row of rows) if (row.data.kind === "assistant_message" && row.data.text.trim()) return row.data.text.trim();
|
|
36458
|
+
return null;
|
|
36459
|
+
}
|
|
36460
|
+
// ../server/src/mcp/tools/pm/questions.ts
|
|
36461
|
+
/**
|
|
36462
|
+
* `answer_question` (§8.2 step 5): the PM answers a question one of its project's agent
|
|
36463
|
+
* sessions asked, so the agent carries on without waiting for the user. Only questions from
|
|
36464
|
+
* work the PM, the orchestrator or an automation started; approvals and the questions of
|
|
36465
|
+
* sessions the user started stay with the user.
|
|
36466
|
+
*/
|
|
36467
|
+
const answerQuestion = defineTool({
|
|
36468
|
+
name: "answer_question",
|
|
36469
|
+
description: "Answer a question one of this project's agent sessions asked (an open inbox item of kind 'question', named in a 'needs input' update), when its brief, your notes, the vision or a sensible default settles it. The agent gets the answer at once (or as its next message) and the inbox item closes as answered by you. Approvals, and questions from sessions the user started, stay with the user.",
|
|
36470
|
+
roles: ["pm"],
|
|
36471
|
+
scope: "project",
|
|
36472
|
+
alwaysLoad: true,
|
|
36473
|
+
input: object({
|
|
36474
|
+
inboxItemId: string().min(1),
|
|
36475
|
+
answer: string().min(1).max(1e4).describe("Your answer: an option's label or free text, with a short reason")
|
|
36476
|
+
}),
|
|
36477
|
+
handler: async ({ inboxItemId, answer }, { services, projectId }) => {
|
|
36478
|
+
const item = services.inbox.find(inboxItemId);
|
|
36479
|
+
if (!item) throw new ToolError(`No inbox item ${inboxItemId}`);
|
|
36480
|
+
if (item.status !== "open") throw new ToolError(`That question was already ${item.status}`);
|
|
36481
|
+
if (item.kind !== "question") throw new ToolError(item.kind === "approval" ? "Approvals stay with the user; you can only answer questions." : `Only questions can be answered here (this item is a ${item.kind}).`);
|
|
36482
|
+
if (!item.sessionId || !item.requestId) throw new ToolError("This question belongs to no agent session");
|
|
36483
|
+
const session = agentSessionOf(services, item.sessionId, projectId);
|
|
36484
|
+
if (!wakesPm(session)) throw new ToolError("The user started that session and answers its questions themselves.");
|
|
36485
|
+
const ids = questionItem(services.db, session.id, item.requestId)?.questions.map((q) => q.id) ?? ["q1"];
|
|
36486
|
+
const answers = Object.fromEntries(ids.map((id) => [id, [answer]]));
|
|
36487
|
+
await services.sessions.respond(session.id, {
|
|
36488
|
+
requestId: item.requestId,
|
|
36489
|
+
response: {
|
|
36490
|
+
kind: "answer",
|
|
36491
|
+
answers
|
|
36492
|
+
}
|
|
36493
|
+
}, "pm");
|
|
36494
|
+
return `Answered. ${session.title ?? session.purpose} (session ${session.id}) has your answer.`;
|
|
36495
|
+
}
|
|
36496
|
+
});
|
|
36497
|
+
// ../server/src/mcp/tools/pm/session-detail.ts
|
|
36498
|
+
/** Longest last message returned; longer ones keep their end, where questions usually are. */
|
|
36499
|
+
const MESSAGE_LIMIT = 8e3;
|
|
36500
|
+
/**
|
|
36501
|
+
* `get_session` (§8.4): one agent session in detail, so the PM can act on what it said or asked
|
|
36502
|
+
* without the user relaying it: its last message in full, what it waits on (with inbox ids, for
|
|
36503
|
+
* `answer_question`) and its result.
|
|
36504
|
+
*/
|
|
36505
|
+
const getSession = defineTool({
|
|
36506
|
+
name: "get_session",
|
|
36507
|
+
description: "One of this project's agent sessions in detail: status, progress and result, its last message in full, and the questions and approvals it is waiting on with their inbox ids (answer questions with answer_question). Takes the full session id or a unique prefix of at least 6 characters, as updates and the status digest show them.",
|
|
36508
|
+
roles: ["pm"],
|
|
36509
|
+
scope: "project",
|
|
36510
|
+
alwaysLoad: true,
|
|
36511
|
+
input: object({ sessionId: string().min(1) }),
|
|
36512
|
+
handler: async ({ sessionId }, { services, projectId }) => {
|
|
36513
|
+
const session = agentSessionOf(services, sessionId, projectId);
|
|
36514
|
+
const last = lastAssistantMessage(services.db, session);
|
|
36515
|
+
const waitingOn = services.inbox.list({ sessionId: session.id }).filter((i) => i.kind === "question" || i.kind === "approval").map((i) => {
|
|
36516
|
+
const asked = i.requestId ? questionItem(services.db, session.id, i.requestId) : void 0;
|
|
36517
|
+
return {
|
|
36518
|
+
inboxItemId: i.id,
|
|
36519
|
+
kind: i.kind,
|
|
36520
|
+
title: i.title,
|
|
36521
|
+
body: i.body,
|
|
36522
|
+
...asked ? { questions: asked.questions.map((q) => ({
|
|
36523
|
+
question: q.question,
|
|
36524
|
+
options: q.options.map((o) => o.label)
|
|
36525
|
+
})) } : {}
|
|
36526
|
+
};
|
|
36527
|
+
});
|
|
36528
|
+
return {
|
|
36529
|
+
session: compactSession(session),
|
|
36530
|
+
lastMessage: last && last.length > MESSAGE_LIMIT ? `…${last.slice(-8e3)}` : last ?? null,
|
|
36531
|
+
waitingOn
|
|
36532
|
+
};
|
|
36533
|
+
}
|
|
36534
|
+
});
|
|
35662
36535
|
// ../server/src/mcp/tools/pm/sessions.ts
|
|
35663
36536
|
const ENDED$3 = /* @__PURE__ */ new Set([
|
|
35664
36537
|
"completed",
|
|
@@ -35695,12 +36568,12 @@ const listSessions = defineTool({
|
|
|
35695
36568
|
/** `start_session` (§8.2, §8.4): start an agent session, with the conflict check built in. */
|
|
35696
36569
|
const startSession = defineTool({
|
|
35697
36570
|
name: "start_session",
|
|
35698
|
-
description: "Start an agent session in its own worktree to do one piece of work. Always pass a workKey (issue:<n>, pr:<n>, automation:<name> or request:<slug>) and a one-line purpose; add scope (paths or area) when known. If matching work is already running or queued, nothing starts and the result is 'conflict' with the existing work:
|
|
36571
|
+
description: "Start an agent session in its own worktree to do one piece of work. Always pass a workKey (issue:<n>, pr:<n>, automation:<name> or request:<slug>) and a one-line purpose; add scope (paths or area) when known. If matching work is already running or queued, nothing starts and the result is 'conflict' with the existing work: choose the resolution yourself and call again with conflict = {resolution: 'wait'} (queue behind it), {resolution: 'skip'} (it is the same work), {resolution: 'join', sessionId} (send the prompt to the existing session) or {resolution: 'force', reason}.",
|
|
35699
36572
|
roles: ["pm"],
|
|
35700
36573
|
scope: "project",
|
|
35701
36574
|
alwaysLoad: true,
|
|
35702
36575
|
input: object({
|
|
35703
|
-
prompt: string().min(1).max(5e4).describe("The brief for the agent: goal, context, acceptance criteria"),
|
|
36576
|
+
prompt: string().min(1).max(5e4).describe("The brief for the agent: goal, context, acceptance criteria, and the decisions you already made (defaults, scope, whether to open a pull request), so the agent doesn't have to ask"),
|
|
35704
36577
|
purpose: string().trim().min(1).max(300),
|
|
35705
36578
|
workKey: WorkKey.optional(),
|
|
35706
36579
|
scope: object({
|
|
@@ -35712,7 +36585,8 @@ const startSession = defineTool({
|
|
|
35712
36585
|
provider: ProviderId.optional(),
|
|
35713
36586
|
model: string().min(1).optional(),
|
|
35714
36587
|
effort: ReasoningEffort.optional(),
|
|
35715
|
-
conflict: ConflictDecision.optional()
|
|
36588
|
+
conflict: ConflictDecision.optional(),
|
|
36589
|
+
reportTo: _enum(["orchestrator"]).optional().describe("'orchestrator' when the orchestrator passed this work on: its finish or failure also reaches the orchestrator, so it can tell the user")
|
|
35716
36590
|
}),
|
|
35717
36591
|
handler: async (input, { services, projectId, sessionId }) => {
|
|
35718
36592
|
const project = projectFor(services, projectId);
|
|
@@ -35733,7 +36607,8 @@ const startSession = defineTool({
|
|
|
35733
36607
|
issue,
|
|
35734
36608
|
startedBy: {
|
|
35735
36609
|
kind: "pm",
|
|
35736
|
-
id: sessionId ?? void 0
|
|
36610
|
+
id: sessionId ?? void 0,
|
|
36611
|
+
reportTo: input.reportTo
|
|
35737
36612
|
},
|
|
35738
36613
|
parentSessionId: sessionId ?? void 0,
|
|
35739
36614
|
input: { text: input.prompt },
|
|
@@ -35761,7 +36636,7 @@ const startSession = defineTool({
|
|
|
35761
36636
|
};
|
|
35762
36637
|
case "conflict": return {
|
|
35763
36638
|
status: "conflict",
|
|
35764
|
-
message: "Matching work is already running or queued; nothing was started.
|
|
36639
|
+
message: "Matching work is already running or queued; nothing was started. Choose a resolution yourself and call start_session again with it: wait (queue behind it), skip (it is the same work), join with sessionId (it adds to that work) or force with a reason. Then say in one line what you chose.",
|
|
35765
36640
|
existing: result.existing.map(compactWork)
|
|
35766
36641
|
};
|
|
35767
36642
|
}
|
|
@@ -35780,8 +36655,8 @@ const messageSession = defineTool({
|
|
|
35780
36655
|
whenBusy: _enum(["queue", "steer"]).optional()
|
|
35781
36656
|
}),
|
|
35782
36657
|
handler: async ({ sessionId, text, whenBusy }, { services, projectId }) => {
|
|
35783
|
-
agentSessionOf(services, sessionId, projectId);
|
|
35784
|
-
return `Message ${(await services.sessions.send(
|
|
36658
|
+
const session = agentSessionOf(services, sessionId, projectId);
|
|
36659
|
+
return `Message ${(await services.sessions.send(session.id, {
|
|
35785
36660
|
input: messageFrom("pm", text),
|
|
35786
36661
|
whenBusy: whenBusy ?? "queue"
|
|
35787
36662
|
}, "pm")).status}.`;
|
|
@@ -35800,10 +36675,10 @@ const stopSession = defineTool({
|
|
|
35800
36675
|
}),
|
|
35801
36676
|
handler: async ({ sessionId }, { services, projectId }) => {
|
|
35802
36677
|
const session = agentSessionOf(services, sessionId, projectId);
|
|
35803
|
-
if (ENDED$3.has(session.status)) throw new ToolError(`Session ${
|
|
36678
|
+
if (ENDED$3.has(session.status)) throw new ToolError(`Session ${session.id} already ${session.status}`);
|
|
35804
36679
|
return {
|
|
35805
|
-
status: (await services.sessions.stop(
|
|
35806
|
-
sessionId
|
|
36680
|
+
status: (await services.sessions.stop(session.id)).status,
|
|
36681
|
+
sessionId: session.id
|
|
35807
36682
|
};
|
|
35808
36683
|
}
|
|
35809
36684
|
});
|
|
@@ -35823,9 +36698,11 @@ const pmTools = [
|
|
|
35823
36698
|
handler: async (_input, { services, projectId }) => renderProjectDigest(services.orchestration.projectStatus(projectId))
|
|
35824
36699
|
}),
|
|
35825
36700
|
listSessions,
|
|
36701
|
+
getSession,
|
|
35826
36702
|
startSession,
|
|
35827
36703
|
messageSession,
|
|
35828
36704
|
stopSession,
|
|
36705
|
+
answerQuestion,
|
|
35829
36706
|
defineTool({
|
|
35830
36707
|
name: "list_scheduled_runs",
|
|
35831
36708
|
description: "Upcoming scheduled automation runs of this project, soonest first.",
|
|
@@ -36139,31 +37016,96 @@ const shareToChat = defineTool({
|
|
|
36139
37016
|
function extensionFor(mediaType) {
|
|
36140
37017
|
return Object.entries(MEDIA_TYPES$1).find(([, type]) => type === mediaType)?.[0] ?? ".bin";
|
|
36141
37018
|
}
|
|
37019
|
+
// ../server/src/mcp/tools/suggest-followups.ts
|
|
37020
|
+
/** `suggest_followups` (§7.5): next prompts shown as chips when the turn completes. */
|
|
37021
|
+
const suggestFollowups = defineTool({
|
|
37022
|
+
name: "suggest_followups",
|
|
37023
|
+
description: "Suggest 2 to 4 short next prompts the user might send after this turn, written as the user would type them (for example 'Add tests for the empty-input case'). Call it as the last action of every turn. Calling it again in the same turn replaces the earlier suggestions.",
|
|
37024
|
+
roles: ["agent"],
|
|
37025
|
+
scope: "session",
|
|
37026
|
+
alwaysLoad: true,
|
|
37027
|
+
input: object({ suggestions: array(string().min(1).max(300)).min(2).max(4).describe("2-4 follow-up prompts, most useful first") }),
|
|
37028
|
+
handler: async ({ suggestions }, { services, sessionId }) => {
|
|
37029
|
+
const turn = currentTurn(services, sessionId);
|
|
37030
|
+
putTurnItem(services, sessionId, turn, {
|
|
37031
|
+
id: `followups-${turn.turnId}`,
|
|
37032
|
+
kind: "followups",
|
|
37033
|
+
status: "completed",
|
|
37034
|
+
source: "agent",
|
|
37035
|
+
suggestions: suggestions.map((prompt) => ({ prompt }))
|
|
37036
|
+
});
|
|
37037
|
+
return "Follow-ups saved; they are shown to the user when the turn ends.";
|
|
37038
|
+
}
|
|
37039
|
+
});
|
|
37040
|
+
// ../server/src/mcp/tools/wakeups.ts
|
|
37041
|
+
/** The orchestrator, or the PM of the caller's project. */
|
|
37042
|
+
function ownerOf(ctx) {
|
|
37043
|
+
if (ctx.role === "orchestrator") return {
|
|
37044
|
+
target: "orchestrator",
|
|
37045
|
+
projectId: null,
|
|
37046
|
+
sessionId: ctx.sessionId
|
|
37047
|
+
};
|
|
37048
|
+
if (!ctx.projectId) throw new ToolError("Only the orchestrator or a project's PM has wake-ups");
|
|
37049
|
+
return {
|
|
37050
|
+
target: "pm",
|
|
37051
|
+
projectId: ctx.projectId,
|
|
37052
|
+
sessionId: ctx.sessionId
|
|
37053
|
+
};
|
|
37054
|
+
}
|
|
37055
|
+
/** The wake-up tools of the orchestrator and PMs. */
|
|
37056
|
+
const wakeupTools = [
|
|
37057
|
+
defineTool({
|
|
37058
|
+
name: "schedule_wakeup",
|
|
37059
|
+
description: "Schedule a wake-up for yourself: at that time CodeDen wakes you with your note (and the usual status digest), so you can follow up later (check on running work, retry something, report back) without waiting for the user. Give inMinutes (1 to 10080) or an ISO 8601 time in at, no more than 7 days ahead. At most 20 pending at once.",
|
|
37060
|
+
roles: ["orchestrator", "pm"],
|
|
37061
|
+
scope: "session",
|
|
37062
|
+
alwaysLoad: true,
|
|
37063
|
+
input: object({
|
|
37064
|
+
inMinutes: int().min(1).max(10080).optional(),
|
|
37065
|
+
at: datetime({ offset: true }).optional().describe("When, e.g. 2026-11-11T09:00:00Z"),
|
|
37066
|
+
note: string().trim().min(1).max(2e3).describe("What to do when you wake up, for your future self")
|
|
37067
|
+
}).refine((v) => v.inMinutes === void 0 !== (v.at === void 0), { message: "Give exactly one of inMinutes or at" }),
|
|
37068
|
+
handler: async ({ inMinutes, at, note }, ctx) => {
|
|
37069
|
+
const when = at ? new Date(at) : new Date(Date.now() + (inMinutes ?? 0) * 6e4);
|
|
37070
|
+
const reminder = ctx.services.orchestration.reminders.schedule(ownerOf(ctx), when, note);
|
|
37071
|
+
return `Wake-up ${reminder.id} scheduled for ${reminder.notBefore}. You will be woken with your note then; nothing more to do now.`;
|
|
37072
|
+
}
|
|
37073
|
+
}),
|
|
37074
|
+
defineTool({
|
|
37075
|
+
name: "list_wakeups",
|
|
37076
|
+
description: "Your pending wake-ups (from schedule_wakeup), soonest first.",
|
|
37077
|
+
roles: ["orchestrator", "pm"],
|
|
37078
|
+
scope: "session",
|
|
37079
|
+
input: object({}),
|
|
37080
|
+
handler: async (_input, ctx) => {
|
|
37081
|
+
const owner = ownerOf(ctx);
|
|
37082
|
+
return { wakeups: ctx.services.orchestration.reminders.pending(owner.target, owner.projectId).flatMap((n) => n.type === "reminder" ? [{
|
|
37083
|
+
id: n.id,
|
|
37084
|
+
at: n.notBefore,
|
|
37085
|
+
note: n.data.note
|
|
37086
|
+
}] : []) };
|
|
37087
|
+
}
|
|
37088
|
+
}),
|
|
37089
|
+
defineTool({
|
|
37090
|
+
name: "cancel_wakeup",
|
|
37091
|
+
description: "Cancel one of your pending wake-ups by its id (from list_wakeups).",
|
|
37092
|
+
roles: ["orchestrator", "pm"],
|
|
37093
|
+
scope: "session",
|
|
37094
|
+
input: object({ id: string().min(1) }),
|
|
37095
|
+
handler: async ({ id }, ctx) => {
|
|
37096
|
+
const owner = ownerOf(ctx);
|
|
37097
|
+
if (!ctx.services.orchestration.reminders.cancel(owner.target, owner.projectId, id)) throw new ToolError(`You have no pending wake-up ${id}. Call list_wakeups for the ids.`);
|
|
37098
|
+
return `Wake-up ${id} cancelled.`;
|
|
37099
|
+
}
|
|
37100
|
+
})
|
|
37101
|
+
];
|
|
36142
37102
|
// ../server/src/mcp/tools/index.ts
|
|
36143
37103
|
/** Every CodeDen tool, in the order agents see them. Add new tools here (one file each). */
|
|
36144
37104
|
const allTools = [
|
|
36145
37105
|
reportProgress,
|
|
36146
37106
|
askUser,
|
|
36147
37107
|
shareToChat,
|
|
36148
|
-
|
|
36149
|
-
name: "suggest_followups",
|
|
36150
|
-
description: "Suggest 2 to 4 short next prompts the user might send after this turn, written as the user would type them (for example 'Add tests for the empty-input case'). Call it as the last action of every turn. Calling it again in the same turn replaces the earlier suggestions.",
|
|
36151
|
-
roles: ["agent"],
|
|
36152
|
-
scope: "session",
|
|
36153
|
-
alwaysLoad: true,
|
|
36154
|
-
input: object({ suggestions: array(string().min(1).max(300)).min(2).max(4).describe("2-4 follow-up prompts, most useful first") }),
|
|
36155
|
-
handler: async ({ suggestions }, { services, sessionId }) => {
|
|
36156
|
-
const turn = currentTurn(services, sessionId);
|
|
36157
|
-
putTurnItem(services, sessionId, turn, {
|
|
36158
|
-
id: `followups-${turn.turnId}`,
|
|
36159
|
-
kind: "followups",
|
|
36160
|
-
status: "completed",
|
|
36161
|
-
source: "agent",
|
|
36162
|
-
suggestions: suggestions.map((prompt) => ({ prompt }))
|
|
36163
|
-
});
|
|
36164
|
-
return "Follow-ups saved; they are shown to the user when the turn ends.";
|
|
36165
|
-
}
|
|
36166
|
-
}),
|
|
37108
|
+
suggestFollowups,
|
|
36167
37109
|
finish,
|
|
36168
37110
|
browserNavigate,
|
|
36169
37111
|
browserSnapshot,
|
|
@@ -36178,65 +37120,18 @@ const allTools = [
|
|
|
36178
37120
|
generateImage,
|
|
36179
37121
|
...orchestratorTools,
|
|
36180
37122
|
...pmTools,
|
|
37123
|
+
...wakeupTools,
|
|
36181
37124
|
...githubTools,
|
|
36182
37125
|
...factoryTools
|
|
36183
37126
|
];
|
|
36184
37127
|
// ../server/src/services/agent-config/paths.ts
|
|
36185
37128
|
/**
|
|
36186
|
-
* Claude Code's folder name for a project under `~/.claude/projects/`: every character that
|
|
36187
|
-
* is not an ASCII letter or digit becomes `-` (`C:\Projects\codeden` -> `C--Projects-codeden`,
|
|
36188
|
-
* `C:\Users\me\.cfg` -> `C--Users-me--cfg`). Verified against real folder names on Windows.
|
|
36189
|
-
*/
|
|
36190
|
-
function claudeProjectSlug(projectPath) {
|
|
36191
|
-
return projectPath.replace(/[^a-zA-Z0-9]/g, "-");
|
|
36192
|
-
}
|
|
36193
|
-
/**
|
|
36194
37129
|
* The project's Claude auto-memory folder (§12.4): `autoMemoryDirectory` from the user's
|
|
36195
37130
|
* `~/.claude/settings.json` when set, else `~/.claude/projects/<slug>/memory`. The slug comes
|
|
36196
|
-
* from the git repository root, so every worktree of a repo shares one folder.
|
|
36197
|
-
* slugs are shortened by Claude Code; an existing folder with the same prefix is used then.
|
|
37131
|
+
* from the git repository root, so every worktree of a repo shares one folder.
|
|
36198
37132
|
*/
|
|
36199
37133
|
function claudeAutoMemoryDir(home, repoRoot) {
|
|
36200
|
-
|
|
36201
|
-
if (typeof custom === "string" && custom.trim()) {
|
|
36202
|
-
const value = custom.trim();
|
|
36203
|
-
return value.startsWith("~/") || value.startsWith("~\\") ? join(home, value.slice(2)) : resolve(value);
|
|
36204
|
-
}
|
|
36205
|
-
const projects = join(home, ".claude", "projects");
|
|
36206
|
-
const slug = claudeProjectSlug(repoRoot);
|
|
36207
|
-
const direct = join(projects, slug);
|
|
36208
|
-
if (slug.length > 200 && !existsSync(direct) && existsSync(projects)) {
|
|
36209
|
-
const prefix = slug.slice(0, 200).toLowerCase();
|
|
36210
|
-
const match = readdirSync(projects).find((name) => name.toLowerCase().startsWith(prefix));
|
|
36211
|
-
if (match) return join(projects, match, "memory");
|
|
36212
|
-
}
|
|
36213
|
-
return join(direct, "memory");
|
|
36214
|
-
}
|
|
36215
|
-
/** Nearest ancestor (or `dir` itself) containing `.git` (a folder, or a file in worktrees). */
|
|
36216
|
-
function findGitRoot(dir) {
|
|
36217
|
-
let current = resolve(dir);
|
|
36218
|
-
for (;;) {
|
|
36219
|
-
if (existsSync(join(current, ".git"))) return current;
|
|
36220
|
-
const parent = dirname(current);
|
|
36221
|
-
if (parent === current) return null;
|
|
36222
|
-
current = parent;
|
|
36223
|
-
}
|
|
36224
|
-
}
|
|
36225
|
-
/** The main checkout of a worktree (`.git` file -> `gitdir: <main>/.git/worktrees/x`). */
|
|
36226
|
-
function mainRepoRoot(gitRoot) {
|
|
36227
|
-
const marker = join(gitRoot, ".git");
|
|
36228
|
-
try {
|
|
36229
|
-
if (!statSync(marker).isFile()) return gitRoot;
|
|
36230
|
-
const line = readFileSync(marker, "utf8").trim();
|
|
36231
|
-
const gitdir = /^gitdir:\s*(.+)$/m.exec(line)?.[1];
|
|
36232
|
-
if (!gitdir) return gitRoot;
|
|
36233
|
-
const absolute = resolve(gitRoot, gitdir);
|
|
36234
|
-
const at = absolute.split(sep).lastIndexOf("worktrees");
|
|
36235
|
-
if (at < 1) return gitRoot;
|
|
36236
|
-
return dirname(absolute.split(sep).slice(0, at).join(sep));
|
|
36237
|
-
} catch {
|
|
36238
|
-
return gitRoot;
|
|
36239
|
-
}
|
|
37134
|
+
return claudeMemoryDir(join(home, ".claude"), repoRoot, home);
|
|
36240
37135
|
}
|
|
36241
37136
|
/** `dir` and its ancestors, nearest first, up to the filesystem root. */
|
|
36242
37137
|
function ancestors(dir) {
|
|
@@ -45130,7 +46025,7 @@ const NOTICE_EVENTS = {
|
|
|
45130
46025
|
"session.failed": "session.failed",
|
|
45131
46026
|
"session.needs_input": "session.needs_input"
|
|
45132
46027
|
};
|
|
45133
|
-
const PR_EVENTS = /* @__PURE__ */ new Set([
|
|
46028
|
+
const PR_EVENTS$1 = /* @__PURE__ */ new Set([
|
|
45134
46029
|
"pr.opened",
|
|
45135
46030
|
"pr.merged",
|
|
45136
46031
|
"pr.checks_failed"
|
|
@@ -45189,7 +46084,7 @@ function triggerEvents(events, session) {
|
|
|
45189
46084
|
author: pr.author,
|
|
45190
46085
|
sessionId: pr.sessionId
|
|
45191
46086
|
});
|
|
45192
|
-
} else if (PR_EVENTS.has(e.type)) {
|
|
46087
|
+
} else if (PR_EVENTS$1.has(e.type)) {
|
|
45193
46088
|
const { pr } = e.payload;
|
|
45194
46089
|
out.push({
|
|
45195
46090
|
event: e.type,
|
|
@@ -45885,6 +46780,43 @@ var CronScheduler = class {
|
|
|
45885
46780
|
this.timer.unref();
|
|
45886
46781
|
}
|
|
45887
46782
|
};
|
|
46783
|
+
// ../server/src/services/automations/triage-prompts.ts
|
|
46784
|
+
/**
|
|
46785
|
+
* The texts of issue triage (§10): the request a triage run sends to the PM, and the brief of
|
|
46786
|
+
* the agent session that resolves a ready issue. Pure, so tests can check the wording.
|
|
46787
|
+
*/
|
|
46788
|
+
/** The PM's instructions for one triage run. `canStart`: ready issues start a session. */
|
|
46789
|
+
function triageRequestText(opts) {
|
|
46790
|
+
const { automation, run, issue, policy } = opts;
|
|
46791
|
+
const labels = Object.entries(policy.labels).map(([decision, names]) => ` - ${decision}: ${names.length ? names.join(", ") : "(none)"}`).join("\n");
|
|
46792
|
+
return [
|
|
46793
|
+
`Issue triage (automation "${automation.name}", run ${run.id}).`,
|
|
46794
|
+
`New issue #${issue.number}: ${issue.title ?? ""}`,
|
|
46795
|
+
issue.url,
|
|
46796
|
+
"",
|
|
46797
|
+
"1. Read it with get_issue (and list_issues to look for duplicates). Compare it with the project notes, the vision and the work already running.",
|
|
46798
|
+
"2. Classify it as exactly one of: ready (clear, in scope, safe to implement), needs_info (unclear or missing reproduction), unsafe (touches protected areas or needs a human decision), out_of_scope, duplicate.",
|
|
46799
|
+
`3. Apply it with triage_issue using these labels:\n${labels}`,
|
|
46800
|
+
policy.comment ? " Include a short, friendly comment explaining the decision (for needs_info, ask the specific questions)." : " Do not comment on the issue.",
|
|
46801
|
+
policy.closeRejected ? " Close duplicates and out-of-scope issues (close: true)." : " Do not close the issue.",
|
|
46802
|
+
"4. Then call report_triage with the issue number, the decision and a two-sentence summary.",
|
|
46803
|
+
opts.canStart ? " For ready issues CodeDen starts an agent session automatically; do not call start_session yourself." : " For ready issues CodeDen asks the user whether to take it on; do not call start_session yourself.",
|
|
46804
|
+
"Decide the classification yourself and don't call ask_user: nobody answers during a triage run, and a run that waits never reports. When you can't tell, choose needs_info or unsafe and say why in the summary.",
|
|
46805
|
+
automation.action.type === "triage" && automation.action.instructions ? `\nProject guidance:\n${automation.action.instructions}` : ""
|
|
46806
|
+
].filter((l) => l !== "").join("\n");
|
|
46807
|
+
}
|
|
46808
|
+
/** The brief of the agent session that resolves a triaged issue. */
|
|
46809
|
+
function issueSessionPrompt(issue, summary) {
|
|
46810
|
+
const title = issue.title ?? `Issue #${issue.number}`;
|
|
46811
|
+
return [
|
|
46812
|
+
`Resolve GitHub issue #${issue.number}: ${title}`,
|
|
46813
|
+
issue.url,
|
|
46814
|
+
summary ? `\nTriage summary: ${summary}` : "",
|
|
46815
|
+
"",
|
|
46816
|
+
`Read the full issue and its comments first (\`gh issue view ${issue.number} --comments\`). Implement the change in your worktree following the project's conventions, add or update tests that prove it, and run the test suite.`,
|
|
46817
|
+
`Then open a pull request with create_pull_request whose body includes "Fixes #${issue.number}", attach evidence (test output, and screenshots for UI changes) with attach_evidence, and call finish. If a detail is unclear, make the reasonable call within the issue's scope and state the assumption in the pull request description. Ask with ask_user only if the change needs a protected area or a destructive or outward-facing step.`
|
|
46818
|
+
].filter((l) => l !== "").join("\n");
|
|
46819
|
+
}
|
|
45888
46820
|
// ../server/src/services/automations/triage.ts
|
|
45889
46821
|
/** Inbox id of the "Should I take this on?" decision for an issue. */
|
|
45890
46822
|
function triageItemId(projectId, issueNumber) {
|
|
@@ -45910,22 +46842,13 @@ var TriageService = class {
|
|
|
45910
46842
|
async request(project, automation, run, issue) {
|
|
45911
46843
|
if (project.kind !== "github") throw badRequest("Triage needs a GitHub project", "not_github");
|
|
45912
46844
|
const policy = policyOf(automation);
|
|
45913
|
-
const
|
|
45914
|
-
|
|
45915
|
-
|
|
45916
|
-
|
|
45917
|
-
|
|
45918
|
-
|
|
45919
|
-
|
|
45920
|
-
"1. Read it with get_issue (and list_issues to look for duplicates). Compare it with the project notes, the vision and the work already running.",
|
|
45921
|
-
"2. Classify it as exactly one of: ready (clear, in scope, safe to implement), needs_info (unclear or missing reproduction), unsafe (touches protected areas or needs a human decision), out_of_scope, duplicate.",
|
|
45922
|
-
`3. Apply it with triage_issue using these labels:\n${labels}`,
|
|
45923
|
-
policy.comment ? " Include a short, friendly comment explaining the decision (for needs_info, ask the specific questions)." : " Do not comment on the issue.",
|
|
45924
|
-
policy.closeRejected ? " Close duplicates and out-of-scope issues (close: true)." : " Do not close the issue.",
|
|
45925
|
-
"4. Then call report_triage with the issue number, the decision and a two-sentence summary.",
|
|
45926
|
-
canStart ? " For ready issues CodeDen starts an agent session automatically; do not call start_session yourself." : " For ready issues CodeDen asks the user whether to take it on; do not call start_session yourself.",
|
|
45927
|
-
automation.action.type === "triage" && automation.action.instructions ? `\nProject guidance:\n${automation.action.instructions}` : ""
|
|
45928
|
-
].filter((l) => l !== "").join("\n");
|
|
46845
|
+
const text = triageRequestText({
|
|
46846
|
+
automation,
|
|
46847
|
+
run,
|
|
46848
|
+
issue,
|
|
46849
|
+
policy,
|
|
46850
|
+
canStart: policy.autoStart && project.autonomyMode !== "manual"
|
|
46851
|
+
});
|
|
45929
46852
|
return (await this.deps.orchestration().sendToRole("pm", project.id, {
|
|
45930
46853
|
text,
|
|
45931
46854
|
attachments: []
|
|
@@ -46025,14 +46948,7 @@ var TriageService = class {
|
|
|
46025
46948
|
}
|
|
46026
46949
|
async startForIssue(project, issue, summary, policy, startedBy, decision) {
|
|
46027
46950
|
const title = issue.title ?? `Issue #${issue.number}`;
|
|
46028
|
-
const prompt =
|
|
46029
|
-
`Resolve GitHub issue #${issue.number}: ${title}`,
|
|
46030
|
-
issue.url,
|
|
46031
|
-
summary ? `\nTriage summary: ${summary}` : "",
|
|
46032
|
-
"",
|
|
46033
|
-
`Read the full issue and its comments first (\`gh issue view ${issue.number} --comments\`). Implement the change in your worktree following the project's conventions, add or update tests that prove it, and run the test suite.`,
|
|
46034
|
-
`Then open a pull request with create_pull_request whose body includes "Fixes #${issue.number}", attach evidence (test output, and screenshots for UI changes) with attach_evidence, and call finish. If the issue turns out to be unclear or unsafe, ask with ask_user instead of guessing.`
|
|
46035
|
-
].filter((l) => l !== "").join("\n");
|
|
46951
|
+
const prompt = issueSessionPrompt(issue, summary);
|
|
46036
46952
|
return this.deps.sessions.start(StartSessionRequest.parse({
|
|
46037
46953
|
projectId: project.id,
|
|
46038
46954
|
role: "agent",
|
|
@@ -46501,7 +47417,8 @@ const runTool = (command, args, opts = {}) => {
|
|
|
46501
47417
|
env: mergeEnv(env, {
|
|
46502
47418
|
NO_COLOR: "1",
|
|
46503
47419
|
FORCE_COLOR: "0",
|
|
46504
|
-
GIT_TERMINAL_PROMPT: "0"
|
|
47420
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
47421
|
+
GCM_INTERACTIVE: "never"
|
|
46505
47422
|
}),
|
|
46506
47423
|
windowsHide: true,
|
|
46507
47424
|
windowsVerbatimArguments: isShim,
|
|
@@ -52470,6 +53387,21 @@ var PrFollowUp = class {
|
|
|
52470
53387
|
for (const number of issues) await commentOnIssue({ client: this.deps.client }, repo, number, `Resolved by #${pr.number}, merged${pr.mergedAt ? ` at ${pr.mergedAt}` : ""}.\n\n<sub>CodeDen session ${label}</sub>`);
|
|
52471
53388
|
}
|
|
52472
53389
|
};
|
|
53390
|
+
// ../server/src/services/sessions/own-branch.ts
|
|
53391
|
+
/**
|
|
53392
|
+
* The branch a session owns: its own worktree's branch, never its base branch or a usual
|
|
53393
|
+
* default branch. The agent may force-push it with a lease (§11.2).
|
|
53394
|
+
*/
|
|
53395
|
+
function ownBranch(session) {
|
|
53396
|
+
const { branch, worktreePath, baseBranch } = session;
|
|
53397
|
+
if (!branch || !worktreePath || branch === baseBranch) return void 0;
|
|
53398
|
+
return [
|
|
53399
|
+
"main",
|
|
53400
|
+
"master",
|
|
53401
|
+
"develop",
|
|
53402
|
+
"trunk"
|
|
53403
|
+
].includes(branch) ? void 0 : branch;
|
|
53404
|
+
}
|
|
52473
53405
|
// ../server/src/services/github/graphql.ts
|
|
52474
53406
|
/** Batched GraphQL reads of pull requests: state, checks, reviews, mergeability, commits. */
|
|
52475
53407
|
const PR_FIELDS = `
|
|
@@ -52646,6 +53578,31 @@ function pullRequestBody(text, issue, session) {
|
|
|
52646
53578
|
parts.push(`---\n<sub>Opened by CodeDen session ${label}</sub>\n${SESSION_MARKER}${session.id} -->`);
|
|
52647
53579
|
return parts.filter(Boolean).join("\n\n");
|
|
52648
53580
|
}
|
|
53581
|
+
/** git's answer when a push would rewrite the remote branch's history. */
|
|
53582
|
+
const NOT_FAST_FORWARD = /\((non-fast-forward|fetch first)\)|updates were rejected/i;
|
|
53583
|
+
/**
|
|
53584
|
+
* Push the session's branch. When the session owns it (its own worktree's branch) and the push
|
|
53585
|
+
* is rejected as not a fast-forward, as after a rebase, push again with a lease: that rewrites
|
|
53586
|
+
* only the session's own work, and refuses if someone else pushed there since.
|
|
53587
|
+
*/
|
|
53588
|
+
async function pushBranch(deps, session, cwd, branch) {
|
|
53589
|
+
const push = {
|
|
53590
|
+
name: "git.push",
|
|
53591
|
+
cwd,
|
|
53592
|
+
branch,
|
|
53593
|
+
setUpstream: true
|
|
53594
|
+
};
|
|
53595
|
+
try {
|
|
53596
|
+
await deps.runner.request(push, 3e5);
|
|
53597
|
+
} catch (error) {
|
|
53598
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
53599
|
+
if (ownBranch(session) !== branch || !NOT_FAST_FORWARD.test(message)) throw error;
|
|
53600
|
+
await deps.runner.request({
|
|
53601
|
+
...push,
|
|
53602
|
+
forceWithLease: true
|
|
53603
|
+
}, 3e5);
|
|
53604
|
+
}
|
|
53605
|
+
}
|
|
52649
53606
|
/**
|
|
52650
53607
|
* Push the session's branch through the runner and open (or update) its pull request (§8.2,
|
|
52651
53608
|
* §9). The PR is projected right away (`pr.opened` / `pr.updated`) and linked to the session.
|
|
@@ -52670,12 +53627,7 @@ async function createPullRequest(deps, session, repo, input) {
|
|
|
52670
53627
|
message: input.commitMessage
|
|
52671
53628
|
}, 3e5)).commit;
|
|
52672
53629
|
}
|
|
52673
|
-
await deps
|
|
52674
|
-
name: "git.push",
|
|
52675
|
-
cwd,
|
|
52676
|
-
branch,
|
|
52677
|
-
setUpstream: true
|
|
52678
|
-
}, 3e5);
|
|
53630
|
+
await pushBranch(deps, session, cwd, branch);
|
|
52679
53631
|
} catch (error) {
|
|
52680
53632
|
throw toServiceError$1(error);
|
|
52681
53633
|
}
|
|
@@ -53891,28 +54843,37 @@ var GithubServicePort = class {
|
|
|
53891
54843
|
// ../server/src/services/orchestration/asks.ts
|
|
53892
54844
|
/** The marker that ties a PM turn to an `ask_pm` call. */
|
|
53893
54845
|
const askMarker = (askId) => `(ask ${askId})`;
|
|
54846
|
+
const MARKER = /\(ask ([0-9a-f]{8})\)/g;
|
|
54847
|
+
/** Asks answered recently (kept so a turn's answer is never sent twice). */
|
|
54848
|
+
const SETTLED_LIMIT = 500;
|
|
53894
54849
|
const MAX_AGE_MS = 216e5;
|
|
53895
54850
|
/**
|
|
53896
54851
|
* `ask_pm` replies (§8.3). The question goes to the PM as a message carrying a marker; the
|
|
53897
54852
|
* PM's turn that handles it is found by the marker in its user message, and the final
|
|
53898
54853
|
* assistant message of that turn is the answer (or a `message_orchestrator` call with
|
|
53899
54854
|
* `replyTo`, whichever comes first). If the call times out, the answer still reaches the
|
|
53900
|
-
* orchestrator later, as a message.
|
|
54855
|
+
* orchestrator later, as a message. So does the answer to an ask nothing tracks any more (a
|
|
54856
|
+
* restart, an ask older than six hours): the marker in the turn's input is enough.
|
|
53901
54857
|
*/
|
|
53902
54858
|
var PmAsks = class {
|
|
53903
54859
|
deps;
|
|
53904
54860
|
waiters = /* @__PURE__ */ new Map();
|
|
54861
|
+
settled = /* @__PURE__ */ new Set();
|
|
53905
54862
|
constructor(deps) {
|
|
53906
54863
|
this.deps = deps;
|
|
53907
54864
|
}
|
|
53908
|
-
|
|
54865
|
+
/**
|
|
54866
|
+
* Track an ask. A `detached` ask has no call waiting for it (the loop guard held the
|
|
54867
|
+
* question back): its answer reaches the orchestrator as a message.
|
|
54868
|
+
*/
|
|
54869
|
+
register(askId, target, opts = {}) {
|
|
53909
54870
|
this.prune();
|
|
53910
54871
|
this.waiters.set(askId, {
|
|
53911
54872
|
askId,
|
|
53912
54873
|
...target,
|
|
53913
54874
|
turnId: void 0,
|
|
53914
54875
|
resolve: void 0,
|
|
53915
|
-
late: false,
|
|
54876
|
+
late: opts.detached ?? false,
|
|
53916
54877
|
createdAt: Date.now()
|
|
53917
54878
|
});
|
|
53918
54879
|
}
|
|
@@ -53949,9 +54910,10 @@ var PmAsks = class {
|
|
|
53949
54910
|
}
|
|
53950
54911
|
/** Follow committed events: find the answering turn, then its final message. */
|
|
53951
54912
|
onEvents(events) {
|
|
53952
|
-
if (this.waiters.size === 0) return;
|
|
53953
54913
|
for (const e of events) {
|
|
53954
54914
|
if (!e.sessionId) continue;
|
|
54915
|
+
if (e.type === "agent.turn.completed") this.answerUntracked(e.sessionId, e.payload.turnId);
|
|
54916
|
+
if (this.waiters.size === 0) continue;
|
|
53955
54917
|
if (e.type === "agent.item.started" || e.type === "agent.item.completed") {
|
|
53956
54918
|
const event = e.payload;
|
|
53957
54919
|
const item = event.item;
|
|
@@ -53973,16 +54935,50 @@ var PmAsks = class {
|
|
|
53973
54935
|
}
|
|
53974
54936
|
finish(waiter, text) {
|
|
53975
54937
|
this.waiters.delete(waiter.askId);
|
|
54938
|
+
this.settle(waiter.askId);
|
|
53976
54939
|
if (waiter.resolve) waiter.resolve(text);
|
|
53977
54940
|
else if (waiter.late) {
|
|
53978
|
-
const
|
|
53979
|
-
|
|
53980
|
-
|
|
54941
|
+
const { askId, projectId, pmSessionId } = waiter;
|
|
54942
|
+
setImmediate(() => this.deps.onLateReply({
|
|
54943
|
+
askId,
|
|
54944
|
+
projectId,
|
|
54945
|
+
pmSessionId,
|
|
53981
54946
|
text
|
|
53982
|
-
};
|
|
53983
|
-
setImmediate(() => this.deps.onLateReply(reply));
|
|
54947
|
+
}));
|
|
53984
54948
|
}
|
|
53985
54949
|
}
|
|
54950
|
+
/**
|
|
54951
|
+
* A PM's turn completed: answer the asks in its input that no waiter tracks (registered
|
|
54952
|
+
* before a restart, or pruned). Tracked ones are answered by their waiter instead.
|
|
54953
|
+
*/
|
|
54954
|
+
answerUntracked(sessionId, turnId) {
|
|
54955
|
+
const session = this.deps.session(sessionId);
|
|
54956
|
+
if (session?.role !== "pm" || !session.projectId) return;
|
|
54957
|
+
const projectId = session.projectId;
|
|
54958
|
+
const askIds = /* @__PURE__ */ new Set();
|
|
54959
|
+
for (const row of this.deps.db.select().from(items).where(and(eq(items.sessionId, sessionId), eq(items.turnId, turnId), eq(items.kind, "user_message"))).all()) {
|
|
54960
|
+
if (row.data.kind !== "user_message") continue;
|
|
54961
|
+
for (const m of row.data.input.text.matchAll(MARKER)) if (m[1]) askIds.add(m[1]);
|
|
54962
|
+
}
|
|
54963
|
+
const untracked = [...askIds].filter((id) => !this.waiters.has(id) && !this.settled.has(id));
|
|
54964
|
+
if (untracked.length === 0) return;
|
|
54965
|
+
const text = this.finalMessage(sessionId, turnId) ?? "(The PM finished without a reply.)";
|
|
54966
|
+
for (const askId of untracked) {
|
|
54967
|
+
this.settle(askId);
|
|
54968
|
+
setImmediate(() => this.deps.onLateReply({
|
|
54969
|
+
askId,
|
|
54970
|
+
projectId,
|
|
54971
|
+
pmSessionId: sessionId,
|
|
54972
|
+
text
|
|
54973
|
+
}));
|
|
54974
|
+
}
|
|
54975
|
+
}
|
|
54976
|
+
settle(askId) {
|
|
54977
|
+
this.settled.add(askId);
|
|
54978
|
+
if (this.settled.size <= SETTLED_LIMIT) return;
|
|
54979
|
+
const oldest = this.settled.values().next().value;
|
|
54980
|
+
if (oldest !== void 0) this.settled.delete(oldest);
|
|
54981
|
+
}
|
|
53986
54982
|
finalMessage(sessionId, turnId) {
|
|
53987
54983
|
return finalAssistantMessage(this.deps.db, sessionId, turnId);
|
|
53988
54984
|
}
|
|
@@ -53997,6 +54993,52 @@ function finalAssistantMessage(db, sessionId, turnId) {
|
|
|
53997
54993
|
for (const row of rows) if (row.data.kind === "assistant_message" && row.data.text.trim()) return row.data.text.trim();
|
|
53998
54994
|
return null;
|
|
53999
54995
|
}
|
|
54996
|
+
// ../server/src/services/orchestration/cards.ts
|
|
54997
|
+
/**
|
|
54998
|
+
* Notices a wake (or any message) delivered to a role session, shown as one batch of cards at
|
|
54999
|
+
* the start of the turn that carries them (§8.2: notices stay structured data up to the UI).
|
|
55000
|
+
*/
|
|
55001
|
+
var NoticeCards = class {
|
|
55002
|
+
store;
|
|
55003
|
+
pending = /* @__PURE__ */ new Map();
|
|
55004
|
+
constructor(store) {
|
|
55005
|
+
this.store = store;
|
|
55006
|
+
}
|
|
55007
|
+
/** Notices delivered with the next turn of `sessionId`. */
|
|
55008
|
+
add(sessionId, notices) {
|
|
55009
|
+
this.pending.set(sessionId, [...this.pending.get(sessionId) ?? [], ...notices]);
|
|
55010
|
+
}
|
|
55011
|
+
/** Forget notices a turn was going to carry: its send failed, they wait for the next one. */
|
|
55012
|
+
remove(sessionId, ids) {
|
|
55013
|
+
const left = (this.pending.get(sessionId) ?? []).filter((n) => !ids.includes(n.id));
|
|
55014
|
+
if (left.length > 0) this.pending.set(sessionId, left);
|
|
55015
|
+
else this.pending.delete(sessionId);
|
|
55016
|
+
}
|
|
55017
|
+
/** Append the waiting cards to each role session whose turn just started. */
|
|
55018
|
+
attach(events) {
|
|
55019
|
+
for (const e of events) {
|
|
55020
|
+
if (e.type !== "agent.turn.started" || !e.sessionId) continue;
|
|
55021
|
+
const cards = this.pending.get(e.sessionId);
|
|
55022
|
+
if (!cards) continue;
|
|
55023
|
+
this.pending.delete(e.sessionId);
|
|
55024
|
+
const event = e.payload;
|
|
55025
|
+
this.store.append([agentEventToNewEvent(e.sessionId, {
|
|
55026
|
+
type: "item.completed",
|
|
55027
|
+
threadId: event.threadId,
|
|
55028
|
+
turnId: event.turnId,
|
|
55029
|
+
item: {
|
|
55030
|
+
id: `notices-${event.turnId}`,
|
|
55031
|
+
kind: "notice",
|
|
55032
|
+
status: "completed",
|
|
55033
|
+
level: "info",
|
|
55034
|
+
code: NOTICE_BATCH_CODE,
|
|
55035
|
+
message: `${cards.length} update${cards.length === 1 ? "" : "s"} since the last turn`,
|
|
55036
|
+
data: JSON.parse(JSON.stringify({ notices: cards }))
|
|
55037
|
+
}
|
|
55038
|
+
})]);
|
|
55039
|
+
}
|
|
55040
|
+
}
|
|
55041
|
+
};
|
|
54000
55042
|
// ../server/src/services/orchestration/conflicts.ts
|
|
54001
55043
|
/**
|
|
54002
55044
|
* Persisted conflicts (§8.4): a start request that matched running work becomes an inbox
|
|
@@ -54115,184 +55157,116 @@ var ConflictService = class {
|
|
|
54115
55157
|
});
|
|
54116
55158
|
}
|
|
54117
55159
|
};
|
|
54118
|
-
// ../server/src/services/orchestration/
|
|
54119
|
-
|
|
54120
|
-
|
|
54121
|
-
|
|
54122
|
-
|
|
54123
|
-
*/
|
|
54124
|
-
function wakesPm(session) {
|
|
54125
|
-
return session.startedBy.kind !== "user" || session.parentSessionId !== null;
|
|
54126
|
-
}
|
|
54127
|
-
/** The id of an opened PR's notice: the same whether it came from the session or GitHub sync. */
|
|
54128
|
-
function prOpenedNoticeId(projectId, number) {
|
|
54129
|
-
return `pr.opened:${projectId}:${number}`;
|
|
54130
|
-
}
|
|
54131
|
-
function workRef(s) {
|
|
54132
|
-
return {
|
|
54133
|
-
sessionId: s.id,
|
|
54134
|
-
workKey: s.workKey,
|
|
54135
|
-
purpose: s.purpose,
|
|
54136
|
-
startedBy: s.startedBy
|
|
54137
|
-
};
|
|
55160
|
+
// ../server/src/services/orchestration/delivery.ts
|
|
55161
|
+
/** Whether a stored notice should wake its target (older notices didn't record it). */
|
|
55162
|
+
function wakesTarget(n) {
|
|
55163
|
+
if (n.wake !== void 0) return n.wake;
|
|
55164
|
+
return !(n.type === "pm.message" && n.data.informational);
|
|
54138
55165
|
}
|
|
55166
|
+
const keyOf$2 = (session) => session.role === "pm" ? `pm:${session.projectId}` : "orchestrator";
|
|
54139
55167
|
/**
|
|
54140
|
-
*
|
|
54141
|
-
*
|
|
54142
|
-
*
|
|
54143
|
-
|
|
54144
|
-
|
|
54145
|
-
|
|
54146
|
-
|
|
54147
|
-
|
|
54148
|
-
|
|
54149
|
-
|
|
54150
|
-
|
|
54151
|
-
|
|
54152
|
-
|
|
54153
|
-
|
|
54154
|
-
|
|
54155
|
-
|
|
54156
|
-
wake: wakesPm(s),
|
|
54157
|
-
notice: {
|
|
54158
|
-
id: `session.finished:${s.id}:${event.turnId}`,
|
|
54159
|
-
type: "session.finished",
|
|
54160
|
-
projectId: s.projectId,
|
|
54161
|
-
target: "pm",
|
|
54162
|
-
sessionId: s.id,
|
|
54163
|
-
title: `Finished: ${s.title ?? s.purpose}`,
|
|
54164
|
-
data: {
|
|
54165
|
-
...workRef(s),
|
|
54166
|
-
summary: s.result?.summary ?? (final ? clip$2(final) : null),
|
|
54167
|
-
outcome: s.result?.outcome ?? null
|
|
54168
|
-
}
|
|
54169
|
-
}
|
|
54170
|
-
});
|
|
54171
|
-
} else if (e.type === "inbox.created") {
|
|
54172
|
-
const { item } = e.payload;
|
|
54173
|
-
const s = agentSession(item.sessionId);
|
|
54174
|
-
if (!s) continue;
|
|
54175
|
-
if (item.kind === "question" || item.kind === "approval") out.push({
|
|
54176
|
-
wake: wakesPm(s),
|
|
54177
|
-
notice: {
|
|
54178
|
-
id: `session.needs_input:${item.id}`,
|
|
54179
|
-
type: "session.needs_input",
|
|
54180
|
-
projectId: s.projectId,
|
|
54181
|
-
target: "pm",
|
|
54182
|
-
sessionId: s.id,
|
|
54183
|
-
title: `Needs input: ${s.title ?? s.purpose}`,
|
|
54184
|
-
data: {
|
|
54185
|
-
...workRef(s),
|
|
54186
|
-
inboxItemId: item.id,
|
|
54187
|
-
kind: item.kind,
|
|
54188
|
-
question: item.title
|
|
54189
|
-
}
|
|
54190
|
-
}
|
|
54191
|
-
});
|
|
54192
|
-
else if (item.kind === "run_failed") out.push({
|
|
54193
|
-
wake: wakesPm(s),
|
|
54194
|
-
notice: {
|
|
54195
|
-
id: `session.failed:${item.id}`,
|
|
54196
|
-
type: "session.failed",
|
|
54197
|
-
projectId: s.projectId,
|
|
54198
|
-
target: "pm",
|
|
54199
|
-
sessionId: s.id,
|
|
54200
|
-
title: `Failed: ${s.title ?? s.purpose}`,
|
|
54201
|
-
data: {
|
|
54202
|
-
...workRef(s),
|
|
54203
|
-
error: clip$2(item.body ?? item.title)
|
|
54204
|
-
}
|
|
54205
|
-
}
|
|
54206
|
-
});
|
|
54207
|
-
} else if (e.type === "session.updated") {
|
|
54208
|
-
const { session } = e.payload;
|
|
54209
|
-
if (session.role !== "agent" || !session.projectId || !session.pr) continue;
|
|
54210
|
-
out.push({
|
|
54211
|
-
wake: wakesPm(session),
|
|
54212
|
-
notice: {
|
|
54213
|
-
id: prOpenedNoticeId(session.projectId, session.pr.number),
|
|
54214
|
-
type: "pr.opened",
|
|
54215
|
-
projectId: session.projectId,
|
|
54216
|
-
target: "pm",
|
|
54217
|
-
sessionId: session.id,
|
|
54218
|
-
title: `PR #${session.pr.number} opened: ${session.pr.title ?? session.purpose}`,
|
|
54219
|
-
data: {
|
|
54220
|
-
sessionId: session.id,
|
|
54221
|
-
pr: session.pr
|
|
54222
|
-
}
|
|
54223
|
-
}
|
|
54224
|
-
});
|
|
54225
|
-
} else if (e.type === "automation_run.updated") {
|
|
54226
|
-
const { run } = e.payload;
|
|
54227
|
-
if (run.status !== "skipped" || !run.projectId) continue;
|
|
54228
|
-
out.push({
|
|
54229
|
-
wake: true,
|
|
54230
|
-
notice: {
|
|
54231
|
-
id: `automation.skipped:${run.id}`,
|
|
54232
|
-
type: "automation.skipped",
|
|
54233
|
-
projectId: run.projectId,
|
|
54234
|
-
target: "pm",
|
|
54235
|
-
sessionId: run.sessionId,
|
|
54236
|
-
title: `Automation skipped: ${run.purpose}`,
|
|
54237
|
-
data: {
|
|
54238
|
-
automationRunId: run.id,
|
|
54239
|
-
automationId: run.automationId,
|
|
54240
|
-
workKey: run.workKey,
|
|
54241
|
-
purpose: run.purpose,
|
|
54242
|
-
reason: run.skipReason ?? "skipped"
|
|
54243
|
-
}
|
|
54244
|
-
}
|
|
54245
|
-
});
|
|
55168
|
+
* Getting notices to the orchestrator and the PMs (§8.2) reliably:
|
|
55169
|
+
*
|
|
55170
|
+
* - a turn's notices count as delivered only once the runner took the turn; until then they
|
|
55171
|
+
* are claimed, so a message sent meanwhile doesn't carry them twice;
|
|
55172
|
+
* - on start, targets with wake-worthy notices still undelivered (a restart cut the wake off)
|
|
55173
|
+
* are woken again;
|
|
55174
|
+
* - when a role session's turn that carried notices fails (an overloaded model, say), the
|
|
55175
|
+
* notices go back to undelivered and the session is woken again a few times, backing off.
|
|
55176
|
+
*/
|
|
55177
|
+
var NoticeDelivery = class {
|
|
55178
|
+
deps;
|
|
55179
|
+
claimed = /* @__PURE__ */ new Set();
|
|
55180
|
+
failures = /* @__PURE__ */ new Map();
|
|
55181
|
+
timers = /* @__PURE__ */ new Set();
|
|
55182
|
+
constructor(deps) {
|
|
55183
|
+
this.deps = deps;
|
|
54246
55184
|
}
|
|
54247
|
-
|
|
54248
|
-
|
|
54249
|
-
|
|
54250
|
-
|
|
54251
|
-
|
|
54252
|
-
|
|
54253
|
-
|
|
54254
|
-
|
|
54255
|
-
|
|
55185
|
+
/** Prepend the status digest and the undelivered notices to a role session's input (§8.4). */
|
|
55186
|
+
decorate(session, input) {
|
|
55187
|
+
if (session.role === "agent") return { input };
|
|
55188
|
+
try {
|
|
55189
|
+
const parts = [this.deps.digestFor(session)];
|
|
55190
|
+
const pending = this.deps.notices.undelivered(session.role === "pm" ? "pm" : "orchestrator", session.role === "pm" ? session.projectId : null).filter((n) => !this.claimed.has(n.id));
|
|
55191
|
+
parts.push(...pending.length > 0 ? [renderNotices(pending, this.deps.projectName)] : []);
|
|
55192
|
+
parts.push(input.text);
|
|
55193
|
+
const ids = pending.map((n) => n.id);
|
|
55194
|
+
for (const id of ids) this.claimed.add(id);
|
|
55195
|
+
if (pending.length > 0) this.deps.cards.add(session.id, pending);
|
|
55196
|
+
const release = () => {
|
|
55197
|
+
for (const id of ids) this.claimed.delete(id);
|
|
55198
|
+
};
|
|
55199
|
+
return {
|
|
55200
|
+
input: {
|
|
55201
|
+
...input,
|
|
55202
|
+
text: parts.join("\n\n")
|
|
55203
|
+
},
|
|
55204
|
+
delivered: () => {
|
|
55205
|
+
release();
|
|
55206
|
+
this.deps.notices.markDelivered(ids);
|
|
55207
|
+
},
|
|
55208
|
+
failed: () => {
|
|
55209
|
+
release();
|
|
55210
|
+
this.deps.cards.remove(session.id, ids);
|
|
55211
|
+
}
|
|
55212
|
+
};
|
|
55213
|
+
} catch (error) {
|
|
55214
|
+
this.deps.log?.(`orchestration digest: ${errorMessage$1(error)}`);
|
|
55215
|
+
return { input };
|
|
55216
|
+
}
|
|
54256
55217
|
}
|
|
54257
|
-
/**
|
|
54258
|
-
|
|
54259
|
-
if (this.
|
|
54260
|
-
const
|
|
54261
|
-
...draft,
|
|
54262
|
-
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
54263
|
-
deliveredAt: null
|
|
54264
|
-
};
|
|
54265
|
-
this.store.append([{
|
|
54266
|
-
type: "notice.created",
|
|
54267
|
-
sessionId: null,
|
|
54268
|
-
payload: { notice }
|
|
54269
|
-
}]);
|
|
54270
|
-
return notice;
|
|
55218
|
+
/** Wake every target whose wake-worthy notices are still undelivered (server start). */
|
|
55219
|
+
rearm() {
|
|
55220
|
+
if (this.deps.notices.undelivered("orchestrator", null).some(wakesTarget)) this.deps.wakes.request("orchestrator");
|
|
55221
|
+
for (const id of this.deps.projectIds()) if (this.deps.notices.undelivered("pm", id).some(wakesTarget)) this.deps.wakes.request(`pm:${id}`);
|
|
54271
55222
|
}
|
|
54272
|
-
|
|
54273
|
-
|
|
54274
|
-
|
|
54275
|
-
|
|
54276
|
-
|
|
55223
|
+
/** Follow role sessions' root turns: a failed one returns its notices and retries. */
|
|
55224
|
+
onEvents(events) {
|
|
55225
|
+
for (const e of events) {
|
|
55226
|
+
if (e.type !== "agent.turn.completed" && e.type !== "agent.turn.failed") continue;
|
|
55227
|
+
const session = e.sessionId ? this.deps.session(e.sessionId) : void 0;
|
|
55228
|
+
if (!session || session.role === "agent") continue;
|
|
55229
|
+
const event = e.payload;
|
|
55230
|
+
if (session.rootThreadId && session.rootThreadId !== event.threadId) continue;
|
|
55231
|
+
if (e.type === "agent.turn.completed") {
|
|
55232
|
+
this.failures.delete(session.id);
|
|
55233
|
+
continue;
|
|
55234
|
+
}
|
|
55235
|
+
this.turnFailed(session, event.turnId);
|
|
55236
|
+
}
|
|
54277
55237
|
}
|
|
54278
|
-
|
|
54279
|
-
|
|
54280
|
-
|
|
55238
|
+
dispose() {
|
|
55239
|
+
for (const timer of this.timers) clearTimeout(timer);
|
|
55240
|
+
this.timers.clear();
|
|
54281
55241
|
}
|
|
54282
|
-
|
|
54283
|
-
|
|
54284
|
-
|
|
55242
|
+
turnFailed(session, turnId) {
|
|
55243
|
+
const carried = this.carried(session.id, turnId);
|
|
55244
|
+
if (carried.length === 0) return;
|
|
55245
|
+
this.deps.notices.markReturned(carried);
|
|
55246
|
+
const attempt = (this.failures.get(session.id) ?? 0) + 1;
|
|
55247
|
+
this.failures.set(session.id, attempt);
|
|
55248
|
+
const delay = (this.deps.retryDelaysMs ?? [
|
|
55249
|
+
3e4,
|
|
55250
|
+
12e4,
|
|
55251
|
+
6e5
|
|
55252
|
+
])[attempt - 1];
|
|
55253
|
+
if (delay === void 0) {
|
|
55254
|
+
this.deps.log?.(`orchestration: ${keyOf$2(session)} keeps failing; its updates wait for its next message`);
|
|
55255
|
+
return;
|
|
55256
|
+
}
|
|
55257
|
+
const timer = setTimeout(() => {
|
|
55258
|
+
this.timers.delete(timer);
|
|
55259
|
+
this.deps.wakes.request(keyOf$2(session));
|
|
55260
|
+
}, delay);
|
|
55261
|
+
timer.unref?.();
|
|
55262
|
+
this.timers.add(timer);
|
|
54285
55263
|
}
|
|
54286
|
-
|
|
54287
|
-
|
|
54288
|
-
this.
|
|
54289
|
-
|
|
54290
|
-
|
|
54291
|
-
|
|
54292
|
-
noticeIds: ids,
|
|
54293
|
-
at: (/* @__PURE__ */ new Date()).toISOString()
|
|
54294
|
-
}
|
|
54295
|
-
}]);
|
|
55264
|
+
/** Ids of the notices a turn carried (its batch of cards). */
|
|
55265
|
+
carried(sessionId, turnId) {
|
|
55266
|
+
const row = this.deps.db.select().from(items).where(and(eq(items.sessionId, sessionId), eq(items.turnId, turnId), eq(items.id, `notices-${turnId}`))).get();
|
|
55267
|
+
if (row?.data.kind !== "notice") return [];
|
|
55268
|
+
const batch = NoticeBatchData.safeParse(row.data.data);
|
|
55269
|
+
return batch.success ? batch.data.notices.map((n) => n.id) : [];
|
|
54296
55270
|
}
|
|
54297
55271
|
};
|
|
54298
55272
|
// ../server/src/services/orchestration/github-notices.ts
|
|
@@ -54305,7 +55279,7 @@ const GITHUB_NOTICE_EVENTS = /* @__PURE__ */ new Set([
|
|
|
54305
55279
|
"issue.opened",
|
|
54306
55280
|
"review.submitted"
|
|
54307
55281
|
]);
|
|
54308
|
-
const clip$
|
|
55282
|
+
const clip$3 = (text, max = 300) => text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
54309
55283
|
function prRef(pr) {
|
|
54310
55284
|
return {
|
|
54311
55285
|
number: pr.number,
|
|
@@ -54441,7 +55415,7 @@ function deriveGithubNotices(events, session) {
|
|
|
54441
55415
|
pr: prRef(pr),
|
|
54442
55416
|
reviewer: review.author,
|
|
54443
55417
|
state: review.state,
|
|
54444
|
-
body: clip$
|
|
55418
|
+
body: clip$3(review.body)
|
|
54445
55419
|
}
|
|
54446
55420
|
}
|
|
54447
55421
|
});
|
|
@@ -54449,6 +55423,243 @@ function deriveGithubNotices(events, session) {
|
|
|
54449
55423
|
}
|
|
54450
55424
|
return out;
|
|
54451
55425
|
}
|
|
55426
|
+
// ../server/src/services/orchestration/wake-scheduler.ts
|
|
55427
|
+
/**
|
|
55428
|
+
* Batches notices into debounced wakes per target (`pm:<projectId>`, `orchestrator`) with a
|
|
55429
|
+
* loop guard (§8.2): notices arriving close together become one "updates since your last
|
|
55430
|
+
* turn" wake, and no target is woken more than `maxWakes` times per window. Held-back wakes
|
|
55431
|
+
* fire as soon as the window allows, or right after `reset` when a person steps in; nothing
|
|
55432
|
+
* is dropped. The orchestration service runs one for notice wakes and one for the
|
|
55433
|
+
* orchestrator's messages to PMs (`LoopGuard`), so each conversation is counted on its own.
|
|
55434
|
+
*/
|
|
55435
|
+
var WakeScheduler = class {
|
|
55436
|
+
fire;
|
|
55437
|
+
pending = /* @__PURE__ */ new Map();
|
|
55438
|
+
history = /* @__PURE__ */ new Map();
|
|
55439
|
+
opts;
|
|
55440
|
+
constructor(fire, opts = {}) {
|
|
55441
|
+
this.fire = fire;
|
|
55442
|
+
this.opts = {
|
|
55443
|
+
debounceMs: opts.debounceMs ?? 5e3,
|
|
55444
|
+
maxWaitMs: opts.maxWaitMs ?? 3e4,
|
|
55445
|
+
maxWakes: opts.maxWakes ?? 6,
|
|
55446
|
+
windowMs: opts.windowMs ?? 6e5,
|
|
55447
|
+
maxRetryMs: opts.maxRetryMs ?? 12e4,
|
|
55448
|
+
maxBusyRetries: opts.maxBusyRetries ?? 20,
|
|
55449
|
+
onGaveUp: opts.onGaveUp,
|
|
55450
|
+
now: opts.now ?? Date.now,
|
|
55451
|
+
setTimer: opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms)),
|
|
55452
|
+
clearTimer: opts.clearTimer ?? ((h) => clearTimeout(h)),
|
|
55453
|
+
onSuppressed: opts.onSuppressed
|
|
55454
|
+
};
|
|
55455
|
+
}
|
|
55456
|
+
/** A wake-worthy notice arrived for `key`: (re)start its debounce. */
|
|
55457
|
+
request(key) {
|
|
55458
|
+
const now = this.opts.now();
|
|
55459
|
+
const entry = this.pending.get(key);
|
|
55460
|
+
if (entry) {
|
|
55461
|
+
if (entry.firing) {
|
|
55462
|
+
entry.again = true;
|
|
55463
|
+
return;
|
|
55464
|
+
}
|
|
55465
|
+
if (entry.suppressed) return;
|
|
55466
|
+
entry.retries = 0;
|
|
55467
|
+
this.opts.clearTimer(entry.timer);
|
|
55468
|
+
const at = Math.min(now + this.opts.debounceMs, entry.firstAt + this.opts.maxWaitMs);
|
|
55469
|
+
entry.timer = this.arm(key, at - now);
|
|
55470
|
+
return;
|
|
55471
|
+
}
|
|
55472
|
+
this.pending.set(key, {
|
|
55473
|
+
firstAt: now,
|
|
55474
|
+
timer: this.arm(key, this.opts.debounceMs),
|
|
55475
|
+
suppressed: false,
|
|
55476
|
+
retries: 0,
|
|
55477
|
+
firing: false,
|
|
55478
|
+
again: false
|
|
55479
|
+
});
|
|
55480
|
+
}
|
|
55481
|
+
/** Whether `key` may be woken now (loop guard). */
|
|
55482
|
+
allowed(key) {
|
|
55483
|
+
return this.recent(key).length < this.opts.maxWakes;
|
|
55484
|
+
}
|
|
55485
|
+
/** When the loop guard lets `key` be woken again (now, while it is under the limit). */
|
|
55486
|
+
freeAt(key) {
|
|
55487
|
+
const recent = this.recent(key);
|
|
55488
|
+
const over = recent.length - this.opts.maxWakes;
|
|
55489
|
+
return over < 0 ? this.opts.now() : (recent[over] ?? this.opts.now()) + this.opts.windowMs;
|
|
55490
|
+
}
|
|
55491
|
+
/** Count a wake that happened outside the scheduler (a direct message). */
|
|
55492
|
+
recordWake(key) {
|
|
55493
|
+
this.history.set(key, [...this.recent(key), this.opts.now()]);
|
|
55494
|
+
}
|
|
55495
|
+
/**
|
|
55496
|
+
* A person stepped in: forget `key`'s counted wakes, so whatever follows is not part of a
|
|
55497
|
+
* loop, and let a wake the guard was holding back go out after the usual quiet period.
|
|
55498
|
+
*/
|
|
55499
|
+
reset(key) {
|
|
55500
|
+
this.history.delete(key);
|
|
55501
|
+
const entry = this.pending.get(key);
|
|
55502
|
+
if (!entry?.suppressed || entry.firing) return;
|
|
55503
|
+
entry.suppressed = false;
|
|
55504
|
+
this.opts.clearTimer(entry.timer);
|
|
55505
|
+
entry.timer = this.arm(key, this.opts.debounceMs);
|
|
55506
|
+
}
|
|
55507
|
+
/** Whether a wake is waiting for `key`. */
|
|
55508
|
+
isPending(key) {
|
|
55509
|
+
return this.pending.has(key);
|
|
55510
|
+
}
|
|
55511
|
+
dispose() {
|
|
55512
|
+
for (const entry of this.pending.values()) this.opts.clearTimer(entry.timer);
|
|
55513
|
+
this.pending.clear();
|
|
55514
|
+
}
|
|
55515
|
+
recent(key) {
|
|
55516
|
+
const since = this.opts.now() - this.opts.windowMs;
|
|
55517
|
+
const kept = (this.history.get(key) ?? []).filter((t) => t > since);
|
|
55518
|
+
this.history.set(key, kept);
|
|
55519
|
+
return kept;
|
|
55520
|
+
}
|
|
55521
|
+
arm(key, ms) {
|
|
55522
|
+
return this.opts.setTimer(() => void this.run(key), Math.max(0, ms));
|
|
55523
|
+
}
|
|
55524
|
+
async run(key) {
|
|
55525
|
+
const entry = this.pending.get(key);
|
|
55526
|
+
if (!entry || entry.firing) return;
|
|
55527
|
+
if (!this.allowed(key)) {
|
|
55528
|
+
const until = this.freeAt(key);
|
|
55529
|
+
if (!entry.suppressed) this.opts.onSuppressed?.(key, until);
|
|
55530
|
+
entry.suppressed = true;
|
|
55531
|
+
entry.timer = this.arm(key, until - this.opts.now() + 1);
|
|
55532
|
+
return;
|
|
55533
|
+
}
|
|
55534
|
+
let result;
|
|
55535
|
+
entry.firing = true;
|
|
55536
|
+
entry.again = false;
|
|
55537
|
+
try {
|
|
55538
|
+
result = await this.fire(key);
|
|
55539
|
+
} catch {
|
|
55540
|
+
result = "busy";
|
|
55541
|
+
} finally {
|
|
55542
|
+
entry.firing = false;
|
|
55543
|
+
}
|
|
55544
|
+
if (this.pending.get(key) !== entry) return;
|
|
55545
|
+
if (entry.again) {
|
|
55546
|
+
entry.again = false;
|
|
55547
|
+
entry.retries = 0;
|
|
55548
|
+
if (result === "sent") this.recordWake(key);
|
|
55549
|
+
if (result !== "busy") {
|
|
55550
|
+
entry.firstAt = this.opts.now();
|
|
55551
|
+
entry.timer = this.arm(key, this.opts.debounceMs);
|
|
55552
|
+
return;
|
|
55553
|
+
}
|
|
55554
|
+
}
|
|
55555
|
+
if (result === "busy") {
|
|
55556
|
+
entry.suppressed = false;
|
|
55557
|
+
entry.retries++;
|
|
55558
|
+
if (entry.retries > this.opts.maxBusyRetries) {
|
|
55559
|
+
this.pending.delete(key);
|
|
55560
|
+
this.opts.onGaveUp?.(key);
|
|
55561
|
+
return;
|
|
55562
|
+
}
|
|
55563
|
+
const backoff = this.opts.debounceMs * 2 ** (entry.retries - 1);
|
|
55564
|
+
entry.timer = this.arm(key, Math.min(backoff, this.opts.maxRetryMs));
|
|
55565
|
+
return;
|
|
55566
|
+
}
|
|
55567
|
+
this.pending.delete(key);
|
|
55568
|
+
if (result === "sent") this.recordWake(key);
|
|
55569
|
+
}
|
|
55570
|
+
};
|
|
55571
|
+
// ../server/src/services/orchestration/loop-guard.ts
|
|
55572
|
+
const pmKey = (projectId) => `pm:${projectId ?? ""}`;
|
|
55573
|
+
/**
|
|
55574
|
+
* Loop protection between role sessions (§8.2). A loop is agents messaging each other with no
|
|
55575
|
+
* person in between: the orchestrator and a PM, or a PM and its sessions. So the guard:
|
|
55576
|
+
*
|
|
55577
|
+
* - counts each conversation on its own: the orchestrator's messages to a PM here, notice
|
|
55578
|
+
* wakes (a PM's sessions reporting, PM messages to the orchestrator) in the `wakes`
|
|
55579
|
+
* scheduler. A PM busy with its sessions' reports never blocks the orchestrator, or the
|
|
55580
|
+
* other way round;
|
|
55581
|
+
* - never holds back what the user asked for: a message sent through the UI, or by the
|
|
55582
|
+
* orchestrator in a turn the user started, always goes straight through, and like any
|
|
55583
|
+
* message the user writes to a role session it clears that session's counts;
|
|
55584
|
+
* - holds back, never refuses, the rest: past the limit an orchestrator message is stored as
|
|
55585
|
+
* an `orchestrator.message` notice and reaches the PM with its next wake, at the latest when
|
|
55586
|
+
* the window allows another message. Nobody has to wait and retry.
|
|
55587
|
+
*/
|
|
55588
|
+
var LoopGuard = class {
|
|
55589
|
+
deps;
|
|
55590
|
+
/** The orchestrator's messages to each PM (`pm:<projectId>`) from turns it started itself. */
|
|
55591
|
+
relays;
|
|
55592
|
+
/** Who sent the input that started each role session's latest turn. */
|
|
55593
|
+
turnFrom = /* @__PURE__ */ new Map();
|
|
55594
|
+
constructor(deps) {
|
|
55595
|
+
this.deps = deps;
|
|
55596
|
+
this.relays = new WakeScheduler((key) => this.deliverHeld(key.slice(3)), {
|
|
55597
|
+
...deps.options,
|
|
55598
|
+
onSuppressed: (key, until) => deps.log?.(`orchestration: loop guard holds the orchestrator's messages to ${key} until ${new Date(until).toISOString()}`),
|
|
55599
|
+
onGaveUp: (key) => deps.log?.(`orchestration: ${key} stayed busy; held messages wait for its next wake`)
|
|
55600
|
+
});
|
|
55601
|
+
}
|
|
55602
|
+
/** A role session's turn starts with input from `from`; the user's input clears its counts. */
|
|
55603
|
+
turnStarted(session, from) {
|
|
55604
|
+
if (session.role === "agent") return;
|
|
55605
|
+
this.turnFrom.set(session.id, from);
|
|
55606
|
+
if (from !== "user") return;
|
|
55607
|
+
this.clear(session.role === "pm" ? pmKey(session.projectId) : "orchestrator");
|
|
55608
|
+
}
|
|
55609
|
+
/** Whether a message carries the user's own request (the UI, or a turn the user started). */
|
|
55610
|
+
humanLed(origin) {
|
|
55611
|
+
if (origin.caller === "ui") return true;
|
|
55612
|
+
return origin.sessionId !== null && this.turnFrom.get(origin.sessionId) === "user";
|
|
55613
|
+
}
|
|
55614
|
+
/**
|
|
55615
|
+
* Whether an orchestrator message may go to the PM of `projectId` now. The user's requests
|
|
55616
|
+
* always may (and clear the PM's counts); others count against the limit, and wait behind
|
|
55617
|
+
* messages already held so none overtakes another. When this is false, `hold` the message.
|
|
55618
|
+
*/
|
|
55619
|
+
mayDeliver(projectId, origin) {
|
|
55620
|
+
const key = pmKey(projectId);
|
|
55621
|
+
if (this.humanLed(origin)) {
|
|
55622
|
+
this.clear(key);
|
|
55623
|
+
return true;
|
|
55624
|
+
}
|
|
55625
|
+
if (!this.relays.allowed(key) || this.relays.isPending(key)) return false;
|
|
55626
|
+
this.relays.recordWake(key);
|
|
55627
|
+
return true;
|
|
55628
|
+
}
|
|
55629
|
+
/**
|
|
55630
|
+
* Keep an orchestrator message for the PM of `projectId` and deliver it with the PM's next
|
|
55631
|
+
* wake. Returns when the guard lets it go (any earlier wake of the PM takes it along, and a
|
|
55632
|
+
* PM in the middle of a turn gets it once the turn ends).
|
|
55633
|
+
*/
|
|
55634
|
+
hold(projectId, fromSessionId, text) {
|
|
55635
|
+
this.deps.notices.create({
|
|
55636
|
+
id: `orchestrator.message:${randomUUID()}`,
|
|
55637
|
+
type: "orchestrator.message",
|
|
55638
|
+
projectId,
|
|
55639
|
+
target: "pm",
|
|
55640
|
+
sessionId: fromSessionId,
|
|
55641
|
+
title: "Message from the orchestrator",
|
|
55642
|
+
data: {
|
|
55643
|
+
fromSessionId,
|
|
55644
|
+
text
|
|
55645
|
+
}
|
|
55646
|
+
});
|
|
55647
|
+
const key = pmKey(projectId);
|
|
55648
|
+
this.relays.request(key);
|
|
55649
|
+
return this.relays.freeAt(key);
|
|
55650
|
+
}
|
|
55651
|
+
dispose() {
|
|
55652
|
+
this.relays.dispose();
|
|
55653
|
+
}
|
|
55654
|
+
/** Wake the PM for its held messages; another wake may have delivered them already. */
|
|
55655
|
+
async deliverHeld(projectId) {
|
|
55656
|
+
return this.deps.notices.undelivered("pm", projectId).some((n) => n.type === "orchestrator.message") ? this.deps.wakePm(projectId) : "empty";
|
|
55657
|
+
}
|
|
55658
|
+
clear(key) {
|
|
55659
|
+
this.deps.wakes.reset(key);
|
|
55660
|
+
this.relays.reset(key);
|
|
55661
|
+
}
|
|
55662
|
+
};
|
|
54452
55663
|
// ../server/src/services/orchestration/ports.ts
|
|
54453
55664
|
const unavailable = (what, code) => new ServiceError(503, code, `${what} is not available in this CodeDen build yet.`);
|
|
54454
55665
|
/** GitHub stub: every call fails with `github_unavailable`. */
|
|
@@ -54484,6 +55695,94 @@ var UnavailableAutomationPort = class {
|
|
|
54484
55695
|
throw unavailable("Automation scheduling", "automations_unavailable");
|
|
54485
55696
|
}
|
|
54486
55697
|
};
|
|
55698
|
+
// ../server/src/services/orchestration/reminders.ts
|
|
55699
|
+
const MAX_DELAY_MS = 6048e5;
|
|
55700
|
+
/** Longest timer wait, so clock changes and sleep are noticed. */
|
|
55701
|
+
const MAX_TIMER_MS$1 = 6e4;
|
|
55702
|
+
const keyOf$1 = (n) => n.target === "orchestrator" ? "orchestrator" : `pm:${n.projectId ?? ""}`;
|
|
55703
|
+
/**
|
|
55704
|
+
* Wake-ups the orchestrator and PMs schedule for themselves (`schedule_wakeup`, §8.2), so they
|
|
55705
|
+
* can follow up later (check on a running audit, retry tomorrow) without the user prompting
|
|
55706
|
+
* them. Each is a `reminder` notice held back until `notBefore`: stored in the event log, so
|
|
55707
|
+
* it survives restarts, and delivered like any other notice, with a wake once it is due.
|
|
55708
|
+
*/
|
|
55709
|
+
var Reminders = class {
|
|
55710
|
+
deps;
|
|
55711
|
+
timer;
|
|
55712
|
+
requested = /* @__PURE__ */ new Set();
|
|
55713
|
+
stopped = true;
|
|
55714
|
+
constructor(deps) {
|
|
55715
|
+
this.deps = deps;
|
|
55716
|
+
}
|
|
55717
|
+
start() {
|
|
55718
|
+
this.stopped = false;
|
|
55719
|
+
this.tick();
|
|
55720
|
+
}
|
|
55721
|
+
dispose() {
|
|
55722
|
+
this.stopped = true;
|
|
55723
|
+
if (this.timer) clearTimeout(this.timer);
|
|
55724
|
+
this.timer = void 0;
|
|
55725
|
+
}
|
|
55726
|
+
/** Schedule a wake-up for `owner` at `at` with `note`. */
|
|
55727
|
+
schedule(owner, at, note) {
|
|
55728
|
+
const delay = at.getTime() - Date.now();
|
|
55729
|
+
const min = this.deps.minDelayMs ?? 6e4;
|
|
55730
|
+
if (!Number.isFinite(delay) || delay < min) throw badRequest(`A wake-up must be at least ${Math.ceil(min / 6e4)} minute(s) ahead`, "wakeup_too_soon");
|
|
55731
|
+
if (delay > MAX_DELAY_MS) throw badRequest("A wake-up can be at most 7 days ahead", "wakeup_too_late");
|
|
55732
|
+
if (this.pending(owner.target, owner.projectId).length >= 20) throw conflict(`You already have 20 wake-ups scheduled; cancel one first`, "too_many_wakeups");
|
|
55733
|
+
const notice = this.deps.notices.create({
|
|
55734
|
+
id: `reminder:${randomUUID()}`,
|
|
55735
|
+
type: "reminder",
|
|
55736
|
+
projectId: owner.projectId,
|
|
55737
|
+
target: owner.target,
|
|
55738
|
+
sessionId: null,
|
|
55739
|
+
title: "Wake-up you scheduled",
|
|
55740
|
+
wake: true,
|
|
55741
|
+
notBefore: at.toISOString(),
|
|
55742
|
+
data: {
|
|
55743
|
+
note,
|
|
55744
|
+
requestedBy: owner.sessionId
|
|
55745
|
+
}
|
|
55746
|
+
});
|
|
55747
|
+
if (!notice) throw conflict("That wake-up already exists", "wakeup_exists");
|
|
55748
|
+
this.tick();
|
|
55749
|
+
return notice;
|
|
55750
|
+
}
|
|
55751
|
+
/** An owner's wake-ups still to come (or due and not yet delivered), soonest first. */
|
|
55752
|
+
pending(target, projectId) {
|
|
55753
|
+
return this.deps.notices.scheduled(target, projectId);
|
|
55754
|
+
}
|
|
55755
|
+
/** Cancel one of an owner's pending wake-ups; false when it has none with that id. */
|
|
55756
|
+
cancel(target, projectId, id) {
|
|
55757
|
+
if (!this.pending(target, projectId).some((n) => n.id === id)) return false;
|
|
55758
|
+
this.deps.notices.markDelivered([id]);
|
|
55759
|
+
return true;
|
|
55760
|
+
}
|
|
55761
|
+
/** Wake the owners of wake-ups that just came due, then wait for the next one. */
|
|
55762
|
+
tick() {
|
|
55763
|
+
if (this.stopped) return;
|
|
55764
|
+
try {
|
|
55765
|
+
for (const n of this.deps.notices.heldBack(true)) {
|
|
55766
|
+
if (this.requested.has(n.id)) continue;
|
|
55767
|
+
this.requested.add(n.id);
|
|
55768
|
+
this.deps.wakes.request(keyOf$1(n));
|
|
55769
|
+
}
|
|
55770
|
+
} catch (error) {
|
|
55771
|
+
this.deps.log?.(`orchestration wake-ups: ${error instanceof Error ? error.message : error}`);
|
|
55772
|
+
}
|
|
55773
|
+
this.arm();
|
|
55774
|
+
}
|
|
55775
|
+
arm() {
|
|
55776
|
+
if (this.timer) clearTimeout(this.timer);
|
|
55777
|
+
this.timer = void 0;
|
|
55778
|
+
if (this.stopped) return;
|
|
55779
|
+
const next = this.deps.notices.heldBack(false)[0]?.notBefore;
|
|
55780
|
+
if (!next) return;
|
|
55781
|
+
const wait = Math.min(Math.max(Date.parse(next) - Date.now(), 0), MAX_TIMER_MS$1);
|
|
55782
|
+
this.timer = setTimeout(() => this.tick(), wait + 5);
|
|
55783
|
+
this.timer.unref?.();
|
|
55784
|
+
}
|
|
55785
|
+
};
|
|
54487
55786
|
// ../server/src/services/orchestration/role-sessions.ts
|
|
54488
55787
|
/**
|
|
54489
55788
|
* The long-lived role sessions (§8.1): one orchestrator (cwd `<home>/orchestrator/`) and one
|
|
@@ -54550,132 +55849,6 @@ var RoleSessions = class {
|
|
|
54550
55849
|
return session;
|
|
54551
55850
|
}
|
|
54552
55851
|
};
|
|
54553
|
-
// ../server/src/services/orchestration/wake-scheduler.ts
|
|
54554
|
-
/**
|
|
54555
|
-
* Batches notices into debounced wakes per target (`pm:<projectId>`, `orchestrator`) with a
|
|
54556
|
-
* loop guard (§8.2): notices arriving close together become one "updates since your last
|
|
54557
|
-
* turn" wake, and no target is woken more than `maxWakes` times per window. Held-back wakes
|
|
54558
|
-
* fire as soon as the window allows; nothing is dropped.
|
|
54559
|
-
*/
|
|
54560
|
-
var WakeScheduler = class {
|
|
54561
|
-
fire;
|
|
54562
|
-
pending = /* @__PURE__ */ new Map();
|
|
54563
|
-
history = /* @__PURE__ */ new Map();
|
|
54564
|
-
opts;
|
|
54565
|
-
constructor(fire, opts = {}) {
|
|
54566
|
-
this.fire = fire;
|
|
54567
|
-
this.opts = {
|
|
54568
|
-
debounceMs: opts.debounceMs ?? 5e3,
|
|
54569
|
-
maxWaitMs: opts.maxWaitMs ?? 3e4,
|
|
54570
|
-
maxWakes: opts.maxWakes ?? 6,
|
|
54571
|
-
windowMs: opts.windowMs ?? 6e5,
|
|
54572
|
-
maxRetryMs: opts.maxRetryMs ?? 12e4,
|
|
54573
|
-
maxBusyRetries: opts.maxBusyRetries ?? 20,
|
|
54574
|
-
onGaveUp: opts.onGaveUp,
|
|
54575
|
-
now: opts.now ?? Date.now,
|
|
54576
|
-
setTimer: opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms)),
|
|
54577
|
-
clearTimer: opts.clearTimer ?? ((h) => clearTimeout(h)),
|
|
54578
|
-
onSuppressed: opts.onSuppressed
|
|
54579
|
-
};
|
|
54580
|
-
}
|
|
54581
|
-
/** A wake-worthy notice arrived for `key`: (re)start its debounce. */
|
|
54582
|
-
request(key) {
|
|
54583
|
-
const now = this.opts.now();
|
|
54584
|
-
const entry = this.pending.get(key);
|
|
54585
|
-
if (entry) {
|
|
54586
|
-
if (entry.firing) {
|
|
54587
|
-
entry.again = true;
|
|
54588
|
-
return;
|
|
54589
|
-
}
|
|
54590
|
-
if (entry.suppressed) return;
|
|
54591
|
-
entry.retries = 0;
|
|
54592
|
-
this.opts.clearTimer(entry.timer);
|
|
54593
|
-
const at = Math.min(now + this.opts.debounceMs, entry.firstAt + this.opts.maxWaitMs);
|
|
54594
|
-
entry.timer = this.arm(key, at - now);
|
|
54595
|
-
return;
|
|
54596
|
-
}
|
|
54597
|
-
this.pending.set(key, {
|
|
54598
|
-
firstAt: now,
|
|
54599
|
-
timer: this.arm(key, this.opts.debounceMs),
|
|
54600
|
-
suppressed: false,
|
|
54601
|
-
retries: 0,
|
|
54602
|
-
firing: false,
|
|
54603
|
-
again: false
|
|
54604
|
-
});
|
|
54605
|
-
}
|
|
54606
|
-
/** Whether `key` may be woken now (loop guard). */
|
|
54607
|
-
allowed(key) {
|
|
54608
|
-
return this.recent(key).length < this.opts.maxWakes;
|
|
54609
|
-
}
|
|
54610
|
-
/** Count a wake that happened outside the scheduler (a direct message). */
|
|
54611
|
-
recordWake(key) {
|
|
54612
|
-
this.history.set(key, [...this.recent(key), this.opts.now()]);
|
|
54613
|
-
}
|
|
54614
|
-
/** Whether a wake is waiting for `key`. */
|
|
54615
|
-
isPending(key) {
|
|
54616
|
-
return this.pending.has(key);
|
|
54617
|
-
}
|
|
54618
|
-
dispose() {
|
|
54619
|
-
for (const entry of this.pending.values()) this.opts.clearTimer(entry.timer);
|
|
54620
|
-
this.pending.clear();
|
|
54621
|
-
}
|
|
54622
|
-
recent(key) {
|
|
54623
|
-
const since = this.opts.now() - this.opts.windowMs;
|
|
54624
|
-
const kept = (this.history.get(key) ?? []).filter((t) => t > since);
|
|
54625
|
-
this.history.set(key, kept);
|
|
54626
|
-
return kept;
|
|
54627
|
-
}
|
|
54628
|
-
arm(key, ms) {
|
|
54629
|
-
return this.opts.setTimer(() => void this.run(key), Math.max(0, ms));
|
|
54630
|
-
}
|
|
54631
|
-
async run(key) {
|
|
54632
|
-
const entry = this.pending.get(key);
|
|
54633
|
-
if (!entry || entry.firing) return;
|
|
54634
|
-
const recent = this.recent(key);
|
|
54635
|
-
if (recent.length >= this.opts.maxWakes) {
|
|
54636
|
-
const until = (recent[0] ?? this.opts.now()) + this.opts.windowMs;
|
|
54637
|
-
if (!entry.suppressed) this.opts.onSuppressed?.(key, until);
|
|
54638
|
-
entry.suppressed = true;
|
|
54639
|
-
entry.timer = this.arm(key, until - this.opts.now() + 1);
|
|
54640
|
-
return;
|
|
54641
|
-
}
|
|
54642
|
-
let result;
|
|
54643
|
-
entry.firing = true;
|
|
54644
|
-
entry.again = false;
|
|
54645
|
-
try {
|
|
54646
|
-
result = await this.fire(key);
|
|
54647
|
-
} catch {
|
|
54648
|
-
result = "busy";
|
|
54649
|
-
} finally {
|
|
54650
|
-
entry.firing = false;
|
|
54651
|
-
}
|
|
54652
|
-
if (this.pending.get(key) !== entry) return;
|
|
54653
|
-
if (entry.again) {
|
|
54654
|
-
entry.again = false;
|
|
54655
|
-
entry.retries = 0;
|
|
54656
|
-
if (result === "sent") this.recordWake(key);
|
|
54657
|
-
if (result !== "busy") {
|
|
54658
|
-
entry.firstAt = this.opts.now();
|
|
54659
|
-
entry.timer = this.arm(key, this.opts.debounceMs);
|
|
54660
|
-
return;
|
|
54661
|
-
}
|
|
54662
|
-
}
|
|
54663
|
-
if (result === "busy") {
|
|
54664
|
-
entry.suppressed = false;
|
|
54665
|
-
entry.retries++;
|
|
54666
|
-
if (entry.retries > this.opts.maxBusyRetries) {
|
|
54667
|
-
this.pending.delete(key);
|
|
54668
|
-
this.opts.onGaveUp?.(key);
|
|
54669
|
-
return;
|
|
54670
|
-
}
|
|
54671
|
-
const backoff = this.opts.debounceMs * 2 ** (entry.retries - 1);
|
|
54672
|
-
entry.timer = this.arm(key, Math.min(backoff, this.opts.maxRetryMs));
|
|
54673
|
-
return;
|
|
54674
|
-
}
|
|
54675
|
-
this.pending.delete(key);
|
|
54676
|
-
if (result === "sent") this.recordWake(key);
|
|
54677
|
-
}
|
|
54678
|
-
};
|
|
54679
55852
|
// ../server/src/services/orchestration/orchestration-service.ts
|
|
54680
55853
|
const NOT_READY = /* @__PURE__ */ new Set([
|
|
54681
55854
|
"created",
|
|
@@ -54700,13 +55873,16 @@ var OrchestrationService = class {
|
|
|
54700
55873
|
github;
|
|
54701
55874
|
automation;
|
|
54702
55875
|
wakes;
|
|
54703
|
-
|
|
54704
|
-
|
|
55876
|
+
loops;
|
|
55877
|
+
reminders;
|
|
55878
|
+
cards;
|
|
55879
|
+
delivery;
|
|
54705
55880
|
unsubscribe;
|
|
54706
55881
|
constructor(deps) {
|
|
54707
55882
|
this.deps = deps;
|
|
54708
55883
|
this.roles = new RoleSessions(deps);
|
|
54709
55884
|
this.notices = new NoticeStore(deps.db, deps.store);
|
|
55885
|
+
this.cards = new NoticeCards(deps.store);
|
|
54710
55886
|
this.notes = new ProjectNotes(deps.projects);
|
|
54711
55887
|
this.github = deps.github ?? new UnavailableGithubPort();
|
|
54712
55888
|
this.automation = deps.automation ?? new UnavailableAutomationPort();
|
|
@@ -54716,21 +55892,55 @@ var OrchestrationService = class {
|
|
|
54716
55892
|
});
|
|
54717
55893
|
this.asks = new PmAsks({
|
|
54718
55894
|
db: deps.db,
|
|
54719
|
-
|
|
55895
|
+
session: (id) => deps.repo.find(id)?.session,
|
|
55896
|
+
onLateReply: (reply) => this.postToOrchestrator(reply.projectId, reply.pmSessionId, `Answer to your earlier ask_pm question:\n${reply.text}`, false, `pm.message:ask:${reply.askId}`)
|
|
54720
55897
|
});
|
|
54721
55898
|
this.wakes = new WakeScheduler((key) => this.fire(key), {
|
|
54722
55899
|
...deps.options?.wake,
|
|
54723
55900
|
onSuppressed: (key, until) => deps.log?.(`orchestration: loop guard holds wakes for ${key} until ${new Date(until).toISOString()}`),
|
|
54724
55901
|
onGaveUp: (key) => deps.log?.(`orchestration: ${key} stayed busy; its notices wait for its next message or notice`)
|
|
54725
55902
|
});
|
|
55903
|
+
this.loops = new LoopGuard({
|
|
55904
|
+
wakes: this.wakes,
|
|
55905
|
+
notices: this.notices,
|
|
55906
|
+
wakePm: (projectId) => this.fire(wakeKey("pm", projectId)),
|
|
55907
|
+
options: deps.options?.wake,
|
|
55908
|
+
log: deps.log
|
|
55909
|
+
});
|
|
55910
|
+
this.delivery = new NoticeDelivery({
|
|
55911
|
+
db: deps.db,
|
|
55912
|
+
notices: this.notices,
|
|
55913
|
+
cards: this.cards,
|
|
55914
|
+
wakes: this.wakes,
|
|
55915
|
+
digestFor: (session) => this.digestFor(session),
|
|
55916
|
+
projectName: (id) => this.projectName(id),
|
|
55917
|
+
session: (id) => deps.repo.find(id)?.session,
|
|
55918
|
+
projectIds: () => deps.projects.list().flatMap((p) => p.archivedAt ? [] : [p.id]),
|
|
55919
|
+
retryDelaysMs: deps.options?.failedTurnRetryMs,
|
|
55920
|
+
log: deps.log
|
|
55921
|
+
});
|
|
55922
|
+
const minDelayMs = deps.options?.wakeupMinDelayMs;
|
|
55923
|
+
this.reminders = new Reminders({
|
|
55924
|
+
notices: this.notices,
|
|
55925
|
+
wakes: this.wakes,
|
|
55926
|
+
minDelayMs
|
|
55927
|
+
});
|
|
54726
55928
|
}
|
|
54727
55929
|
start() {
|
|
54728
55930
|
this.unsubscribe = this.deps.store.subscribe((events) => this.onEvents(events));
|
|
54729
|
-
this.deps.sessions.setInputDecorator((session, input) =>
|
|
55931
|
+
this.deps.sessions.setInputDecorator((session, input, from) => {
|
|
55932
|
+
this.loops.turnStarted(session, from);
|
|
55933
|
+
return this.delivery.decorate(session, input);
|
|
55934
|
+
});
|
|
55935
|
+
this.delivery.rearm();
|
|
55936
|
+
this.reminders.start();
|
|
54730
55937
|
}
|
|
54731
55938
|
stop() {
|
|
54732
55939
|
this.unsubscribe?.();
|
|
54733
55940
|
this.wakes.dispose();
|
|
55941
|
+
this.loops.dispose();
|
|
55942
|
+
this.delivery.dispose();
|
|
55943
|
+
this.reminders.dispose();
|
|
54734
55944
|
}
|
|
54735
55945
|
projectStatus(projectId) {
|
|
54736
55946
|
const project = this.deps.projects.get(projectId);
|
|
@@ -54766,31 +55976,6 @@ var OrchestrationService = class {
|
|
|
54766
55976
|
});
|
|
54767
55977
|
}
|
|
54768
55978
|
/**
|
|
54769
|
-
* Prepend the status digest and any undelivered notices to a role session's input (§8.4).
|
|
54770
|
-
* Runs at send time, so queued messages get a fresh digest too.
|
|
54771
|
-
*/
|
|
54772
|
-
decorate(session, input) {
|
|
54773
|
-
if (session.role === "agent") return input;
|
|
54774
|
-
try {
|
|
54775
|
-
const parts = [this.digestFor(session)];
|
|
54776
|
-
const target = session.role === "pm" ? "pm" : "orchestrator";
|
|
54777
|
-
const pending = this.notices.undelivered(target, session.role === "pm" ? session.projectId : null);
|
|
54778
|
-
if (pending.length > 0) {
|
|
54779
|
-
parts.push(renderNotices(pending, (id) => this.projectName(id)));
|
|
54780
|
-
this.notices.markDelivered(pending.map((n) => n.id));
|
|
54781
|
-
this.pendingCards.set(session.id, [...this.pendingCards.get(session.id) ?? [], ...pending]);
|
|
54782
|
-
}
|
|
54783
|
-
parts.push(input.text);
|
|
54784
|
-
return {
|
|
54785
|
-
...input,
|
|
54786
|
-
text: parts.join("\n\n")
|
|
54787
|
-
};
|
|
54788
|
-
} catch (error) {
|
|
54789
|
-
this.deps.log?.(`orchestration digest: ${errorMessage$1(error)}`);
|
|
54790
|
-
return input;
|
|
54791
|
-
}
|
|
54792
|
-
}
|
|
54793
|
-
/**
|
|
54794
55979
|
* Send a message to the orchestrator or a project's PM, creating the session if needed.
|
|
54795
55980
|
* `from` labels the message in the queue tray when it has to wait for a running turn.
|
|
54796
55981
|
*/
|
|
@@ -54805,28 +55990,45 @@ var OrchestrationService = class {
|
|
|
54805
55990
|
status: res.status
|
|
54806
55991
|
};
|
|
54807
55992
|
}
|
|
54808
|
-
/**
|
|
54809
|
-
|
|
54810
|
-
|
|
55993
|
+
/**
|
|
55994
|
+
* `message_pm`: hand a PM a task without waiting. Past the loop guard's limit (§8.2) the
|
|
55995
|
+
* message is held and delivered automatically; the user's own requests are never held.
|
|
55996
|
+
*/
|
|
55997
|
+
async messagePm(projectId, text, origin) {
|
|
55998
|
+
await this.roles.ensure("pm", projectId);
|
|
55999
|
+
if (!this.loops.mayDeliver(projectId, origin)) return {
|
|
56000
|
+
status: "held",
|
|
56001
|
+
until: this.loops.hold(projectId, origin.sessionId, text)
|
|
56002
|
+
};
|
|
54811
56003
|
return this.sendToRole("pm", projectId, messageFrom("orchestrator", text), "queue", "orchestrator");
|
|
54812
56004
|
}
|
|
54813
|
-
/**
|
|
56005
|
+
/**
|
|
56006
|
+
* `ask_pm`: ask a PM and wait (up to `timeoutMs`) for the answer. A question the loop guard
|
|
56007
|
+
* holds back (§8.2) is answered later, as a message to the orchestrator.
|
|
56008
|
+
*/
|
|
54814
56009
|
async askPm(projectId, question, opts) {
|
|
54815
|
-
this.guard(`pm:${projectId}`);
|
|
54816
56010
|
const pm = await this.roles.ensure("pm", projectId);
|
|
54817
56011
|
const askId = randomUUID().slice(0, 8);
|
|
54818
|
-
|
|
56012
|
+
const target = {
|
|
54819
56013
|
pmSessionId: pm.id,
|
|
54820
56014
|
projectId,
|
|
54821
|
-
fromSessionId: opts.
|
|
54822
|
-
}
|
|
54823
|
-
const answer = this.asks.wait(askId, opts.timeoutMs ?? this.deps.options?.askTimeoutMs ?? 18e4, opts.signal);
|
|
56015
|
+
fromSessionId: opts.origin.sessionId
|
|
56016
|
+
};
|
|
54824
56017
|
const text = [
|
|
54825
56018
|
`Question from the orchestrator ${askMarker(askId)}:`,
|
|
54826
56019
|
question,
|
|
54827
56020
|
"",
|
|
54828
56021
|
"Answer in your final message of this turn: it is returned to the orchestrator directly."
|
|
54829
56022
|
].join("\n");
|
|
56023
|
+
if (!this.loops.mayDeliver(projectId, opts.origin)) {
|
|
56024
|
+
this.asks.register(askId, target, { detached: true });
|
|
56025
|
+
return {
|
|
56026
|
+
status: "held",
|
|
56027
|
+
until: this.loops.hold(projectId, opts.origin.sessionId, text)
|
|
56028
|
+
};
|
|
56029
|
+
}
|
|
56030
|
+
this.asks.register(askId, target);
|
|
56031
|
+
const answer = this.asks.wait(askId, opts.timeoutMs ?? this.deps.options?.askTimeoutMs ?? 18e4, opts.signal);
|
|
54830
56032
|
await this.deps.sessions.send(pm.id, {
|
|
54831
56033
|
input: {
|
|
54832
56034
|
text,
|
|
@@ -54847,14 +56049,17 @@ var OrchestrationService = class {
|
|
|
54847
56049
|
}
|
|
54848
56050
|
/** Record a notice (other services: GitHub checks, automations); wakes its target when asked. */
|
|
54849
56051
|
notify(draft, wake = true) {
|
|
54850
|
-
const created = this.notices.create(
|
|
56052
|
+
const created = this.notices.create({
|
|
56053
|
+
...draft,
|
|
56054
|
+
wake
|
|
56055
|
+
});
|
|
54851
56056
|
if (created && wake) this.wakes.request(wakeKey(created.target, created.projectId));
|
|
54852
56057
|
return created;
|
|
54853
56058
|
}
|
|
54854
|
-
postToOrchestrator(projectId, fromSessionId, text, informational) {
|
|
56059
|
+
postToOrchestrator(projectId, fromSessionId, text, informational, id = `pm.message:${randomUUID()}`) {
|
|
54855
56060
|
const name = projectId ? this.projectName(projectId) : "a project";
|
|
54856
56061
|
this.notify({
|
|
54857
|
-
id
|
|
56062
|
+
id,
|
|
54858
56063
|
type: "pm.message",
|
|
54859
56064
|
projectId,
|
|
54860
56065
|
target: "orchestrator",
|
|
@@ -54885,16 +56090,13 @@ var OrchestrationService = class {
|
|
|
54885
56090
|
}
|
|
54886
56091
|
});
|
|
54887
56092
|
}
|
|
54888
|
-
guard(key) {
|
|
54889
|
-
if (!this.wakes.allowed(key)) throw new ServiceError(409, "loop_guard", "Too many messages to this session recently (loop guard). Wait a few minutes or ask the user.");
|
|
54890
|
-
this.wakes.recordWake(key);
|
|
54891
|
-
}
|
|
54892
56093
|
onEvents(events) {
|
|
54893
56094
|
this.asks.onEvents(events);
|
|
54894
|
-
if (!events.some((e) => e.type === "agent.turn.completed" || e.type === "agent.turn.started" || e.type === "inbox.created" || e.type === "session.updated" || e.type === "automation_run.updated" || GITHUB_NOTICE_EVENTS.has(e.type))) return;
|
|
56095
|
+
if (!events.some((e) => e.type === "agent.turn.completed" || e.type === "agent.turn.failed" || e.type === "agent.turn.started" || e.type === "inbox.created" || e.type === "session.updated" || e.type === "automation_run.updated" || GITHUB_NOTICE_EVENTS.has(e.type))) return;
|
|
54895
56096
|
setImmediate(() => {
|
|
54896
56097
|
try {
|
|
54897
|
-
this.
|
|
56098
|
+
this.cards.attach(events);
|
|
56099
|
+
this.delivery.onEvents(events);
|
|
54898
56100
|
const session = (id) => this.deps.repo.find(id)?.session;
|
|
54899
56101
|
const derived = [...deriveNotices(events, {
|
|
54900
56102
|
session,
|
|
@@ -54906,30 +56108,6 @@ var OrchestrationService = class {
|
|
|
54906
56108
|
}
|
|
54907
56109
|
});
|
|
54908
56110
|
}
|
|
54909
|
-
/** Show the notices a wake delivered as cards at the start of the role session's turn. */
|
|
54910
|
-
attachCards(events) {
|
|
54911
|
-
for (const e of events) {
|
|
54912
|
-
if (e.type !== "agent.turn.started" || !e.sessionId) continue;
|
|
54913
|
-
const cards = this.pendingCards.get(e.sessionId);
|
|
54914
|
-
if (!cards) continue;
|
|
54915
|
-
this.pendingCards.delete(e.sessionId);
|
|
54916
|
-
const event = e.payload;
|
|
54917
|
-
this.deps.store.append([agentEventToNewEvent(e.sessionId, {
|
|
54918
|
-
type: "item.completed",
|
|
54919
|
-
threadId: event.threadId,
|
|
54920
|
-
turnId: event.turnId,
|
|
54921
|
-
item: {
|
|
54922
|
-
id: `notices-${event.turnId}`,
|
|
54923
|
-
kind: "notice",
|
|
54924
|
-
status: "completed",
|
|
54925
|
-
level: "info",
|
|
54926
|
-
code: NOTICE_BATCH_CODE,
|
|
54927
|
-
message: `${cards.length} update${cards.length === 1 ? "" : "s"} since the last turn`,
|
|
54928
|
-
data: JSON.parse(JSON.stringify({ notices: cards }))
|
|
54929
|
-
}
|
|
54930
|
-
})]);
|
|
54931
|
-
}
|
|
54932
|
-
}
|
|
54933
56111
|
/** Deliver pending notices to their target with one wake (called by the scheduler). */
|
|
54934
56112
|
async fire(key) {
|
|
54935
56113
|
const role = key === "orchestrator" ? "orchestrator" : "pm";
|
|
@@ -55454,19 +56632,260 @@ var ProjectRedetector = class {
|
|
|
55454
56632
|
this.refresh(projectId).catch((error) => this.deps.log?.(`projects: checking ${projectId}: ${errorMessage$1(error)}`));
|
|
55455
56633
|
}
|
|
55456
56634
|
};
|
|
56635
|
+
// ../server/src/services/sessions/auto-retry.ts
|
|
56636
|
+
/** Waits before the 1st, 2nd and 3rd automatic retry after a transient failure. */
|
|
56637
|
+
const RETRY_DELAYS_MS = [
|
|
56638
|
+
2e4,
|
|
56639
|
+
9e4,
|
|
56640
|
+
3e5
|
|
56641
|
+
];
|
|
56642
|
+
/** Longest single timer (setTimeout's limit); longer waits re-arm. */
|
|
56643
|
+
const MAX_TIMER_MS = 2 ** 31 - 1;
|
|
56644
|
+
/**
|
|
56645
|
+
* Decide, from a session and its root turns (newest first), whether its last turn is retried
|
|
56646
|
+
* and when. Only an idle agent session whose last root turn failed for a passing reason
|
|
56647
|
+
* qualifies (`awaitsRetryDecision`); `attempt` counts the consecutive such failures. A transient
|
|
56648
|
+
* failure waits `delaysMs[attempt - 1]` after the turn ended, a usage limit until `retryAt`
|
|
56649
|
+
* plus the margin. Past the last delay, without a reset time, or switched off: give up. Pure.
|
|
56650
|
+
*/
|
|
56651
|
+
function planRetry(session, rootTurns, opts) {
|
|
56652
|
+
const turn = rootTurns[0];
|
|
56653
|
+
if (!turn?.error || turn.status !== "failed" || session.status !== "idle") return { action: "none" };
|
|
56654
|
+
const error = turn.error;
|
|
56655
|
+
if (!awaitsRetryDecision(session, error)) return { action: "none" };
|
|
56656
|
+
let attempt = 0;
|
|
56657
|
+
for (const t of rootTurns) {
|
|
56658
|
+
if (t.status !== "failed" || !t.error || !awaitsRetryDecision(session, t.error)) break;
|
|
56659
|
+
attempt += 1;
|
|
56660
|
+
}
|
|
56661
|
+
const giveUp = (exhausted) => ({
|
|
56662
|
+
action: "give_up",
|
|
56663
|
+
turn,
|
|
56664
|
+
error,
|
|
56665
|
+
attempt,
|
|
56666
|
+
exhausted
|
|
56667
|
+
});
|
|
56668
|
+
if (!opts.enabled) return giveUp(false);
|
|
56669
|
+
if (attempt > opts.delaysMs.length) return giveUp(true);
|
|
56670
|
+
const endedAt = Date.parse(turn.completedAt ?? turn.startedAt);
|
|
56671
|
+
if (error.kind === "usage_limit") {
|
|
56672
|
+
const resetsAt = error.retryAt ? Date.parse(error.retryAt) : NaN;
|
|
56673
|
+
if (!Number.isFinite(resetsAt)) return giveUp(false);
|
|
56674
|
+
return {
|
|
56675
|
+
action: "retry",
|
|
56676
|
+
turn,
|
|
56677
|
+
error,
|
|
56678
|
+
attempt,
|
|
56679
|
+
at: resetsAt + opts.usageMarginMs
|
|
56680
|
+
};
|
|
56681
|
+
}
|
|
56682
|
+
const delay = opts.delaysMs[attempt - 1] ?? 0;
|
|
56683
|
+
return {
|
|
56684
|
+
action: "retry",
|
|
56685
|
+
turn,
|
|
56686
|
+
error,
|
|
56687
|
+
attempt,
|
|
56688
|
+
at: endedAt + delay
|
|
56689
|
+
};
|
|
56690
|
+
}
|
|
56691
|
+
/** "in 20s", "in 3 min", "in about 2 h 15 min". */
|
|
56692
|
+
function inAbout(ms) {
|
|
56693
|
+
const s = Math.max(1, Math.round(ms / 1e3));
|
|
56694
|
+
if (s < 60) return `in ${s}s`;
|
|
56695
|
+
const m = Math.round(s / 60);
|
|
56696
|
+
if (m < 60) return `in ${m} min`;
|
|
56697
|
+
return `in about ${Math.floor(m / 60)} h${m % 60 ? ` ${m % 60} min` : ""}`;
|
|
56698
|
+
}
|
|
56699
|
+
/** The `codeden` message that continues the session. */
|
|
56700
|
+
function continueMessage(error) {
|
|
56701
|
+
return `Previous turn stopped (${error.kind === "usage_limit" ? "usage limit reached" : error.message.length > 160 ? `${error.message.slice(0, 157)}...` : error.message}); continue where you left off.`;
|
|
56702
|
+
}
|
|
56703
|
+
/**
|
|
56704
|
+
* Automatic retries for agent sessions (§7.3 `ErrorInfo.kind`). A root turn that failed for a
|
|
56705
|
+
* passing reason leaves the session idle with no inbox item (the reducer, `awaitsRetryDecision`);
|
|
56706
|
+
* this service then continues it with a `codeden` message after a backoff (transient: 20 s,
|
|
56707
|
+
* 90 s, 5 min, at most three in a row) or a minute after a usage limit resets, and says so in
|
|
56708
|
+
* the transcript. A user message or Stop cancels a pending retry. When the retries run out, or
|
|
56709
|
+
* `sessions.autoRetry` is off, the session fails as it always did: `failed` with a `run_failed`
|
|
56710
|
+
* inbox item (and the PM notices that come from it). Pending retries are derived from the turns
|
|
56711
|
+
* and sessions tables, so a server restart picks them up again. Orchestrator and PM sessions
|
|
56712
|
+
* are never retried here.
|
|
56713
|
+
*/
|
|
56714
|
+
var AutoRetry = class {
|
|
56715
|
+
deps;
|
|
56716
|
+
timers = /* @__PURE__ */ new Map();
|
|
56717
|
+
unsubscribe;
|
|
56718
|
+
stopped = false;
|
|
56719
|
+
constructor(deps) {
|
|
56720
|
+
this.deps = deps;
|
|
56721
|
+
}
|
|
56722
|
+
start() {
|
|
56723
|
+
this.stopped = false;
|
|
56724
|
+
this.unsubscribe = this.deps.store.subscribe((events) => this.onEvents(events));
|
|
56725
|
+
setImmediate(() => {
|
|
56726
|
+
if (this.stopped) return;
|
|
56727
|
+
for (const session of this.deps.repo.list({
|
|
56728
|
+
role: "agent",
|
|
56729
|
+
status: ["idle"]
|
|
56730
|
+
})) this.evaluate(session.id);
|
|
56731
|
+
});
|
|
56732
|
+
}
|
|
56733
|
+
stop() {
|
|
56734
|
+
this.stopped = true;
|
|
56735
|
+
this.unsubscribe?.();
|
|
56736
|
+
for (const { timer } of this.timers.values()) clearTimeout(timer);
|
|
56737
|
+
this.timers.clear();
|
|
56738
|
+
}
|
|
56739
|
+
/** Sessions with a retry waiting (tests). */
|
|
56740
|
+
pending() {
|
|
56741
|
+
return [...this.timers.keys()];
|
|
56742
|
+
}
|
|
56743
|
+
onEvents(events) {
|
|
56744
|
+
for (const e of events) {
|
|
56745
|
+
if (!e.sessionId) continue;
|
|
56746
|
+
const sessionId = e.sessionId;
|
|
56747
|
+
if (e.type === "agent.turn.failed") queueMicrotask(() => this.evaluate(sessionId));
|
|
56748
|
+
else if (e.type === "agent.turn.started" && this.timers.has(sessionId)) this.cancel(sessionId, "A new message was sent, so the automatic retry was cancelled.");
|
|
56749
|
+
else if (e.type === "session.updated" && this.timers.has(sessionId)) {
|
|
56750
|
+
const { session } = e.payload;
|
|
56751
|
+
if (session.status !== "idle" || session.archivedAt) this.cancel(sessionId, "The automatic retry was cancelled.");
|
|
56752
|
+
}
|
|
56753
|
+
}
|
|
56754
|
+
}
|
|
56755
|
+
/** Decide about a session's last root turn now: schedule, give up, or nothing. */
|
|
56756
|
+
evaluate(sessionId) {
|
|
56757
|
+
try {
|
|
56758
|
+
const decision = this.decide(sessionId);
|
|
56759
|
+
if (decision.action === "retry") this.schedule(sessionId, decision);
|
|
56760
|
+
else if (decision.action === "give_up") this.giveUp(sessionId, decision);
|
|
56761
|
+
} catch (error) {
|
|
56762
|
+
this.deps.log?.(`auto-retry for ${sessionId}: ${errorMessage$1(error)}`);
|
|
56763
|
+
}
|
|
56764
|
+
}
|
|
56765
|
+
decide(sessionId) {
|
|
56766
|
+
const session = this.deps.repo.find(sessionId)?.session;
|
|
56767
|
+
if (!session?.rootThreadId) return { action: "none" };
|
|
56768
|
+
return planRetry(session, this.deps.db.select().from(turns).where(and(eq(turns.sessionId, sessionId), eq(turns.threadId, session.rootThreadId))).orderBy(desc(turns.startedAt)).limit(10).all().map(rowToTurnRecord), {
|
|
56769
|
+
enabled: this.deps.settings().sessions.autoRetry,
|
|
56770
|
+
delaysMs: this.deps.delaysMs ?? RETRY_DELAYS_MS,
|
|
56771
|
+
usageMarginMs: this.deps.usageMarginMs ?? 6e4
|
|
56772
|
+
});
|
|
56773
|
+
}
|
|
56774
|
+
schedule(sessionId, plan) {
|
|
56775
|
+
const existing = this.timers.get(sessionId);
|
|
56776
|
+
if (existing) clearTimeout(existing.timer);
|
|
56777
|
+
const wait = Math.max(0, plan.at - this.now());
|
|
56778
|
+
const max = (this.deps.delaysMs ?? RETRY_DELAYS_MS).length;
|
|
56779
|
+
this.notice(sessionId, plan.turn, {
|
|
56780
|
+
message: plan.error.kind === "usage_limit" ? `Usage limit reached. CodeDen continues the session ${inAbout(wait)}, a minute after it resets.` : `Retrying automatically ${inAbout(wait)} (attempt ${plan.attempt} of ${max}).`,
|
|
56781
|
+
data: {
|
|
56782
|
+
attempt: plan.attempt,
|
|
56783
|
+
maxAttempts: max,
|
|
56784
|
+
retryAt: new Date(plan.at).toISOString()
|
|
56785
|
+
}
|
|
56786
|
+
});
|
|
56787
|
+
const timer = setTimeout(() => void this.fire(sessionId, plan.turn.id), Math.min(wait, MAX_TIMER_MS));
|
|
56788
|
+
timer.unref?.();
|
|
56789
|
+
this.timers.set(sessionId, {
|
|
56790
|
+
turnId: plan.turn.id,
|
|
56791
|
+
timer
|
|
56792
|
+
});
|
|
56793
|
+
}
|
|
56794
|
+
async fire(sessionId, turnId) {
|
|
56795
|
+
this.timers.delete(sessionId);
|
|
56796
|
+
if (this.stopped) return;
|
|
56797
|
+
let decision;
|
|
56798
|
+
try {
|
|
56799
|
+
decision = this.decide(sessionId);
|
|
56800
|
+
} catch (error) {
|
|
56801
|
+
this.deps.log?.(`auto-retry for ${sessionId}: ${errorMessage$1(error)}`);
|
|
56802
|
+
return;
|
|
56803
|
+
}
|
|
56804
|
+
if (decision.action === "give_up") return this.giveUp(sessionId, decision);
|
|
56805
|
+
if (decision.action !== "retry" || decision.turn.id !== turnId) return;
|
|
56806
|
+
if (decision.at > this.now()) return this.schedule(sessionId, decision);
|
|
56807
|
+
this.deps.log?.(`auto-retry: continuing session ${sessionId} (attempt ${decision.attempt})`);
|
|
56808
|
+
try {
|
|
56809
|
+
const text = continueMessage(decision.error);
|
|
56810
|
+
await this.deps.send(sessionId, {
|
|
56811
|
+
text,
|
|
56812
|
+
attachments: [],
|
|
56813
|
+
from: "codeden"
|
|
56814
|
+
});
|
|
56815
|
+
} catch (error) {
|
|
56816
|
+
this.deps.log?.(`auto-retry for ${sessionId}: ${errorMessage$1(error)}`);
|
|
56817
|
+
this.giveUp(sessionId, {
|
|
56818
|
+
...decision,
|
|
56819
|
+
action: "give_up",
|
|
56820
|
+
exhausted: false
|
|
56821
|
+
});
|
|
56822
|
+
}
|
|
56823
|
+
}
|
|
56824
|
+
/** Fail the session the usual way, with the `run_failed` item the reducer would have raised. */
|
|
56825
|
+
giveUp(sessionId, decision) {
|
|
56826
|
+
const pending = this.timers.get(sessionId);
|
|
56827
|
+
if (pending) clearTimeout(pending.timer);
|
|
56828
|
+
this.timers.delete(sessionId);
|
|
56829
|
+
if (decision.exhausted) this.notice(sessionId, decision.turn, {
|
|
56830
|
+
message: `Stopped retrying after ${decision.attempt - 1} automatic ${decision.attempt === 2 ? "retry" : "retries"}.`,
|
|
56831
|
+
data: { attempt: decision.attempt }
|
|
56832
|
+
});
|
|
56833
|
+
const session = this.deps.repo.update(sessionId, (s) => s.status === "idle" ? {
|
|
56834
|
+
...s,
|
|
56835
|
+
status: "failed"
|
|
56836
|
+
} : s);
|
|
56837
|
+
if (session.status !== "failed") return;
|
|
56838
|
+
this.deps.inbox.raiseRunFailed(session, `${decision.turn.id}:failed`, decision.error.message);
|
|
56839
|
+
}
|
|
56840
|
+
cancel(sessionId, message) {
|
|
56841
|
+
const pending = this.timers.get(sessionId);
|
|
56842
|
+
if (!pending) return;
|
|
56843
|
+
clearTimeout(pending.timer);
|
|
56844
|
+
this.timers.delete(sessionId);
|
|
56845
|
+
const turn = this.deps.db.select().from(turns).where(and(eq(turns.sessionId, sessionId), eq(turns.id, pending.turnId))).get();
|
|
56846
|
+
if (turn) this.notice(sessionId, rowToTurnRecord(turn), {
|
|
56847
|
+
message,
|
|
56848
|
+
data: { cancelled: true }
|
|
56849
|
+
});
|
|
56850
|
+
}
|
|
56851
|
+
/** The retry's notice in the failed turn, replaced as the retry's state changes. */
|
|
56852
|
+
notice(sessionId, turn, body) {
|
|
56853
|
+
this.deps.store.append([agentEventToNewEvent(sessionId, {
|
|
56854
|
+
type: "item.completed",
|
|
56855
|
+
threadId: turn.threadId,
|
|
56856
|
+
turnId: turn.id,
|
|
56857
|
+
item: {
|
|
56858
|
+
id: `auto-retry-${turn.id}`,
|
|
56859
|
+
kind: "notice",
|
|
56860
|
+
status: "completed",
|
|
56861
|
+
level: "info",
|
|
56862
|
+
code: "auto_retry",
|
|
56863
|
+
message: body.message,
|
|
56864
|
+
data: body.data
|
|
56865
|
+
}
|
|
56866
|
+
})]);
|
|
56867
|
+
}
|
|
56868
|
+
now() {
|
|
56869
|
+
return this.deps.now?.() ?? Date.now();
|
|
56870
|
+
}
|
|
56871
|
+
};
|
|
55457
56872
|
// ../server/src/services/orchestration/prompts.ts
|
|
55458
56873
|
/**
|
|
55459
56874
|
* System prompt sections for the orchestrator and the project managers (§8.1). Kept short:
|
|
55460
56875
|
* the live state arrives with every wake in the status digest, so the prompts only describe
|
|
55461
|
-
* responsibilities, tools,
|
|
56876
|
+
* responsibilities, tools, when to involve the user, and conflict behaviour. Both roles decide
|
|
56877
|
+
* routine questions with sensible defaults and keep the user for the decisions that are theirs.
|
|
55462
56878
|
*/
|
|
56879
|
+
/** The decisions that go to the user; everything else the PM or the orchestrator decides. */
|
|
56880
|
+
const USER_DECISIONS$1 = "destructive or irreversible actions; spending money or new accounts or credentials; outward-facing actions (releases, publishing, deploys, messages to people outside the project); protected areas; conflicting priorities";
|
|
55463
56881
|
const SHARED = `## How CodeDen keeps you informed
|
|
55464
56882
|
|
|
55465
56883
|
- 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.
|
|
55466
56884
|
- 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.
|
|
55467
56885
|
- Everything after the last of these blocks is what the sender actually wrote.
|
|
55468
56886
|
- 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.
|
|
55469
|
-
- 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)
|
|
56887
|
+
- 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 yourself 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).
|
|
56888
|
+
- To follow up later, schedule it with \`schedule_wakeup\`; never promise a follow-up you haven't scheduled.`;
|
|
55470
56889
|
const ORCHESTRATOR_PROMPT = `# You are the CodeDen orchestrator
|
|
55471
56890
|
|
|
55472
56891
|
You are the user's front door to every project in CodeDen. You create and configure projects, route requests to each project's project manager (PM), and summarise status across projects. You never do project work yourself and never start agent sessions directly: the PM of the project does that.
|
|
@@ -55478,34 +56897,51 @@ You are the user's front door to every project in CodeDen. You create and config
|
|
|
55478
56897
|
- \`ask_pm\`: ask a project's PM and wait for its answer. Use it whenever a request concerns a project's work (for example "run a security audit on X"): the PM knows what is already running there and decides how to proceed.
|
|
55479
56898
|
- \`message_pm\`: hand a PM a task or information without waiting; its reply arrives later as a message.
|
|
55480
56899
|
- \`list_inbox\`: what is waiting on the user.
|
|
56900
|
+
- \`schedule_wakeup\`, \`list_wakeups\`, \`cancel_wakeup\`: follow-ups you want to come back to later.
|
|
55481
56901
|
|
|
55482
56902
|
## Rules
|
|
55483
56903
|
|
|
55484
56904
|
- Route project work through the PM. For any request to do something in a project (run, start, fix, audit, review, update), call \`ask_pm\` first and pass the request on, even when the digest already shows related work: the PM owns the project's sessions and schedules and decides how to proceed. Never answer a work request from the digest alone.
|
|
55485
|
-
- If the PM reports that matching work is already running, tell the user
|
|
55486
|
-
-
|
|
56905
|
+
- If the PM reports that matching work is already running, tell the user in one line what is running and what the PM did about it (joined, queued or skipped).
|
|
56906
|
+
- Decide with sensible defaults. Don't pass the PM's open questions on to the user as a menu: when a sensible default exists, tell the PM to go ahead with it (\`message_pm\`) and tell the user the default in one line so they can override it.
|
|
56907
|
+
- Involve the user only for decisions the PMs cannot make: ${USER_DECISIONS$1} across projects. Then ask one question with your recommendation; never end a reply with a list of options.
|
|
56908
|
+
- When a PM reports on work you passed on, tell the user in one or two lines.
|
|
55487
56909
|
- PM messages marked informational do not wake you; they appear in your next digest. Do not reply to them unless they need a decision.
|
|
55488
56910
|
- Be brief with the user: lead with the answer, then the detail that matters.
|
|
55489
56911
|
|
|
55490
56912
|
${SHARED}`;
|
|
55491
56913
|
const PM_PROMPT = `# You are a CodeDen project manager (PM)
|
|
55492
56914
|
|
|
55493
|
-
You manage one project. You triage its GitHub issues, plan work, start and steer agent sessions, follow their progress
|
|
56915
|
+
You manage one project. You triage its GitHub issues, plan work, start and steer agent sessions, follow their progress and review results. Agent sessions do the coding, each in its own worktree; you never edit code yourself. You make the routine calls yourself and keep work moving; the user only gets the decisions listed under "When to involve the user".
|
|
55494
56916
|
|
|
55495
56917
|
## Tools
|
|
55496
56918
|
|
|
55497
56919
|
- Awareness: \`get_project_status\`, \`list_sessions\` (filter by status, work key, startedBy), \`list_scheduled_runs\`.
|
|
55498
|
-
- Sessions: \`start_session\` (always give a \`workKey\` such as \`issue:142\`, \`automation:security-audit\` or \`request:<slug>\`, a one-line \`purpose
|
|
55499
|
-
- Automations: \`skip_next_run\`, \`pause_automation\`, \`resume_automation\`, \`cancel_queued_run\`.
|
|
55500
|
-
- GitHub: \`list_issues\`, \`get_issue\`, \`triage_issue\`, \`create_issue\`, \`get_pr_status\`.
|
|
56920
|
+
- Sessions: \`start_session\` (always give a \`workKey\` such as \`issue:142\`, \`automation:security-audit\` or \`request:<slug>\`, a one-line \`purpose\`, a \`scope\` when you know the area, and a brief that includes the decisions you already made), \`message_session\`, \`stop_session\`.
|
|
56921
|
+
- Automations: \`list_automations\`, \`run_automation\`, \`create_automation\`, \`update_automation\`, \`skip_next_run\`, \`pause_automation\`, \`resume_automation\`, \`cancel_queued_run\`.
|
|
56922
|
+
- GitHub: \`list_issues\`, \`get_issue\`, \`triage_issue\`, \`report_triage\`, \`create_issue\`, \`get_pr_status\`.
|
|
55501
56923
|
- Notes: \`read_project_notes\`, \`update_project_notes\` (your durable memory for this project: decisions, conventions, follow-ups).
|
|
55502
|
-
- People: \`ask_user\` for decisions only the user can make
|
|
56924
|
+
- People: \`answer_question\` for your agents' questions, \`ask_user\` for decisions only the user can make, \`message_orchestrator\` to report to the orchestrator.
|
|
56925
|
+
- Follow-ups: \`schedule_wakeup\`, \`list_wakeups\`, \`cancel_wakeup\`.
|
|
55503
56926
|
|
|
55504
|
-
##
|
|
56927
|
+
## Decide, don't ask
|
|
56928
|
+
|
|
56929
|
+
- When a request leaves details open (provider, model, scope, approach, naming, order of work), choose sensible defaults from the project notes, the vision, the code and its conventions. Write them into the agent's brief and mention them in one line so the user can override them. Never end a turn with a menu of options.
|
|
56930
|
+
- Before starting work, check the digest and \`list_sessions\` for the same work key or area. If matching work is already running or queued (including scheduled automation runs), don't start a duplicate: choose the resolution yourself (\`join\` when the request adds detail or scope to the running work, \`wait\` when it has to follow it, \`skip\` when it is the same work, \`force\` only with a concrete reason) and say in one line what you chose.
|
|
56931
|
+
- Answer your agents' questions with \`answer_question\` when the brief, the notes or the vision settle them; otherwise leave them for the user.
|
|
56932
|
+
- You can't merge pull requests. The \`mode\` in your status digest says who does: in manual and assisted the user reviews and merges them; in autopilot and dark factory the factory loop merges when every check passes. Report a ready pull request once; don't ask whether to merge it.
|
|
56933
|
+
|
|
56934
|
+
## When to involve the user
|
|
55505
56935
|
|
|
55506
|
-
-
|
|
55507
|
-
-
|
|
55508
|
-
-
|
|
56936
|
+
- Only for ${USER_DECISIONS$1}; agents that keep failing at the same thing.
|
|
56937
|
+
- Ask one specific question with your default ("I'll go with X unless you say otherwise") and keep doing the work that doesn't depend on the answer.
|
|
56938
|
+
- When the user writes to you directly, ask in your reply. Use \`ask_user\` only on an automatic wake-up, when nobody is in the conversation.
|
|
56939
|
+
- Never call \`ask_user\` while the orchestrator waits for your answer or during an issue triage run (a triage run that waits on the user never reports). Classify an issue you can't decide as \`needs_info\` (ask its author in the comment) or \`unsafe\`, and say why in \`report_triage\`.
|
|
56940
|
+
|
|
56941
|
+
## The orchestrator
|
|
56942
|
+
|
|
56943
|
+
- When the orchestrator asks you something (a message marked "Question from the orchestrator"), answer in your final message of that turn; it is returned to the orchestrator directly, and the orchestrator is talking to the user. Answer with what you did or will do and the defaults you chose.
|
|
56944
|
+
- Work the orchestrator handed you (a message or a question) is yours to close: when it finishes, fails or needs a decision only the user can make, report it with \`message_orchestrator\` (informational: false), with the session, the PR and the outcome.
|
|
55509
56945
|
- Use \`message_orchestrator\` with \`informational: true\` for routine updates (progress, a finished run that needs nothing); leave it false only when the orchestrator must act or answer.
|
|
55510
56946
|
- After an automatic wake-up with nothing to act on, reply in one short line.
|
|
55511
56947
|
|
|
@@ -55517,21 +56953,104 @@ function rolePrompt(role) {
|
|
|
55517
56953
|
// ../server/src/mcp/prompt.ts
|
|
55518
56954
|
/** Name of CodeDen's MCP server inside agent sessions (tools appear as `codeden` tools). */
|
|
55519
56955
|
const MCP_SERVER_NAME = "codeden";
|
|
55520
|
-
|
|
56956
|
+
/**
|
|
56957
|
+
* The decisions that are the user's to make. Everything else an agent decides itself, states
|
|
56958
|
+
* what it assumed, and carries on.
|
|
56959
|
+
*/
|
|
56960
|
+
const USER_DECISIONS = "destructive or irreversible steps (deleting data or someone else's work, force-pushing, rewriting shared history), spending money or creating accounts or credentials, outward-facing actions your brief didn't ask for (publishing, releases, deploys, messages or comments to other people), protected areas, or a request with two readings that lead to substantially different work";
|
|
56961
|
+
const TOOLS = `## CodeDen tools
|
|
55521
56962
|
|
|
55522
|
-
|
|
56963
|
+
CodeDen tools are on the \`${MCP_SERVER_NAME}\` MCP server:
|
|
55523
56964
|
|
|
55524
56965
|
- \`report_progress\`: one line about what you are doing now, at the start of each phase.
|
|
55525
|
-
- \`ask_user\`: only
|
|
56966
|
+
- \`ask_user\`: only for a decision that is the user's to make (see above). If it returns "pending", finish what you can and end your turn; the answer arrives as your next message.
|
|
55526
56967
|
- \`share_to_chat\`: show the user a file or image instead of describing it.
|
|
55527
56968
|
- Browser (\`browser_navigate\`, \`browser_snapshot\`, \`browser_click\`, \`browser_type\`, \`browser_press\`, \`browser_screenshot\`, \`browser_logs\`, \`browser_wait\`, \`browser_evaluate\`, \`browser_open_dev_server\`): the same browser the user watches. Use it to check UI work: open the dev server, read the page with \`browser_snapshot\`, act on element refs, check \`browser_logs\` for errors. Call \`browser_screenshot\` with \`share: true\` for screenshots that show your result.
|
|
55528
56969
|
- \`generate_image\` (when listed): create images into your worktree, e.g. \`public/hero.png\`.
|
|
55529
56970
|
- GitHub projects: \`create_pull_request\` pushes your branch and opens the PR (linked to your issue and this session); \`attach_evidence\` posts screenshots, files and test output on it; \`comment_on_issue\` and \`get_pr_status\` (checks, reviews, mergeability).
|
|
55530
56971
|
- \`finish\`: record the outcome and a short summary once your assigned task is done.
|
|
55531
|
-
- \`suggest_followups\`: 2 to 4 short next prompts the user might send. Call it as the last action of every turn.`;
|
|
56972
|
+
- \`suggest_followups\`: 2 to 4 short next prompts the user might send, including optional next steps you would otherwise ask about. Call it as the last action of every turn.`;
|
|
56973
|
+
/** Where the agent works: its own worktree, or a folder it shares with the user. */
|
|
56974
|
+
function workspaceLine(session) {
|
|
56975
|
+
return session.worktreePath ? "Your working directory is your own git worktree and branch." : "Your working directory is not a worktree of your own: it is the project's checkout or a folder the user chose, and the user may be working in it too.";
|
|
56976
|
+
}
|
|
56977
|
+
/**
|
|
56978
|
+
* The session's permission level (§11.2) in the agent's terms: what is already approved, and
|
|
56979
|
+
* that approvals ask the user by themselves, so the agent doesn't ask first.
|
|
56980
|
+
*/
|
|
56981
|
+
function permissionLine(session) {
|
|
56982
|
+
const where = session.worktreePath ? "your worktree" : "your working directory";
|
|
56983
|
+
switch (session.autonomy) {
|
|
56984
|
+
case "read_only": return "- Your permission level is Read-only: you can read and run the project's checks, but not edit files, commit or push. Report what you found instead of asking to change things.";
|
|
56985
|
+
case "supervised": return "- Your permission level is Supervised: the user approves edits and commands as you make them, so go ahead and make them; don't ask before each one.";
|
|
56986
|
+
case "auto": return `- Your permission level is Auto: edits and commands inside ${where} are pre-approved, so go ahead. Risky actions ask the user for approval automatically, so you don't need to ask first.`;
|
|
56987
|
+
case "full": return `- Your permission level is Full: edits and commands inside ${where} are pre-approved, so go ahead. Anything outside it is refused or goes to the user for approval automatically, so you don't need to ask first.`;
|
|
56988
|
+
}
|
|
56989
|
+
}
|
|
56990
|
+
/** Committing and pull requests: free in the agent's own worktree, opt-in elsewhere. */
|
|
56991
|
+
function gitLines(session, project) {
|
|
56992
|
+
if (!session.worktreePath) return ["- Others may be working in this directory: don't commit, switch branches, stash or discard changes unless you're asked to."];
|
|
56993
|
+
if (session.autonomy === "read_only") return [];
|
|
56994
|
+
const lines = ["- This worktree and its branch are yours: edit files, run the project's checks and commit as you go without asking, unless your brief says otherwise."];
|
|
56995
|
+
if (project?.kind !== "github") return lines;
|
|
56996
|
+
lines.push(session.startedBy.kind === "user" ? "- Open a pull request with `create_pull_request` when the user asks for one; otherwise offer it with `suggest_followups` instead of asking." : "- When the work is done, open the pull request with `create_pull_request` (it links your issue), unless your brief says otherwise.");
|
|
56997
|
+
return lines;
|
|
56998
|
+
}
|
|
56999
|
+
/** Who started the session, when it wasn't the user: the PM sees the agent's questions first. */
|
|
57000
|
+
function starterLine(session) {
|
|
57001
|
+
switch (session.startedBy.kind) {
|
|
57002
|
+
case "user": return null;
|
|
57003
|
+
case "pm": return "- Your project manager started this session and sees your questions first; it answers what it can. Its brief is your first message, its later messages start with \"Message from your project manager:\", and nobody may be watching live.";
|
|
57004
|
+
case "automation": return "- An automation started this session, not the user, and nobody may be watching live. Your project manager sees your questions first; it answers what it can.";
|
|
57005
|
+
case "orchestrator": return "- The orchestrator started this session, not the user. Your project manager sees your questions first; it answers what it can.";
|
|
57006
|
+
}
|
|
57007
|
+
}
|
|
57008
|
+
/** The agent's section of the system prompt (§8.3): how to work, then CodeDen's tools. */
|
|
57009
|
+
function agentPrompt(session, project) {
|
|
57010
|
+
const lines = [
|
|
57011
|
+
"- Keep going until the task is done. When a detail is open (naming, structure, approach, test strategy, a choice between reasonable designs), pick what fits the brief and the project's conventions, note the assumption, and carry on. Don't ask about things you can decide yourself, check in the code, or easily change later.",
|
|
57012
|
+
permissionLine(session),
|
|
57013
|
+
...gitLines(session, project),
|
|
57014
|
+
`- Ask with \`ask_user\` only for a decision that is the user's to make: ${USER_DECISIONS}. First do everything that doesn't depend on the answer, then ask one question with your recommended option first.`,
|
|
57015
|
+
starterLine(session),
|
|
57016
|
+
"- End each turn with what you did and what you assumed, not with a question. Put optional next steps in `suggest_followups` instead of asking \"Want me to ...?\"."
|
|
57017
|
+
].filter((l) => l !== null);
|
|
57018
|
+
return [
|
|
57019
|
+
"# Working inside CodeDen",
|
|
57020
|
+
"",
|
|
57021
|
+
`You are an agent session in CodeDen, a local harness that shows your work to the user live. ${workspaceLine(session)}`,
|
|
57022
|
+
"",
|
|
57023
|
+
"## How to work",
|
|
57024
|
+
"",
|
|
57025
|
+
...lines,
|
|
57026
|
+
"",
|
|
57027
|
+
TOOLS
|
|
57028
|
+
].join("\n");
|
|
57029
|
+
}
|
|
55532
57030
|
/** The CodeDen section appended to a session's system prompt, by role (§7.5, §8.3). */
|
|
55533
|
-
function codedenSystemPrompt(
|
|
55534
|
-
return role === "agent" ?
|
|
57031
|
+
function codedenSystemPrompt(session, project) {
|
|
57032
|
+
return session.role === "agent" ? agentPrompt(session, project) : rolePrompt(session.role);
|
|
57033
|
+
}
|
|
57034
|
+
// ../server/src/services/sessions/setup-failure.ts
|
|
57035
|
+
/**
|
|
57036
|
+
* What the agent is told when a worktree setup command failed (§9 setup scripts): the command,
|
|
57037
|
+
* its exit code and the end of its output. Null when every command succeeded. Setup stops at
|
|
57038
|
+
* the first failure, so there is at most one.
|
|
57039
|
+
*/
|
|
57040
|
+
function setupFailureNote(setup) {
|
|
57041
|
+
const failed = setup?.find((step) => !step.ok);
|
|
57042
|
+
if (!failed) return null;
|
|
57043
|
+
const exit = failed.exitCode === null ? "timed out" : `exit ${failed.exitCode}`;
|
|
57044
|
+
const head = `Setup command \`${failed.command}\` failed (${exit})`;
|
|
57045
|
+
return failed.outputTail ? `${head}:\n\`\`\`\n${failed.outputTail}\n\`\`\`` : `${head}.`;
|
|
57046
|
+
}
|
|
57047
|
+
/** The agent's input with a setup failure note in front, so its first turn knows. */
|
|
57048
|
+
function withSetupNote(input, note) {
|
|
57049
|
+
if (!input || !note) return input;
|
|
57050
|
+
return {
|
|
57051
|
+
...input,
|
|
57052
|
+
text: `${note}\n\n${input.text}`
|
|
57053
|
+
};
|
|
55535
57054
|
}
|
|
55536
57055
|
// ../server/src/services/sessions/launcher.ts
|
|
55537
57056
|
const PREPARE_TIMEOUT_MS = 6e5;
|
|
@@ -55574,9 +57093,12 @@ var SessionLauncher = class {
|
|
|
55574
57093
|
providerId: session.provider,
|
|
55575
57094
|
options: this.sessionOptions({
|
|
55576
57095
|
...session,
|
|
55577
|
-
cwd: ws.cwd
|
|
57096
|
+
cwd: ws.cwd,
|
|
57097
|
+
worktreePath: ws.worktreePath,
|
|
57098
|
+
branch: ws.branch,
|
|
57099
|
+
baseBranch: ws.baseBranch
|
|
55578
57100
|
}),
|
|
55579
|
-
input: opts.input
|
|
57101
|
+
input: withSetupNote(opts.input, ws.setupNote)
|
|
55580
57102
|
}, START_TIMEOUT_MS);
|
|
55581
57103
|
repo.update(sessionId, (s) => ({
|
|
55582
57104
|
...s,
|
|
@@ -55594,13 +57116,13 @@ var SessionLauncher = class {
|
|
|
55594
57116
|
*/
|
|
55595
57117
|
async resume(session, input) {
|
|
55596
57118
|
await this.requireRunner();
|
|
55597
|
-
const current = await this.restoreWorktree(session);
|
|
57119
|
+
const { session: current, setupNote } = await this.restoreWorktree(session);
|
|
55598
57120
|
const result = await this.deps.runner.request({
|
|
55599
57121
|
name: "session.start",
|
|
55600
57122
|
sessionId: current.id,
|
|
55601
57123
|
providerId: current.provider,
|
|
55602
57124
|
options: this.sessionOptions(current),
|
|
55603
|
-
input
|
|
57125
|
+
input: withSetupNote(input, setupNote)
|
|
55604
57126
|
}, START_TIMEOUT_MS);
|
|
55605
57127
|
this.deps.repo.update(session.id, (s) => ({
|
|
55606
57128
|
...s,
|
|
@@ -55613,7 +57135,10 @@ var SessionLauncher = class {
|
|
|
55613
57135
|
* running the project's setup again. A no-op for sessions that still have their worktree.
|
|
55614
57136
|
*/
|
|
55615
57137
|
async restoreWorktree(session) {
|
|
55616
|
-
if (!session.worktreeRemovedAt || !session.worktreePath || !session.projectId) return
|
|
57138
|
+
if (!session.worktreeRemovedAt || !session.worktreePath || !session.projectId) return {
|
|
57139
|
+
session,
|
|
57140
|
+
setupNote: null
|
|
57141
|
+
};
|
|
55617
57142
|
const project = this.deps.projects.get(session.projectId);
|
|
55618
57143
|
const result = await this.deps.runner.request({
|
|
55619
57144
|
name: "workspace.prepare",
|
|
@@ -55626,13 +57151,16 @@ var SessionLauncher = class {
|
|
|
55626
57151
|
setupCommands: []
|
|
55627
57152
|
}, PREPARE_TIMEOUT_MS);
|
|
55628
57153
|
this.deps.log?.(`session ${session.id}: recreated worktree ${result.cwd}`);
|
|
55629
|
-
return
|
|
55630
|
-
|
|
55631
|
-
|
|
55632
|
-
|
|
55633
|
-
|
|
55634
|
-
|
|
55635
|
-
|
|
57154
|
+
return {
|
|
57155
|
+
session: this.deps.repo.update(session.id, (s) => ({
|
|
57156
|
+
...s,
|
|
57157
|
+
cwd: result.cwd,
|
|
57158
|
+
worktreePath: result.cwd,
|
|
57159
|
+
branch: result.branch ?? s.branch,
|
|
57160
|
+
worktreeRemovedAt: null
|
|
57161
|
+
})),
|
|
57162
|
+
setupNote: setupFailureNote(result.setup)
|
|
57163
|
+
};
|
|
55636
57164
|
}
|
|
55637
57165
|
/** Mark a session failed with an inbox item explaining why. */
|
|
55638
57166
|
fail(sessionId, key, error) {
|
|
@@ -55676,11 +57204,21 @@ var SessionLauncher = class {
|
|
|
55676
57204
|
effort: session.effort ?? void 0,
|
|
55677
57205
|
autonomy: session.autonomy,
|
|
55678
57206
|
resumeToken: session.resumeToken ?? void 0,
|
|
55679
|
-
systemPrompt: codedenSystemPrompt(session.
|
|
57207
|
+
systemPrompt: codedenSystemPrompt(session, this.projectOf(session)),
|
|
55680
57208
|
readOnly: session.role !== "agent",
|
|
55681
|
-
mcpServers
|
|
57209
|
+
mcpServers,
|
|
57210
|
+
...ownBranch(session) ? { ownBranch: ownBranch(session) } : {}
|
|
55682
57211
|
});
|
|
55683
57212
|
}
|
|
57213
|
+
/** The session's project for its system prompt; null without one (or once it is gone). */
|
|
57214
|
+
projectOf(session) {
|
|
57215
|
+
if (!session.projectId) return null;
|
|
57216
|
+
try {
|
|
57217
|
+
return this.deps.projects.get(session.projectId);
|
|
57218
|
+
} catch {
|
|
57219
|
+
return null;
|
|
57220
|
+
}
|
|
57221
|
+
}
|
|
55684
57222
|
async requireRunner() {
|
|
55685
57223
|
if (!await this.deps.runner.waitForConnection(this.deps.connectTimeoutMs ?? 15e3)) throw new ServiceError(503, "runner_unavailable", "The runner is not connected");
|
|
55686
57224
|
}
|
|
@@ -55696,6 +57234,7 @@ var SessionLauncher = class {
|
|
|
55696
57234
|
const path = req.mode === "existing" ? req.path : join(this.deps.worktreesDir, project.slug, session.id.slice(0, 8));
|
|
55697
57235
|
const branch = req.branch ?? (req.mode === "worktree" ? `codeden/${slugWords(session.title ?? session.purpose, 5, 40)}-${session.id.slice(0, 4)}` : void 0);
|
|
55698
57236
|
const baseRef = req.baseRef ?? project.defaultBranch ?? void 0;
|
|
57237
|
+
const fetch = req.fetch ?? (project.kind === "github" && req.mode === "worktree" && !req.baseRef);
|
|
55699
57238
|
try {
|
|
55700
57239
|
const result = await this.deps.runner.request({
|
|
55701
57240
|
name: "workspace.prepare",
|
|
@@ -55706,14 +57245,15 @@ var SessionLauncher = class {
|
|
|
55706
57245
|
branch,
|
|
55707
57246
|
baseRef,
|
|
55708
57247
|
setupCommands: req.setupCommands ?? [],
|
|
55709
|
-
...
|
|
57248
|
+
...fetch ? { fetch: true } : {}
|
|
55710
57249
|
}, PREPARE_TIMEOUT_MS);
|
|
55711
57250
|
return {
|
|
55712
57251
|
cwd: result.cwd,
|
|
55713
57252
|
worktreePath: req.mode === "worktree" ? result.cwd : null,
|
|
55714
57253
|
branch: result.branch,
|
|
55715
57254
|
baseBranch: baseRef ?? null,
|
|
55716
|
-
fallback: null
|
|
57255
|
+
fallback: null,
|
|
57256
|
+
setupNote: setupFailureNote(result.setup)
|
|
55717
57257
|
};
|
|
55718
57258
|
} catch (error) {
|
|
55719
57259
|
if (runnerErrorCode(error) !== "not_implemented") throw error;
|
|
@@ -55731,7 +57271,8 @@ function plain(cwd) {
|
|
|
55731
57271
|
worktreePath: null,
|
|
55732
57272
|
branch: null,
|
|
55733
57273
|
baseBranch: null,
|
|
55734
|
-
fallback: null
|
|
57274
|
+
fallback: null,
|
|
57275
|
+
setupNote: null
|
|
55735
57276
|
};
|
|
55736
57277
|
}
|
|
55737
57278
|
function defaultMeta(workspace) {
|
|
@@ -55743,12 +57284,13 @@ function defaultMeta(workspace) {
|
|
|
55743
57284
|
};
|
|
55744
57285
|
}
|
|
55745
57286
|
// ../server/src/services/sessions/lifecycle.ts
|
|
55746
|
-
/**
|
|
55747
|
-
|
|
55748
|
-
|
|
55749
|
-
|
|
55750
|
-
|
|
55751
|
-
|
|
57287
|
+
/**
|
|
57288
|
+
* Session statuses after which its MCP tokens are revoked (§8.3). Not `failed`: a failed turn
|
|
57289
|
+
* leaves the provider process running with its token, and the next message reaches that same
|
|
57290
|
+
* process; a session whose process is gone is resumed with a freshly minted token, which
|
|
57291
|
+
* revokes the old one.
|
|
57292
|
+
*/
|
|
57293
|
+
const ENDED = /* @__PURE__ */ new Set(["stopped", "completed"]);
|
|
55752
57294
|
/**
|
|
55753
57295
|
* Reacts to committed events: starts queued sessions whose blockers finished (§8.4 `wait`),
|
|
55754
57296
|
* sends queued messages when a session goes idle, and runs utility-model hooks after a root
|
|
@@ -55788,7 +57330,7 @@ var SessionLifecycle = class {
|
|
|
55788
57330
|
sessionsChanged = true;
|
|
55789
57331
|
const { session } = e.payload;
|
|
55790
57332
|
if (ENDED.has(session.status)) this.deps.tokens.revokeSession(session.id);
|
|
55791
|
-
if (session.status === "idle" && this.deps.sessions.hasOutbox(session.id)) setImmediate(() => void this.deps.sessions.flushOutbox(session.id));
|
|
57333
|
+
if ((session.status === "idle" || session.status === "failed") && this.deps.sessions.hasOutbox(session.id)) setImmediate(() => void this.deps.sessions.flushOutbox(session.id));
|
|
55792
57334
|
} else if ((e.type === "agent.item.started" || e.type === "agent.item.completed") && e.sessionId) {
|
|
55793
57335
|
const { item } = e.payload;
|
|
55794
57336
|
if (item.kind === "user_message" && item.input.text.trim()) {
|
|
@@ -56000,6 +57542,151 @@ var Outbox = class {
|
|
|
56000
57542
|
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;
|
|
56001
57543
|
}
|
|
56002
57544
|
};
|
|
57545
|
+
/** Sessions that can take a message now or after their turn. */
|
|
57546
|
+
const OPEN = /* @__PURE__ */ new Set([
|
|
57547
|
+
"idle",
|
|
57548
|
+
"running",
|
|
57549
|
+
"waiting",
|
|
57550
|
+
"interrupted"
|
|
57551
|
+
]);
|
|
57552
|
+
const PR_EVENTS = /* @__PURE__ */ new Set([
|
|
57553
|
+
"pr.opened",
|
|
57554
|
+
"pr.updated",
|
|
57555
|
+
"pr.checks_failed",
|
|
57556
|
+
"pr.checks_passed",
|
|
57557
|
+
"review.submitted"
|
|
57558
|
+
]);
|
|
57559
|
+
const clip$2 = (text, max = 1500) => text.length > max ? `${text.slice(0, max - 3)}...` : text;
|
|
57560
|
+
function failingChecks(pr) {
|
|
57561
|
+
return pr.checks.checks.filter((c) => c.conclusion && ![
|
|
57562
|
+
"success",
|
|
57563
|
+
"neutral",
|
|
57564
|
+
"skipped"
|
|
57565
|
+
].includes(c.conclusion));
|
|
57566
|
+
}
|
|
57567
|
+
/**
|
|
57568
|
+
* Whether PR feedback goes to this session: an agent session the user started themselves
|
|
57569
|
+
* (the PM follows up on work it, the orchestrator or an automation started), not archived and
|
|
57570
|
+
* able to take a message.
|
|
57571
|
+
*/
|
|
57572
|
+
function receivesPrFeedback(session) {
|
|
57573
|
+
return session.role === "agent" && session.startedBy.kind === "user" && session.parentSessionId === null && !session.archivedAt && OPEN.has(session.status);
|
|
57574
|
+
}
|
|
57575
|
+
/** The feedback one GitHub event carries for the PR's session, if any. Pure. */
|
|
57576
|
+
function prFeedbackFor(event) {
|
|
57577
|
+
if (!PR_EVENTS.has(event.type)) return [];
|
|
57578
|
+
const { pr, review } = event.payload;
|
|
57579
|
+
if (pr.state !== "open" && pr.state !== "draft") return [];
|
|
57580
|
+
const head = pr.headSha ?? "unknown";
|
|
57581
|
+
const short = head.slice(0, 7);
|
|
57582
|
+
const out = [];
|
|
57583
|
+
if (event.type === "pr.checks_failed") {
|
|
57584
|
+
const failed = failingChecks(pr);
|
|
57585
|
+
const list = failed.length ? failed.map((c) => `- ${c.name} (${c.conclusion})${c.url ? `: ${c.url}` : ""}`).join("\n") : "- (GitHub did not say which)";
|
|
57586
|
+
out.push({
|
|
57587
|
+
kind: "checks_failed",
|
|
57588
|
+
key: `checks_failed:${head}`,
|
|
57589
|
+
text: [
|
|
57590
|
+
`CI failed on your pull request #${pr.number} (commit ${short}). Failing checks:`,
|
|
57591
|
+
list,
|
|
57592
|
+
"Read the failure logs (for GitHub Actions: `gh run view <run-id> --log-failed`), fix the cause, and push."
|
|
57593
|
+
].join("\n")
|
|
57594
|
+
});
|
|
57595
|
+
}
|
|
57596
|
+
if (event.type === "review.submitted" && review?.state === "changes_requested") {
|
|
57597
|
+
const who = review.author ? `@${review.author}` : "A reviewer";
|
|
57598
|
+
const body = review.body.trim() ? `\n\n${clip$2(review.body.trim())}` : "";
|
|
57599
|
+
out.push({
|
|
57600
|
+
kind: "changes_requested",
|
|
57601
|
+
key: `changes_requested:${head}`,
|
|
57602
|
+
text: `${who} requested changes on your pull request #${pr.number}.${body}\n\nRead the review and its comments (\`gh pr view ${pr.number} --comments\`), address them, and push.`
|
|
57603
|
+
});
|
|
57604
|
+
}
|
|
57605
|
+
if (pr.mergeable === "conflicting") out.push({
|
|
57606
|
+
kind: "conflicts",
|
|
57607
|
+
key: `conflicts:${head}`,
|
|
57608
|
+
text: `Your pull request #${pr.number} has merge conflicts with \`${pr.baseBranch}\`. Fetch, merge \`origin/${pr.baseBranch}\` into your branch, resolve the conflicts, run the checks, and push.`
|
|
57609
|
+
});
|
|
57610
|
+
return out;
|
|
57611
|
+
}
|
|
57612
|
+
/**
|
|
57613
|
+
* PR feedback for sessions the user started (§9): when the pull request a session owns fails
|
|
57614
|
+
* its checks, gets a review requesting changes or runs into merge conflicts, the session is
|
|
57615
|
+
* sent a message from CodeDen (queued behind a running turn) so the agent can fix it without
|
|
57616
|
+
* the user relaying it. Each problem is sent once per head commit, and at most
|
|
57617
|
+
* `PR_FEEDBACK_MAX_ROUNDS` messages go out per PR. Off with `sessions.prFeedback`.
|
|
57618
|
+
*/
|
|
57619
|
+
var PrFeedbackService = class {
|
|
57620
|
+
deps;
|
|
57621
|
+
unsubscribe;
|
|
57622
|
+
chain = Promise.resolve();
|
|
57623
|
+
constructor(deps) {
|
|
57624
|
+
this.deps = deps;
|
|
57625
|
+
}
|
|
57626
|
+
start() {
|
|
57627
|
+
this.unsubscribe = this.deps.store.subscribe((events) => {
|
|
57628
|
+
this.chain = this.chain.then(() => this.onEvents(events));
|
|
57629
|
+
});
|
|
57630
|
+
}
|
|
57631
|
+
stop() {
|
|
57632
|
+
this.unsubscribe?.();
|
|
57633
|
+
}
|
|
57634
|
+
/** Resolves once the events seen so far are handled (tests). */
|
|
57635
|
+
idle() {
|
|
57636
|
+
return this.chain;
|
|
57637
|
+
}
|
|
57638
|
+
async onEvents(events) {
|
|
57639
|
+
if (!this.deps.settings().sessions.prFeedback) return;
|
|
57640
|
+
for (const event of events) {
|
|
57641
|
+
const feedback = prFeedbackFor(event);
|
|
57642
|
+
if (feedback.length === 0) continue;
|
|
57643
|
+
const { pr } = event.payload;
|
|
57644
|
+
const found = pr.sessionId ? this.deps.repo.find(pr.sessionId) : void 0;
|
|
57645
|
+
if (!found || !receivesPrFeedback(found.session)) continue;
|
|
57646
|
+
for (const item of feedback) await this.deliver(found.session, pr, item);
|
|
57647
|
+
}
|
|
57648
|
+
}
|
|
57649
|
+
async deliver(session, pr, item) {
|
|
57650
|
+
const record = (this.deps.repo.find(session.id)?.startMeta ?? null)?.prFeedback?.[String(pr.number)] ?? {
|
|
57651
|
+
rounds: 0,
|
|
57652
|
+
keys: []
|
|
57653
|
+
};
|
|
57654
|
+
if (record.keys.includes(item.key)) return;
|
|
57655
|
+
if (record.rounds >= 3) {
|
|
57656
|
+
this.deps.log?.(`pr feedback: PR #${pr.number} reached ${record.rounds} rounds; not sent`);
|
|
57657
|
+
return;
|
|
57658
|
+
}
|
|
57659
|
+
const next = {
|
|
57660
|
+
rounds: record.rounds + 1,
|
|
57661
|
+
keys: [...record.keys, item.key].slice(-20)
|
|
57662
|
+
};
|
|
57663
|
+
this.deps.repo.setStartMeta(session.id, (m) => withRecord(m, pr.number, next));
|
|
57664
|
+
try {
|
|
57665
|
+
await this.deps.send(session.id, {
|
|
57666
|
+
text: item.text,
|
|
57667
|
+
attachments: [],
|
|
57668
|
+
from: "codeden"
|
|
57669
|
+
});
|
|
57670
|
+
} catch (error) {
|
|
57671
|
+
this.deps.log?.(`pr feedback for session ${session.id}: ${errorMessage$1(error)}`);
|
|
57672
|
+
}
|
|
57673
|
+
}
|
|
57674
|
+
};
|
|
57675
|
+
function withRecord(meta, number, record) {
|
|
57676
|
+
const base = meta ?? {
|
|
57677
|
+
workspaceMode: "worktree",
|
|
57678
|
+
workspaceFallback: null,
|
|
57679
|
+
forced: null,
|
|
57680
|
+
queuedBehind: []
|
|
57681
|
+
};
|
|
57682
|
+
return {
|
|
57683
|
+
...base,
|
|
57684
|
+
prFeedback: {
|
|
57685
|
+
...base.prFeedback,
|
|
57686
|
+
[String(number)]: record
|
|
57687
|
+
}
|
|
57688
|
+
};
|
|
57689
|
+
}
|
|
56003
57690
|
// ../server/src/services/sessions/questions.ts
|
|
56004
57691
|
/**
|
|
56005
57692
|
* Questions agents ask through CodeDen's `ask_user` tool (§8.3). The question becomes a
|
|
@@ -56053,7 +57740,7 @@ var QuestionBroker = class {
|
|
|
56053
57740
|
* Handle a response if `requestId` belongs to an `ask_user` question. Returns false for
|
|
56054
57741
|
* provider-native requests (the caller forwards those to the runner).
|
|
56055
57742
|
*/
|
|
56056
|
-
async respond(sessionId, requestId, response) {
|
|
57743
|
+
async respond(sessionId, requestId, response, by = "user") {
|
|
56057
57744
|
const record = this.find(sessionId, requestId);
|
|
56058
57745
|
if (!record) return false;
|
|
56059
57746
|
const item = record.item;
|
|
@@ -56062,7 +57749,8 @@ var QuestionBroker = class {
|
|
|
56062
57749
|
const done = answers ? {
|
|
56063
57750
|
...item,
|
|
56064
57751
|
status: "completed",
|
|
56065
|
-
answers
|
|
57752
|
+
answers,
|
|
57753
|
+
decidedBy: by
|
|
56066
57754
|
} : {
|
|
56067
57755
|
...item,
|
|
56068
57756
|
status: "cancelled"
|
|
@@ -56071,11 +57759,12 @@ var QuestionBroker = class {
|
|
|
56071
57759
|
const outcome = answers ? {
|
|
56072
57760
|
status: "answered",
|
|
56073
57761
|
answers,
|
|
56074
|
-
text: formatAnswers(item, answers)
|
|
57762
|
+
text: formatAnswers(item, answers),
|
|
57763
|
+
by
|
|
56075
57764
|
} : { status: "dismissed" };
|
|
56076
57765
|
const waiter = this.waiters.get(requestId);
|
|
56077
57766
|
if (waiter) waiter(outcome);
|
|
56078
|
-
else if (outcome.status === "answered") await this.deps.sendMessage()(sessionId, `Answer to your earlier question:\n${outcome.text}
|
|
57767
|
+
else if (outcome.status === "answered") await this.deps.sendMessage()(sessionId, `Answer to your earlier question:\n${outcome.text}`, by);
|
|
56079
57768
|
return true;
|
|
56080
57769
|
}
|
|
56081
57770
|
get waitMs() {
|
|
@@ -56200,12 +57889,15 @@ var SessionRecovery = class {
|
|
|
56200
57889
|
return;
|
|
56201
57890
|
}
|
|
56202
57891
|
for (const id of candidates) {
|
|
57892
|
+
const found = repo.find(id)?.session;
|
|
57893
|
+
if (!found || hosted.has(id) || sessions.launching.has(id)) continue;
|
|
57894
|
+
if (!CUT_OFF.includes(found.status)) continue;
|
|
57895
|
+
repo.closeOrphanedTurns(id);
|
|
56203
57896
|
const session = repo.find(id)?.session;
|
|
56204
|
-
if (
|
|
56205
|
-
if (!CUT_OFF.includes(session.status)) continue;
|
|
56206
|
-
await this.resume(session);
|
|
57897
|
+
if (session && CUT_OFF.includes(session.status)) await this.resume(session);
|
|
56207
57898
|
}
|
|
56208
|
-
|
|
57899
|
+
sessions.releaseOutboxes();
|
|
57900
|
+
for (const id of sessions.sessionsWithQueuedMessages()) await sessions.flushOutbox(id).catch(() => void 0);
|
|
56209
57901
|
}
|
|
56210
57902
|
async resume(session) {
|
|
56211
57903
|
const provider = this.deps.runner.providers().find((p) => p.id === session.provider);
|
|
@@ -56323,6 +58015,37 @@ var SessionRepo = class {
|
|
|
56323
58015
|
startedAt: r.startedAt
|
|
56324
58016
|
}))];
|
|
56325
58017
|
}
|
|
58018
|
+
/**
|
|
58019
|
+
* Close what a runner that went away without a word (crash, kill, power loss) left open in a
|
|
58020
|
+
* session it no longer hosts (§4.2). Turns still `running` end as interrupted by a restart,
|
|
58021
|
+
* which cancels their approvals and provider questions and dismisses their inbox items; any
|
|
58022
|
+
* other provider request still open is cancelled too, so none of them can keep the session
|
|
58023
|
+
* `waiting`. CodeDen's deferred `ask_user` questions outlive their turn and stay open.
|
|
58024
|
+
* Returns how many turns and requests it closed.
|
|
58025
|
+
*/
|
|
58026
|
+
closeOrphanedTurns(sessionId) {
|
|
58027
|
+
const running = this.db.select().from(turns).where(and(eq(turns.sessionId, sessionId), eq(turns.status, "running"))).orderBy(asc(turns.startedAt)).all();
|
|
58028
|
+
const ended = new Set(running.map((t) => t.id));
|
|
58029
|
+
const requests = this.db.select().from(items).where(and(eq(items.sessionId, sessionId), inArray(items.kind, ["approval_request", "user_question"]), eq(items.status, "in_progress"))).all().map(rowToItemRecord).filter((r) => !ended.has(r.turnId)).filter((r) => !(r.item.kind === "user_question" && r.item.deferred));
|
|
58030
|
+
const root = this.find(sessionId)?.session.rootThreadId;
|
|
58031
|
+
const isRoot = (t) => Number(t.threadId === root);
|
|
58032
|
+
const events = [...requests.map((r) => agentEventToNewEvent(sessionId, {
|
|
58033
|
+
type: "item.completed",
|
|
58034
|
+
threadId: r.threadId,
|
|
58035
|
+
turnId: r.turnId,
|
|
58036
|
+
item: {
|
|
58037
|
+
...r.item,
|
|
58038
|
+
status: "cancelled"
|
|
58039
|
+
}
|
|
58040
|
+
})), ...running.sort((a, b) => isRoot(a) - isRoot(b)).map((t) => agentEventToNewEvent(sessionId, {
|
|
58041
|
+
type: "turn.interrupted",
|
|
58042
|
+
threadId: t.threadId,
|
|
58043
|
+
turnId: t.id,
|
|
58044
|
+
reason: "runner_restart"
|
|
58045
|
+
}))];
|
|
58046
|
+
if (events.length > 0) this.store.append(events);
|
|
58047
|
+
return events.length;
|
|
58048
|
+
}
|
|
56326
58049
|
item(sessionId, itemId) {
|
|
56327
58050
|
const row = this.db.select().from(items).where(and(eq(items.sessionId, sessionId), eq(items.id, itemId))).get();
|
|
56328
58051
|
return row ? rowToItemRecord(row) : void 0;
|
|
@@ -56374,6 +58097,46 @@ function readProjectConfig(projectPath) {
|
|
|
56374
58097
|
function projectDefaultProvider(project) {
|
|
56375
58098
|
return project.defaultProvider ?? readProjectConfig(project.path)?.defaultProvider ?? null;
|
|
56376
58099
|
}
|
|
58100
|
+
// ../server/src/services/sessions/outbox-holds.ts
|
|
58101
|
+
/** Runner failures worth retrying: the runner is away or slow, not refusing the message. */
|
|
58102
|
+
function isTransientRunnerError(error) {
|
|
58103
|
+
const code = runnerErrorCode(error);
|
|
58104
|
+
return code === "runner_unavailable" || code === "timeout";
|
|
58105
|
+
}
|
|
58106
|
+
/**
|
|
58107
|
+
* Sessions whose queued-message delivery failed. The message went back in its place and
|
|
58108
|
+
* delivery pauses, so a failing send never loops on the status change it causes. It resumes
|
|
58109
|
+
* after `retryMs` when the failure was transient, when the runner is ready again
|
|
58110
|
+
* (`releaseAll`), or once any message reaches the session (`release`).
|
|
58111
|
+
*/
|
|
58112
|
+
var OutboxHolds = class {
|
|
58113
|
+
opts;
|
|
58114
|
+
held = /* @__PURE__ */ new Map();
|
|
58115
|
+
constructor(opts) {
|
|
58116
|
+
this.opts = opts;
|
|
58117
|
+
}
|
|
58118
|
+
isHeld(sessionId) {
|
|
58119
|
+
return this.held.has(sessionId);
|
|
58120
|
+
}
|
|
58121
|
+
/** Pause delivery; `retry` resumes it on its own after the retry delay. */
|
|
58122
|
+
hold(sessionId, retry) {
|
|
58123
|
+
this.release(sessionId);
|
|
58124
|
+
const timer = retry ? setTimeout(() => {
|
|
58125
|
+
this.held.delete(sessionId);
|
|
58126
|
+
this.opts.onRetry(sessionId);
|
|
58127
|
+
}, this.opts.retryMs ?? 3e4) : void 0;
|
|
58128
|
+
timer?.unref?.();
|
|
58129
|
+
this.held.set(sessionId, timer);
|
|
58130
|
+
}
|
|
58131
|
+
release(sessionId) {
|
|
58132
|
+
const timer = this.held.get(sessionId);
|
|
58133
|
+
if (timer) clearTimeout(timer);
|
|
58134
|
+
this.held.delete(sessionId);
|
|
58135
|
+
}
|
|
58136
|
+
releaseAll() {
|
|
58137
|
+
for (const id of [...this.held.keys()]) this.release(id);
|
|
58138
|
+
}
|
|
58139
|
+
};
|
|
56377
58140
|
// ../server/src/services/sessions/permissions.ts
|
|
56378
58141
|
/**
|
|
56379
58142
|
* Why a session cannot use a permission level on a provider, or null when it can. The
|
|
@@ -56492,6 +58255,12 @@ const NOT_STARTED = /* @__PURE__ */ new Set([
|
|
|
56492
58255
|
"queued",
|
|
56493
58256
|
"starting"
|
|
56494
58257
|
]);
|
|
58258
|
+
/** Statuses in which the next queued message is delivered. */
|
|
58259
|
+
const FLUSHABLE = /* @__PURE__ */ new Set([
|
|
58260
|
+
"idle",
|
|
58261
|
+
"interrupted",
|
|
58262
|
+
"failed"
|
|
58263
|
+
]);
|
|
56495
58264
|
/**
|
|
56496
58265
|
* Agent sessions: start (with the §8.4 conflict check), messages, steering, interrupts,
|
|
56497
58266
|
* approvals and stop. Starts return immediately; the runner work happens in the background
|
|
@@ -56503,8 +58272,13 @@ var SessionService = class {
|
|
|
56503
58272
|
launching = /* @__PURE__ */ new Map();
|
|
56504
58273
|
/** Rewrites a turn's input at send time (the orchestration status digest, §8.4). */
|
|
56505
58274
|
inputDecorator;
|
|
58275
|
+
holds;
|
|
56506
58276
|
constructor(deps) {
|
|
56507
58277
|
this.deps = deps;
|
|
58278
|
+
this.holds = new OutboxHolds({
|
|
58279
|
+
retryMs: deps.outboxRetryMs,
|
|
58280
|
+
onRetry: (id) => void this.flushOutbox(id).catch(() => void 0)
|
|
58281
|
+
});
|
|
56508
58282
|
}
|
|
56509
58283
|
setInputDecorator(fn) {
|
|
56510
58284
|
this.inputDecorator = fn;
|
|
@@ -56637,7 +58411,14 @@ var SessionService = class {
|
|
|
56637
58411
|
}
|
|
56638
58412
|
return this.enqueue(id, req.input, from);
|
|
56639
58413
|
}
|
|
56640
|
-
|
|
58414
|
+
try {
|
|
58415
|
+
await this.sendNow(session, req.input, from);
|
|
58416
|
+
} catch (error) {
|
|
58417
|
+
if (from === "user" || req.whenBusy === "reject" || !isTransientRunnerError(error)) throw error;
|
|
58418
|
+
this.holds.hold(id, true);
|
|
58419
|
+
return this.enqueue(id, req.input, from);
|
|
58420
|
+
}
|
|
58421
|
+
this.holds.release(id);
|
|
56641
58422
|
return { status: "sent" };
|
|
56642
58423
|
}
|
|
56643
58424
|
async steer(id, raw) {
|
|
@@ -56663,9 +58444,10 @@ var SessionService = class {
|
|
|
56663
58444
|
throw toServiceError$1(e);
|
|
56664
58445
|
});
|
|
56665
58446
|
}
|
|
56666
|
-
|
|
58447
|
+
/** Answer a question or approval; `by` is who answered a question (the user or the PM). */
|
|
58448
|
+
async respond(id, req, by = "user") {
|
|
56667
58449
|
this.deps.repo.get(id);
|
|
56668
|
-
if (await this.deps.questions.respond(id, req.requestId, req.response)) return;
|
|
58450
|
+
if (await this.deps.questions.respond(id, req.requestId, req.response, by)) return;
|
|
56669
58451
|
await this.deps.runner.request({
|
|
56670
58452
|
name: "session.respond",
|
|
56671
58453
|
sessionId: id,
|
|
@@ -56676,13 +58458,17 @@ var SessionService = class {
|
|
|
56676
58458
|
});
|
|
56677
58459
|
const resolution = req.response.kind === "approval" ? {
|
|
56678
58460
|
decision: req.response.decision,
|
|
56679
|
-
decidedBy: "user"
|
|
56680
|
-
} : req.response.kind === "answer" ? {
|
|
58461
|
+
decidedBy: req.response.decidedBy ?? "user"
|
|
58462
|
+
} : req.response.kind === "answer" ? {
|
|
58463
|
+
answers: req.response.answers,
|
|
58464
|
+
decidedBy: by
|
|
58465
|
+
} : { status: "cancelled" };
|
|
56681
58466
|
this.deps.inbox.closeForRequest(id, req.requestId, resolution);
|
|
56682
58467
|
}
|
|
56683
58468
|
async stop(id) {
|
|
56684
58469
|
const session = this.deps.repo.get(id);
|
|
56685
58470
|
this.deps.outbox.clear(id);
|
|
58471
|
+
this.holds.release(id);
|
|
56686
58472
|
if (session.status !== "queued" && session.status !== "created") try {
|
|
56687
58473
|
await this.deps.runner.request({
|
|
56688
58474
|
name: "session.stop",
|
|
@@ -56699,19 +58485,31 @@ var SessionService = class {
|
|
|
56699
58485
|
endedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
56700
58486
|
});
|
|
56701
58487
|
}
|
|
56702
|
-
/**
|
|
58488
|
+
/**
|
|
58489
|
+
* Send the next queued message once no turn is running (idle, or after a failed or
|
|
58490
|
+
* interrupted turn). If delivery fails the message goes back in its place and delivery
|
|
58491
|
+
* pauses (`OutboxHolds`); only an error that is not a runner restart fails the session.
|
|
58492
|
+
*/
|
|
56703
58493
|
async flushOutbox(id) {
|
|
58494
|
+
if (this.holds.isHeld(id)) return;
|
|
56704
58495
|
const next = this.deps.outbox.peek(id);
|
|
56705
58496
|
if (!next) return;
|
|
56706
58497
|
const session = this.deps.repo.get(id);
|
|
56707
|
-
if (
|
|
58498
|
+
if (!FLUSHABLE.has(session.status)) return;
|
|
56708
58499
|
this.deps.outbox.remove(next, "sent");
|
|
56709
58500
|
try {
|
|
56710
|
-
await this.sendNow(session, next.input);
|
|
58501
|
+
await this.sendNow(session, next.input, next.from);
|
|
56711
58502
|
} catch (error) {
|
|
56712
|
-
|
|
58503
|
+
const transient = isTransientRunnerError(error);
|
|
58504
|
+
this.holds.hold(id, transient);
|
|
58505
|
+
this.deps.outbox.restore(next);
|
|
58506
|
+
if (!transient && session.status !== "failed") this.deps.launcher.fail(id, `send-${Date.now()}`, error);
|
|
56713
58507
|
}
|
|
56714
58508
|
}
|
|
58509
|
+
/** The runner is ready again: retry every paused delivery (§4.2). */
|
|
58510
|
+
releaseOutboxes() {
|
|
58511
|
+
this.holds.releaseAll();
|
|
58512
|
+
}
|
|
56715
58513
|
/**
|
|
56716
58514
|
* Re-open a session whose turn was cut off by a runner restart and tell the agent to carry
|
|
56717
58515
|
* on (§4.2). Resumes through the provider's resume token.
|
|
@@ -56721,7 +58519,7 @@ var SessionService = class {
|
|
|
56721
58519
|
await this.sendNow(session, {
|
|
56722
58520
|
text,
|
|
56723
58521
|
attachments: []
|
|
56724
|
-
});
|
|
58522
|
+
}, "codeden");
|
|
56725
58523
|
}
|
|
56726
58524
|
sessionsWithQueuedMessages() {
|
|
56727
58525
|
return this.deps.outbox.sessionsWithMessages();
|
|
@@ -56729,8 +58527,18 @@ var SessionService = class {
|
|
|
56729
58527
|
hasOutbox(id) {
|
|
56730
58528
|
return this.deps.outbox.count(id) > 0;
|
|
56731
58529
|
}
|
|
56732
|
-
async sendNow(session, raw) {
|
|
56733
|
-
const
|
|
58530
|
+
async sendNow(session, raw, from) {
|
|
58531
|
+
const decorated = this.inputDecorator?.(session, raw, from) ?? { input: raw };
|
|
58532
|
+
try {
|
|
58533
|
+
await this.deliver(session, this.forRunner(session, decorated.input));
|
|
58534
|
+
} catch (error) {
|
|
58535
|
+
decorated.failed?.();
|
|
58536
|
+
throw error;
|
|
58537
|
+
}
|
|
58538
|
+
decorated.delivered?.();
|
|
58539
|
+
}
|
|
58540
|
+
/** Hand a turn's input to the runner, resuming the session when it no longer hosts it. */
|
|
58541
|
+
async deliver(session, input) {
|
|
56734
58542
|
await applyPendingConfig(this.deps.repo, this.deps.runner, session);
|
|
56735
58543
|
const previous = session.status;
|
|
56736
58544
|
this.deps.repo.update(session.id, (s) => ({
|
|
@@ -56851,6 +58659,98 @@ function fallbackMeta() {
|
|
|
56851
58659
|
queuedBehind: []
|
|
56852
58660
|
};
|
|
56853
58661
|
}
|
|
58662
|
+
/** The denial an unwatched session gets when nobody answered in time. */
|
|
58663
|
+
function noAnswerDenial(waitMs) {
|
|
58664
|
+
const minutes = Math.max(1, Math.round(waitMs / 6e4));
|
|
58665
|
+
return [
|
|
58666
|
+
`Not approved: nobody answered within ${minutes} minute${minutes === 1 ? "" : "s"}, and this session runs unattended (started by an automation, a project manager or the factory). Don't wait for it or try the same thing again.`,
|
|
58667
|
+
"- Find a way that stays inside your worktree, or skip this step. To publish your work, commit it and call `create_pull_request`.",
|
|
58668
|
+
"- If the task can't be finished without it, say what you needed and why in your final message, or call `finish` with outcome `blocked`, so the user sees it."
|
|
58669
|
+
].join("\n");
|
|
58670
|
+
}
|
|
58671
|
+
/**
|
|
58672
|
+
* True for sessions the user is unlikely to be watching: started by an automation, a PM or the
|
|
58673
|
+
* factory rather than by the user or the orchestrator they talk to.
|
|
58674
|
+
*/
|
|
58675
|
+
function isUnwatched(session) {
|
|
58676
|
+
return session.automationRunId !== null || session.startedBy.kind === "automation" || session.startedBy.kind === "pm";
|
|
58677
|
+
}
|
|
58678
|
+
/**
|
|
58679
|
+
* Approvals nobody is there to answer (§10.1, §11.2). Nothing here ever approves: it denies, with
|
|
58680
|
+
* guidance, so the agent carries on instead of waiting forever (fail closed).
|
|
58681
|
+
* - The PM and the factory's sessions in a dark factory are denied at once, as their questions
|
|
58682
|
+
* are answered at once.
|
|
58683
|
+
* - Sessions started by an automation, a PM or the factory otherwise wait
|
|
58684
|
+
* {@link UNWATCHED_APPROVAL_WAIT_MS} for the user, then are denied. A scheduled run then
|
|
58685
|
+
* finishes instead of blocking the next ones.
|
|
58686
|
+
* Sessions the user started wait for the user, however long.
|
|
58687
|
+
*/
|
|
58688
|
+
var UnwatchedApprovals = class {
|
|
58689
|
+
deps;
|
|
58690
|
+
unsubscribe;
|
|
58691
|
+
timers = /* @__PURE__ */ new Map();
|
|
58692
|
+
constructor(deps) {
|
|
58693
|
+
this.deps = deps;
|
|
58694
|
+
}
|
|
58695
|
+
/** Watch new approvals, and pick up the ones already open (a restart keeps their age). */
|
|
58696
|
+
start() {
|
|
58697
|
+
this.unsubscribe ??= this.deps.store.subscribe((events) => this.onEvents(events));
|
|
58698
|
+
for (const item of this.deps.inbox.list()) if (item.kind === "approval") this.track(item);
|
|
58699
|
+
}
|
|
58700
|
+
stop() {
|
|
58701
|
+
this.unsubscribe?.();
|
|
58702
|
+
this.unsubscribe = void 0;
|
|
58703
|
+
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
58704
|
+
this.timers.clear();
|
|
58705
|
+
}
|
|
58706
|
+
onEvents(events) {
|
|
58707
|
+
for (const e of events) {
|
|
58708
|
+
if (e.type !== "inbox.created" && e.type !== "inbox.updated") continue;
|
|
58709
|
+
const { item } = e.payload;
|
|
58710
|
+
if (item.kind !== "approval") continue;
|
|
58711
|
+
if (item.status !== "open") {
|
|
58712
|
+
clearTimeout(this.timers.get(item.id));
|
|
58713
|
+
this.timers.delete(item.id);
|
|
58714
|
+
} else if (e.type === "inbox.created") setImmediate(() => this.track(item));
|
|
58715
|
+
}
|
|
58716
|
+
}
|
|
58717
|
+
track(item) {
|
|
58718
|
+
const { sessionId, requestId } = item;
|
|
58719
|
+
if (item.status !== "open" || !sessionId || !requestId || this.timers.has(item.id)) return;
|
|
58720
|
+
const session = this.deps.repo.find(sessionId)?.session;
|
|
58721
|
+
if (!session) return;
|
|
58722
|
+
if (unattendedSession(this.deps, sessionId)) {
|
|
58723
|
+
this.deny(item, UNATTENDED_APPROVAL);
|
|
58724
|
+
return;
|
|
58725
|
+
}
|
|
58726
|
+
if (!isUnwatched(session)) return;
|
|
58727
|
+
const waitMs = this.deps.waitMs ?? 18e5;
|
|
58728
|
+
const age = (this.deps.now?.() ?? Date.now()) - Date.parse(item.createdAt);
|
|
58729
|
+
const timer = setTimeout(() => {
|
|
58730
|
+
this.timers.delete(item.id);
|
|
58731
|
+
this.deny(item, noAnswerDenial(waitMs));
|
|
58732
|
+
}, Math.max(0, waitMs - (Number.isFinite(age) ? age : 0)));
|
|
58733
|
+
timer.unref?.();
|
|
58734
|
+
this.timers.set(item.id, timer);
|
|
58735
|
+
}
|
|
58736
|
+
async deny(item, message) {
|
|
58737
|
+
if (this.deps.inbox.find(item.id)?.status !== "open") return;
|
|
58738
|
+
if (!item.sessionId || !item.requestId) return;
|
|
58739
|
+
try {
|
|
58740
|
+
await this.deps.respond(item.sessionId, {
|
|
58741
|
+
requestId: item.requestId,
|
|
58742
|
+
response: {
|
|
58743
|
+
kind: "approval",
|
|
58744
|
+
decision: "deny",
|
|
58745
|
+
message,
|
|
58746
|
+
decidedBy: "policy"
|
|
58747
|
+
}
|
|
58748
|
+
});
|
|
58749
|
+
} catch (error) {
|
|
58750
|
+
this.deps.log?.(`approvals: deny ${item.id}: ${errorMessage$1(error)}`);
|
|
58751
|
+
}
|
|
58752
|
+
}
|
|
58753
|
+
};
|
|
56854
58754
|
// ../server/src/services/sessions/worktree-cleanup.ts
|
|
56855
58755
|
const DAY_MS = 864e5;
|
|
56856
58756
|
const REMOVE_TIMEOUT_MS = 12e4;
|
|
@@ -56986,6 +58886,7 @@ var WorktreeCleanup = class {
|
|
|
56986
58886
|
const message = errorMessage$1(error);
|
|
56987
58887
|
if (runnerErrorCode(error) === "worktree_dirty" || /modified or untracked/.test(message)) return outcome("kept_changes", "The worktree has uncommitted changes, so it was kept");
|
|
56988
58888
|
this.deps.log?.(`worktree cleanup: could not remove ${session.worktreePath}: ${message}`);
|
|
58889
|
+
this.reportFailure(session, message);
|
|
56989
58890
|
return outcome("failed", message);
|
|
56990
58891
|
}
|
|
56991
58892
|
this.deps.repo.update(id, (s) => ({
|
|
@@ -56994,6 +58895,7 @@ var WorktreeCleanup = class {
|
|
|
56994
58895
|
}));
|
|
56995
58896
|
this.kept.delete(id);
|
|
56996
58897
|
this.forced.delete(id);
|
|
58898
|
+
this.clearFailure(id);
|
|
56997
58899
|
this.deps.log?.(`worktree cleanup: removed ${session.worktreePath}`);
|
|
56998
58900
|
return outcome("removed", discarded ? "Its uncommitted changes were discarded" : null);
|
|
56999
58901
|
}
|
|
@@ -57045,10 +58947,53 @@ var WorktreeCleanup = class {
|
|
|
57045
58947
|
if (typeof projectId === "string") this.cleanProject(projectId).catch((error) => this.deps.log?.(`worktree cleanup for project ${projectId}: ${errorMessage$1(error)}`));
|
|
57046
58948
|
}
|
|
57047
58949
|
}
|
|
58950
|
+
/**
|
|
58951
|
+
* Put a failed removal in the inbox (`cleanup_failed`), so it can't go unnoticed while the
|
|
58952
|
+
* sweep keeps retrying. One item per session: updated when the reason changes, reopened
|
|
58953
|
+
* after an earlier one was resolved, and left alone once the user dismissed it.
|
|
58954
|
+
*/
|
|
58955
|
+
reportFailure(session, message) {
|
|
58956
|
+
const inbox = this.deps.inbox;
|
|
58957
|
+
if (!inbox) return;
|
|
58958
|
+
const id = cleanupInboxId(session.id);
|
|
58959
|
+
const existing = inbox.find(id);
|
|
58960
|
+
const body = `${message}\n\nCodeDen tries again every hour; archive the session again to retry now.`;
|
|
58961
|
+
if (existing?.status === "dismissed" || existing?.status === "open" && existing.body === body) return;
|
|
58962
|
+
const now = this.now().toISOString();
|
|
58963
|
+
inbox.put({
|
|
58964
|
+
id,
|
|
58965
|
+
kind: "cleanup_failed",
|
|
58966
|
+
status: "open",
|
|
58967
|
+
priority: "normal",
|
|
58968
|
+
title: `Couldn't remove the worktree: ${session.title ?? session.purpose}`,
|
|
58969
|
+
body,
|
|
58970
|
+
projectId: session.projectId,
|
|
58971
|
+
sessionId: session.id,
|
|
58972
|
+
threadId: session.rootThreadId,
|
|
58973
|
+
itemId: null,
|
|
58974
|
+
requestId: null,
|
|
58975
|
+
issue: session.issue,
|
|
58976
|
+
pr: session.pr,
|
|
58977
|
+
automationRunId: session.automationRunId,
|
|
58978
|
+
createdAt: existing?.status === "open" ? existing.createdAt : now,
|
|
58979
|
+
updatedAt: now,
|
|
58980
|
+
resolvedAt: null,
|
|
58981
|
+
resolution: null
|
|
58982
|
+
});
|
|
58983
|
+
}
|
|
58984
|
+
/** The worktree is gone: resolve its open `cleanup_failed` item. */
|
|
58985
|
+
clearFailure(sessionId) {
|
|
58986
|
+
const item = this.deps.inbox?.find(cleanupInboxId(sessionId));
|
|
58987
|
+
if (item?.status === "open") this.deps.inbox?.close(item, "resolved", { removed: true });
|
|
58988
|
+
}
|
|
57048
58989
|
now() {
|
|
57049
58990
|
return this.deps.now?.() ?? /* @__PURE__ */ new Date();
|
|
57050
58991
|
}
|
|
57051
58992
|
};
|
|
58993
|
+
/** The inbox item reporting that a session's worktree could not be removed. */
|
|
58994
|
+
function cleanupInboxId(sessionId) {
|
|
58995
|
+
return `inbox:${sessionId}:worktree-cleanup`;
|
|
58996
|
+
}
|
|
57052
58997
|
// ../server/src/services/setup/claude-sdk.ts
|
|
57053
58998
|
/**
|
|
57054
58999
|
* Claude Code support (§14): reports and installs the Claude Agent SDK into `<home>/deps`.
|
|
@@ -57256,8 +59201,30 @@ function expiresWithin(iso, days, now = Date.now()) {
|
|
|
57256
59201
|
if (!iso) return false;
|
|
57257
59202
|
return Date.parse(iso) - now < days * 24 * 60 * 60 * 1e3;
|
|
57258
59203
|
}
|
|
57259
|
-
// ../server/src/services/setup/
|
|
57260
|
-
/**
|
|
59204
|
+
// ../server/src/services/setup/tool-lookup.ts
|
|
59205
|
+
/**
|
|
59206
|
+
* A look-up through an `Exec` only (`<name> --version`): used when a server is given its own
|
|
59207
|
+
* `exec` (tests with fake CLIs) and no `locate`, so no real tool is ever looked for or run.
|
|
59208
|
+
*/
|
|
59209
|
+
function execLocate(exec) {
|
|
59210
|
+
return async (name) => {
|
|
59211
|
+
const run = await exec(name, ["--version"], { timeoutMs: 1e4 });
|
|
59212
|
+
if (run.notFound || run.code !== 0) return {
|
|
59213
|
+
version: null,
|
|
59214
|
+
broken: []
|
|
59215
|
+
};
|
|
59216
|
+
const version = /\d+\.\d+\.\d+(?:-[\w.]+)?/.exec(run.stdout)?.[0] ?? null;
|
|
59217
|
+
return {
|
|
59218
|
+
command: {
|
|
59219
|
+
kind: "native",
|
|
59220
|
+
path: run.path ?? name
|
|
59221
|
+
},
|
|
59222
|
+
version,
|
|
59223
|
+
broken: []
|
|
59224
|
+
};
|
|
59225
|
+
};
|
|
59226
|
+
}
|
|
59227
|
+
/** Tools whose sign-in runs in the setup terminal, with the arguments that sign them in. */
|
|
57261
59228
|
const LOGIN_COMMANDS = {
|
|
57262
59229
|
claude: [
|
|
57263
59230
|
"claude",
|
|
@@ -57274,6 +59241,68 @@ const LOGIN_COMMANDS = {
|
|
|
57274
59241
|
"https"
|
|
57275
59242
|
]
|
|
57276
59243
|
};
|
|
59244
|
+
/**
|
|
59245
|
+
* The sign-in command typed into the setup terminal (§14 step 3). With a resolved `program`
|
|
59246
|
+
* it runs that exact file, quoted for `shell`, so a broken shim or an outdated PATH in the
|
|
59247
|
+
* terminal cannot get in the way: `& 'C:\Users\x\.local\bin\claude.exe' auth login`.
|
|
59248
|
+
*/
|
|
59249
|
+
function loginCommandLine(tool, shell, program) {
|
|
59250
|
+
const [name, ...args] = LOGIN_COMMANDS[tool];
|
|
59251
|
+
return shellCommandLine(shell, program || name, args);
|
|
59252
|
+
}
|
|
59253
|
+
function loginFix(tool, shell, program) {
|
|
59254
|
+
return {
|
|
59255
|
+
kind: "login",
|
|
59256
|
+
command: loginCommandLine(tool, shell, program)
|
|
59257
|
+
};
|
|
59258
|
+
}
|
|
59259
|
+
/**
|
|
59260
|
+
* Look a tool up and turn "nothing found" and "found, but nothing runs" into honest checks:
|
|
59261
|
+
* not installed (with the install fix) or installed but broken (with the paths).
|
|
59262
|
+
*/
|
|
59263
|
+
async function lookupTool(locate, name, check, notInstalled) {
|
|
59264
|
+
const lookup = await locate(name);
|
|
59265
|
+
const extra = lookup.broken.length > 0 ? { broken: lookup.broken } : {};
|
|
59266
|
+
if (lookup.command) return {
|
|
59267
|
+
found: true,
|
|
59268
|
+
path: lookup.command.path,
|
|
59269
|
+
version: lookup.version,
|
|
59270
|
+
extra
|
|
59271
|
+
};
|
|
59272
|
+
if (lookup.broken.length === 0) return {
|
|
59273
|
+
found: false,
|
|
59274
|
+
result: {
|
|
59275
|
+
...check,
|
|
59276
|
+
state: "missing",
|
|
59277
|
+
...notInstalled
|
|
59278
|
+
}
|
|
59279
|
+
};
|
|
59280
|
+
return {
|
|
59281
|
+
found: false,
|
|
59282
|
+
result: {
|
|
59283
|
+
...check,
|
|
59284
|
+
...extra,
|
|
59285
|
+
state: "missing",
|
|
59286
|
+
message: brokenInstallMessage(check.label, lookup.broken),
|
|
59287
|
+
fix: notInstalled.fix
|
|
59288
|
+
}
|
|
59289
|
+
};
|
|
59290
|
+
}
|
|
59291
|
+
/** What went wrong running a command, for a check's details (never includes tokens). */
|
|
59292
|
+
function runDetail(command, run) {
|
|
59293
|
+
if (run.timedOut) return `\`${command}\` did not finish in time.`;
|
|
59294
|
+
const parts = [`\`${command}\` exited with code ${run.code ?? "none"}.`];
|
|
59295
|
+
const stderr = clip$1(run.stderr);
|
|
59296
|
+
const stdout = clip$1(run.stdout);
|
|
59297
|
+
if (stderr) parts.push(`stderr: ${stderr}`);
|
|
59298
|
+
if (stdout) parts.push(`stdout: ${stdout}`);
|
|
59299
|
+
if (!stderr && !stdout) parts.push("It printed nothing.");
|
|
59300
|
+
return parts.join("\n");
|
|
59301
|
+
}
|
|
59302
|
+
function clip$1(text) {
|
|
59303
|
+
const t = text.trim();
|
|
59304
|
+
return t.length > 300 ? `${t.slice(0, 297)}...` : t;
|
|
59305
|
+
}
|
|
57277
59306
|
const LABELS = {
|
|
57278
59307
|
claude: "Claude Code",
|
|
57279
59308
|
codex: "Codex",
|
|
@@ -57314,10 +59343,6 @@ function installFix(id, platform) {
|
|
|
57314
59343
|
};
|
|
57315
59344
|
}
|
|
57316
59345
|
}
|
|
57317
|
-
const loginFix = (tool) => ({
|
|
57318
|
-
kind: "login",
|
|
57319
|
-
command: LOGIN_COMMANDS[tool].join(" ")
|
|
57320
|
-
});
|
|
57321
59346
|
function base(id) {
|
|
57322
59347
|
return {
|
|
57323
59348
|
id,
|
|
@@ -57332,27 +59357,28 @@ function base(id) {
|
|
|
57332
59357
|
fix: null
|
|
57333
59358
|
};
|
|
57334
59359
|
}
|
|
57335
|
-
async function checkClaude({ exec, platform, now }) {
|
|
57336
|
-
const
|
|
57337
|
-
const version = await exec("claude", ["--version"], { timeoutMs: 1e4 });
|
|
57338
|
-
if (version.notFound || version.code !== 0) return {
|
|
57339
|
-
...check,
|
|
57340
|
-
state: "missing",
|
|
59360
|
+
async function checkClaude({ exec, locate, shell, platform, now }) {
|
|
59361
|
+
const tool = await lookupTool(locate, "claude", base("claude"), {
|
|
57341
59362
|
message: "Claude Code is not installed",
|
|
57342
59363
|
fix: installFix("claude", platform)
|
|
57343
|
-
};
|
|
59364
|
+
});
|
|
59365
|
+
if (!tool.found) return tool.result;
|
|
57344
59366
|
const installed = {
|
|
57345
|
-
...
|
|
59367
|
+
...base("claude"),
|
|
59368
|
+
...tool.extra,
|
|
57346
59369
|
installed: true,
|
|
57347
|
-
version:
|
|
57348
|
-
executablePath:
|
|
59370
|
+
version: tool.version,
|
|
59371
|
+
executablePath: tool.path
|
|
57349
59372
|
};
|
|
57350
|
-
const
|
|
59373
|
+
const login = loginFix("claude", shell, tool.path);
|
|
59374
|
+
const status = await exec(tool.path, ["auth", "status"], { timeoutMs: 1e4 });
|
|
59375
|
+
const auth = parseClaudeAuth(status.stdout);
|
|
57351
59376
|
if (!auth) return {
|
|
57352
59377
|
...installed,
|
|
57353
59378
|
state: "warning",
|
|
57354
59379
|
message: "Could not read the Claude login state",
|
|
57355
|
-
|
|
59380
|
+
detail: runDetail("claude auth status", status),
|
|
59381
|
+
fix: login
|
|
57356
59382
|
};
|
|
57357
59383
|
const withAuth = {
|
|
57358
59384
|
...installed,
|
|
@@ -57362,13 +59388,13 @@ async function checkClaude({ exec, platform, now }) {
|
|
|
57362
59388
|
...withAuth,
|
|
57363
59389
|
state: "missing",
|
|
57364
59390
|
message: "Not signed in to Claude",
|
|
57365
|
-
fix:
|
|
59391
|
+
fix: login
|
|
57366
59392
|
};
|
|
57367
59393
|
if (expiresWithin(auth.loginExpiresAt, 3, now)) return {
|
|
57368
59394
|
...withAuth,
|
|
57369
59395
|
state: "warning",
|
|
57370
59396
|
message: "Your Claude login expires soon; sign in again",
|
|
57371
|
-
fix:
|
|
59397
|
+
fix: login
|
|
57372
59398
|
};
|
|
57373
59399
|
return {
|
|
57374
59400
|
...withAuth,
|
|
@@ -57376,29 +59402,29 @@ async function checkClaude({ exec, platform, now }) {
|
|
|
57376
59402
|
message: "Signed in"
|
|
57377
59403
|
};
|
|
57378
59404
|
}
|
|
57379
|
-
async function checkCodex({ exec, platform }) {
|
|
57380
|
-
const
|
|
57381
|
-
const version = await exec("codex", ["--version"], { timeoutMs: 1e4 });
|
|
57382
|
-
if (version.notFound || version.code !== 0) return {
|
|
57383
|
-
...check,
|
|
57384
|
-
state: "missing",
|
|
59405
|
+
async function checkCodex({ exec, locate, shell, platform }) {
|
|
59406
|
+
const tool = await lookupTool(locate, "codex", base("codex"), {
|
|
57385
59407
|
message: "Codex is not installed",
|
|
57386
59408
|
fix: installFix("codex", platform)
|
|
57387
|
-
};
|
|
59409
|
+
});
|
|
59410
|
+
if (!tool.found) return tool.result;
|
|
57388
59411
|
const installed = {
|
|
57389
|
-
...
|
|
59412
|
+
...base("codex"),
|
|
59413
|
+
...tool.extra,
|
|
57390
59414
|
installed: true,
|
|
57391
|
-
version:
|
|
57392
|
-
executablePath:
|
|
59415
|
+
version: tool.version,
|
|
59416
|
+
executablePath: tool.path
|
|
57393
59417
|
};
|
|
57394
|
-
const status = await exec(
|
|
57395
|
-
const login = parseCodexLogin(status.code, `${status.stdout}
|
|
59418
|
+
const status = await exec(tool.path, ["login", "status"], { timeoutMs: 1e4 });
|
|
59419
|
+
const login = parseCodexLogin(status.code, `${status.stdout}
|
|
59420
|
+
${status.stderr}`);
|
|
57396
59421
|
if (!login.loggedIn) return {
|
|
57397
59422
|
...installed,
|
|
57398
59423
|
loggedIn: false,
|
|
57399
59424
|
state: "missing",
|
|
57400
59425
|
message: "Not signed in to Codex",
|
|
57401
|
-
|
|
59426
|
+
...status.code !== 0 && !/not logged in/i.test(`${status.stdout}${status.stderr}`) ? { detail: runDetail("codex login status", status) } : {},
|
|
59427
|
+
fix: loginFix("codex", shell, tool.path)
|
|
57402
59428
|
};
|
|
57403
59429
|
return {
|
|
57404
59430
|
...installed,
|
|
@@ -57408,37 +59434,36 @@ async function checkCodex({ exec, platform }) {
|
|
|
57408
59434
|
message: "Signed in"
|
|
57409
59435
|
};
|
|
57410
59436
|
}
|
|
57411
|
-
async function checkGh({ exec, platform }) {
|
|
57412
|
-
const
|
|
57413
|
-
const version = await exec("gh", ["--version"], { timeoutMs: 1e4 });
|
|
57414
|
-
if (version.notFound || version.code !== 0) return {
|
|
57415
|
-
...check,
|
|
57416
|
-
state: "missing",
|
|
59437
|
+
async function checkGh({ exec, locate, shell, platform }) {
|
|
59438
|
+
const tool = await lookupTool(locate, "gh", base("gh"), {
|
|
57417
59439
|
message: "GitHub CLI is not installed (optional)",
|
|
57418
59440
|
fix: installFix("gh", platform)
|
|
57419
|
-
};
|
|
59441
|
+
});
|
|
59442
|
+
if (!tool.found) return tool.result;
|
|
57420
59443
|
const installed = {
|
|
57421
|
-
...
|
|
59444
|
+
...base("gh"),
|
|
59445
|
+
...tool.extra,
|
|
57422
59446
|
installed: true,
|
|
57423
|
-
version:
|
|
57424
|
-
executablePath:
|
|
59447
|
+
version: tool.version,
|
|
59448
|
+
executablePath: tool.path
|
|
57425
59449
|
};
|
|
57426
|
-
let auth = parseGhAuthJson((await exec(
|
|
59450
|
+
let auth = parseGhAuthJson((await exec(tool.path, [
|
|
57427
59451
|
"auth",
|
|
57428
59452
|
"status",
|
|
57429
59453
|
"--json",
|
|
57430
59454
|
"hosts"
|
|
57431
59455
|
], { timeoutMs: 1e4 })).stdout);
|
|
57432
59456
|
if (!auth) {
|
|
57433
|
-
const text = await exec(
|
|
57434
|
-
auth = parseGhAuthText(text.code, `${text.stdout}
|
|
59457
|
+
const text = await exec(tool.path, ["auth", "status"], { timeoutMs: 1e4 });
|
|
59458
|
+
auth = parseGhAuthText(text.code, `${text.stdout}
|
|
59459
|
+
${text.stderr}`);
|
|
57435
59460
|
}
|
|
57436
59461
|
if (!auth.loggedIn) return {
|
|
57437
59462
|
...installed,
|
|
57438
59463
|
loggedIn: false,
|
|
57439
59464
|
state: "missing",
|
|
57440
59465
|
message: "Not signed in to GitHub",
|
|
57441
|
-
fix: loginFix("gh")
|
|
59466
|
+
fix: loginFix("gh", shell, tool.path)
|
|
57442
59467
|
};
|
|
57443
59468
|
return {
|
|
57444
59469
|
...installed,
|
|
@@ -57576,7 +59601,7 @@ var SetupService = class {
|
|
|
57576
59601
|
...this.cached,
|
|
57577
59602
|
setupCompleted: this.deps.settings.get().setup.completed
|
|
57578
59603
|
};
|
|
57579
|
-
this.running ??= this.
|
|
59604
|
+
this.running ??= this.refreshThenCheck(opts.refresh === true).finally(() => {
|
|
57580
59605
|
this.running = void 0;
|
|
57581
59606
|
});
|
|
57582
59607
|
return this.running;
|
|
@@ -57589,18 +59614,35 @@ var SetupService = class {
|
|
|
57589
59614
|
} });
|
|
57590
59615
|
return true;
|
|
57591
59616
|
}
|
|
57592
|
-
/**
|
|
59617
|
+
/**
|
|
59618
|
+
* The command to run in a terminal to sign `tool` in: the copy the last check found, by
|
|
59619
|
+
* its full path, so the terminal's own PATH lookup is not involved.
|
|
59620
|
+
*/
|
|
57593
59621
|
loginCommand(tool) {
|
|
57594
|
-
const
|
|
59622
|
+
const check = this.cached?.checks.find((c) => c.id === tool);
|
|
59623
|
+
const program = check?.installed ? check.executablePath : null;
|
|
59624
|
+
const args = LOGIN_COMMANDS[tool].slice(1);
|
|
57595
59625
|
return {
|
|
57596
59626
|
tool,
|
|
57597
|
-
command:
|
|
59627
|
+
command: loginCommandLine(tool, this.shell(), program),
|
|
57598
59628
|
args
|
|
57599
59629
|
};
|
|
57600
59630
|
}
|
|
59631
|
+
async refreshThenCheck(refresh) {
|
|
59632
|
+
if (refresh) try {
|
|
59633
|
+
await (this.deps.refreshPath ?? refreshProcessPath)();
|
|
59634
|
+
} catch {}
|
|
59635
|
+
return this.runChecks();
|
|
59636
|
+
}
|
|
59637
|
+
shell() {
|
|
59638
|
+
return this.deps.shell ?? defaultShell();
|
|
59639
|
+
}
|
|
57601
59640
|
async runChecks() {
|
|
59641
|
+
const userHome = this.deps.userHome;
|
|
57602
59642
|
const env = {
|
|
57603
59643
|
exec: this.deps.exec,
|
|
59644
|
+
locate: this.deps.locate ?? ((name) => findExecutable(name, { home: userHome })),
|
|
59645
|
+
shell: this.shell(),
|
|
57604
59646
|
platform: this.deps.platform ?? process.platform,
|
|
57605
59647
|
nodeVersion: this.deps.nodeVersion ?? process.version,
|
|
57606
59648
|
now: this.now(),
|
|
@@ -58300,7 +60342,7 @@ function billingFromSetup(status) {
|
|
|
58300
60342
|
// ../server/src/services/usage/prices.ts
|
|
58301
60343
|
const ANTHROPIC = "https://platform.claude.com/docs/en/about-claude/pricing";
|
|
58302
60344
|
const OPENAI = "https://developers.openai.com/api/docs/pricing";
|
|
58303
|
-
const AS_OF = "2026-11-
|
|
60345
|
+
const AS_OF = "2026-11-10";
|
|
58304
60346
|
function table(provider, sourceUrl, rows) {
|
|
58305
60347
|
return rows.map(([model, label, input, output, read, write]) => ({
|
|
58306
60348
|
provider,
|
|
@@ -58321,6 +60363,14 @@ function table(provider, sourceUrl, rows) {
|
|
|
58321
60363
|
* override or add models in the usage settings.
|
|
58322
60364
|
*/
|
|
58323
60365
|
const BUNDLED_PRICES = [...table("claude", ANTHROPIC, [
|
|
60366
|
+
[
|
|
60367
|
+
"claude-fable-5-2",
|
|
60368
|
+
"Fable 5.2",
|
|
60369
|
+
8,
|
|
60370
|
+
40,
|
|
60371
|
+
.2,
|
|
60372
|
+
10
|
|
60373
|
+
],
|
|
58324
60374
|
[
|
|
58325
60375
|
"claude-fable-5-1",
|
|
58326
60376
|
"Fable 5.1",
|
|
@@ -58449,6 +60499,14 @@ const BUNDLED_PRICES = [...table("claude", ANTHROPIC, [
|
|
|
58449
60499
|
.1,
|
|
58450
60500
|
1.25
|
|
58451
60501
|
],
|
|
60502
|
+
[
|
|
60503
|
+
"fable",
|
|
60504
|
+
"Fable (latest)",
|
|
60505
|
+
8,
|
|
60506
|
+
40,
|
|
60507
|
+
.2,
|
|
60508
|
+
10
|
|
60509
|
+
],
|
|
58452
60510
|
[
|
|
58453
60511
|
"opus",
|
|
58454
60512
|
"Opus (latest)",
|
|
@@ -60159,14 +62217,15 @@ function createServices(opts) {
|
|
|
60159
62217
|
db: opts.db,
|
|
60160
62218
|
store: opts.store,
|
|
60161
62219
|
waitMs: opts.askUserWaitMs,
|
|
60162
|
-
sendMessage: () => async (sessionId, text) => {
|
|
62220
|
+
sendMessage: () => async (sessionId, text, by) => {
|
|
62221
|
+
const input = by === "pm" ? messageFrom("pm", text) : {
|
|
62222
|
+
text,
|
|
62223
|
+
attachments: []
|
|
62224
|
+
};
|
|
60163
62225
|
await sessions?.send(sessionId, {
|
|
60164
|
-
input
|
|
60165
|
-
text,
|
|
60166
|
-
attachments: []
|
|
60167
|
-
},
|
|
62226
|
+
input,
|
|
60168
62227
|
whenBusy: "queue"
|
|
60169
|
-
});
|
|
62228
|
+
}, by);
|
|
60170
62229
|
}
|
|
60171
62230
|
});
|
|
60172
62231
|
let sessions;
|
|
@@ -60238,6 +62297,9 @@ function createServices(opts) {
|
|
|
60238
62297
|
const setup = new SetupService({
|
|
60239
62298
|
exec,
|
|
60240
62299
|
settings: opts.settings,
|
|
62300
|
+
locate: opts.locate ?? (opts.exec ? execLocate(opts.exec) : void 0),
|
|
62301
|
+
refreshPath: opts.refreshPath,
|
|
62302
|
+
userHome,
|
|
60241
62303
|
onChecked: () => redetectProviders(opts.runner, opts.log)
|
|
60242
62304
|
});
|
|
60243
62305
|
const claudeSdk = new ClaudeSdkService({
|
|
@@ -60306,9 +62368,19 @@ function createServices(opts) {
|
|
|
60306
62368
|
store: opts.store,
|
|
60307
62369
|
settings: () => opts.settings.get(),
|
|
60308
62370
|
stopSession: (id) => sessions.stop(id),
|
|
62371
|
+
inbox,
|
|
60309
62372
|
...opts.worktreeCleanup,
|
|
60310
62373
|
log: opts.log
|
|
60311
62374
|
});
|
|
62375
|
+
const unwatchedApprovals = new UnwatchedApprovals({
|
|
62376
|
+
store: opts.store,
|
|
62377
|
+
repo: sessionRepo,
|
|
62378
|
+
projects,
|
|
62379
|
+
inbox,
|
|
62380
|
+
respond: (id, req) => sessions.respond(id, req),
|
|
62381
|
+
...opts.unwatchedApprovals,
|
|
62382
|
+
log: opts.log
|
|
62383
|
+
});
|
|
60312
62384
|
const services = {
|
|
60313
62385
|
db: opts.db,
|
|
60314
62386
|
store: opts.store,
|
|
@@ -60334,7 +62406,9 @@ function createServices(opts) {
|
|
|
60334
62406
|
artifacts,
|
|
60335
62407
|
browser: new BrowserService({
|
|
60336
62408
|
runner: opts.runner,
|
|
60337
|
-
repo: sessionRepo
|
|
62409
|
+
repo: sessionRepo,
|
|
62410
|
+
settings: () => opts.settings.get(),
|
|
62411
|
+
log: opts.log
|
|
60338
62412
|
}),
|
|
60339
62413
|
updates: createUpdates(opts),
|
|
60340
62414
|
images: new ImageService({
|
|
@@ -60358,6 +62432,30 @@ function createServices(opts) {
|
|
|
60358
62432
|
usage,
|
|
60359
62433
|
factory,
|
|
60360
62434
|
worktreeCleanup,
|
|
62435
|
+
unwatchedApprovals,
|
|
62436
|
+
prFeedback: new PrFeedbackService({
|
|
62437
|
+
store: opts.store,
|
|
62438
|
+
repo: sessionRepo,
|
|
62439
|
+
settings: () => opts.settings.get(),
|
|
62440
|
+
send: (id, input) => sessions.send(id, {
|
|
62441
|
+
input,
|
|
62442
|
+
whenBusy: "queue"
|
|
62443
|
+
}, "codeden"),
|
|
62444
|
+
log: opts.log
|
|
62445
|
+
}),
|
|
62446
|
+
autoRetry: new AutoRetry({
|
|
62447
|
+
db: opts.db,
|
|
62448
|
+
store: opts.store,
|
|
62449
|
+
repo: sessionRepo,
|
|
62450
|
+
inbox,
|
|
62451
|
+
settings: () => opts.settings.get(),
|
|
62452
|
+
send: (id, input) => sessions.send(id, {
|
|
62453
|
+
input,
|
|
62454
|
+
whenBusy: "queue"
|
|
62455
|
+
}, "codeden"),
|
|
62456
|
+
...opts.autoRetry,
|
|
62457
|
+
log: opts.log
|
|
62458
|
+
}),
|
|
60361
62459
|
configFiles: new ConfigFiles({
|
|
60362
62460
|
projects,
|
|
60363
62461
|
home: userHome,
|
|
@@ -60555,6 +62653,8 @@ async function startServer(opts) {
|
|
|
60555
62653
|
runner: runnerPort(() => runner),
|
|
60556
62654
|
secrets,
|
|
60557
62655
|
exec: opts.exec,
|
|
62656
|
+
locate: opts.locate,
|
|
62657
|
+
refreshPath: opts.refreshPath,
|
|
60558
62658
|
utility: opts.utility,
|
|
60559
62659
|
userHome: opts.userHome,
|
|
60560
62660
|
projectsDir: opts.projectsDir,
|
|
@@ -60569,6 +62669,7 @@ async function startServer(opts) {
|
|
|
60569
62669
|
selfUpdate: opts.selfUpdate,
|
|
60570
62670
|
updates: opts.updates,
|
|
60571
62671
|
worktreeCleanup: opts.worktreeCleanup,
|
|
62672
|
+
autoRetry: opts.autoRetry,
|
|
60572
62673
|
log
|
|
60573
62674
|
});
|
|
60574
62675
|
services.images.refresh();
|
|
@@ -60580,6 +62681,9 @@ async function startServer(opts) {
|
|
|
60580
62681
|
services.automations.start();
|
|
60581
62682
|
services.factory.start();
|
|
60582
62683
|
services.worktreeCleanup.start();
|
|
62684
|
+
services.unwatchedApprovals.start();
|
|
62685
|
+
services.prFeedback.start();
|
|
62686
|
+
services.autoRetry.start();
|
|
60583
62687
|
const stopUpdateEvents = services.updates.onChange((status) => hub.broadcastUpdate(status));
|
|
60584
62688
|
services.updates.start();
|
|
60585
62689
|
let baseOrigins = /* @__PURE__ */ new Set();
|
|
@@ -60608,7 +62712,7 @@ async function startServer(opts) {
|
|
|
60608
62712
|
cookieName: tokenCookie,
|
|
60609
62713
|
allowedOrigins,
|
|
60610
62714
|
publicPaths: /* @__PURE__ */ new Set(["/api/health", "/mcp"]),
|
|
60611
|
-
loopbackOnlyPaths: /* @__PURE__ */ new Set(["/mcp"]),
|
|
62715
|
+
loopbackOnlyPaths: /* @__PURE__ */ new Set(["/mcp", ...BROWSER_LOOPBACK_ONLY_PATHS]),
|
|
60612
62716
|
remote: {
|
|
60613
62717
|
enabled: () => network.enabled,
|
|
60614
62718
|
ownHosts: () => network.ownHosts(),
|
|
@@ -60659,22 +62763,7 @@ async function startServer(opts) {
|
|
|
60659
62763
|
} }, 400);
|
|
60660
62764
|
return upgradeWebSocket(() => terminals.handlers(query.data))(c, next);
|
|
60661
62765
|
});
|
|
60662
|
-
const browserBridge =
|
|
60663
|
-
browser: services.browser,
|
|
60664
|
-
log
|
|
60665
|
-
});
|
|
60666
|
-
app.get("/ws/browser", async (c, next) => {
|
|
60667
|
-
const query = BrowserWsQuery.safeParse(c.req.query());
|
|
60668
|
-
if (!query.success) return c.json({ error: {
|
|
60669
|
-
code: "bad_request",
|
|
60670
|
-
message: "Invalid browser query"
|
|
60671
|
-
} }, 400);
|
|
60672
|
-
if (!services.sessionRepo.find(query.data.sessionId)) return c.json({ error: {
|
|
60673
|
-
code: "session_not_found",
|
|
60674
|
-
message: "No such session"
|
|
60675
|
-
} }, 404);
|
|
60676
|
-
return upgradeWebSocket(() => browserBridge.handlers(query.data))(c, next);
|
|
60677
|
-
});
|
|
62766
|
+
const browserBridge = mountBrowserSockets(app, services, log);
|
|
60678
62767
|
mountWeb(app, findWebDir(opts.webDir));
|
|
60679
62768
|
if (network.store.enabled && !network.store.passwordSet) log("network access is on but no password is set: listening on 127.0.0.1 only");
|
|
60680
62769
|
port = await listener.listen(opts.port, network.desiredHost());
|
|
@@ -60723,6 +62812,9 @@ async function startServer(opts) {
|
|
|
60723
62812
|
services.automations.stop();
|
|
60724
62813
|
services.factory.stop();
|
|
60725
62814
|
services.worktreeCleanup.stop();
|
|
62815
|
+
services.unwatchedApprovals.stop();
|
|
62816
|
+
services.prFeedback.stop();
|
|
62817
|
+
services.autoRetry.stop();
|
|
60726
62818
|
services.updates.stop();
|
|
60727
62819
|
await services.localStt.stop();
|
|
60728
62820
|
stopUpdateEvents();
|
|
@@ -61612,4 +63704,4 @@ main().then((code) => process.exit(code), (error) => {
|
|
|
61612
63704
|
});
|
|
61613
63705
|
export {};
|
|
61614
63706
|
|
|
61615
|
-
//# sourceMappingURL=main-
|
|
63707
|
+
//# sourceMappingURL=main-BQwSt6Az.js.map
|