@outerlayer/cli 0.5.1 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +9 -0
  3. package/dist/{agent-setup-IFH3FB5X.js → agent-setup-USZ2SAXC.js} +2 -2
  4. package/dist/build-info.json +1 -1
  5. package/dist/{check-4Q33B5ZE.js → check-CCVPBN7A.js} +24 -14
  6. package/dist/{chunk-WOBNRVQT.js → chunk-3DEVYTNQ.js} +1 -1
  7. package/dist/{chunk-F4NU4H3V.js → chunk-43J3M6KB.js} +95 -11
  8. package/dist/{chunk-W7UTFZDH.js → chunk-4NLKY4WR.js} +2 -2
  9. package/dist/{lima-XN65D7GN.js → chunk-77MM5RYD.js} +0 -1
  10. package/dist/{chunk-D2KWEAN6.js → chunk-7DBI6SNT.js} +1 -1
  11. package/dist/{chunk-ZXBITXTS.js → chunk-AIW6GLET.js} +1 -1
  12. package/dist/{chunk-EX6M5JPR.js → chunk-ANK2B36Y.js} +131 -15
  13. package/dist/{chunk-RMAZ2NWA.js → chunk-BNMCXPG7.js} +14 -2
  14. package/dist/{chunk-UFFXNXLQ.js → chunk-FA2BOX4O.js} +31 -29
  15. package/dist/chunk-FBTE2A2X.js +38 -0
  16. package/dist/{chunk-DVDEBNQJ.js → chunk-GQD22KKN.js} +17 -6
  17. package/dist/{chunk-BCJNHMZT.js → chunk-HHJDN6AU.js} +18 -3
  18. package/dist/{chunk-JLDZTG3A.js → chunk-IASWYRVG.js} +1 -1
  19. package/dist/{install-cmd-7WXOIFDO.js → chunk-JWOQNNN2.js} +0 -1
  20. package/dist/{chunk-H4CK2YK4.js → chunk-MIGEDH5P.js} +16 -8
  21. package/dist/{chunk-CHLVJNTE.js → chunk-MXB5VB4Y.js} +17 -5
  22. package/dist/chunk-NQXFAGHU.js +211 -0
  23. package/dist/chunk-OUUINEYB.js +27 -0
  24. package/dist/{chunk-OR6AEEDO.js → chunk-PMAMOSTP.js} +3 -3
  25. package/dist/{chunk-RTXKYLCA.js → chunk-RBOMWNOG.js} +1 -1
  26. package/dist/{chunk-T52PKE2Y.js → chunk-RZSF5B46.js} +149 -7
  27. package/dist/chunk-T6OIJQRT.js +31 -0
  28. package/dist/{chunk-EGABNHZG.js → chunk-UMFWVGJO.js} +148 -18
  29. package/dist/{chunk-UWH5DSUP.js → chunk-VMJ3MMBX.js} +203 -80
  30. package/dist/chunk-ZILGTTY6.js +75 -0
  31. package/dist/{cli-LHSUYYMI.js → cli-7WCNPBYC.js} +134 -61
  32. package/dist/command-ARFK3OYF.js +162 -0
  33. package/dist/{connect-cmd-OJBFHZWP.js → connect-cmd-PCESZLTR.js} +4 -4
  34. package/dist/{context-adopt-UMA4O5NY.js → context-adopt-G5JAELE7.js} +2 -2
  35. package/dist/{context-materialize-G2FUFC72.js → context-materialize-3HLPZAF6.js} +2 -2
  36. package/dist/{dist-WDV7U7UJ.js → dist-RDLP4GGJ.js} +1 -1
  37. package/dist/{doctor-MOA3RIEP.js → doctor-VOMYOHQU.js} +32 -12
  38. package/dist/{emit-artifact-cmd-FDWYKURA.js → emit-artifact-cmd-TBF46VXZ.js} +6 -7
  39. package/dist/{emit-criteria-cmd-OCXHLK34.js → emit-criteria-cmd-3427XKFW.js} +3 -3
  40. package/dist/{emit-finding-cmd-POW7PKIA.js → emit-finding-cmd-GSI75K36.js} +3 -3
  41. package/dist/{emit-result-cmd-ID35SH53.js → emit-result-cmd-3PRQKB55.js} +6 -7
  42. package/dist/{hook-fast-EYENPK6F.js → hook-fast-NJBBZQ5Q.js} +1 -1
  43. package/dist/index.js +3 -2
  44. package/dist/{init-2IPOIXJV.js → init-OHYFIKZD.js} +26 -55
  45. package/dist/{init-FYDJAKTM.js → init-WVR4BZQ4.js} +8 -8
  46. package/dist/{init-cmd-KLIDRVXQ.js → init-cmd-HMFNIFPI.js} +7 -7
  47. package/dist/inspect-XAUSB7L3.js +63 -0
  48. package/dist/install-DSUZE2TJ.js +13 -0
  49. package/dist/install-cmd-453C7IZT.js +8 -0
  50. package/dist/lima-7NPCNGWT.js +6 -0
  51. package/dist/{loop-NEBUGD2Q.js → loop-UAJB34KA.js} +24 -11
  52. package/dist/{machine-ECU3QCZD.js → machine-2JOWGRSZ.js} +7 -7
  53. package/dist/session-sender-ED26V45B.js +28 -0
  54. package/dist/{status-CLATHCTQ.js → status-6WNXX2O7.js} +22 -8
  55. package/dist/{sync-cmd-SWQ5KLPL.js → sync-cmd-YONVGUYS.js} +6 -7
  56. package/dist/{watch-3YW6Z3DQ.js → watch-HHKAF26L.js} +6 -7
  57. package/dist/{work-claim-cmd-3OPWA77E.js → work-claim-cmd-LXBZTMJA.js} +5 -5
  58. package/dist/{work-cmd-NUR4CAG6.js → work-cmd-OVNF44FY.js} +3 -3
  59. package/dist/{work-comment-cmd-6TTTYGHM.js → work-comment-cmd-6DGQNTAH.js} +3 -3
  60. package/dist/{work-launch-YI4CJDAP.js → work-launch-R5HTAP6X.js} +1 -1
  61. package/dist/{work-open-pr-cmd-KZ7QQFLD.js → work-open-pr-cmd-FCZHFHZS.js} +4 -4
  62. package/dist/{work-pr-cmd-QNUBE6IQ.js → work-pr-cmd-QJJVHZDC.js} +2 -2
  63. package/package.json +2 -1
  64. package/dist/chunk-ZOC7JSYC.js +0 -83
  65. package/dist/{chunk-XDDW4FRS.js → chunk-OIRNH6UO.js} +1 -1
