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.
Files changed (29) hide show
  1. package/dist/bin/glimmervoid.js +13 -13
  2. package/dist/chunks/{adapters-CyDflXHy.js → adapters-CWIHOfVo.js} +5 -5
  3. package/dist/chunks/{adapters-D8BJffFW.js → adapters-DzClvFt5.js} +1 -1
  4. package/dist/chunks/{agent-api-cli-B7-ToJul.js → agent-api-cli-2FKWf5jj.js} +10 -5
  5. package/dist/chunks/{agent-api-cli-CvCov9_e.js → agent-api-cli-Cn-g_ej_.js} +1 -1
  6. package/dist/chunks/{agent-api-core-CLxZAole.js → agent-api-core-CP4J2B39.js} +19 -5
  7. package/dist/chunks/{agent-setup-cli-py1gUMvJ.js → agent-setup-cli-C9qDV2g5.js} +1 -1
  8. package/dist/chunks/{config-store-CxYqTNbe.js → config-store-Br2PY4Tg.js} +2 -2
  9. package/dist/chunks/config-store-DwYjz6h1.js +1 -0
  10. package/dist/chunks/{contracts-C9Jj9iDo.js → contracts-DJqeCtR3.js} +2 -2
  11. package/dist/chunks/{control-messages-C6JGCTuy.js → control-messages-DHDEMTLI.js} +20 -2
  12. package/dist/chunks/{git-version-D_iXgYTZ.js → git-version-DqagvnAZ.js} +1 -1
  13. package/dist/chunks/{grok-BUGgQVE3.js → grok-DwrAcNHC.js} +1 -1
  14. package/dist/chunks/{main-CRU5ahjl.js → main-ZpBLKI1h.js} +587 -126
  15. package/dist/chunks/{pair-cli-CL1D9xc7.js → pair-cli-DW5mUBZa.js} +2 -2
  16. package/dist/chunks/{remote-config-CaW0mpwr.js → remote-config-DcmhRuRc.js} +1 -1
  17. package/dist/chunks/{session-C8IpFmGQ.js → session-3eSP3t1i.js} +8 -2
  18. package/dist/chunks/{session-routes-nySS7DUE.js → session-routes-B2oSzmQn.js} +1 -1
  19. package/dist/chunks/{settings-cli-Duculey3.js → settings-cli-BnM9rg8B.js} +5 -5
  20. package/dist/chunks/{settings-readout-core-CX7DmEuG.js → settings-readout-core-2XunjdBA.js} +2 -2
  21. package/dist/chunks/{ssh-sign-cli-BEAAFjXg.js → ssh-sign-cli-CmnA5lFM.js} +2 -2
  22. package/dist/client/assets/index-CBGmkbVV.js +176 -0
  23. package/dist/client/assets/index-NhlaT6eP.css +2 -0
  24. package/dist/client/index.html +2 -2
  25. package/dist/server/index.js +1 -1
  26. package/package.json +1 -1
  27. package/dist/chunks/config-store-DcVWBJOV.js +0 -1
  28. package/dist/client/assets/index-CBjFGcSs.js +0 -176
  29. 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 TeamReviewThreadCommentsResponse, B as MyPr, Bt as GH_SEGMENT, Ct as ReviewVerdict, Dt as TeamReviewResolveResponse, E as SESSION_DOC_BODY_CAP_BYTES, Et as TeamReviewCompareFiles, H as MyPrCheckContext, Ht as REPO_SLUG_RE, I as SshSignRequest, It as hasStandingViewerApproval, J as MyPrMergeRequest, Jt as BADGE_LABELS, K as MyPrLaneEntry, L as TASK_TITLE_CONTROL_CHARACTERS, Mt as TeamReviewThreadsRepository, Nt as canApproveAfterComment, O as SESSION_NAME_PATTERN, Ot as TeamReviewStateEntry, Pt as decisionAsIfApprovalRequired, Qt as STATES, R as TaskTitle, Rt as HEX_SHA_RE, S as ConversationQuestion, St as ReviewThreadId, U as MyPrCheckContextsResponse, Ut as repoParts, V as MyPrAutoRebase, Vt as NODE_ID_RE, W as MyPrKeepMergeableRequest, X as MyPrMergeStateResponse, Xt as MERGEABLE_LIVE_STATES, Yt as KILLABLE_STATES, Z as MyPrMergeWhenReadyRequest, Zt as RESTARTABLE_STATES, _ as CONVERSATION_TOOL_RESULT_HOOK_EVENT, bt as ReviewFinding, ct as CommitSha, dt as FindingSeverity, et as MyPrThreadNode, g as CONVERSATION_PERMISSION_HOOK_EVENT, gt as QueuedReview, h as CONVERSATION_APPROVAL_SUMMARY_MAX_CHARS, ht as PrDetail, j as SSH_SIGN_BODY_CAP_BYTES, jt as TeamReviewThreadResult, k as SESSION_NAME_RULE_TEXT, kt as TeamReviewStatus, lt as DECIDING_REVIEW_STATES, m as COHERENCE_HOOKS_INACTIVE, mt as PostingPlan, n as AGENT_URL_ENV, nt as MyPrsSnapshot, pt as GithubReviewState, q as MyPrMergeMethod, r as ASK_USER_QUESTION_TOOL_NAME, rt as MyPrsState, tt as MyPrThreadsResponse, ut as EarlierReview, vt as ReviewComment, w as RefinedTaskTitle, wt as TeamReviewActionRequest, xt as ReviewResult, yt as ReviewDraft, z as LinkedPr, zt as shortSha } from "./session-C8IpFmGQ.js";
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-C6JGCTuy.js";
8
- import { a as permitsHelpCommand, r as describeSessionLimits } from "./session-routes-nySS7DUE.js";
9
- import { a as decideSpawnAllowance, c as describeRoleRefusal, d as projectMyPrs, f as projectReviews, i as decideAgentRequest, n as REFUSAL_REASON, o as deriveChildSessionName, s as describePushLanding, u as parseAgentVerb } from "./agent-api-core-CLxZAole.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-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-D_iXgYTZ.js";
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-CxYqTNbe.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-C9Jj9iDo.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-CyDflXHy.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-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-CaW0mpwr.js";
30
- import { n as configFileViewOf, u as readSettingsForAgent } from "./settings-readout-core-CX7DmEuG.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-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
- await spawnGate.run(() => {
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
- if (!childSession) return refusal(409, `${owned.channel.childName} has already ended`);
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" || Boolean(opts && opts.signal === "resume") || !opts || !opts.hookSeen) {
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
- let hash = 2166136261;
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
- "task-notification"
12820
- ].map((tag) => new RegExp(`<${tag}\\b[^>]*>[\\s\\S]*?</${tag}\\s*>`, "gi"));
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
- if (!DISMISSIBLE_STATES.has(this.state)) return false;
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
- keptUnmapped: unmapped.filter((workspace) => !isStale(workspace)).map((workspace) => workspace.name)
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 errorLine = stderr.split("\n").map((line) => line.trim()).findLast((line) => line.startsWith("error:"));
29715
- if (!errorLine) return null;
29716
- return errorLine.slice(6).trim().slice(0, CODER_ERROR_DETAIL_LIMIT) || null;
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
- function createCoderEnvWiring({ config, persistMapping, onReconciled = () => {}, runCoder: executeCoder, env = process.env, platform = process.platform, fetch: fetchReady = globalThis.fetch, createPoller = createCoderEnvPoller, now = Date.now, log = console, ...pollerDeps }) {
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 detail = coderErrorDetail(error instanceof Error && "stderr" in error ? error.stderr : null);
29824
- throw new Error(detail ? `Coder ${args[0]} failed: ${detail}` : `Coder ${args[0]} failed`);
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: () => config.projects.flatMap((project) => project.id && project.environment ? [{
29914
- projectId: project.id,
29915
- workspace: project.environment.workspace,
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()).find((candidate) => candidate.name === workspaceName);
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: "Coder environments are unavailable on this host"
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("Coder environments are unavailable on this host");
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: "Coder environments are unavailable on this host"
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: "Coder environments are unavailable on this host"
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("Coder environments are unavailable on this host");
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
- clearPending(session);
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({ prompt }) {
35297
+ function onPrompt() {
34921
35298
  state.substantivePromptsSinceRefinement++;
34922
- if (!canRefine(session)) {
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 [session, state] of statesBySession) {
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 liveWorkspaceCount(environmentName) {
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 projectPath = path.resolve((msg.path || "").trim());
50787
- const repoRemoteUrl = await remoteRepoGit.originUrl({ projectPath });
50788
- if (!repoRemoteUrl) {
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 handleRemoveSession(msg, ws) {
51294
+ async function removeSession(msg) {
50856
51295
  const sess = findSession(msg);
50857
- if (!sess) {
50858
- sendError(ws, "Session not found");
50859
- return;
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
- const s = findSession(msg);
52111
- if (s) s.restart({ fresh: msg.fresh === true });
52112
- },
52113
- "force-restart": (msg) => {
52114
- const s = findSession(msg);
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;