glimmervoid 0.41.1 → 0.42.1
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/dist/bin/glimmervoid.js +13 -13
- package/dist/chunks/{adapters-CyDflXHy.js → adapters-CWIHOfVo.js} +5 -5
- package/dist/chunks/{adapters-D8BJffFW.js → adapters-DzClvFt5.js} +1 -1
- package/dist/chunks/{agent-api-cli-B7-ToJul.js → agent-api-cli-2FKWf5jj.js} +10 -5
- package/dist/chunks/{agent-api-cli-CvCov9_e.js → agent-api-cli-Cn-g_ej_.js} +1 -1
- package/dist/chunks/{agent-api-core-CLxZAole.js → agent-api-core-CP4J2B39.js} +19 -5
- package/dist/chunks/{agent-setup-cli-py1gUMvJ.js → agent-setup-cli-C9qDV2g5.js} +1 -1
- package/dist/chunks/{config-store-CxYqTNbe.js → config-store-Br2PY4Tg.js} +2 -2
- package/dist/chunks/config-store-DwYjz6h1.js +1 -0
- package/dist/chunks/{contracts-C9Jj9iDo.js → contracts-DJqeCtR3.js} +2 -2
- package/dist/chunks/{control-messages-C6JGCTuy.js → control-messages-DHDEMTLI.js} +20 -2
- package/dist/chunks/{git-version-D_iXgYTZ.js → git-version-DqagvnAZ.js} +1 -1
- package/dist/chunks/{grok-BUGgQVE3.js → grok-DwrAcNHC.js} +1 -1
- package/dist/chunks/{main-CRU5ahjl.js → main-ZpBLKI1h.js} +587 -126
- package/dist/chunks/{pair-cli-CL1D9xc7.js → pair-cli-DW5mUBZa.js} +2 -2
- package/dist/chunks/{remote-config-CaW0mpwr.js → remote-config-DcmhRuRc.js} +1 -1
- package/dist/chunks/{session-C8IpFmGQ.js → session-3eSP3t1i.js} +8 -2
- package/dist/chunks/{session-routes-nySS7DUE.js → session-routes-B2oSzmQn.js} +1 -1
- package/dist/chunks/{settings-cli-Duculey3.js → settings-cli-BnM9rg8B.js} +5 -5
- package/dist/chunks/{settings-readout-core-CX7DmEuG.js → settings-readout-core-2XunjdBA.js} +2 -2
- package/dist/chunks/{ssh-sign-cli-BEAAFjXg.js → ssh-sign-cli-CmnA5lFM.js} +2 -2
- package/dist/client/assets/index-CBGmkbVV.js +176 -0
- package/dist/client/assets/index-NhlaT6eP.css +2 -0
- package/dist/client/index.html +2 -2
- package/dist/server/index.js +1 -1
- package/package.json +1 -1
- package/dist/chunks/config-store-DcVWBJOV.js +0 -1
- package/dist/client/assets/index-CBjFGcSs.js +0 -176
- package/dist/client/assets/index-Cbb5NKHK.css +0 -2
|
@@ -2,13 +2,13 @@ import { i as sanitizeOneLine, n as requireExecutableSpawnHelper, r as firstLine
|
|
|
2
2
|
import { a as isMissingFileError, i as errorText$1, r as errorMessage, t as errorCode } from "./text-DoirglWV.js";
|
|
3
3
|
import { a as sanitizeWorktreeName, i as safePathSegment, n as equalsIgnoringCaseOnWindows, r as isPathInside, t as caseFoldedPathKey } from "./paths-Bu4eH1Yd.js";
|
|
4
4
|
import { a as packageRoot, n as cliPath, o as relayPath, r as clientDir, s as resolvePackageBin, t as bundled } from "./runtime-paths-BkeftE60.js";
|
|
5
|
-
import { A as SIGN_URL_ENV, At as
|
|
5
|
+
import { $t as STATES, A as SIGN_URL_ENV, At as TeamReviewStatus, B as LinkedPr, Bt as shortSha, Ct as ReviewThreadId, Dt as TeamReviewCompareFiles, E as SESSION_DOC_BODY_CAP_BYTES, Ft as decisionAsIfApprovalRequired, G as MyPrKeepMergeableRequest, H as MyPrAutoRebase, Ht as NODE_ID_RE, I as SshSignRequest, J as MyPrMergeMethod, L as TASK_TITLE_CONTROL_CHARACTERS, Lt as hasStandingViewerApproval, Mt as TeamReviewThreadResult, Nt as TeamReviewThreadsRepository, O as SESSION_NAME_PATTERN, Ot as TeamReviewResolveResponse, Pt as canApproveAfterComment, Q as MyPrMergeWhenReadyRequest, Qt as RESTARTABLE_STATES, R as TaskTitle, S as ConversationQuestion, St as ReviewResult, Tt as TeamReviewActionRequest, U as MyPrCheckContext, Ut as REPO_SLUG_RE, V as MyPr, Vt as GH_SEGMENT, W as MyPrCheckContextsResponse, Wt as repoParts, Xt as KILLABLE_STATES, Y as MyPrMergeRequest, Yt as BADGE_LABELS, Z as MyPrMergeStateResponse, Zt as MERGEABLE_LIVE_STATES, _ as CONVERSATION_TOOL_RESULT_HOOK_EVENT, _t as QueuedReview, bt as ReviewDraft, dt as EarlierReview, ft as FindingSeverity, g as CONVERSATION_PERMISSION_HOOK_EVENT, gt as PrDetail, h as CONVERSATION_APPROVAL_SUMMARY_MAX_CHARS, ht as PostingPlan, it as MyPrsState, j as SSH_SIGN_BODY_CAP_BYTES, jt as TeamReviewThreadCommentsResponse, k as SESSION_NAME_RULE_TEXT, kt as TeamReviewStateEntry, lt as CommitSha, m as COHERENCE_HOOKS_INACTIVE, mt as GithubReviewState, n as AGENT_URL_ENV, nt as MyPrThreadsResponse, q as MyPrLaneEntry, r as ASK_USER_QUESTION_TOOL_NAME, rt as MyPrsSnapshot, tt as MyPrThreadNode, ut as DECIDING_REVIEW_STATES, w as RefinedTaskTitle, wt as ReviewVerdict, xt as ReviewFinding, yt as ReviewComment, zt as HEX_SHA_RE } from "./session-3eSP3t1i.js";
|
|
6
6
|
import { n as execFileAsync, o as spawn, r as execFileSync, t as execFile } from "./child-process-safe-D4hyVIXj.js";
|
|
7
|
-
import { At as planTitle, C as GithubPrsFrame, Ct as PLAN_HOOK_EVENT, D as WorkflowsSettings, Dt as PlanDecision, E as WorkflowLabelName, Et as PLAN_TOOL_NAME, Ft as IssuesSearchResponse, It as IssuesState, L as KEEP_MERGEABLE_TIMEOUT_MINUTES_RANGE, Lt as IssuesStatus, Mt as GithubIssueResponse, Nt as IssueKey, Ot as PlanRevision, Pt as IssueUpdate, Rt as issuePullRequestStateFromGithub, S as GithubPrLightSearchNode, St as PLAN_BODY_CAP_BYTES, T as WorkflowCommentBody, Tt as PLAN_RESULT_HOOK_EVENT, _ as MergedPrListing, _t as TraceCheckpoint, b as GithubPrDetailNode, bt as ChangeMap, c as pullRequestKeyFromReference, d as BenchmarkId, f as BenchmarkJudgement, g as CommitComparison, gt as MAX_RAW_LINE_CHARS, h as BenchmarkSuite, ht as SessionSetup, i as DIFF_ANNOTATION_NOTE_MAX_CHARS, jt as GithubIssuePullRequestsRepository, k as WorkflowsState, kt as parseExitPlanModeHookPayload, l as BenchmarkActionRequest, m as BenchmarkStatus, mt as UpdateJournal, n as ClientMessage, p as BenchmarkRun, rt as USAGE_INTEGER_RANGES, t as CONTROL_FRAME_MAX_BYTES, u as BenchmarkCase, v as MinedPrReviewData, vt as TraceRecord, w as comparablePrKey, wt as PLAN_HOOK_OUTPUT_MAX_CHARS, x as GithubPrKey, xt as changeMapFactId, y as PrCheckoutCaseInput, yt as CoherenceJournalOutput } from "./control-messages-
|
|
8
|
-
import { a as permitsHelpCommand, r as describeSessionLimits } from "./session-routes-
|
|
9
|
-
import { a as decideSpawnAllowance, c as
|
|
7
|
+
import { At as planTitle, C as GithubPrsFrame, Ct as PLAN_HOOK_EVENT, D as WorkflowsSettings, Dt as PlanDecision, E as WorkflowLabelName, Et as PLAN_TOOL_NAME, Ft as IssuesSearchResponse, It as IssuesState, L as KEEP_MERGEABLE_TIMEOUT_MINUTES_RANGE, Lt as IssuesStatus, Mt as GithubIssueResponse, Nt as IssueKey, Ot as PlanRevision, Pt as IssueUpdate, Rt as issuePullRequestStateFromGithub, S as GithubPrLightSearchNode, St as PLAN_BODY_CAP_BYTES, T as WorkflowCommentBody, Tt as PLAN_RESULT_HOOK_EVENT, _ as MergedPrListing, _t as TraceCheckpoint, b as GithubPrDetailNode, bt as ChangeMap, c as pullRequestKeyFromReference, d as BenchmarkId, f as BenchmarkJudgement, g as CommitComparison, gt as MAX_RAW_LINE_CHARS, h as BenchmarkSuite, ht as SessionSetup, i as DIFF_ANNOTATION_NOTE_MAX_CHARS, jt as GithubIssuePullRequestsRepository, k as WorkflowsState, kt as parseExitPlanModeHookPayload, l as BenchmarkActionRequest, m as BenchmarkStatus, mt as UpdateJournal, n as ClientMessage, p as BenchmarkRun, rt as USAGE_INTEGER_RANGES, t as CONTROL_FRAME_MAX_BYTES, u as BenchmarkCase, v as MinedPrReviewData, vt as TraceRecord, w as comparablePrKey, wt as PLAN_HOOK_OUTPUT_MAX_CHARS, x as GithubPrKey, xt as changeMapFactId, y as PrCheckoutCaseInput, yt as CoherenceJournalOutput } from "./control-messages-DHDEMTLI.js";
|
|
8
|
+
import { a as permitsHelpCommand, r as describeSessionLimits } from "./session-routes-B2oSzmQn.js";
|
|
9
|
+
import { a as decideSpawnAllowance, c as describeKeptChildAttention, d as describeUnpushedCloseRefusal, h as projectReviews, i as decideAgentRequest, l as describePushLanding, m as projectMyPrs, n as REFUSAL_REASON, o as deriveChildSessionName, p as parseAgentVerb, s as describeChildEndNote, u as describeRoleRefusal } from "./agent-api-core-CP4J2B39.js";
|
|
10
10
|
import { n as glimmervoidHomeDir, t as decideConfigPath } from "./config-path-core-DFt-TemS.js";
|
|
11
|
-
import { i as parseCommandLine, n as warnWhenGitIsTooOld } from "./git-version-
|
|
11
|
+
import { i as parseCommandLine, n as warnWhenGitIsTooOld } from "./git-version-DqagvnAZ.js";
|
|
12
12
|
import { n as checkGuardrailsDependencies, t as allowGuardedSpawn } from "./guardrails-deps-BjPzk1rE.js";
|
|
13
13
|
import { t as HOOK_URL_ENV } from "./hook-relay-core-DA_bz2KA.js";
|
|
14
14
|
import { t as HOOK_VIA_GUARDRAILS_PROXY_ENV } from "./guardrails-proxy-core-BI3rdY-m.js";
|
|
@@ -21,13 +21,13 @@ import { n as DEFAULT_MAX_MS, r as nextBackoffMs, t as DEFAULT_BASE_MS } from ".
|
|
|
21
21
|
import { n as unrefTimer, t as DEFAULT_TIMER_FNS } from "./timer-deps-u2DAQLok.js";
|
|
22
22
|
import { n as shellQuote } from "./hook-command-core-qTNK-xMF.js";
|
|
23
23
|
import { C as parseCgroupBytes, S as memoryReserveBytes, T as parseMemAvailableBytes, _ as createSpawnGate, a as jsonStateLoadError, b as cgroupLimitBytes, c as loadedJsonValue, d as writeJsonAtomicSync, f as writeTextAtomic, g as createSerialQueue, h as createMemoryAdmission, i as createJsonStateWriter, m as createKeyedSerialQueue, n as appendJsonLines, o as loadJsonStateFile, p as writeTextAtomicSync, r as createJsonStateStore, s as loadJsonStateFileSync, t as appendJsonLine, u as writeJsonAtomic, v as isQueueAdmissionTimeout, w as parseCgroupV2Path, x as headroomFromReadings, y as cgroupAncestorPaths } from "./json-file-DFCTvPjj.js";
|
|
24
|
-
import { $ as DEFAULT_BRANCH_GC_PREFIXES, B as buildExceptionProperties, F as TELEMETRY_BATCH_URL, G as exceptionFingerprint, H as decideIsDevInstall, I as TELEMETRY_FLAGS_URL, J as resolveProjectToken, K as nodeMajorVersion, L as adapterBucket, P as FIRST_RUN_NOTICE, R as buildAiGenerationEvents, U as decideRemoteTelemetryState, V as classifySessionExit, W as decideTelemetryConsent, Z as stableConfigKey, a as generateProjectId, at as worktreeIntegrationTips, b as validateSettingsBlock, ct as isSameDirectoryPath, d as assetForPlatform, et as matchedPrefix, f as decideRtkInstall, g as verifyDigest, h as isRtkBinaryName, i as ensureProjectIds, it as usablePrefixes, m as installTargetPath, n as DEFAULT_CONFIG, nt as planWorktreeGc, o as glimmervoidHomeDir$1, ot as canonicalizePath, p as findEscapingArchiveMember, q as parsePosthogReleaseId, r as createConfigStore, rt as sessionIdFromBranch, st as comparableDirectoryPath, t as ABORT_CONFIG_SAVE, tt as planBranchGc, v as mergeSettingsBlock, y as settingsBlockSpecFor, z as buildBrowserExceptionProperties } from "./config-store-
|
|
25
|
-
import { E as USAGE_VENDOR_KEYS, T as USAGE_COST_MODES, _ as PersistedTaskTitle, a as parseDataClientMessage, b as configIssueMessage, c as PendingCrashReport, d as TelemetryState, g as ConfigUpdate, i as parseDataAttachHistory, l as TELEMETRY_EVENT_SCHEMAS, m as BrowserConfig, n as HookEnvelope, o as DashboardTelemetryReport, r as DEEP_HISTORY_SCROLLBACK_LINES, t as HTTP_URL_RE, v as RUNTIME_CONFIG_SCALAR_KEYS, y as TASK_TITLE_SOURCE_PRIORITY } from "./contracts-
|
|
26
|
-
import { A as dedupeCodexFiles, B as shouldPersistWarehouse, C as shortSha$1, D as codexSessionIdFromPath, E as codexRootCandidates, F as passOutcome, H as claudeCode, I as projectDirCandidates, L as resolveProjectsDirs, M as grokHomes, N as grokRootCandidates, O as continuationDelayMs, P as isUsageFile, R as scopedClaudeRoots, S as parseResolvedSha, T as codexHomes, V as splitLines$1, _ as isCheckFresh, a as describeAgentResolvability, b as parseRegistryDeprecation, c as isKnownAgentId, d as resolveAdapter, f as setCustomAgents, g as decideUpdateStatus, h as decideInstallFlavor, i as customAgentFingerprint, j as expandTilde, k as decideFileRead, l as listAgentIds, m as readHostCodexVersion, n as cachedAgentResolvability, o as getAdapter, p as codex, r as commandFor, s as hookProfileOf, t as DEFAULT_AGENT_ID, u as resetCommandCache, v as normalizeSha, w as codexFallbackRoots, x as parseRegistryLatest, y as normalizeUpdateChannel, z as shouldEvaluateDespiteIoFailures } from "./adapters-
|
|
24
|
+
import { $ as DEFAULT_BRANCH_GC_PREFIXES, B as buildExceptionProperties, F as TELEMETRY_BATCH_URL, G as exceptionFingerprint, H as decideIsDevInstall, I as TELEMETRY_FLAGS_URL, J as resolveProjectToken, K as nodeMajorVersion, L as adapterBucket, P as FIRST_RUN_NOTICE, R as buildAiGenerationEvents, U as decideRemoteTelemetryState, V as classifySessionExit, W as decideTelemetryConsent, Z as stableConfigKey, a as generateProjectId, at as worktreeIntegrationTips, b as validateSettingsBlock, ct as isSameDirectoryPath, d as assetForPlatform, et as matchedPrefix, f as decideRtkInstall, g as verifyDigest, h as isRtkBinaryName, i as ensureProjectIds, it as usablePrefixes, m as installTargetPath, n as DEFAULT_CONFIG, nt as planWorktreeGc, o as glimmervoidHomeDir$1, ot as canonicalizePath, p as findEscapingArchiveMember, q as parsePosthogReleaseId, r as createConfigStore, rt as sessionIdFromBranch, st as comparableDirectoryPath, t as ABORT_CONFIG_SAVE, tt as planBranchGc, v as mergeSettingsBlock, y as settingsBlockSpecFor, z as buildBrowserExceptionProperties } from "./config-store-Br2PY4Tg.js";
|
|
25
|
+
import { E as USAGE_VENDOR_KEYS, T as USAGE_COST_MODES, _ as PersistedTaskTitle, a as parseDataClientMessage, b as configIssueMessage, c as PendingCrashReport, d as TelemetryState, g as ConfigUpdate, i as parseDataAttachHistory, l as TELEMETRY_EVENT_SCHEMAS, m as BrowserConfig, n as HookEnvelope, o as DashboardTelemetryReport, r as DEEP_HISTORY_SCROLLBACK_LINES, t as HTTP_URL_RE, v as RUNTIME_CONFIG_SCALAR_KEYS, y as TASK_TITLE_SOURCE_PRIORITY } from "./contracts-DJqeCtR3.js";
|
|
26
|
+
import { A as dedupeCodexFiles, B as shouldPersistWarehouse, C as shortSha$1, D as codexSessionIdFromPath, E as codexRootCandidates, F as passOutcome, H as claudeCode, I as projectDirCandidates, L as resolveProjectsDirs, M as grokHomes, N as grokRootCandidates, O as continuationDelayMs, P as isUsageFile, R as scopedClaudeRoots, S as parseResolvedSha, T as codexHomes, V as splitLines$1, _ as isCheckFresh, a as describeAgentResolvability, b as parseRegistryDeprecation, c as isKnownAgentId, d as resolveAdapter, f as setCustomAgents, g as decideUpdateStatus, h as decideInstallFlavor, i as customAgentFingerprint, j as expandTilde, k as decideFileRead, l as listAgentIds, m as readHostCodexVersion, n as cachedAgentResolvability, o as getAdapter, p as codex, r as commandFor, s as hookProfileOf, t as DEFAULT_AGENT_ID, u as resetCommandCache, v as normalizeSha, w as codexFallbackRoots, x as parseRegistryLatest, y as normalizeUpdateChannel, z as shouldEvaluateDespiteIoFailures } from "./adapters-CWIHOfVo.js";
|
|
27
27
|
import { t as HOOK_TOOLS } from "./hook-tools-Dl7-r2Eb.js";
|
|
28
28
|
import { a as formatSshPublicKey, c as orderAgentSockets, i as commitSigningShimText, l as parseSshPublicKey, n as NO_AGENT_HOLDS_KEY, o as isForwardedAgentDirName, r as agentListsKey, s as isSignableGitObject, t as COMMIT_SIGNING_SHIM_NAME } from "./ssh-signing-core-CRhVVWNW.js";
|
|
29
|
-
import { a as configSiblingPath, c as defaultPairingsPath, d as decideDeviceAuth, f as deviceNameFromUserAgent, l as defaultSeenPath, n as normalizeRemoteConfig, o as createPairingsStore, p as hashSecret, r as validateRemoteConfig, s as createSeenStore, t as decideBindHost, u as DEFAULT_DEVICE_MAX_AGE_MS } from "./remote-config-
|
|
30
|
-
import { n as configFileViewOf, u as readSettingsForAgent } from "./settings-readout-core-
|
|
29
|
+
import { a as configSiblingPath, c as defaultPairingsPath, d as decideDeviceAuth, f as deviceNameFromUserAgent, l as defaultSeenPath, n as normalizeRemoteConfig, o as createPairingsStore, p as hashSecret, r as validateRemoteConfig, s as createSeenStore, t as decideBindHost, u as DEFAULT_DEVICE_MAX_AGE_MS } from "./remote-config-DcmhRuRc.js";
|
|
30
|
+
import { n as configFileViewOf, u as readSettingsForAgent } from "./settings-readout-core-2XunjdBA.js";
|
|
31
31
|
import { _ as escapeMarkup, g as createLaneLog, n as createVisionsSetup } from "./visions-setup-DB2P8JUL.js";
|
|
32
32
|
import { t as getRtkPath } from "./rtk-resolver-BDHi6RMv.js";
|
|
33
33
|
import { createRequire } from "node:module";
|
|
@@ -2040,6 +2040,7 @@ function projectSessionCard(source, { id, name }) {
|
|
|
2040
2040
|
guardrailsReleased: source.isGuardrailsReleased === true,
|
|
2041
2041
|
resumeSessionId: source.resumeSessionId || null,
|
|
2042
2042
|
...source.isWorkspace ? { workspace: true } : {},
|
|
2043
|
+
...source.remoteWorkspace ? { remoteWorkspace: source.remoteWorkspace } : {},
|
|
2043
2044
|
...source.linkedPrs?.length ? { linkedPrs: source.linkedPrs } : {},
|
|
2044
2045
|
...source.ownerId ? { ownerId: source.ownerId } : {}
|
|
2045
2046
|
};
|
|
@@ -2065,6 +2066,7 @@ function projectSessionSnapshots(source) {
|
|
|
2065
2066
|
ownerId: source.ownerId ?? null,
|
|
2066
2067
|
isWorktree: source.isWorktree,
|
|
2067
2068
|
isWorkspace: source.isWorkspace,
|
|
2069
|
+
remoteWorkspace: source.remoteWorkspace ?? null,
|
|
2068
2070
|
isGuarded: source.isGuarded === true,
|
|
2069
2071
|
isGuardrailsReleased: source.isGuardrailsReleased === true,
|
|
2070
2072
|
resumeSessionId: source.resumeSessionId,
|
|
@@ -2082,6 +2084,7 @@ function projectSessionSnapshots(source) {
|
|
|
2082
2084
|
mergeReason: source.mergeReason,
|
|
2083
2085
|
worktreeNotice: source.worktreeNotice,
|
|
2084
2086
|
failureReason: source.failureReason ?? null,
|
|
2087
|
+
unseenOutcome: source.unseenOutcome ?? null,
|
|
2085
2088
|
effectiveBase: source.effectiveBase ?? null,
|
|
2086
2089
|
linkedPrs: source.linkedPrs,
|
|
2087
2090
|
coherenceHooks: source.coherenceHooks ?? null,
|
|
@@ -2561,6 +2564,7 @@ function createAgentApiWiring({ config, agentSessions, listAllSessions, listBoar
|
|
|
2561
2564
|
const parentsWithChannelCleanup = /* @__PURE__ */ new WeakSet();
|
|
2562
2565
|
const sessionsWithOperatorWatch = /* @__PURE__ */ new WeakSet();
|
|
2563
2566
|
const lastOperatorInputAtBySession = /* @__PURE__ */ new WeakMap();
|
|
2567
|
+
const childIdsToDiscard = /* @__PURE__ */ new Set();
|
|
2564
2568
|
function forgetLink(childId) {
|
|
2565
2569
|
const link = linksByChildId.get(childId);
|
|
2566
2570
|
if (link) for (const timer of link.retryTimersBySide.values()) clearTimeoutFn(timer);
|
|
@@ -2695,6 +2699,45 @@ function createAgentApiWiring({ config, agentSessions, listAllSessions, listBoar
|
|
|
2695
2699
|
parent.off("teardown", closeChildWithParent);
|
|
2696
2700
|
};
|
|
2697
2701
|
}
|
|
2702
|
+
async function keepOrDiscardChildWorktree(child, worktree, isParentRemoved) {
|
|
2703
|
+
const isDiscardRequested = childIdsToDiscard.delete(child.id);
|
|
2704
|
+
if (!worktree) return false;
|
|
2705
|
+
if (isDiscardRequested || isParentRemoved) {
|
|
2706
|
+
await child.discardWorktree();
|
|
2707
|
+
return false;
|
|
2708
|
+
}
|
|
2709
|
+
return !await child.discardWorktreeIfClean();
|
|
2710
|
+
}
|
|
2711
|
+
async function settleChildWorktree(parentId, child, childName, startAttempt, worktreeAtStart) {
|
|
2712
|
+
try {
|
|
2713
|
+
await startAttempt.catch(() => {});
|
|
2714
|
+
await child._awaitPendingReaps();
|
|
2715
|
+
const worktree = child.getWorktreeCarry();
|
|
2716
|
+
const registeredParent = listAllSessions().find((session) => session.id === parentId);
|
|
2717
|
+
const isKept = await keepOrDiscardChildWorktree(child, worktree, !registeredParent);
|
|
2718
|
+
if (registeredParent && worktree && isKept) {
|
|
2719
|
+
logger.warn(`[${AGENT_LANE_TAG} ${childName}] kept a worktree with unpushed work: ${worktree.worktreeDir}`);
|
|
2720
|
+
registeredParent.noteAttention(describeKeptChildAttention(childName, worktree));
|
|
2721
|
+
post(child.id, "toParent", {
|
|
2722
|
+
author: "glimmervoid",
|
|
2723
|
+
text: describeChildEndNote(childName, {
|
|
2724
|
+
kind: "kept",
|
|
2725
|
+
worktree
|
|
2726
|
+
}),
|
|
2727
|
+
at: now()
|
|
2728
|
+
});
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2731
|
+
const kind = worktree || worktreeAtStart ? "discarded" : "none";
|
|
2732
|
+
post(child.id, "toParent", {
|
|
2733
|
+
author: "glimmervoid",
|
|
2734
|
+
text: describeChildEndNote(childName, { kind }),
|
|
2735
|
+
at: now()
|
|
2736
|
+
});
|
|
2737
|
+
} catch (error) {
|
|
2738
|
+
logger.warn(`[${AGENT_LANE_TAG} ${childName}] could not settle the worktree: ${errorMessage(error)}`);
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2698
2741
|
function post(childId, side, entry) {
|
|
2699
2742
|
const channel = channelsByChildId.get(childId);
|
|
2700
2743
|
if (!channel) return;
|
|
@@ -2796,6 +2839,8 @@ function createAgentApiWiring({ config, agentSessions, listAllSessions, listBoar
|
|
|
2796
2839
|
unguarded: !parent.isGuarded
|
|
2797
2840
|
};
|
|
2798
2841
|
let child = null;
|
|
2842
|
+
let worktreeAtStart = null;
|
|
2843
|
+
let startAttempt = Promise.resolve();
|
|
2799
2844
|
let released = false;
|
|
2800
2845
|
let detachFromParent = () => {};
|
|
2801
2846
|
const releaseChildSlot = () => {
|
|
@@ -2808,6 +2853,7 @@ function createAgentApiWiring({ config, agentSessions, listAllSessions, listBoar
|
|
|
2808
2853
|
id,
|
|
2809
2854
|
session: name
|
|
2810
2855
|
});
|
|
2856
|
+
if (child) settleChildWorktree(parent.id, child, name, startAttempt, worktreeAtStart);
|
|
2811
2857
|
};
|
|
2812
2858
|
const abandonChild = (reason) => {
|
|
2813
2859
|
logger.warn(`[${AGENT_LANE_TAG} ${name}] ${reason}`);
|
|
@@ -2843,7 +2889,7 @@ function createAgentApiWiring({ config, agentSessions, listAllSessions, listBoar
|
|
|
2843
2889
|
});
|
|
2844
2890
|
spawned.on("exit", releaseChildSlot);
|
|
2845
2891
|
spawned.on("teardown", releaseChildSlot);
|
|
2846
|
-
|
|
2892
|
+
startAttempt = spawnGate.run(() => {
|
|
2847
2893
|
if (spawned._destroyed) return void 0;
|
|
2848
2894
|
broadcastControl({
|
|
2849
2895
|
type: "session-added",
|
|
@@ -2854,7 +2900,9 @@ function createAgentApiWiring({ config, agentSessions, listAllSessions, listBoar
|
|
|
2854
2900
|
});
|
|
2855
2901
|
return spawned.start();
|
|
2856
2902
|
});
|
|
2903
|
+
await startAttempt;
|
|
2857
2904
|
if (!spawned.hasLivePty) return abandonChild("the child never reached a live terminal");
|
|
2905
|
+
worktreeAtStart = spawned.getWorktreeCarry();
|
|
2858
2906
|
return {
|
|
2859
2907
|
ok: true,
|
|
2860
2908
|
sessionId: id,
|
|
@@ -2981,7 +3029,7 @@ function createAgentApiWiring({ config, agentSessions, listAllSessions, listBoar
|
|
|
2981
3029
|
error: outcome.error
|
|
2982
3030
|
}
|
|
2983
3031
|
};
|
|
2984
|
-
const note = `this session owns ${outcome.name} until the task is done: glimmervoid read ${outcome.sessionId} reads its messages, glimmervoid send ${outcome.sessionId} <text> steers it, and glimmervoid close ${outcome.sessionId} ends it`;
|
|
3032
|
+
const note = `this session owns ${outcome.name} until the task is done: glimmervoid read ${outcome.sessionId} reads its messages, glimmervoid send ${outcome.sessionId} <text> steers it, and glimmervoid close ${outcome.sessionId} ends it and discards its worktree, refusing while it holds work that is not on the integration branch unless you add --discard`;
|
|
2985
3033
|
return {
|
|
2986
3034
|
status: 200,
|
|
2987
3035
|
body: {
|
|
@@ -3050,11 +3098,16 @@ function createAgentApiWiring({ config, agentSessions, listAllSessions, listBoar
|
|
|
3050
3098
|
}
|
|
3051
3099
|
};
|
|
3052
3100
|
}
|
|
3053
|
-
function close(session, { child }) {
|
|
3101
|
+
async function close(session, { child, discard = false }) {
|
|
3054
3102
|
const owned = findChildOf(session, child);
|
|
3055
3103
|
if (!owned.ok) return refusal(404, owned.error);
|
|
3056
3104
|
const childSession = agentSessions.get(owned.channel.childId);
|
|
3057
|
-
|
|
3105
|
+
const alreadyEnded = refusal(409, `${owned.channel.childName} has already ended`);
|
|
3106
|
+
if (!childSession) return alreadyEnded;
|
|
3107
|
+
const worktree = childSession.getWorktreeCarry();
|
|
3108
|
+
if (!discard && worktree && await childSession.hasUnmergedWork()) return refusal(409, describeUnpushedCloseRefusal(owned.channel.childName, owned.channel.childId, worktree));
|
|
3109
|
+
if (agentSessions.get(owned.channel.childId) !== childSession) return alreadyEnded;
|
|
3110
|
+
if (discard) childIdsToDiscard.add(childSession.id);
|
|
3058
3111
|
childSession.destroy();
|
|
3059
3112
|
return {
|
|
3060
3113
|
status: 200,
|
|
@@ -4113,7 +4166,7 @@ function explainNotification(to, gate, event, opts) {
|
|
|
4113
4166
|
};
|
|
4114
4167
|
}
|
|
4115
4168
|
if (to === STATES.RUNNING) {
|
|
4116
|
-
if (event === "user_input" ||
|
|
4169
|
+
if (event === "user_input" || !opts || !opts.hookSeen) {
|
|
4117
4170
|
gate.reset();
|
|
4118
4171
|
return {
|
|
4119
4172
|
category: null,
|
|
@@ -5371,10 +5424,10 @@ function createSessionEventWiring(dependencies) {
|
|
|
5371
5424
|
session.on("agent-attention", ({ note }) => {
|
|
5372
5425
|
pendingAgentNote = pendingAgentNote === null ? note : `${pendingAgentNote} | ${note}`;
|
|
5373
5426
|
});
|
|
5374
|
-
session.on("user-prompt", () => {
|
|
5427
|
+
session.on("user-prompt", ({ isSelfWake }) => {
|
|
5375
5428
|
markTurnWorking();
|
|
5376
5429
|
pendingAgentNote = null;
|
|
5377
|
-
notifyGate.reset();
|
|
5430
|
+
if (!isSelfWake) notifyGate.reset();
|
|
5378
5431
|
releaseDeferredAcknowledge();
|
|
5379
5432
|
});
|
|
5380
5433
|
session.on("waiting-input-acknowledged", () => dependencies.notificationManager.acknowledge(session.id));
|
|
@@ -5405,6 +5458,7 @@ function createSessionEventWiring(dependencies) {
|
|
|
5405
5458
|
relay("wakeup-change", "session-wakeup");
|
|
5406
5459
|
session.on("wakeup-change", persistWasWorking);
|
|
5407
5460
|
relay("prompt-kind-change", "session-prompt");
|
|
5461
|
+
relay("unseen-outcome-change", "session-unseen");
|
|
5408
5462
|
relay("conversation-change", "session-conversation");
|
|
5409
5463
|
session.on("merge-status", ({ mergeStatus, reason, parked }) => {
|
|
5410
5464
|
dependencies.broadcastControl({
|
|
@@ -6569,6 +6623,18 @@ function formatTouchedRanges(ranges) {
|
|
|
6569
6623
|
return (Array.isArray(ranges) ? ranges : []).map((range) => range.start === range.end ? `${range.start}` : `${range.start}-${range.end}`).join(", ");
|
|
6570
6624
|
}
|
|
6571
6625
|
//#endregion
|
|
6626
|
+
//#region shared/fnv-hash.ts
|
|
6627
|
+
var FNV_OFFSET_BASIS = 2166136261;
|
|
6628
|
+
var FNV_PRIME = 16777619;
|
|
6629
|
+
function fnv1a32(text) {
|
|
6630
|
+
let hash = FNV_OFFSET_BASIS;
|
|
6631
|
+
for (let index = 0; index < text.length; index += 1) {
|
|
6632
|
+
hash ^= text.charCodeAt(index);
|
|
6633
|
+
hash = Math.imul(hash, FNV_PRIME) >>> 0;
|
|
6634
|
+
}
|
|
6635
|
+
return hash;
|
|
6636
|
+
}
|
|
6637
|
+
//#endregion
|
|
6572
6638
|
//#region server/visions/core/visions-dispatch-core.ts
|
|
6573
6639
|
var DEFAULT_QUIET_MS = 3e4;
|
|
6574
6640
|
var DEFAULT_COOLDOWN_MS = 3e5;
|
|
@@ -6647,12 +6713,7 @@ function resolveVisionsConfig(raw) {
|
|
|
6647
6713
|
}
|
|
6648
6714
|
function hashText$1(text) {
|
|
6649
6715
|
const value = typeof text === "string" ? text : "";
|
|
6650
|
-
|
|
6651
|
-
for (let index = 0; index < value.length; index += 1) {
|
|
6652
|
-
hash ^= value.charCodeAt(index);
|
|
6653
|
-
hash = Math.imul(hash, 16777619) >>> 0;
|
|
6654
|
-
}
|
|
6655
|
-
return `${value.length.toString(36)}-${hash.toString(36)}`;
|
|
6716
|
+
return `${value.length.toString(36)}-${fnv1a32(value).toString(36)}`;
|
|
6656
6717
|
}
|
|
6657
6718
|
function createDispatchState(dispatchTimes = []) {
|
|
6658
6719
|
return {
|
|
@@ -12813,11 +12874,13 @@ function seedTaskTitleSources({ persistedTitle, resumeSessionId, customTitle, in
|
|
|
12813
12874
|
customTitle
|
|
12814
12875
|
};
|
|
12815
12876
|
}
|
|
12877
|
+
var envelopePattern = (tag) => new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?</${tag}\\s*>`, "gi");
|
|
12878
|
+
var TASK_NOTIFICATION_ENVELOPE_PATTERN = envelopePattern("task-notification");
|
|
12816
12879
|
var PROMPT_ENVELOPE_PATTERNS = [
|
|
12817
|
-
"pasted_content",
|
|
12818
|
-
"system-reminder",
|
|
12819
|
-
|
|
12820
|
-
]
|
|
12880
|
+
envelopePattern("pasted_content"),
|
|
12881
|
+
envelopePattern("system-reminder"),
|
|
12882
|
+
TASK_NOTIFICATION_ENVELOPE_PATTERN
|
|
12883
|
+
];
|
|
12821
12884
|
var MIN_SUBSTANTIVE_WORDS = 3;
|
|
12822
12885
|
var CONTINUATION_PROMPTS = new Set([
|
|
12823
12886
|
"yes please continue",
|
|
@@ -12837,6 +12900,10 @@ function cleanTaskPrompt(prompt) {
|
|
|
12837
12900
|
for (const pattern of PROMPT_ENVELOPE_PATTERNS) cleanedPrompt = cleanedPrompt.replace(pattern, " ");
|
|
12838
12901
|
return cleanedPrompt.replace(/\s+/g, " ").trim();
|
|
12839
12902
|
}
|
|
12903
|
+
function isSelfWakePrompt(prompt) {
|
|
12904
|
+
if (typeof prompt !== "string") return false;
|
|
12905
|
+
return prompt.replace(TASK_NOTIFICATION_ENVELOPE_PATTERN, " ").trim() === "" && prompt.trim() !== "";
|
|
12906
|
+
}
|
|
12840
12907
|
function isSubstantivePrompt(prompt) {
|
|
12841
12908
|
if (typeof prompt !== "string") return false;
|
|
12842
12909
|
const cleanedPrompt = cleanTaskPrompt(prompt);
|
|
@@ -14469,6 +14536,13 @@ var ENTRY_HOOKS = {
|
|
|
14469
14536
|
session.emit("session-done", { name: session.name });
|
|
14470
14537
|
}
|
|
14471
14538
|
};
|
|
14539
|
+
var UNSEEN_OUTCOME_ON_ENTRY = /* @__PURE__ */ new Map([
|
|
14540
|
+
[STATES.COMPLETE, "finished"],
|
|
14541
|
+
[STATES.FAILED, "failed"],
|
|
14542
|
+
[STATES.INITIALIZING, null],
|
|
14543
|
+
[STATES.DORMANT, null],
|
|
14544
|
+
[STATES.DONE, null]
|
|
14545
|
+
]);
|
|
14472
14546
|
var EXIT_HOOKS = { [STATES.WAITING](session) {
|
|
14473
14547
|
session.emit("attention-cleared", { name: session.name });
|
|
14474
14548
|
} };
|
|
@@ -18349,6 +18423,7 @@ var Session = class extends EventEmitter {
|
|
|
18349
18423
|
state;
|
|
18350
18424
|
stateSince;
|
|
18351
18425
|
failureReason;
|
|
18426
|
+
unseenOutcome;
|
|
18352
18427
|
ephemeral;
|
|
18353
18428
|
agentId;
|
|
18354
18429
|
usageVendor;
|
|
@@ -18456,6 +18531,7 @@ var Session = class extends EventEmitter {
|
|
|
18456
18531
|
this.state = STATES.DORMANT;
|
|
18457
18532
|
this.stateSince = Date.now();
|
|
18458
18533
|
this.failureReason = null;
|
|
18534
|
+
this.unseenOutcome = null;
|
|
18459
18535
|
this._receivedFirstOutput = false;
|
|
18460
18536
|
this._killPollTimer = null;
|
|
18461
18537
|
this._killReapTimer = null;
|
|
@@ -18696,13 +18772,8 @@ var Session = class extends EventEmitter {
|
|
|
18696
18772
|
get recentTaskPrompts() {
|
|
18697
18773
|
return [...this._recentTaskPrompts];
|
|
18698
18774
|
}
|
|
18699
|
-
setPendingTaskTitle(prompt) {
|
|
18700
|
-
this._taskTitleSources.pendingPromptTitle = extractPromptTaskTitle(prompt);
|
|
18701
|
-
this._updateTaskTitle();
|
|
18702
|
-
}
|
|
18703
18775
|
applyTaskTitleRefinement(refinement) {
|
|
18704
18776
|
if (refinement.action === "replace") this._taskTitleSources.refinedTitle = refinement.title;
|
|
18705
|
-
this._taskTitleSources.pendingPromptTitle = null;
|
|
18706
18777
|
this._updateTaskTitle();
|
|
18707
18778
|
}
|
|
18708
18779
|
_updateTaskTitle() {
|
|
@@ -18829,7 +18900,8 @@ var Session = class extends EventEmitter {
|
|
|
18829
18900
|
this.emit("user-prompt", {
|
|
18830
18901
|
state: this.state,
|
|
18831
18902
|
stateSince: this.stateSince,
|
|
18832
|
-
ts: Date.now()
|
|
18903
|
+
ts: Date.now(),
|
|
18904
|
+
isSelfWake: isSelfWakePrompt(raw.payload?.prompt)
|
|
18833
18905
|
});
|
|
18834
18906
|
}
|
|
18835
18907
|
if (raw && raw.signal === "ready" && raw.source === "hook") this.backgroundTracking.applyBackgroundTasks(raw.payload);
|
|
@@ -19153,6 +19225,9 @@ var Session = class extends EventEmitter {
|
|
|
19153
19225
|
get isWorkspace() {
|
|
19154
19226
|
return this.worktreeLifecycle.snapshot().isWorkspace;
|
|
19155
19227
|
}
|
|
19228
|
+
get remoteWorkspace() {
|
|
19229
|
+
return this._getRemoteLaunch()?.workspace ?? null;
|
|
19230
|
+
}
|
|
19156
19231
|
getWorktreeCarry() {
|
|
19157
19232
|
return this.worktreeLifecycle.getCarry();
|
|
19158
19233
|
}
|
|
@@ -19274,6 +19349,7 @@ var Session = class extends EventEmitter {
|
|
|
19274
19349
|
ownerId: this.ownerId,
|
|
19275
19350
|
isWorktree: this.isWorktree,
|
|
19276
19351
|
isWorkspace: this.isWorkspace,
|
|
19352
|
+
remoteWorkspace: this.remoteWorkspace,
|
|
19277
19353
|
isGuarded: this.isGuarded,
|
|
19278
19354
|
isGuardrailsReleased: this.isGuardrailsReleased,
|
|
19279
19355
|
resumeSessionId: this._resumeSessionId,
|
|
@@ -19291,6 +19367,7 @@ var Session = class extends EventEmitter {
|
|
|
19291
19367
|
mergeReason: this.mergeReason,
|
|
19292
19368
|
worktreeNotice: this.worktreeNotice,
|
|
19293
19369
|
failureReason: this.failureReason,
|
|
19370
|
+
unseenOutcome: this.unseenOutcome,
|
|
19294
19371
|
effectiveBase: this.worktreeLifecycle.snapshot().effectiveBase,
|
|
19295
19372
|
linkedPrs: this.linkedPrs,
|
|
19296
19373
|
coherenceHooks: this.coherenceHooks,
|
|
@@ -19335,6 +19412,8 @@ var Session = class extends EventEmitter {
|
|
|
19335
19412
|
this.state = to;
|
|
19336
19413
|
if (event === "spawn_fail") this.failureReason = describeSpawnFailure(detail);
|
|
19337
19414
|
if (to === STATES.INITIALIZING) this.failureReason = null;
|
|
19415
|
+
const unseenOutcomeOnEntry = UNSEEN_OUTCOME_ON_ENTRY.get(to);
|
|
19416
|
+
if (unseenOutcomeOnEntry !== void 0 && !isCompactionRestoreEvent(event)) this._setUnseenOutcome(unseenOutcomeOnEntry);
|
|
19338
19417
|
if (to === STATES.IDLE || to === STATES.COMPLETE) this._observeCompaction("settled");
|
|
19339
19418
|
const entryHook = ENTRY_HOOKS[to];
|
|
19340
19419
|
if (entryHook && !isCompactionRestoreEvent(event)) entryHook(this);
|
|
@@ -19983,6 +20062,10 @@ var Session = class extends EventEmitter {
|
|
|
19983
20062
|
this._output.push(data);
|
|
19984
20063
|
if (this.listenerCount("data") > 0) this.emit("data", data);
|
|
19985
20064
|
}
|
|
20065
|
+
_isExitHeadedForFailure(event) {
|
|
20066
|
+
if (this.state === STATES.FAILED) return false;
|
|
20067
|
+
return TRANSITIONS[this.state]?.[event] === STATES.FAILED;
|
|
20068
|
+
}
|
|
19986
20069
|
_handleSpawnedPtyExit(guardrailsLaunch, exitCode, signal) {
|
|
19987
20070
|
this._disposeGuardrailsLaunch(guardrailsLaunch);
|
|
19988
20071
|
this._stopConversationBridge();
|
|
@@ -20002,6 +20085,11 @@ var Session = class extends EventEmitter {
|
|
|
20002
20085
|
if (pid) this._exitReap = this._reapTreeOnce(pid);
|
|
20003
20086
|
const { event, detail } = decideExitTransition(this.state, exitCode, signal, this._receivedFirstOutput);
|
|
20004
20087
|
const reason = detail.reason || null;
|
|
20088
|
+
const pendingRemoteExitReason = this._isExitHeadedForFailure(event) ? this._getRemoteLaunch()?.describeExit().catch(() => null) : void 0;
|
|
20089
|
+
if (pendingRemoteExitReason) {
|
|
20090
|
+
const remoteExitReason = await pendingRemoteExitReason;
|
|
20091
|
+
if (remoteExitReason && this._isExitHeadedForFailure(event)) this.failureReason = remoteExitReason;
|
|
20092
|
+
}
|
|
20005
20093
|
this.transition(event, detail);
|
|
20006
20094
|
if (this._recorder) {
|
|
20007
20095
|
this._recorder.writeFooter("pty_exit", exitCode);
|
|
@@ -20182,9 +20270,16 @@ var Session = class extends EventEmitter {
|
|
|
20182
20270
|
this._armTimer("_killPollTimer", KILL_POLL_INTERVAL_MS, poll);
|
|
20183
20271
|
}
|
|
20184
20272
|
dismiss() {
|
|
20185
|
-
|
|
20273
|
+
const hadUnseenOutcome = this.unseenOutcome !== null;
|
|
20274
|
+
this._setUnseenOutcome(null);
|
|
20275
|
+
if (!DISMISSIBLE_STATES.has(this.state)) return hadUnseenOutcome;
|
|
20186
20276
|
return this.transition("user_dismiss");
|
|
20187
20277
|
}
|
|
20278
|
+
_setUnseenOutcome(nextUnseenOutcome) {
|
|
20279
|
+
if (this.unseenOutcome === nextUnseenOutcome) return;
|
|
20280
|
+
this.unseenOutcome = nextUnseenOutcome;
|
|
20281
|
+
this.emit("unseen-outcome-change", { unseenOutcome: nextUnseenOutcome });
|
|
20282
|
+
}
|
|
20188
20283
|
_teardownPending() {
|
|
20189
20284
|
return this._pendingRestart || this._finishing;
|
|
20190
20285
|
}
|
|
@@ -29594,7 +29689,9 @@ var GLIMMERVOID_WORKSPACE_RE = /^gv-[0-9a-f]{12}$/;
|
|
|
29594
29689
|
var IDLE_STOP_STATES = /* @__PURE__ */ new Set([
|
|
29595
29690
|
STATES.IDLE,
|
|
29596
29691
|
STATES.COMPLETE,
|
|
29597
|
-
STATES.DORMANT
|
|
29692
|
+
STATES.DORMANT,
|
|
29693
|
+
STATES.FAILED,
|
|
29694
|
+
STATES.DONE
|
|
29598
29695
|
]);
|
|
29599
29696
|
var CODER_ENVIRONMENT_DEFAULTS = Object.freeze({
|
|
29600
29697
|
idleStopMinutes: 30,
|
|
@@ -29679,19 +29776,24 @@ function shouldIdleStop({ sessionState, idleSinceMs, nowMs, idleStopMinutes }) {
|
|
|
29679
29776
|
if (!IDLE_STOP_STATES.has(sessionState) || idleSinceMs === null) return false;
|
|
29680
29777
|
return nowMs - idleSinceMs >= idleStopMinutes * 6e4;
|
|
29681
29778
|
}
|
|
29682
|
-
function planReconcile({ workspaces, mappings, busyWorkspaces, nowMs, deleteAfterStoppedDays }) {
|
|
29779
|
+
function planReconcile({ workspaces, mappings, busyWorkspaces, ownedWorkspaces, nowMs, deleteAfterStoppedDays }) {
|
|
29683
29780
|
const mappedNames = new Set(mappings.map((mapping) => mapping.workspace));
|
|
29684
29781
|
const listedNames = new Set(workspaces.map((workspace) => workspace.name));
|
|
29685
29782
|
const presentMappings = mappings.filter((mapping) => !busyWorkspaces.has(mapping.workspace)).filter((mapping) => listedNames.has(mapping.workspace));
|
|
29686
|
-
const unmapped = workspaces.filter((workspace) => !mappedNames.has(workspace.name) && !busyWorkspaces.has(workspace.name) && workspace.status !== "deleting");
|
|
29783
|
+
const unmapped = workspaces.filter((workspace) => !mappedNames.has(workspace.name) && !busyWorkspaces.has(workspace.name) && workspace.status !== "deleting" && ownedWorkspaces.has(workspace.name));
|
|
29687
29784
|
const isStale = (workspace) => workspace.stoppedAtMs !== null && nowMs - workspace.stoppedAtMs >= deleteAfterStoppedDays * DAY_MS;
|
|
29688
29785
|
return {
|
|
29689
29786
|
resume: presentMappings.filter((mapping) => mapping.wasActive),
|
|
29690
29787
|
stop: presentMappings.filter((mapping) => !mapping.wasActive),
|
|
29691
29788
|
delete: unmapped.filter(isStale).map((workspace) => workspace.name),
|
|
29692
|
-
|
|
29789
|
+
stopUnmapped: unmapped.filter((workspace) => workspace.status === "running").map((workspace) => workspace.name)
|
|
29693
29790
|
};
|
|
29694
29791
|
}
|
|
29792
|
+
function retainedOwnedWorkspaces({ ownedWorkspaces, workspaces, mappings, busyWorkspaces }) {
|
|
29793
|
+
const listedNames = new Set(workspaces.map((workspace) => workspace.name));
|
|
29794
|
+
const mappedNames = new Set(mappings.map((mapping) => mapping.workspace));
|
|
29795
|
+
return new Set([...ownedWorkspaces].filter((workspace) => listedNames.has(workspace) || mappedNames.has(workspace) || busyWorkspaces.has(workspace)));
|
|
29796
|
+
}
|
|
29695
29797
|
function failedProvisionCleanup({ didCreate, didStart }) {
|
|
29696
29798
|
if (didCreate) return "delete";
|
|
29697
29799
|
if (didStart) return "stop";
|
|
@@ -29711,14 +29813,48 @@ function remoteCheckoutDirectory({ checkoutRoot, workspace }) {
|
|
|
29711
29813
|
}
|
|
29712
29814
|
function coderErrorDetail(stderr) {
|
|
29713
29815
|
if (typeof stderr !== "string") return null;
|
|
29714
|
-
const
|
|
29715
|
-
|
|
29716
|
-
|
|
29816
|
+
const lines = stderr.split("\n").map((line) => line.trim());
|
|
29817
|
+
const errorLineIndex = lines.findLastIndex((line) => line.startsWith("error:"));
|
|
29818
|
+
if (errorLineIndex === -1) return null;
|
|
29819
|
+
return lines.slice(errorLineIndex).filter(Boolean).join(" ").slice(6).trim().slice(0, CODER_ERROR_DETAIL_LIMIT) || null;
|
|
29820
|
+
}
|
|
29821
|
+
function isCoderSignedOutError(stderr) {
|
|
29822
|
+
return typeof stderr === "string" && stderr.includes("You are signed out or your session has expired");
|
|
29823
|
+
}
|
|
29824
|
+
function remoteWorkRefusal({ uncommittedStatus, unpushedLog, stashList, branch, workspace }) {
|
|
29825
|
+
if (uncommittedStatus.trim()) return `Branch ${branch} in Coder workspace ${workspace} has uncommitted changes`;
|
|
29826
|
+
if (unpushedLog.trim()) return `Branch ${branch} in Coder workspace ${workspace} has commits that are not pushed`;
|
|
29827
|
+
if (stashList.trim()) return `Branch ${branch} in Coder workspace ${workspace} has stashed changes`;
|
|
29828
|
+
return null;
|
|
29829
|
+
}
|
|
29830
|
+
var describeStoppedExit = (workspace) => `Coder workspace ${workspace} stopped, by its idle stop or its stop-after limit. Restart to start it again.`;
|
|
29831
|
+
var describeDeletedExit = (workspace) => `Coder workspace ${workspace} was deleted. Restart to get a fresh workspace.`;
|
|
29832
|
+
var REMOTE_EXIT_REASON_BY_STATUS = {
|
|
29833
|
+
stopped: describeStoppedExit,
|
|
29834
|
+
stopping: describeStoppedExit,
|
|
29835
|
+
deleting: describeDeletedExit,
|
|
29836
|
+
gone: describeDeletedExit,
|
|
29837
|
+
failed: (workspace) => `Coder workspace ${workspace} failed. Restart to get a fresh workspace.`
|
|
29838
|
+
};
|
|
29839
|
+
function remoteExitReason({ workspace, status }) {
|
|
29840
|
+
const describeExit = status === null ? void 0 : REMOTE_EXIT_REASON_BY_STATUS[status];
|
|
29841
|
+
if (describeExit) return describeExit(workspace);
|
|
29842
|
+
return `The connection to Coder workspace ${workspace} was lost. Restart to reconnect.`;
|
|
29843
|
+
}
|
|
29844
|
+
function coderNotSignedInReason(failure) {
|
|
29845
|
+
return `The coder CLI on this host is not signed in. Run coder login, then try again. (${failure})`;
|
|
29717
29846
|
}
|
|
29718
29847
|
//#endregion
|
|
29719
29848
|
//#region server/coder/coder-env-poller.ts
|
|
29720
|
-
function createCoderEnvPoller({ listWorkspaces, stopWorkspace, deleteWorkspace, sessionsForIdleCheck, mappings, busyWorkspaces, isReconcilePending, deleteAfterStoppedDays, onReconciled, now = Date.now, log = console, ...loopOptions }) {
|
|
29849
|
+
function createCoderEnvPoller({ listWorkspaces, stopWorkspace, deleteWorkspace, sessionsForIdleCheck, mappings, busyWorkspaces, ownedWorkspaces, isReconcilePending, deleteAfterStoppedDays, onReconciled, now = Date.now, log = console, ...loopOptions }) {
|
|
29721
29850
|
const stoppedIdlePeriods = /* @__PURE__ */ new Map();
|
|
29851
|
+
async function mutateUnmappedWorkspace(mutate, workspace) {
|
|
29852
|
+
try {
|
|
29853
|
+
await mutate(workspace);
|
|
29854
|
+
} catch (error) {
|
|
29855
|
+
log.warn(`[coder-env] ${errorMessage(error)}; skipping ${workspace}`);
|
|
29856
|
+
}
|
|
29857
|
+
}
|
|
29722
29858
|
async function runTick() {
|
|
29723
29859
|
try {
|
|
29724
29860
|
const workspaces = parseGlimmervoidWorkspaces(await listWorkspaces());
|
|
@@ -29726,20 +29862,24 @@ function createCoderEnvPoller({ listWorkspaces, stopWorkspace, deleteWorkspace,
|
|
|
29726
29862
|
if (loop.isStopped()) return;
|
|
29727
29863
|
const workspaceByName = new Map(workspaces.map((workspace) => [workspace.name, workspace]));
|
|
29728
29864
|
const stoppedThisTick = /* @__PURE__ */ new Set();
|
|
29865
|
+
const plan = planReconcile({
|
|
29866
|
+
workspaces,
|
|
29867
|
+
mappings: mappings(),
|
|
29868
|
+
busyWorkspaces: busyWorkspaces(),
|
|
29869
|
+
ownedWorkspaces: ownedWorkspaces(),
|
|
29870
|
+
nowMs: now(),
|
|
29871
|
+
deleteAfterStoppedDays: deleteAfterStoppedDays()
|
|
29872
|
+
});
|
|
29873
|
+
for (const workspace of plan.delete) {
|
|
29874
|
+
if (loop.isStopped()) return;
|
|
29875
|
+
await mutateUnmappedWorkspace(deleteWorkspace, workspace);
|
|
29876
|
+
}
|
|
29877
|
+
for (const workspace of plan.stopUnmapped) {
|
|
29878
|
+
if (loop.isStopped()) return;
|
|
29879
|
+
log.warn(`[coder-env] stopping unmapped workspace ${workspace}`);
|
|
29880
|
+
await mutateUnmappedWorkspace(stopWorkspace, workspace);
|
|
29881
|
+
}
|
|
29729
29882
|
if (isReconcilePending()) {
|
|
29730
|
-
const staleAfterDays = deleteAfterStoppedDays();
|
|
29731
|
-
const plan = planReconcile({
|
|
29732
|
-
workspaces,
|
|
29733
|
-
mappings: mappings(),
|
|
29734
|
-
busyWorkspaces: busyWorkspaces(),
|
|
29735
|
-
nowMs: now(),
|
|
29736
|
-
deleteAfterStoppedDays: staleAfterDays
|
|
29737
|
-
});
|
|
29738
|
-
if (plan.keptUnmapped.length > 0) log.warn(`[coder-env] keeping unmapped workspaces until stopped for ${staleAfterDays} days: ${plan.keptUnmapped.join(", ")}`);
|
|
29739
|
-
for (const workspace of plan.delete) {
|
|
29740
|
-
if (loop.isStopped()) return;
|
|
29741
|
-
await deleteWorkspace(workspace);
|
|
29742
|
-
}
|
|
29743
29883
|
for (const mapping of plan.stop) {
|
|
29744
29884
|
if (loop.isStopped()) return;
|
|
29745
29885
|
const status = workspaceByName.get(mapping.workspace)?.status;
|
|
@@ -29792,15 +29932,55 @@ function createCoderEnvPoller({ listWorkspaces, stopWorkspace, deleteWorkspace,
|
|
|
29792
29932
|
}
|
|
29793
29933
|
//#endregion
|
|
29794
29934
|
//#region server/coder/coder-env-wiring.ts
|
|
29795
|
-
|
|
29935
|
+
var CODER_UNAVAILABLE_REASON = "Coder environments are unavailable on this host";
|
|
29936
|
+
var REMOTE_SESSIONS_OFF_REFUSAL = {
|
|
29937
|
+
ok: false,
|
|
29938
|
+
reason: "Remote Coder sessions are off. Turn on Remote Coder sessions (experimental) in Settings."
|
|
29939
|
+
};
|
|
29940
|
+
function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {}, loadOwnedWorkspaces, saveOwnedWorkspaces, runCoder: executeCoder, env = process.env, platform = process.platform, fetch: fetchReady = globalThis.fetch, createPoller = createCoderEnvPoller, now = Date.now, log = console, ...pollerDeps }) {
|
|
29796
29941
|
let binaryProbe = null;
|
|
29797
29942
|
let isCoderAvailable = false;
|
|
29943
|
+
let unavailableReason = CODER_UNAVAILABLE_REASON;
|
|
29944
|
+
const signedOutErrors = /* @__PURE__ */ new WeakSet();
|
|
29798
29945
|
let hasReconciled = false;
|
|
29799
29946
|
const operationCountByWorkspace = /* @__PURE__ */ new Map();
|
|
29800
29947
|
const lastSeenStatusByWorkspace = /* @__PURE__ */ new Map();
|
|
29801
29948
|
const provisionByProject = /* @__PURE__ */ new Map();
|
|
29802
29949
|
const failedProvisionByProject = /* @__PURE__ */ new Map();
|
|
29950
|
+
const renewalByProject = /* @__PURE__ */ new Map();
|
|
29803
29951
|
const setTimeoutFn = pollerDeps.setTimeoutFn ?? DEFAULT_TIMER_FNS.setTimeoutFn;
|
|
29952
|
+
let ownedWorkspaces = /* @__PURE__ */ new Set([...loadOwnedWorkspaces(), ...currentMappings().map((mapping) => mapping.workspace)]);
|
|
29953
|
+
let ownedSaveChain = Promise.resolve();
|
|
29954
|
+
function currentMappings() {
|
|
29955
|
+
return config.projects.flatMap((project) => project.id && project.environment ? [{
|
|
29956
|
+
projectId: project.id,
|
|
29957
|
+
workspace: project.environment.workspace,
|
|
29958
|
+
wasActive: project.wasActive === true
|
|
29959
|
+
}] : []);
|
|
29960
|
+
}
|
|
29961
|
+
function busyWorkspaces() {
|
|
29962
|
+
return new Set(operationCountByWorkspace.keys());
|
|
29963
|
+
}
|
|
29964
|
+
function saveOwned() {
|
|
29965
|
+
const save = ownedSaveChain.then(() => saveOwnedWorkspaces([...ownedWorkspaces]));
|
|
29966
|
+
ownedSaveChain = save.catch(() => {});
|
|
29967
|
+
return save;
|
|
29968
|
+
}
|
|
29969
|
+
async function pruneOwned(workspaces) {
|
|
29970
|
+
const retained = retainedOwnedWorkspaces({
|
|
29971
|
+
ownedWorkspaces,
|
|
29972
|
+
workspaces,
|
|
29973
|
+
mappings: currentMappings(),
|
|
29974
|
+
busyWorkspaces: busyWorkspaces()
|
|
29975
|
+
});
|
|
29976
|
+
if (retained.size === ownedWorkspaces.size) return;
|
|
29977
|
+
ownedWorkspaces = retained;
|
|
29978
|
+
try {
|
|
29979
|
+
await saveOwned();
|
|
29980
|
+
} catch (error) {
|
|
29981
|
+
log.warn(`[coder-env] could not save owned Coder workspaces: ${errorMessage(error)}`);
|
|
29982
|
+
}
|
|
29983
|
+
}
|
|
29804
29984
|
async function runCoder(args, { timeoutMs, input = "", env: commandEnv }) {
|
|
29805
29985
|
try {
|
|
29806
29986
|
const options = {
|
|
@@ -29820,10 +30000,21 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
29820
30000
|
maxBuffer: 16777216
|
|
29821
30001
|
});
|
|
29822
30002
|
} catch (error) {
|
|
29823
|
-
const
|
|
29824
|
-
|
|
30003
|
+
const stderr = error instanceof Error && "stderr" in error ? error.stderr : null;
|
|
30004
|
+
const detail = coderErrorDetail(stderr);
|
|
30005
|
+
const failure = detail ? `Coder ${args[0]} failed: ${detail}` : `Coder ${args[0]} failed`;
|
|
30006
|
+
if (!isCoderSignedOutError(stderr)) throw new Error(failure);
|
|
30007
|
+
throw markSignedOut(failure);
|
|
29825
30008
|
}
|
|
29826
30009
|
}
|
|
30010
|
+
function markSignedOut(failure) {
|
|
30011
|
+
binaryProbe = null;
|
|
30012
|
+
isCoderAvailable = false;
|
|
30013
|
+
unavailableReason = coderNotSignedInReason(failure);
|
|
30014
|
+
const signedOutError = new Error(unavailableReason);
|
|
30015
|
+
signedOutErrors.add(signedOutError);
|
|
30016
|
+
return signedOutError;
|
|
30017
|
+
}
|
|
29827
30018
|
function command(args, timeoutMs = 3e4) {
|
|
29828
30019
|
return runCoder(args, { timeoutMs });
|
|
29829
30020
|
}
|
|
@@ -29840,33 +30031,53 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
29840
30031
|
"-o",
|
|
29841
30032
|
"json"
|
|
29842
30033
|
])).stdout;
|
|
29843
|
-
recordListing(listingJson);
|
|
30034
|
+
const workspaces = recordListing(listingJson);
|
|
30035
|
+
if (workspaces !== null) await pruneOwned(workspaces);
|
|
29844
30036
|
return listingJson;
|
|
29845
30037
|
}
|
|
29846
|
-
async function listWorkspaces() {
|
|
30038
|
+
async function listWorkspaces(timeoutMs) {
|
|
29847
30039
|
const workspaces = recordListing((await command([
|
|
29848
30040
|
"list",
|
|
29849
30041
|
"-o",
|
|
29850
30042
|
"json"
|
|
29851
|
-
])).stdout);
|
|
30043
|
+
], timeoutMs)).stdout);
|
|
29852
30044
|
if (workspaces === null) throw new Error("Invalid Coder workspace listing");
|
|
29853
30045
|
return workspaces;
|
|
29854
30046
|
}
|
|
29855
30047
|
function workspaceStatus(workspace) {
|
|
29856
30048
|
return lastSeenStatusByWorkspace.get(workspace) ?? null;
|
|
29857
30049
|
}
|
|
30050
|
+
async function describeRemoteExit(workspace) {
|
|
30051
|
+
const workspaces = await listWorkspaces(1e4).catch(() => null);
|
|
30052
|
+
if (workspaces === null) return remoteExitReason({
|
|
30053
|
+
workspace,
|
|
30054
|
+
status: null
|
|
30055
|
+
});
|
|
30056
|
+
return remoteExitReason({
|
|
30057
|
+
workspace,
|
|
30058
|
+
status: workspaces.find((candidate) => candidate.name === workspace)?.status ?? "gone"
|
|
30059
|
+
});
|
|
30060
|
+
}
|
|
29858
30061
|
function isConfigured() {
|
|
29859
30062
|
return Boolean(config.coder?.environments?.length) && (platform === "linux" || platform === "darwin");
|
|
29860
30063
|
}
|
|
29861
30064
|
async function probeBinary() {
|
|
29862
30065
|
try {
|
|
29863
30066
|
await command(["version"], 5e3);
|
|
29864
|
-
isCoderAvailable = true;
|
|
29865
|
-
return true;
|
|
29866
30067
|
} catch {
|
|
30068
|
+
unavailableReason = CODER_UNAVAILABLE_REASON;
|
|
29867
30069
|
log.warn("[coder-env] coder binary is unavailable");
|
|
29868
30070
|
return false;
|
|
29869
30071
|
}
|
|
30072
|
+
try {
|
|
30073
|
+
await command(["whoami"], 15e3);
|
|
30074
|
+
} catch (error) {
|
|
30075
|
+
if (!(error instanceof Error && signedOutErrors.has(error))) unavailableReason = `${CODER_UNAVAILABLE_REASON}: ${errorMessage(error)}`;
|
|
30076
|
+
log.warn(`[coder-env] ${errorMessage(error)}`);
|
|
30077
|
+
return false;
|
|
30078
|
+
}
|
|
30079
|
+
isCoderAvailable = true;
|
|
30080
|
+
return true;
|
|
29870
30081
|
}
|
|
29871
30082
|
async function ensureAvailable() {
|
|
29872
30083
|
if (!isConfigured()) return false;
|
|
@@ -29910,12 +30121,9 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
29910
30121
|
"--yes"
|
|
29911
30122
|
]);
|
|
29912
30123
|
},
|
|
29913
|
-
mappings:
|
|
29914
|
-
|
|
29915
|
-
|
|
29916
|
-
wasActive: project.wasActive === true
|
|
29917
|
-
}] : []),
|
|
29918
|
-
busyWorkspaces: () => new Set(operationCountByWorkspace.keys()),
|
|
30124
|
+
mappings: currentMappings,
|
|
30125
|
+
busyWorkspaces,
|
|
30126
|
+
ownedWorkspaces: () => ownedWorkspaces,
|
|
29919
30127
|
isReconcilePending: () => !hasReconciled,
|
|
29920
30128
|
deleteAfterStoppedDays: () => Math.max(...(config.coder?.environments ?? []).map((environment) => resolveCoderEnvironment(environment).deleteAfterStoppedDays)),
|
|
29921
30129
|
onReconciled: async (plan) => {
|
|
@@ -29961,8 +30169,16 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
29961
30169
|
}
|
|
29962
30170
|
async function waitForReady(workspaceName, environment) {
|
|
29963
30171
|
const startedAtMs = now();
|
|
30172
|
+
let consecutiveListFailures = 0;
|
|
29964
30173
|
for (;;) {
|
|
29965
|
-
const workspace = (await listWorkspaces()
|
|
30174
|
+
const workspace = (await listWorkspaces().then((listed) => {
|
|
30175
|
+
consecutiveListFailures = 0;
|
|
30176
|
+
return listed;
|
|
30177
|
+
}, (listError) => {
|
|
30178
|
+
consecutiveListFailures += 1;
|
|
30179
|
+
if (consecutiveListFailures >= 3) throw listError;
|
|
30180
|
+
return [];
|
|
30181
|
+
})).find((candidate) => candidate.name === workspaceName);
|
|
29966
30182
|
let isReadyUrlUp = null;
|
|
29967
30183
|
if (environment.readyUrl) {
|
|
29968
30184
|
isReadyUrlUp = false;
|
|
@@ -30004,7 +30220,7 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30004
30220
|
cleanup,
|
|
30005
30221
|
workspace,
|
|
30006
30222
|
"--yes"
|
|
30007
|
-
]);
|
|
30223
|
+
], 12e5);
|
|
30008
30224
|
} catch {
|
|
30009
30225
|
if (await isWorkspaceListed(workspace)) reasons.push(`Coder cleanup ${cleanup} failed for ${workspace}`);
|
|
30010
30226
|
}
|
|
@@ -30014,10 +30230,7 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30014
30230
|
};
|
|
30015
30231
|
}
|
|
30016
30232
|
async function reserve({ projectId, environmentName, branch, liveCount }) {
|
|
30017
|
-
if (config.remoteSessions !== true) return
|
|
30018
|
-
ok: false,
|
|
30019
|
-
reason: "Remote Coder sessions are off. Turn on Remote Coder sessions (experimental) in Settings."
|
|
30020
|
-
};
|
|
30233
|
+
if (config.remoteSessions !== true) return REMOTE_SESSIONS_OFF_REFUSAL;
|
|
30021
30234
|
const environment = resolveEnvironment(environmentName);
|
|
30022
30235
|
if (!environment) return {
|
|
30023
30236
|
ok: false,
|
|
@@ -30038,8 +30251,14 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30038
30251
|
};
|
|
30039
30252
|
if (!await ensureAvailable()) return {
|
|
30040
30253
|
ok: false,
|
|
30041
|
-
reason:
|
|
30254
|
+
reason: unavailableReason
|
|
30042
30255
|
};
|
|
30256
|
+
return freshMapping({
|
|
30257
|
+
environmentName,
|
|
30258
|
+
branch
|
|
30259
|
+
});
|
|
30260
|
+
}
|
|
30261
|
+
function freshMapping({ environmentName, branch }) {
|
|
30043
30262
|
const workspace = workspaceNameFromHex(randomBytes(6).toString("hex"));
|
|
30044
30263
|
if (workspace === null) return {
|
|
30045
30264
|
ok: false,
|
|
@@ -30055,6 +30274,74 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30055
30274
|
}
|
|
30056
30275
|
};
|
|
30057
30276
|
}
|
|
30277
|
+
function renew(request) {
|
|
30278
|
+
if (config.remoteSessions !== true) return Promise.resolve(REMOTE_SESSIONS_OFF_REFUSAL);
|
|
30279
|
+
const inFlightRenewal = renewalByProject.get(request.projectId);
|
|
30280
|
+
if (inFlightRenewal) return inFlightRenewal;
|
|
30281
|
+
const renewal = renewUnclaimed(request);
|
|
30282
|
+
renewalByProject.set(request.projectId, renewal);
|
|
30283
|
+
renewal.finally(() => renewalByProject.delete(request.projectId));
|
|
30284
|
+
return renewal;
|
|
30285
|
+
}
|
|
30286
|
+
async function renewUnclaimed({ projectId, repoRemoteUrl, baseBranch, liveCount }) {
|
|
30287
|
+
if (provisionByProject.has(projectId)) return {
|
|
30288
|
+
ok: true,
|
|
30289
|
+
renewed: false
|
|
30290
|
+
};
|
|
30291
|
+
const mapping = config.projects.find((project) => project.id === projectId)?.environment;
|
|
30292
|
+
if (!mapping) return {
|
|
30293
|
+
ok: false,
|
|
30294
|
+
reason: `Project ${projectId} has no Coder workspace mapping`
|
|
30295
|
+
};
|
|
30296
|
+
if (!failedProvisionByProject.has(projectId) && await isWorkspaceListed(mapping.workspace) && workspaceStatus(mapping.workspace) !== "failed") return {
|
|
30297
|
+
ok: true,
|
|
30298
|
+
renewed: false
|
|
30299
|
+
};
|
|
30300
|
+
const environment = resolveEnvironment(mapping.name);
|
|
30301
|
+
if (!environment) return {
|
|
30302
|
+
ok: false,
|
|
30303
|
+
reason: `Unknown Coder environment: ${mapping.name}`
|
|
30304
|
+
};
|
|
30305
|
+
const limitReason = liveLimitRefusal({
|
|
30306
|
+
liveCount,
|
|
30307
|
+
maxLive: environment.maxLive,
|
|
30308
|
+
environmentName: mapping.name
|
|
30309
|
+
});
|
|
30310
|
+
if (limitReason) return {
|
|
30311
|
+
ok: false,
|
|
30312
|
+
reason: limitReason
|
|
30313
|
+
};
|
|
30314
|
+
if (!await ensureAvailable()) return {
|
|
30315
|
+
ok: false,
|
|
30316
|
+
reason: unavailableReason
|
|
30317
|
+
};
|
|
30318
|
+
const fresh = freshMapping({
|
|
30319
|
+
environmentName: mapping.name,
|
|
30320
|
+
branch: mapping.branch
|
|
30321
|
+
});
|
|
30322
|
+
if (!fresh.ok) return fresh;
|
|
30323
|
+
try {
|
|
30324
|
+
await saveMapping(projectId, fresh.mapping);
|
|
30325
|
+
} catch (error) {
|
|
30326
|
+
return {
|
|
30327
|
+
ok: false,
|
|
30328
|
+
reason: `Could not save the new Coder workspace mapping: ${errorMessage(error)}`
|
|
30329
|
+
};
|
|
30330
|
+
}
|
|
30331
|
+
failedProvisionByProject.delete(projectId);
|
|
30332
|
+
provision({
|
|
30333
|
+
projectId,
|
|
30334
|
+
mapping: fresh.mapping,
|
|
30335
|
+
repoRemoteUrl,
|
|
30336
|
+
baseBranch
|
|
30337
|
+
}).then((provisioned) => {
|
|
30338
|
+
if (!provisioned.ok) log.warn(`[coder-env] fresh Coder workspace for project ${projectId} failed: ${provisioned.reason}`);
|
|
30339
|
+
});
|
|
30340
|
+
return {
|
|
30341
|
+
ok: true,
|
|
30342
|
+
renewed: true
|
|
30343
|
+
};
|
|
30344
|
+
}
|
|
30058
30345
|
function provision({ projectId, mapping, repoRemoteUrl, baseBranch }) {
|
|
30059
30346
|
const savedMapping = config.projects.find((candidate) => candidate.id === projectId)?.environment;
|
|
30060
30347
|
if (JSON.stringify(savedMapping) !== JSON.stringify(mapping)) return Promise.resolve({
|
|
@@ -30083,7 +30370,7 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30083
30370
|
try {
|
|
30084
30371
|
const environment = resolveEnvironment(mapping.name);
|
|
30085
30372
|
if (!environment) throw new Error(`Unknown Coder environment: ${mapping.name}`);
|
|
30086
|
-
if (!await ensureAvailable()) throw new Error(
|
|
30373
|
+
if (!await ensureAvailable()) throw new Error(unavailableReason);
|
|
30087
30374
|
const createArgs = coderCreateArgs({
|
|
30088
30375
|
workspace,
|
|
30089
30376
|
template: environment.template,
|
|
@@ -30095,8 +30382,14 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30095
30382
|
checkoutRoot: environment.checkoutRoot,
|
|
30096
30383
|
workspace
|
|
30097
30384
|
});
|
|
30385
|
+
ownedWorkspaces.add(workspace);
|
|
30386
|
+
await saveOwned().catch((saveError) => {
|
|
30387
|
+
throw new Error(`Could not record Coder workspace ${workspace} as owned by this host: ${errorMessage(saveError)}`);
|
|
30388
|
+
});
|
|
30098
30389
|
didCreate = true;
|
|
30099
|
-
await command(createArgs, 12e5)
|
|
30390
|
+
await command(createArgs, 12e5).catch(async (createError) => {
|
|
30391
|
+
if (!(await listWorkspaces().catch(() => [])).some((candidate) => candidate.name === workspace)) throw createError;
|
|
30392
|
+
});
|
|
30100
30393
|
await waitForReady(workspace, environment);
|
|
30101
30394
|
await checkSharing(workspace);
|
|
30102
30395
|
const nodeVersion = await command([
|
|
@@ -30170,7 +30463,7 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30170
30463
|
};
|
|
30171
30464
|
if (!await ensureAvailable()) return {
|
|
30172
30465
|
ok: false,
|
|
30173
|
-
reason:
|
|
30466
|
+
reason: unavailableReason
|
|
30174
30467
|
};
|
|
30175
30468
|
return whileWorkspaceBusy(mapping.workspace, () => startWorkspace({
|
|
30176
30469
|
mapping,
|
|
@@ -30211,7 +30504,7 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30211
30504
|
if (!mapping) return { ok: true };
|
|
30212
30505
|
if (!await ensureAvailable()) return {
|
|
30213
30506
|
ok: false,
|
|
30214
|
-
reason:
|
|
30507
|
+
reason: unavailableReason
|
|
30215
30508
|
};
|
|
30216
30509
|
return whileWorkspaceBusy(mapping.workspace, async () => {
|
|
30217
30510
|
try {
|
|
@@ -30230,12 +30523,94 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30230
30523
|
}
|
|
30231
30524
|
});
|
|
30232
30525
|
}
|
|
30526
|
+
async function uncheckedRemoteWorkReason(mapping) {
|
|
30527
|
+
const environment = resolveEnvironment(mapping.name);
|
|
30528
|
+
if (!environment) throw new Error(`Unknown Coder environment: ${mapping.name}`);
|
|
30529
|
+
if (!await ensureAvailable()) throw new Error(unavailableReason);
|
|
30530
|
+
const status = (await listWorkspaces()).find((candidate) => candidate.name === mapping.workspace)?.status ?? "gone";
|
|
30531
|
+
if (status === "gone") return null;
|
|
30532
|
+
if (status !== "running") throw new Error(`Coder workspace ${mapping.workspace} is ${status}`);
|
|
30533
|
+
const checkoutDirectory = remoteCheckoutDirectory({
|
|
30534
|
+
checkoutRoot: environment.checkoutRoot,
|
|
30535
|
+
workspace: mapping.workspace
|
|
30536
|
+
});
|
|
30537
|
+
const uncommittedStatus = (await command([
|
|
30538
|
+
"ssh",
|
|
30539
|
+
mapping.workspace,
|
|
30540
|
+
"--",
|
|
30541
|
+
remoteCommand([
|
|
30542
|
+
"git",
|
|
30543
|
+
"--no-optional-locks",
|
|
30544
|
+
"-C",
|
|
30545
|
+
checkoutDirectory,
|
|
30546
|
+
"status",
|
|
30547
|
+
"--porcelain"
|
|
30548
|
+
])
|
|
30549
|
+
])).stdout;
|
|
30550
|
+
const unpushedLog = (await command([
|
|
30551
|
+
"ssh",
|
|
30552
|
+
mapping.workspace,
|
|
30553
|
+
"--",
|
|
30554
|
+
remoteCommand([
|
|
30555
|
+
"git",
|
|
30556
|
+
"-C",
|
|
30557
|
+
checkoutDirectory,
|
|
30558
|
+
"log",
|
|
30559
|
+
"--branches",
|
|
30560
|
+
"HEAD",
|
|
30561
|
+
"--not",
|
|
30562
|
+
"--remotes",
|
|
30563
|
+
"--oneline"
|
|
30564
|
+
])
|
|
30565
|
+
])).stdout;
|
|
30566
|
+
const stashList = (await command([
|
|
30567
|
+
"ssh",
|
|
30568
|
+
mapping.workspace,
|
|
30569
|
+
"--",
|
|
30570
|
+
remoteCommand([
|
|
30571
|
+
"git",
|
|
30572
|
+
"-C",
|
|
30573
|
+
checkoutDirectory,
|
|
30574
|
+
"stash",
|
|
30575
|
+
"list"
|
|
30576
|
+
])
|
|
30577
|
+
])).stdout;
|
|
30578
|
+
return remoteWorkRefusal({
|
|
30579
|
+
uncommittedStatus,
|
|
30580
|
+
unpushedLog,
|
|
30581
|
+
stashList,
|
|
30582
|
+
branch: mapping.branch,
|
|
30583
|
+
workspace: mapping.workspace
|
|
30584
|
+
});
|
|
30585
|
+
}
|
|
30586
|
+
async function remoteWorkReason(mapping) {
|
|
30587
|
+
try {
|
|
30588
|
+
return await whileWorkspaceBusy(mapping.workspace, () => uncheckedRemoteWorkReason(mapping));
|
|
30589
|
+
} catch (error) {
|
|
30590
|
+
return `Could not check remote work on ${mapping.branch} in Coder workspace ${mapping.workspace}: ${errorMessage(error)}`;
|
|
30591
|
+
}
|
|
30592
|
+
}
|
|
30593
|
+
async function settleRemoval({ projectId, discardRemoteWork }) {
|
|
30594
|
+
await provisionByProject.get(projectId);
|
|
30595
|
+
const mapping = config.projects.find((project) => project.id === projectId)?.environment;
|
|
30596
|
+
if (!mapping) return { ok: true };
|
|
30597
|
+
const keptWorkReason = discardRemoteWork ? null : await remoteWorkReason(mapping);
|
|
30598
|
+
if (keptWorkReason) return {
|
|
30599
|
+
ok: false,
|
|
30600
|
+
reason: keptWorkReason,
|
|
30601
|
+
canDiscardRemoteWork: true
|
|
30602
|
+
};
|
|
30603
|
+
release({ projectId }).then((released) => {
|
|
30604
|
+
if (!released.ok) log.warn(`[coder-env] ${released.reason}; the orphan sweep will stop ${mapping.workspace}`);
|
|
30605
|
+
});
|
|
30606
|
+
return { ok: true };
|
|
30607
|
+
}
|
|
30233
30608
|
async function uploadFile({ workspace, remotePath, contents, mode }) {
|
|
30234
30609
|
if (workspaceNameFromHex(workspace.slice(3)) !== workspace) throw new Error("Invalid Coder workspace name");
|
|
30235
30610
|
const fileMode = typeof mode === "number" ? mode.toString(8) : mode;
|
|
30236
30611
|
if (!/^[0-7]{3,4}$/.test(fileMode)) throw new Error("Invalid remote file mode");
|
|
30237
30612
|
if (!path.posix.isAbsolute(remotePath)) throw new Error("Remote upload path must be absolute");
|
|
30238
|
-
if (!await ensureAvailable()) throw new Error(
|
|
30613
|
+
if (!await ensureAvailable()) throw new Error(unavailableReason);
|
|
30239
30614
|
await runCoder([
|
|
30240
30615
|
"ssh",
|
|
30241
30616
|
workspace,
|
|
@@ -30259,10 +30634,13 @@ function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {},
|
|
|
30259
30634
|
availableEnvironments,
|
|
30260
30635
|
reserve,
|
|
30261
30636
|
provision,
|
|
30637
|
+
renew,
|
|
30262
30638
|
resume,
|
|
30263
30639
|
release,
|
|
30640
|
+
settleRemoval,
|
|
30264
30641
|
uploadFile,
|
|
30265
|
-
workspaceStatus
|
|
30642
|
+
workspaceStatus,
|
|
30643
|
+
describeRemoteExit
|
|
30266
30644
|
};
|
|
30267
30645
|
}
|
|
30268
30646
|
var DEFAULT_INTERVAL_MS$1 = 216e5;
|
|
@@ -30879,6 +31257,12 @@ function createBranchGcWiring({ config, gitWorkspace, liveSessionIds, liveWorktr
|
|
|
30879
31257
|
};
|
|
30880
31258
|
}
|
|
30881
31259
|
//#endregion
|
|
31260
|
+
//#region shared/contracts/coder-owned-workspaces.ts
|
|
31261
|
+
var CoderOwnedWorkspaces = z.object({
|
|
31262
|
+
version: z.literal(1),
|
|
31263
|
+
workspaces: z.array(z.string().regex(/^gv-[0-9a-f]{12}$/))
|
|
31264
|
+
});
|
|
31265
|
+
//#endregion
|
|
30882
31266
|
//#region shared/display-text.ts
|
|
30883
31267
|
var SECONDS_PER_MINUTE = 60;
|
|
30884
31268
|
var HOURS_PER_DAY = 24;
|
|
@@ -34793,9 +35177,6 @@ function createTaskTitleRefiner({ getConfig, spawnLane, renameSession, nowFn = (
|
|
|
34793
35177
|
function canRefine(session) {
|
|
34794
35178
|
return !isStopped && !session._destroyed && !session.ephemeral && !session.customTitle && settings().enabled;
|
|
34795
35179
|
}
|
|
34796
|
-
function clearPending(session) {
|
|
34797
|
-
session.setPendingTaskTitle(null);
|
|
34798
|
-
}
|
|
34799
35180
|
function clearRecheck(session) {
|
|
34800
35181
|
const state = statesBySession.get(session);
|
|
34801
35182
|
if (!state?.recheckTimer) return;
|
|
@@ -34830,11 +35211,8 @@ function createTaskTitleRefiner({ getConfig, spawnLane, renameSession, nowFn = (
|
|
|
34830
35211
|
for (const [session, request] of queuedBySession) {
|
|
34831
35212
|
queuedBySession.delete(session);
|
|
34832
35213
|
if (session.taskTitlePromptRevision !== request.revision) continue;
|
|
34833
|
-
if (!canRefine(session))
|
|
34834
|
-
|
|
34835
|
-
continue;
|
|
34836
|
-
}
|
|
34837
|
-
if (session.state !== STATES.IDLE && session.state !== STATES.COMPLETE) {
|
|
35214
|
+
if (!canRefine(session)) continue;
|
|
35215
|
+
if (request.currentTitle !== null && session.state !== STATES.IDLE && session.state !== STATES.COMPLETE) {
|
|
34838
35216
|
scheduleRecheck(session);
|
|
34839
35217
|
continue;
|
|
34840
35218
|
}
|
|
@@ -34889,7 +35267,6 @@ function createTaskTitleRefiner({ getConfig, spawnLane, renameSession, nowFn = (
|
|
|
34889
35267
|
clearRecheck(session);
|
|
34890
35268
|
if (!canRefine(session)) {
|
|
34891
35269
|
queuedBySession.delete(session);
|
|
34892
|
-
clearPending(session);
|
|
34893
35270
|
return;
|
|
34894
35271
|
}
|
|
34895
35272
|
const decision = decisionFor(session);
|
|
@@ -34917,13 +35294,9 @@ function createTaskTitleRefiner({ getConfig, spawnLane, renameSession, nowFn = (
|
|
|
34917
35294
|
detach
|
|
34918
35295
|
};
|
|
34919
35296
|
statesBySession.set(session, state);
|
|
34920
|
-
function onPrompt(
|
|
35297
|
+
function onPrompt() {
|
|
34921
35298
|
state.substantivePromptsSinceRefinement++;
|
|
34922
|
-
if (
|
|
34923
|
-
clearPending(session);
|
|
34924
|
-
return;
|
|
34925
|
-
}
|
|
34926
|
-
session.setPendingTaskTitle(prompt);
|
|
35299
|
+
if (session.refinedTaskTitle === null) onTurnEnd(session);
|
|
34927
35300
|
}
|
|
34928
35301
|
function onReset() {
|
|
34929
35302
|
clearRecheck(session);
|
|
@@ -34937,7 +35310,6 @@ function createTaskTitleRefiner({ getConfig, spawnLane, renameSession, nowFn = (
|
|
|
34937
35310
|
clearRecheck(session);
|
|
34938
35311
|
queuedBySession.delete(session);
|
|
34939
35312
|
if (active?.request.session === session) active.controller.abort();
|
|
34940
|
-
clearPending(session);
|
|
34941
35313
|
}
|
|
34942
35314
|
function detach() {
|
|
34943
35315
|
session.off("task-prompt", onPrompt);
|
|
@@ -34956,10 +35328,7 @@ function createTaskTitleRefiner({ getConfig, spawnLane, renameSession, nowFn = (
|
|
|
34956
35328
|
}
|
|
34957
35329
|
async function stop() {
|
|
34958
35330
|
isStopped = true;
|
|
34959
|
-
for (const
|
|
34960
|
-
state.detach();
|
|
34961
|
-
clearPending(session);
|
|
34962
|
-
}
|
|
35331
|
+
for (const state of statesBySession.values()) state.detach();
|
|
34963
35332
|
queuedBySession.clear();
|
|
34964
35333
|
active?.controller.abort();
|
|
34965
35334
|
await active?.settled;
|
|
@@ -46132,8 +46501,29 @@ function createBackendLanes(dependencies) {
|
|
|
46132
46501
|
log: logger
|
|
46133
46502
|
});
|
|
46134
46503
|
const laneTelemetry = telemetry;
|
|
46504
|
+
const coderOwnedWorkspacesPath = path.join(glimmervoidHomeDir$1(), "coder-owned-workspaces.json");
|
|
46135
46505
|
const coderEnv = createCoderEnvWiring({
|
|
46136
46506
|
config,
|
|
46507
|
+
loadOwnedWorkspaces: () => {
|
|
46508
|
+
const outcome = loadJsonStateFileSync({
|
|
46509
|
+
filePath: coderOwnedWorkspacesPath,
|
|
46510
|
+
fsSync: fs,
|
|
46511
|
+
parse: (raw) => CoderOwnedWorkspaces.safeParse(raw).data?.workspaces ?? null,
|
|
46512
|
+
quarantine: false,
|
|
46513
|
+
includeNotDir: false
|
|
46514
|
+
});
|
|
46515
|
+
if (outcome.status === "loaded") return outcome.value;
|
|
46516
|
+
if (outcome.status === "missing") return [];
|
|
46517
|
+
logger.warn(`[coder-env] ignoring ${coderOwnedWorkspacesPath}: ${errorMessage(jsonStateLoadError(outcome))}`);
|
|
46518
|
+
return [];
|
|
46519
|
+
},
|
|
46520
|
+
saveOwnedWorkspaces: (workspaces) => writeJsonAtomic(coderOwnedWorkspacesPath, {
|
|
46521
|
+
version: 1,
|
|
46522
|
+
workspaces
|
|
46523
|
+
}, {
|
|
46524
|
+
mode: 384,
|
|
46525
|
+
mkdir: true
|
|
46526
|
+
}),
|
|
46137
46527
|
persistMapping: (projectId, mapping) => persistSessionField(configStore, config, projectId, "environment", mapping),
|
|
46138
46528
|
sessionsForIdleCheck: () => {
|
|
46139
46529
|
const environmentsByName = new Map((config.coder?.environments ?? []).map((environment) => [environment.name, resolveCoderEnvironment(environment)]));
|
|
@@ -48919,6 +49309,7 @@ function createSessionFactory(dependencies) {
|
|
|
48919
49309
|
}),
|
|
48920
49310
|
nodePath: environment?.nodePath ?? "node",
|
|
48921
49311
|
ensureRunning: async () => coderEnv ? coderEnv.resume({ projectId }) : refusal,
|
|
49312
|
+
describeExit: async () => coderEnv ? coderEnv.describeRemoteExit(mapping.workspace) : null,
|
|
48922
49313
|
uploadFile: async (file) => {
|
|
48923
49314
|
if (!coderEnv) throw new Error(refusal.reason);
|
|
48924
49315
|
await coderEnv.uploadFile({
|
|
@@ -50566,6 +50957,12 @@ function requestValidationErrorReply(msg, message) {
|
|
|
50566
50957
|
ok: false,
|
|
50567
50958
|
error: message
|
|
50568
50959
|
}),
|
|
50960
|
+
"remove-session": () => ({
|
|
50961
|
+
type: "remove-session-result",
|
|
50962
|
+
requestId,
|
|
50963
|
+
ok: false,
|
|
50964
|
+
error: message
|
|
50965
|
+
}),
|
|
50569
50966
|
"delete-hook": () => ({
|
|
50570
50967
|
type: "delete-hook-result",
|
|
50571
50968
|
requestId,
|
|
@@ -50761,9 +51158,56 @@ function registerControlHandlers(controlWss, deps) {
|
|
|
50761
51158
|
console.log(`[control] Added session via UI: ${name}${skipPerms ? " (skip permissions)" : " (permission prompts)"}${remote ? ` (Coder environment ${remote.environment.name})` : ""}`);
|
|
50762
51159
|
return created.project;
|
|
50763
51160
|
}
|
|
50764
|
-
function
|
|
51161
|
+
async function remoteRepoSource(projectPath) {
|
|
51162
|
+
if (!remoteRepoGit) return {
|
|
51163
|
+
ok: false,
|
|
51164
|
+
error: "Coder environments are unavailable on this host"
|
|
51165
|
+
};
|
|
51166
|
+
const repoRemoteUrl = await remoteRepoGit.originUrl({ projectPath });
|
|
51167
|
+
if (!repoRemoteUrl) return {
|
|
51168
|
+
ok: false,
|
|
51169
|
+
error: `${projectPath} has no origin remote, so a Coder workspace cannot clone it.`
|
|
51170
|
+
};
|
|
51171
|
+
const baseBranch = configuredIntegrationBranch(config) ?? await remoteRepoGit.detectDefaultBranch({ projectPath });
|
|
51172
|
+
if (!baseBranch) return {
|
|
51173
|
+
ok: false,
|
|
51174
|
+
error: `Could not find the default branch of ${projectPath} on origin.`
|
|
51175
|
+
};
|
|
51176
|
+
return {
|
|
51177
|
+
ok: true,
|
|
51178
|
+
repoRemoteUrl,
|
|
51179
|
+
baseBranch
|
|
51180
|
+
};
|
|
51181
|
+
}
|
|
51182
|
+
async function restartSession(msg, ws, restart) {
|
|
51183
|
+
const sess = findSession(msg);
|
|
51184
|
+
if (!sess) return;
|
|
51185
|
+
const project = config.projects.find((candidate) => candidate.id === sess.id);
|
|
51186
|
+
if (sess.state !== STATES.FAILED || !project?.environment || !coderEnv) {
|
|
51187
|
+
restart(sess);
|
|
51188
|
+
return;
|
|
51189
|
+
}
|
|
51190
|
+
const source = await remoteRepoSource(project.path);
|
|
51191
|
+
if (!source.ok) {
|
|
51192
|
+
sendError(ws, source.error, { id: sess.id });
|
|
51193
|
+
return;
|
|
51194
|
+
}
|
|
51195
|
+
const renewal = await coderEnv.renew({
|
|
51196
|
+
projectId: project.id,
|
|
51197
|
+
repoRemoteUrl: source.repoRemoteUrl,
|
|
51198
|
+
baseBranch: source.baseBranch,
|
|
51199
|
+
liveCount: liveWorkspaceCount(project.environment.name, project.id)
|
|
51200
|
+
});
|
|
51201
|
+
if (!renewal.ok) {
|
|
51202
|
+
sendError(ws, renewal.reason, { id: sess.id });
|
|
51203
|
+
return;
|
|
51204
|
+
}
|
|
51205
|
+
restart(sess);
|
|
51206
|
+
}
|
|
51207
|
+
function liveWorkspaceCount(environmentName, excludedProjectId) {
|
|
50765
51208
|
return config.projects.filter((project) => {
|
|
50766
51209
|
if (project.environment?.name !== environmentName) return false;
|
|
51210
|
+
if (project.id === excludedProjectId) return false;
|
|
50767
51211
|
const session = sessions.get(project.id);
|
|
50768
51212
|
return session !== void 0 && session.state !== STATES.DORMANT;
|
|
50769
51213
|
}).length;
|
|
@@ -50783,17 +51227,12 @@ function registerControlHandlers(controlWss, deps) {
|
|
|
50783
51227
|
refuseAddSession(msg, ws, { error: "Coder environments are unavailable on this host" });
|
|
50784
51228
|
return;
|
|
50785
51229
|
}
|
|
50786
|
-
const
|
|
50787
|
-
|
|
50788
|
-
|
|
50789
|
-
refuseAddSession(msg, ws, { error: `${projectPath} has no origin remote, so a Coder workspace cannot clone it.` });
|
|
50790
|
-
return;
|
|
50791
|
-
}
|
|
50792
|
-
const baseBranch = configuredIntegrationBranch(config) ?? await remoteRepoGit.detectDefaultBranch({ projectPath });
|
|
50793
|
-
if (!baseBranch) {
|
|
50794
|
-
refuseAddSession(msg, ws, { error: `Could not find the default branch of ${projectPath} on origin.` });
|
|
51230
|
+
const source = await remoteRepoSource(path.resolve((msg.path || "").trim()));
|
|
51231
|
+
if (!source.ok) {
|
|
51232
|
+
refuseAddSession(msg, ws, { error: source.error });
|
|
50795
51233
|
return;
|
|
50796
51234
|
}
|
|
51235
|
+
const { repoRemoteUrl, baseBranch } = source;
|
|
50797
51236
|
const id = generateProjectId();
|
|
50798
51237
|
const reserved = await coderEnv.reserve({
|
|
50799
51238
|
projectId: id,
|
|
@@ -50852,22 +51291,46 @@ function registerControlHandlers(controlWss, deps) {
|
|
|
50852
51291
|
if (sess.state !== STATES.DORMANT) return;
|
|
50853
51292
|
sess.start();
|
|
50854
51293
|
}
|
|
50855
|
-
function
|
|
51294
|
+
async function removeSession(msg) {
|
|
50856
51295
|
const sess = findSession(msg);
|
|
50857
|
-
if (!sess) {
|
|
50858
|
-
|
|
50859
|
-
|
|
50860
|
-
}
|
|
51296
|
+
if (!sess) return {
|
|
51297
|
+
ok: false,
|
|
51298
|
+
error: "Session not found"
|
|
51299
|
+
};
|
|
50861
51300
|
if (agentSessions.get(sess.id) === sess) {
|
|
50862
51301
|
sess.destroy();
|
|
50863
51302
|
console.log(`[control] Removed spawned session via UI: ${sess.name}`);
|
|
50864
|
-
return;
|
|
51303
|
+
return { ok: true };
|
|
51304
|
+
}
|
|
51305
|
+
if (config.projects.some((project) => project.id === sess.id && project.environment) && coderEnv) {
|
|
51306
|
+
sess.killSession();
|
|
51307
|
+
const settlement = await coderEnv.settleRemoval({
|
|
51308
|
+
projectId: sess.id,
|
|
51309
|
+
discardRemoteWork: msg.discardRemoteWork === true
|
|
51310
|
+
});
|
|
51311
|
+
if (!settlement.ok) return {
|
|
51312
|
+
ok: false,
|
|
51313
|
+
error: settlement.reason,
|
|
51314
|
+
canDiscardRemoteWork: settlement.canDiscardRemoteWork
|
|
51315
|
+
};
|
|
50865
51316
|
}
|
|
50866
51317
|
const freshConfig = configStore.save((cfg) => {
|
|
50867
51318
|
cfg.projects = cfg.projects.filter((p) => p.id !== sess.id);
|
|
50868
51319
|
});
|
|
50869
51320
|
if (freshConfig) applyConfigReload(freshConfig);
|
|
50870
51321
|
console.log(`[control] Removed session via UI: ${sess.name}`);
|
|
51322
|
+
return { ok: true };
|
|
51323
|
+
}
|
|
51324
|
+
async function handleRemoveSession(msg, ws) {
|
|
51325
|
+
const removal = await removeSession(msg);
|
|
51326
|
+
if (msg.requestId) {
|
|
51327
|
+
replyTo(ws, msg, "remove-session-result", removal.ok ? {
|
|
51328
|
+
ok: true,
|
|
51329
|
+
error: null
|
|
51330
|
+
} : removal);
|
|
51331
|
+
return;
|
|
51332
|
+
}
|
|
51333
|
+
if (!removal.ok) sendError(ws, removal.error);
|
|
50871
51334
|
}
|
|
50872
51335
|
function handleRenameSession(msg, ws) {
|
|
50873
51336
|
const sess = findSession(msg);
|
|
@@ -52106,14 +52569,12 @@ function registerControlHandlers(controlWss, deps) {
|
|
|
52106
52569
|
if (s) s.killSession();
|
|
52107
52570
|
},
|
|
52108
52571
|
"start-session": handleStartSession,
|
|
52109
|
-
"restart": (msg) => {
|
|
52110
|
-
|
|
52111
|
-
|
|
52112
|
-
|
|
52113
|
-
|
|
52114
|
-
|
|
52115
|
-
if (s) s.forceRestart({ fresh: msg.fresh === true });
|
|
52116
|
-
},
|
|
52572
|
+
"restart": (msg, ws) => restartSession(msg, ws, (s) => {
|
|
52573
|
+
s.restart({ fresh: msg.fresh === true });
|
|
52574
|
+
}),
|
|
52575
|
+
"force-restart": (msg, ws) => restartSession(msg, ws, (s) => {
|
|
52576
|
+
s.forceRestart({ fresh: msg.fresh === true });
|
|
52577
|
+
}),
|
|
52117
52578
|
"dismiss": (msg) => {
|
|
52118
52579
|
const session = findSession(msg);
|
|
52119
52580
|
if (!session) return;
|