@@ -7,23 +7,23 @@ import { createDockerHost, childEnvironment, spawnRun } from './chunk-774HEQL6.j
7
7
  import { RecipeError } from './chunk-QOZKPLVJ.js';
8
8
  import { execCapture } from './chunk-W4SQCOZU.js';
9
9
  import { GUEST_CID } from './chunk-B2G7JHHB.js';
10
- import { WorkCommandError, postJson } from './chunk-RMAZ2NWA.js';
10
+ import { WorkCommandError, postJson } from './chunk-BNMCXPG7.js';
11
11
  import { parseDuKilobytes, hostAllowed } from './chunk-F6GFZXZ3.js';
12
12
  import { RELAY_PORT, CHECKOUT_DIR, JOB_MOUNT, claudeCredential, buildEnvironment, startBuildServices, AGENT_CREDENTIAL_MISSING, GATEWAY_PORT, GATEWAY_SOCKET, CLAUDE_PORT, CLAUDE_SOCKET, DESTINATION_BASE_PORT } from './chunk-AUFG23AA.js';
13
- import { defaultGitRunner, materializeContext } from './chunk-XDDW4FRS.js';
13
+ import { defaultGitRunner, materializeContext } from './chunk-OIRNH6UO.js';
14
14
  import { readContextManifest, readSourceCache } from './chunk-WIOZAJ2W.js';
15
15
  import { signingFetchFor } from './chunk-BCLJSQCV.js';
16
16
  import { signerFor } from './chunk-M5POMKKW.js';
17
17
  import { rootfsPathFor, diskTemplatesDir, RUNNER_FILE_MODE, stagedContextDir, contextHomeDir, ghConfigDir, runnerDirOfBuild, spoolDir, mirrorDir, buildDir, runnerDir, RUNNER_DIR_MODE, vmAssetsDir } from './chunk-ZM2IMMYN.js';
18
18
  import { relative, join, dirname, resolve, basename } from 'path';
19
+ import { readFileSync, existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, accessSync, constants, readdirSync, statSync, rmdirSync, realpathSync, renameSync, lstatSync, unlinkSync, openSync, closeSync, mkdtempSync, createWriteStream, createReadStream } from 'fs';
19
20
  import { spawnSync, spawn } from 'child_process';
20
21
  import { createHash, randomBytes } from 'crypto';
21
- import { existsSync, mkdirSync, chmodSync, truncateSync, rmSync, writeFileSync, realpathSync, renameSync, lstatSync, readFileSync, unlinkSync, readdirSync, openSync, closeSync, mkdtempSync, createWriteStream, statSync, createReadStream } from 'fs';
22
22
  import { createServer, request } from 'http';
23
23
  import { request as request$1 } from 'https';
24
24
  import { Transform } from 'stream';
25
25
  import { pipeline, finished } from 'stream/promises';
26
- import { createGunzip, gunzipSync, constants } from 'zlib';
26
+ import { createGunzip, gunzipSync, constants as constants$1 } from 'zlib';
27
27
  import { isIP, connect, createServer as createServer$1 } from 'net';
28
28
  import { lookup } from 'dns/promises';
29
29
  import { networkInterfaces } from 'os';
@@ -255,6 +255,111 @@ var BuildTokens = class {
255
255
  );
256
256
  }
257
257
  };
258
+ var BUILD_PREFIX = "build-";
259
+ var LEAF = "runner";
260
+ function serviceDirectory(selfCgroup, mount) {
261
+ const line = selfCgroup?.split("\n").find((l) => l.startsWith("0::"));
262
+ if (line === void 0) return void 0;
263
+ const path = line.slice(3).trim();
264
+ if (!path.endsWith(".service")) return void 0;
265
+ return join(mount, path);
266
+ }
267
+ function prepareRunnerCgroup(setup) {
268
+ if (setup.invocationId === void 0 || setup.invocationId === "") return void 0;
269
+ const root = serviceDirectory(setup.selfCgroup, setup.mount);
270
+ if (root === void 0) return void 0;
271
+ const { fs } = setup;
272
+ if (!fs.exists(root) || !fs.writable(join(root, "cgroup.procs"))) return void 0;
273
+ if (!(fs.read(join(root, "cgroup.controllers")) ?? "").split(/\s+/).includes("memory")) return void 0;
274
+ for (const name of fs.list(root)) {
275
+ if (!name.startsWith(BUILD_PREFIX)) continue;
276
+ try {
277
+ fs.rmdir(join(root, name));
278
+ } catch {
279
+ }
280
+ }
281
+ if (!fs.exists(join(root, LEAF))) fs.mkdir(join(root, LEAF));
282
+ fs.write(join(root, LEAF, "cgroup.procs"), String(setup.pid));
283
+ fs.write(join(root, "cgroup.subtree_control"), "+memory");
284
+ return { root };
285
+ }
286
+ function createBuildCgroup(fs, root, buildId, buildMemoryBytes) {
287
+ const path = join(root, `${BUILD_PREFIX}${buildId}`);
288
+ const firecrackerOverheadBytes = 256 * 1024 ** 2;
289
+ fs.mkdir(path);
290
+ try {
291
+ fs.write(join(path, "memory.max"), String(buildMemoryBytes + firecrackerOverheadBytes));
292
+ fs.write(join(path, "memory.swap.max"), "0");
293
+ } catch (err) {
294
+ removeBuildCgroup(fs, path);
295
+ throw err;
296
+ }
297
+ return path;
298
+ }
299
+ function oomKills(fs, path) {
300
+ const match = /^oom_kill (\d+)$/m.exec(fs.read(join(path, "memory.events")) ?? "");
301
+ return match === null ? 0 : Number(match[1]);
302
+ }
303
+ function removeBuildCgroup(fs, path) {
304
+ try {
305
+ fs.rmdir(path);
306
+ } catch (err) {
307
+ if (err.code !== "ENOENT") throw err;
308
+ }
309
+ }
310
+ function realCgroupFs() {
311
+ return {
312
+ exists: (path) => existsSync(path),
313
+ read: (path) => {
314
+ try {
315
+ return readFileSync(path, "utf8");
316
+ } catch {
317
+ return void 0;
318
+ }
319
+ },
320
+ write: (path, data) => writeFileSync(path, data),
321
+ mkdir: (path) => mkdirSync(path),
322
+ rmdir: (path) => rmdirSync(path),
323
+ list: (path) => readdirSync(path).filter((name) => statSync(join(path, name)).isDirectory()),
324
+ writable: (path) => {
325
+ try {
326
+ accessSync(path, constants.W_OK);
327
+ return true;
328
+ } catch {
329
+ return false;
330
+ }
331
+ }
332
+ };
333
+ }
334
+ var delegatedRoot;
335
+ function setUpRunnerCgroup(over = {}) {
336
+ let selfCgroup;
337
+ try {
338
+ selfCgroup = readFileSync("/proc/self/cgroup", "utf8");
339
+ } catch {
340
+ selfCgroup = void 0;
341
+ }
342
+ let refusal;
343
+ try {
344
+ const prepared = prepareRunnerCgroup({ fs: realCgroupFs(), selfCgroup, pid: process.pid, invocationId: process.env.INVOCATION_ID, mount: "/sys/fs/cgroup", ...over });
345
+ delegatedRoot = prepared?.root;
346
+ } catch (err) {
347
+ delegatedRoot = void 0;
348
+ refusal = err instanceof Error ? err.message : String(err);
349
+ }
350
+ return { delegated: delegatedRoot !== void 0, ...refusal === void 0 ? {} : { refusal } };
351
+ }
352
+ function realCgroupPort() {
353
+ const fs = realCgroupFs();
354
+ return {
355
+ create: (buildId, bytes) => {
356
+ if (delegatedRoot === void 0) return void 0;
357
+ return createBuildCgroup(fs, delegatedRoot, buildId, bytes);
358
+ },
359
+ oomKills: (path) => oomKills(fs, path),
360
+ remove: (path) => removeBuildCgroup(fs, path)
361
+ };
362
+ }
258
363
  var BranchPrepareError = class extends Error {
259
364
  };
260
365
  var OWN_PREFIX = "outerlayer/";
@@ -747,7 +852,9 @@ function machineConfig(limits2, paths) {
747
852
  };
748
853
  }
749
854
  function firecrackerCommand(input) {
750
- return { command: input.binary, args: ["--no-api", "--config-file", input.configPath] };
855
+ const args = ["--no-api", "--config-file", input.configPath];
856
+ if (input.cgroup === void 0) return { command: input.binary, args };
857
+ return { command: "sh", args: ["-c", 'echo $$ > "$1/cgroup.procs" && shift && exec "$@"', "sh", input.cgroup, input.binary, ...args] };
751
858
  }
752
859
  var STOP_WAIT_MS = 1e4;
753
860
  var POLL_MS = 20;
@@ -804,6 +911,7 @@ function createMicroVmPorts(over = {}) {
804
911
  assets: over.assets ?? ((runnerDir2) => ensureAssets({ runnerDir: runnerDir2 })),
805
912
  rootfs: over.rootfs ?? createRootfsHost(),
806
913
  firecracker: over.firecracker ?? createFirecrackerHost(),
914
+ cgroup: over.cgroup ?? realCgroupPort(),
807
915
  exec: over.exec ?? execCapture
808
916
  };
809
917
  }
@@ -1093,7 +1201,7 @@ function decode(prefix, gzip) {
1093
1201
  if (!gzip) return prefix;
1094
1202
  try {
1095
1203
  const { compressedWindow, inflatedBytes } = limits();
1096
- return gunzipSync(prefix.subarray(0, compressedWindow), { finishFlush: constants.Z_SYNC_FLUSH, maxOutputLength: inflatedBytes });
1204
+ return gunzipSync(prefix.subarray(0, compressedWindow), { finishFlush: constants$1.Z_SYNC_FLUSH, maxOutputLength: inflatedBytes });
1097
1205
  } catch (err) {
1098
1206
  const code = err.code;
1099
1207
  if (code === "ERR_BUFFER_TOO_LARGE") return refused(TOO_LONG);
@@ -2149,6 +2257,40 @@ function containerCommandLine(input) {
2149
2257
  env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed }
2150
2258
  };
2151
2259
  }
2260
+ function containerSenderCommand(input) {
2261
+ const state = input.job.container;
2262
+ const env = buildEnvironment({
2263
+ config: input.config,
2264
+ item: input.job.item,
2265
+ itemKey: input.job.itemKey,
2266
+ branch: input.job.branch,
2267
+ repository: state.repository,
2268
+ hostEnv: input.runtime.hostEnv,
2269
+ containerEnv: state.containerEnv ?? {},
2270
+ builtIns: state.builtIns === true,
2271
+ identity: input.job.commitIdentity
2272
+ });
2273
+ const args = containerExecArgs({
2274
+ container: state.container,
2275
+ user: state.agentUser ?? ADDED_USER,
2276
+ workdir: CHECKOUT_DIR,
2277
+ env,
2278
+ argv: ["outerlayer", "session-sender"]
2279
+ });
2280
+ return { command: "docker", args: ["exec", ...args], env: { ...childEnvironment(input.runtime.hostEnv), ...env.passed } };
2281
+ }
2282
+ function senderSignalScript(pidfile, signal) {
2283
+ const file = shellQuote(pidfile);
2284
+ return `p="$(cat ${file})" && [ -n "$p" ] && grep -qa session-sender "/proc/$p/cmdline" && kill -${signal} "$p"`;
2285
+ }
2286
+ async function signalInContainer(runtime, state, pidfile, signal) {
2287
+ await runtime.host.exec({
2288
+ container: state.container,
2289
+ user: state.agentUser ?? ADDED_USER,
2290
+ env: NO_ENV,
2291
+ argv: ["/bin/sh", "-c", senderSignalScript(pidfile, signal)]
2292
+ });
2293
+ }
2152
2294
  async function stopContainer(runtime, state) {
2153
2295
  await runtime.host.stop(state.container);
2154
2296
  state.stopped = true;
@@ -2247,4 +2389,4 @@ async function cleanupContainer(runtime, state, jobDir) {
2247
2389
  if (failures.length > 0) throw new Error(failures.join("; "));
2248
2390
  }
2249
2391
 
2250
- export { ADDED_USER, AgentUserError, AssetError, BUILD_LABEL, BuildTokens, GH_CONFIG_YML, MIN_ENGINE_MAJOR, RUNNER_LABEL, classifyRefusal, cleanupContainer, containerCommandLine, createBuildDisk, createContainerRuntime, createRunnerLog, deleteBuildDisk, describeCallFailure, ensureRootfs, ensureTunnel, formatClock, formatDuration, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, prepareBranch, provisionContainer, refusalCode, relativeToRunner, removeStagedContext, resolveAgentUser, retrySend, runBuildSteps, runnerLabelValue, shellQuote, shortClaim, stageGovernedContext, startServicesFor, stopContainer, syncInContainer };
2392
+ export { ADDED_USER, AgentUserError, AssetError, BUILD_LABEL, BuildTokens, GH_CONFIG_YML, MIN_ENGINE_MAJOR, RUNNER_LABEL, classifyRefusal, cleanupContainer, containerCommandLine, containerSenderCommand, createBuildDisk, createContainerRuntime, createRunnerLog, deleteBuildDisk, describeCallFailure, ensureRootfs, ensureTunnel, formatClock, formatDuration, ghHostsYml, gitRemoteFor, isLiteralAddress, keepGhConfigCurrent, lifecycleFailureReason, lifecycleSteps, machineConfig, measureDisk, obtainImage, prepareBranch, provisionContainer, refusalCode, relativeToRunner, removeStagedContext, resolveAgentUser, retrySend, runBuildSteps, runnerLabelValue, senderSignalScript, setUpRunnerCgroup, shellQuote, shortClaim, signalInContainer, stageGovernedContext, startServicesFor, stopContainer, syncInContainer };
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from 'node:module';
3
+ import { RUNNER_DIR_MODE, RUNNER_FILE_MODE } from './chunk-ZM2IMMYN.js';
4
+ import { existsSync, readFileSync, mkdirSync, writeFileSync } from 'fs';
5
+ import { dirname } from 'path';
6
+
7
+ createRequire(import.meta.url);
8
+ function readJobFile(path) {
9
+ if (!existsSync(path)) return null;
10
+ try {
11
+ return JSON.parse(readFileSync(path, "utf8"));
12
+ } catch {
13
+ return null;
14
+ }
15
+ }
16
+ function readJobFileChecked(path) {
17
+ if (!existsSync(path)) return { kind: "missing" };
18
+ try {
19
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
20
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return { kind: "unreadable" };
21
+ return { kind: "ok", job: parsed };
22
+ } catch {
23
+ return { kind: "unreadable" };
24
+ }
25
+ }
26
+ function writeJobFile(path, job) {
27
+ mkdirSync(dirname(path), { recursive: true, mode: RUNNER_DIR_MODE });
28
+ writeFileSync(path, JSON.stringify(job, null, 2), { mode: RUNNER_FILE_MODE });
29
+ }
30
+
31
+ export { readJobFile, readJobFileChecked, writeJobFile };
@@ -1,29 +1,104 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
- import { applyContextProvenance, installScrubContext, prepareSessionForUpload, wholeSessionCounts, sliceTurnIo } from './chunk-ZOC7JSYC.js';
4
- import { LaunchFilter } from './chunk-BCJNHMZT.js';
3
+ import { LaunchFilter } from './chunk-HHJDN6AU.js';
5
4
  import { RepoFilter } from './chunk-TBT347UY.js';
6
5
  import { hookExecSpoolPath } from './chunk-F7CU5ABH.js';
7
6
  import { applyReportedCost } from './chunk-BKO6JEZI.js';
8
- import { migrateLegacyArtifactSidecars, planArtifactUpload, artifactsStatePath, sanitizeTerminal, artifactBlobsDir, resolveArtifactTurnIndex, isArtifactTombstoned, ARTIFACT_RETRY_MAX_AGE_MS, writeArtifactSpoolState, reclaimUnneededArtifactBlobs, neededArtifactShas } from './chunk-WOBNRVQT.js';
9
- import { tailTranscript, enrichSessionRepo, CAPTURE_TIERS, scanAll, setRepoRoot, resolveRepoIdentity, inferArtifactKind, bannedPathsForTier, sessionIdFromPath, SOURCE_ADAPTERS, EVENT_TYPES } from './chunk-EX6M5JPR.js';
7
+ import { migrateLegacyArtifactSidecars, planArtifactUpload, artifactsStatePath, sanitizeTerminal, artifactBlobsDir, resolveArtifactTurnIndex, isArtifactTombstoned, ARTIFACT_RETRY_MAX_AGE_MS, writeArtifactSpoolState, reclaimUnneededArtifactBlobs, neededArtifactShas } from './chunk-3DEVYTNQ.js';
8
+ import { setCustomScrubbers, setLocalIdentity, tailTranscript, enrichSessionRepo, CAPTURE_TIERS, scanAll, setRepoRoot, resolveRepoIdentity, inferArtifactKind, bannedPathsForTier, sessionIdFromPath, scrubDeep, downconvertSession, isHumanUserTurn, turnCountBound, SOURCE_ADAPTERS, EVENT_TYPES } from './chunk-ANK2B36Y.js';
10
9
  import { C } from './chunk-YRYMNBGQ.js';
11
- import { readLaunchSpoolState, launchStatusIsDecisive, launchedItemsBySession, launchesPath } from './chunk-DVDEBNQJ.js';
10
+ import { readLaunchSpoolState, launchStatusIsDecisive, launchedItemsBySession, launchesPath } from './chunk-GQD22KKN.js';
12
11
  import { readLatestProvenanceForSession, readContextProvenance } from './chunk-ZJSNLVCE.js';
13
12
  import { defaultFetch } from './chunk-BCLJSQCV.js';
14
13
  import { resolveGatewayCredentials, unauthorizedMessage } from './chunk-BOLTI6LR.js';
15
14
  import { readCloudConfig, cloudConfigPath, writeCloudConfig } from './chunk-EABW6AJQ.js';
16
15
  import { signerFor, signRunnerRequest } from './chunk-M5POMKKW.js';
17
- import { writeSyncStatus, spoolDir } from './chunk-UFFXNXLQ.js';
16
+ import { writeSyncStatus, spoolDir } from './chunk-FA2BOX4O.js';
18
17
  import { removeJournalHooks } from './chunk-XCCLFVXM.js';
19
18
  import { readRepoConnection, findGitDirs } from './chunk-BJ3KTMDH.js';
20
19
  import { homeFromEnv } from './chunk-VNVZDWO3.js';
21
20
  import { dirname, basename, join } from 'path';
22
21
  import { statSync, readFileSync, readdirSync, mkdirSync, writeFileSync, renameSync, existsSync, rmSync } from 'fs';
22
+ import { execFileSync } from 'child_process';
23
+ import { hostname, userInfo } from 'os';
23
24
  import { fileURLToPath } from 'url';
24
25
  import { createRequire as createRequire$1 } from 'module';
25
26
 
26
27
  createRequire(import.meta.url);
28
+ function detectCi(env = process.env) {
29
+ return env.CI === "true" || env.CI === "1" || !!env.JENKINS_URL;
30
+ }
31
+ function gitGlobalConfig(key) {
32
+ try {
33
+ return execFileSync("git", ["config", "--global", key], { encoding: "utf8", timeout: 2e3 }).trim() || void 0;
34
+ } catch {
35
+ return void 0;
36
+ }
37
+ }
38
+ function installScrubContext(home, scrub) {
39
+ setCustomScrubbers(scrub);
40
+ setLocalIdentity({
41
+ name: gitGlobalConfig("user.name"),
42
+ email: gitGlobalConfig("user.email"),
43
+ username: (() => {
44
+ try {
45
+ return userInfo().username;
46
+ } catch {
47
+ return void 0;
48
+ }
49
+ })(),
50
+ hostname: (() => {
51
+ try {
52
+ return hostname();
53
+ } catch {
54
+ return void 0;
55
+ }
56
+ })(),
57
+ home
58
+ });
59
+ }
60
+ function applyContextProvenance(session, record) {
61
+ if (!record) return;
62
+ if (record.repository !== void 0) session.env.contextSourceRepo = record.repository;
63
+ if (record.ref !== void 0) session.env.contextSourceRef = record.ref;
64
+ if (record.sha !== null) session.env.contextSourceSha = record.sha;
65
+ session.env.contextSourceVerification = record.verification;
66
+ }
67
+ function prepareSessionForUpload(session, tier, envVars = process.env) {
68
+ setRepoRoot(resolveRepoIdentity(session.env.cwd).repoRoot);
69
+ scrubDeep(session);
70
+ session.workerKind ??= detectCi(envVars) ? "ci" : "seat";
71
+ return downconvertSession(session, tier);
72
+ }
73
+ var MAX_TOOL_IO_CHARS = 64e3;
74
+ function sliceToolIo(value) {
75
+ const text = typeof value === "string" ? value : JSON.stringify(value);
76
+ if (typeof text !== "string" || text.length <= MAX_TOOL_IO_CHARS) return value;
77
+ return `${text.slice(0, MAX_TOOL_IO_CHARS)}\u2026[sliced: ${text.length} chars exceeded the sync per-call cap]`;
78
+ }
79
+ function sliceTurnIo(turn) {
80
+ return {
81
+ ...turn,
82
+ toolCalls: (turn.toolCalls ?? []).map((call) => ({
83
+ ...call,
84
+ ...call.input !== void 0 ? { input: sliceToolIo(call.input) } : {},
85
+ ...call.output !== void 0 ? { output: sliceToolIo(call.output) } : {}
86
+ }))
87
+ };
88
+ }
89
+ function wholeSessionCounts(turns) {
90
+ const allCalls = turns.flatMap((t) => t.toolCalls ?? []);
91
+ return {
92
+ // An index BOUND (`1 + max(turn.index)`), not the shipped turn count —
93
+ // turn indexes are not guaranteed dense from zero, and a late artifact
94
+ // anchored at its real turn index must still validate against the
95
+ // summary row this produces (see turnCountBound).
96
+ turns: turnCountBound(turns),
97
+ toolCalls: allCalls.length,
98
+ errors: allCalls.filter((c) => c.status === "error").length,
99
+ userTurns: turns.filter(isHumanUserTurn).length
100
+ };
101
+ }
27
102
  var IRRELEVANT = /* @__PURE__ */ new Set(["__tests__", "__mocks__", "node_modules"]);
28
103
  function newestSourceMs(dir) {
29
104
  let newest = 0;
@@ -481,6 +556,8 @@ function retryAfterMs(response) {
481
556
  }
482
557
  var LiveUploader = class {
483
558
  constructor(opts) {
559
+ /** Set by `stop()`: no more debounce or retry waits, so what is pending is sent once and given up on. */
560
+ this.stopping = false;
484
561
  this.cycles = /* @__PURE__ */ new Map();
485
562
  /** Sessions this sender is done with: ended, or permanently rejected. The
486
563
  * daemon keeps mirroring a transcript after its session closes, and every
@@ -491,12 +568,31 @@ var LiveUploader = class {
491
568
  * every session, so the sender stops entirely for the rest of this
492
569
  * process rather than retrying session by session. */
493
570
  this.stopped = false;
571
+ this.warned = /* @__PURE__ */ new Set();
494
572
  this.home = opts.home;
495
573
  this.fetchImpl = opts.fetchImpl ?? fetch;
496
574
  this.sleepImpl = opts.sleepImpl ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
497
575
  this.debounceMs = opts.debounceMs ?? DEFAULT_DEBOUNCE_MS;
498
576
  this.env = opts.env ?? process.env;
499
577
  this.stderr = opts.stderr ?? ((s) => void process.stderr.write(s));
578
+ this.build = opts.build;
579
+ this.onUnauthorized = opts.onUnauthorized;
580
+ }
581
+ /**
582
+ * Final pass for a build sender: sends what is pending now, without waiting out the debounce and without
583
+ * retrying a failed send, and resolves once every cycle has finished or `timeoutMs` has passed.
584
+ */
585
+ async stop(timeoutMs) {
586
+ this.stopping = true;
587
+ let timer;
588
+ const expired = new Promise((resolve) => {
589
+ timer = setTimeout(resolve, timeoutMs);
590
+ });
591
+ try {
592
+ await Promise.race([this.flush(), expired]);
593
+ } finally {
594
+ clearTimeout(timer);
595
+ }
500
596
  }
501
597
  /** Called for every daemon `mirrored` event. Cheap and synchronous — the
502
598
  * actual read/send work happens on the debounced cycle. */
@@ -536,7 +632,7 @@ var LiveUploader = class {
536
632
  for (; ; ) {
537
633
  for (; ; ) {
538
634
  const generationAtWaitStart = cycle.generation;
539
- await this.sleepImpl(this.debounceMs);
635
+ if (!this.stopping) await this.sleepImpl(this.debounceMs);
540
636
  if (cycle.generation !== generationAtWaitStart) continue;
541
637
  break;
542
638
  }
@@ -562,8 +658,21 @@ var LiveUploader = class {
562
658
  const priorEntry = state.sessions[key];
563
659
  if (priorEntry?.ended || priorEntry?.failed) return true;
564
660
  const cloudConfig = readCloudConfig(this.home);
565
- if (cloudConfig.autoSync === false) return false;
566
- if (!cloudConfig.url || !(cloudConfig.apiKey || cloudConfig.accountToken)) return false;
661
+ let destination;
662
+ if (this.build === void 0) {
663
+ if (cloudConfig.autoSync === false) return false;
664
+ if (!cloudConfig.url || !(cloudConfig.apiKey || cloudConfig.accountToken)) return false;
665
+ } else {
666
+ const url = this.env.OUTERLAYER_URL;
667
+ const credential = this.env.OUTERLAYER_API_KEY;
668
+ const appId = this.env.OUTERLAYER_APP_ID;
669
+ if (!url || !credential || !appId) {
670
+ this.warnOnce("incomplete", `live upload is idle: this build's environment lacks ${[!url && "OUTERLAYER_URL", !credential && "OUTERLAYER_API_KEY", !appId && "OUTERLAYER_APP_ID"].filter(Boolean).join(", ")}, so nothing streams until \`outerlayer sync\` runs.
671
+ `);
672
+ return false;
673
+ }
674
+ destination = { ok: true, url, credential, appId, kind: "key" };
675
+ }
567
676
  const info = subagentInfo(mirrorPath);
568
677
  const tail = tailTranscript(mirrorPath, void 0, {
569
678
  isSubagent: info.isSubagent,
@@ -573,17 +682,16 @@ var LiveUploader = class {
573
682
  if (!tail.session) return false;
574
683
  const session = tail.session;
575
684
  enrichSessionRepo(session);
576
- const destination = resolveGatewayCredentials({ home: this.home, env: {}, cwd: session.env.cwd ?? "/" });
685
+ destination ??= resolveGatewayCredentials({ home: this.home, env: {}, cwd: session.env.cwd ?? "/" });
577
686
  if (!destination.ok) return false;
578
- const repoFilter = new RepoFilter(cloudConfig.repos);
579
- if (!repoFilter.allows(session.env.gitRepo)) return false;
687
+ if (this.build === void 0 && !new RepoFilter(cloudConfig.repos).allows(session.env.gitRepo)) return false;
580
688
  const launchSpool = readLaunchSpoolState(this.home);
581
689
  if (!launchStatusIsDecisive(launchSpool)) return false;
582
- const launchFilter = new LaunchFilter(launchSpool.ids);
690
+ const launchFilter = new LaunchFilter(launchSpool.ids, launchSpool.records, this.build);
583
691
  if (!launchFilter.allows(session)) return false;
584
692
  let tier;
585
693
  try {
586
- tier = resolveTier(cloudConfig.tier);
694
+ tier = resolveTier(this.build === void 0 ? cloudConfig.tier : this.env.OUTERLAYER_TIER ?? cloudConfig.tier);
587
695
  } catch (err) {
588
696
  if (err instanceof SyncConfigError) return false;
589
697
  throw err;
@@ -613,6 +721,7 @@ var LiveUploader = class {
613
721
  const outcome = await this.sendWithRetry(endpoint, destination.credential, destination.appId, prepared.id, payload);
614
722
  if (outcome === "unauthorized") {
615
723
  this.stopped = true;
724
+ this.onUnauthorized?.();
616
725
  this.stderr(
617
726
  destination.credential.startsWith("olu_") ? `live upload stopped: the login was refused (expired or revoked). No session streams from this daemon until you run \`outerlayer login\` and it restarts.
618
727
  ` : `live upload stopped: the API key was refused. No session streams from this daemon until it restarts with a valid key.
@@ -620,8 +729,9 @@ var LiveUploader = class {
620
729
  );
621
730
  return true;
622
731
  }
732
+ if (outcome === "unreachable") return false;
623
733
  if (outcome === "rejected") {
624
- const reason = "the server refused this delta permanently";
734
+ const reason = `the server refused this delta permanently${this.lastRejectStatus === void 0 ? "" : ` (HTTP ${this.lastRejectStatus})`}`;
625
735
  this.mergeSessionState(key, {
626
736
  ackedTurnIndex: currentAcked,
627
737
  failed: true,
@@ -654,6 +764,7 @@ var LiveUploader = class {
654
764
  * key bound to a host key is refused on every route unless that key signed the request. A login token is
655
765
  * never signed: only the saved factory key is bound to a host key. */
656
766
  async signatureHeaders(endpoint, body, credential) {
767
+ if (this.build !== void 0) return {};
657
768
  if (readCloudConfig(this.home).apiKey !== credential) return {};
658
769
  let signer;
659
770
  try {
@@ -664,6 +775,12 @@ var LiveUploader = class {
664
775
  if (signer === void 0) return {};
665
776
  return { ...await signRunnerRequest({ method: "POST", url: endpoint, body: new TextEncoder().encode(body), signer }) };
666
777
  }
778
+ /** Writes `message` the first time `key` is seen, so a condition that holds on every fire logs once. */
779
+ warnOnce(key, message) {
780
+ if (this.warned.has(key)) return;
781
+ this.warned.add(key);
782
+ this.stderr(message);
783
+ }
667
784
  /** Sends one request, retrying a network error or a retryable 5xx forever
668
785
  * with exponential backoff (2s → 60s cap) — the daemon runs unattended, so
669
786
  * there is no bounded attempt count. A 401 or a permanent 4xx/rejection
@@ -673,6 +790,7 @@ var LiveUploader = class {
673
790
  let delayMs = RETRY_BASE_DELAY_MS;
674
791
  for (; ; ) {
675
792
  let response;
793
+ let failure = "";
676
794
  try {
677
795
  const signature = await this.signatureHeaders(endpoint, body, apiKey);
678
796
  response = await this.fetchImpl(endpoint, {
@@ -685,18 +803,30 @@ var LiveUploader = class {
685
803
  },
686
804
  body
687
805
  });
688
- } catch {
806
+ } catch (err) {
689
807
  response = void 0;
808
+ failure = err instanceof Error ? err.message : String(err);
690
809
  }
691
810
  if (response) {
692
811
  if (response.status === 401) return "unauthorized";
693
812
  if (response.ok) {
694
813
  const accepted = await acceptedSessionIds(response);
695
- if (accepted) return accepted.includes(sessionId) ? "accepted" : "rejected";
814
+ if (accepted) {
815
+ if (accepted.includes(sessionId)) return "accepted";
816
+ this.lastRejectStatus = void 0;
817
+ return "rejected";
818
+ }
696
819
  } else if (!RETRYABLE_STATUSES.has(response.status)) {
820
+ this.lastRejectStatus = response.status;
697
821
  return "rejected";
698
822
  }
699
823
  }
824
+ if (this.build !== void 0) {
825
+ const why = response === void 0 ? failure : `the gateway answered ${response.status}`;
826
+ this.stderr(`live upload of session ${sessionId} failed: ${why}${this.stopping ? "" : " \xB7 retrying"}
827
+ `);
828
+ }
829
+ if (this.stopping) return "unreachable";
700
830
  await this.sleepImpl(Math.max(retryAfterMs(response), delayMs));
701
831
  delayMs = Math.min(delayMs * 2, RETRY_MAX_DELAY_MS);
702
832
  }
@@ -1683,4 +1813,4 @@ ${C.yellow}!${C.reset} batch ${shippedBatches + 1}/${batches.length} accounting
1683
1813
  }
1684
1814
  }
1685
1815
 
1686
- export { CHUNK_PART_TARGET_BYTES, LiveUploader, RETRYABLE_STATUSES, SyncConfigError, SyncTransportError, isLoopbackUrl, resolveTier, runSync, staleBuildRefusal, staleBuildWarning };
1816
+ export { CHUNK_PART_TARGET_BYTES, LiveUploader, RETRYABLE_STATUSES, SyncConfigError, SyncTransportError, detectCi, installScrubContext, isLoopbackUrl, resolveTier, runSync, staleBuildRefusal, staleBuildWarning };