@amaster.ai/employee-runtime-connector 0.1.0-beta.21 → 0.1.0-beta.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -38,7 +38,7 @@ The daemon reports its connector contract, exact package/build, platform/archite
38
38
 
39
39
  `AMASTER_RUNTIME_ATTESTATION_MODE` defaults to `shadow`. Set it to `enforce` only after the expected version and build marker are configured and shadow diagnostics are clean; enforce mode refuses to create or lease Runtime V2 mutation commands without a current exact attestation. `AMASTER_RUNTIME_ATTESTATION_TTL_SECONDS` defaults to `300` and accepts `60`–`3600`. Invalid mode or TTL values fail server startup visibly.
40
40
 
41
- Threat model: these facts are self-reported by a connector authenticated with its existing connector credential. There is no hardware-backed signature, remote attestation, or independent trust root. The proof detects accidental version/schema/executor drift by an honest connector and correlates commands to the observed facts; it does not prove that a compromised or malicious connector is running the claimed code. Keep the default `shadow` mode observational. Do not treat this mechanism as a production security boundary or enable `enforce` until credential-epoch identity, transactional create/lease revalidation, batch-local rejection, and recovery from honest drift have passed review.
41
+ Threat model: these facts are self-reported by a connector authenticated with its existing connector credential. There is no hardware-backed signature, remote attestation, or independent trust root. The proof detects accidental version/schema/executor drift by an honest connector and correlates commands to the observed facts; it does not prove that a compromised or malicious connector is running the claimed code. Proof identity is stable for the exact connector credential and fact set, create/lease revalidate it under the connector transaction lock, stale commands are isolated within a poll batch, and an active connector can recover after restoring exact facts. Keep the default `shadow` mode observational, and do not treat this mechanism as a standalone production security boundary or enable `enforce` before expected markers are configured and shadow diagnostics are clean.
42
42
 
43
43
  The AMaster remote stack derives its runtime image from the Pi base image with
44
44
  `docker/Dockerfile.amaster-employee-pi-cli-runtime`. The build installs one exact
@@ -3,7 +3,7 @@
3
3
 
4
4
  // src/amaster-runtime-daemon.mjs
5
5
  import { createHash as createHash6 } from "node:crypto";
6
- import { chmodSync as chmodSync3, copyFileSync as copyFileSync2, existsSync as existsSync9, lstatSync as lstatSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync7, readdirSync as readdirSync6, realpathSync as realpathSync3, renameSync as renameSync2, rmSync as rmSync4, statSync as statSync7, symlinkSync as symlinkSync2, unlinkSync, writeFileSync as writeFileSync5 } from "node:fs";
6
+ import { chmodSync as chmodSync3, copyFileSync as copyFileSync2, existsSync as existsSync9, lstatSync as lstatSync4, mkdirSync as mkdirSync5, readFileSync as readFileSync7, readdirSync as readdirSync6, realpathSync as realpathSync3, renameSync as renameSync3, rmSync as rmSync5, statSync as statSync7, symlinkSync as symlinkSync2, unlinkSync, writeFileSync as writeFileSync5 } from "node:fs";
7
7
  import { arch as arch3, homedir as homedir3, hostname as hostname2, platform as platform3 } from "node:os";
8
8
  import { basename as basename6, delimiter as delimiter2, dirname as dirname6, extname as extname2, isAbsolute as isAbsolute6, join as join10, relative as relative6, resolve as resolve8 } from "node:path";
9
9
  import { spawn, spawnSync as spawnSync5 } from "node:child_process";
@@ -2144,6 +2144,27 @@ function selectExecutor(config, command) {
2144
2144
  function processGroupIdForChild(child, processPlatform = process.platform) {
2145
2145
  return processPlatform === "win32" || typeof child.pid !== "number" || child.pid <= 0 ? null : child.pid;
2146
2146
  }
2147
+ function isExecutorProcessAlive(child, processGroupId = processGroupIdForChild(child), processApi = process) {
2148
+ if (processGroupId !== null) {
2149
+ try {
2150
+ processApi.kill(-processGroupId, 0);
2151
+ return true;
2152
+ } catch (error) {
2153
+ if (error?.code === "ESRCH") return false;
2154
+ return true;
2155
+ }
2156
+ }
2157
+ return child.exitCode === null && child.signalCode === null;
2158
+ }
2159
+ function isProcessIdAlive(pid, processApi = process) {
2160
+ if (!Number.isInteger(pid) || pid <= 0) return false;
2161
+ try {
2162
+ processApi.kill(pid, 0);
2163
+ return true;
2164
+ } catch (error) {
2165
+ return error?.code !== "ESRCH";
2166
+ }
2167
+ }
2147
2168
  function signalExecutorProcess(child, signal, processGroupId = processGroupIdForChild(child), processApi = process) {
2148
2169
  if (processGroupId !== null) {
2149
2170
  try {
@@ -2156,7 +2177,7 @@ function signalExecutorProcess(child, signal, processGroupId = processGroupIdFor
2156
2177
  }
2157
2178
  }
2158
2179
  }
2159
- if (!child.killed) {
2180
+ if (child.exitCode === null && child.signalCode === null) {
2160
2181
  try {
2161
2182
  return child.kill(signal);
2162
2183
  } catch {
@@ -2435,7 +2456,7 @@ function compileCommandPromptWithManifest(input, options = {}) {
2435
2456
  }
2436
2457
 
2437
2458
  // src/amaster-runtime-daemon/config-state.mjs
2438
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3 } from "node:fs";
2459
+ import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync3, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "node:fs";
2439
2460
  import { homedir as homedir2, hostname } from "node:os";
2440
2461
  import { dirname as dirname3, join as join4 } from "node:path";
2441
2462
 
@@ -2559,16 +2580,53 @@ function runtimeHome(env) {
2559
2580
  function stateFilePath(env) {
2560
2581
  return env.AMASTER_DAEMON_STATE_FILE ? expandHomePath(String(env.AMASTER_DAEMON_STATE_FILE)) : join4(runtimeHome(env), "runtime-connector-state.json");
2561
2582
  }
2583
+ function corruptStatePath(path) {
2584
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/\D/g, "").slice(0, 14);
2585
+ const base = `${path}.corrupt-${timestamp}Z`;
2586
+ let candidate = base;
2587
+ for (let suffix = 1; existsSync3(candidate); suffix += 1) {
2588
+ candidate = `${base}.${suffix}`;
2589
+ }
2590
+ return candidate;
2591
+ }
2562
2592
  function readState(env) {
2563
2593
  const path = stateFilePath(env);
2564
2594
  if (!existsSync3(path)) return {};
2565
- return JSON.parse(readFileSync3(path, "utf8"));
2595
+ try {
2596
+ const state = JSON.parse(readFileSync3(path, "utf8"));
2597
+ if (!state || typeof state !== "object" || Array.isArray(state)) {
2598
+ throw new TypeError("runtime connector state must be a JSON object");
2599
+ }
2600
+ return state;
2601
+ } catch {
2602
+ const quarantinePath = corruptStatePath(path);
2603
+ try {
2604
+ renameSync(path, quarantinePath);
2605
+ process.stderr.write(`AMaster runtime state was invalid and quarantined at ${quarantinePath}
2606
+ `);
2607
+ } catch (error) {
2608
+ process.stderr.write(
2609
+ `AMaster runtime state was invalid but could not be quarantined (${error?.code ?? "unknown error"})
2610
+ `
2611
+ );
2612
+ }
2613
+ return {};
2614
+ }
2566
2615
  }
2567
2616
  function writeState(env, state) {
2568
2617
  const path = stateFilePath(env);
2569
2618
  mkdirSync3(dirname3(path), { recursive: true });
2570
- writeFileSync3(path, `${JSON.stringify(state, null, 2)}
2571
- `);
2619
+ const temporaryPath = `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
2620
+ try {
2621
+ writeFileSync3(temporaryPath, `${JSON.stringify(state, null, 2)}
2622
+ `, {
2623
+ flag: "wx",
2624
+ mode: 384
2625
+ });
2626
+ renameSync(temporaryPath, path);
2627
+ } finally {
2628
+ rmSync3(temporaryPath, { force: true });
2629
+ }
2572
2630
  }
2573
2631
  function buildConfig(env = process.env, flags = {}) {
2574
2632
  const serverUrl = String(flags.serverUrl ?? env.AMASTER_EMPLOYEE_SERVER_URL ?? "http://127.0.0.1:3100").replace(/\/+$/, "");
@@ -3722,14 +3780,15 @@ function buildRuntimeConnectorAuthHeaders(config) {
3722
3780
  if (config.token) return { Authorization: `Bearer ${config.token}` };
3723
3781
  return {};
3724
3782
  }
3725
- async function postRuntimeConnectorJson(config, path, payload) {
3783
+ async function postRuntimeConnectorJson(config, path, payload, options = {}) {
3726
3784
  const res = await fetch(`${config.serverUrl}${path}`, {
3727
3785
  method: "POST",
3728
3786
  headers: {
3729
3787
  "content-type": "application/json",
3730
3788
  ...buildRuntimeConnectorAuthHeaders(config)
3731
3789
  },
3732
- body: JSON.stringify(payload)
3790
+ body: JSON.stringify(payload),
3791
+ signal: options.signal
3733
3792
  });
3734
3793
  const text = await res.text();
3735
3794
  const body = text ? JSON.parse(text) : null;
@@ -3760,6 +3819,26 @@ async function postRuntimeConnectorBytes(config, path, body, headers = {}) {
3760
3819
  }
3761
3820
  return response;
3762
3821
  }
3822
+ function retryableRuntimeConnectorPost(error) {
3823
+ const status = Number(error?.httpStatus);
3824
+ return !Number.isInteger(status) || status === 408 || status === 429 || status >= 500;
3825
+ }
3826
+ async function postRuntimeConnectorJsonWithRetry(config, path, payload, options = {}) {
3827
+ const maxAttempts = Math.max(1, Math.min(5, Number(options.maxAttempts ?? 3)));
3828
+ const timeoutMs = Math.max(1, Number(options.timeoutMs ?? 5e3));
3829
+ const delayMs = Math.max(0, Number(options.delayMs ?? 100));
3830
+ let lastError;
3831
+ for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
3832
+ try {
3833
+ return await postRuntimeConnectorJson(config, path, payload, { signal: AbortSignal.timeout(timeoutMs) });
3834
+ } catch (error) {
3835
+ lastError = error;
3836
+ if (attempt >= maxAttempts || !retryableRuntimeConnectorPost(error)) throw error;
3837
+ if (delayMs > 0) await new Promise((resolve9) => setTimeout(resolve9, delayMs));
3838
+ }
3839
+ }
3840
+ throw lastError;
3841
+ }
3763
3842
  async function bestEffortPostRuntimeConnectorJson(config, path, payload, options = {}) {
3764
3843
  try {
3765
3844
  return await postRuntimeConnectorJson(config, path, payload);
@@ -3775,6 +3854,7 @@ async function bestEffortPostRuntimeConnectorJson(config, path, payload, options
3775
3854
  }
3776
3855
  }
3777
3856
  var postJson = postRuntimeConnectorJson;
3857
+ var postJsonWithRetry = postRuntimeConnectorJsonWithRetry;
3778
3858
  var bestEffortPostJson = bestEffortPostRuntimeConnectorJson;
3779
3859
 
3780
3860
  // src/amaster-runtime-daemon/runtime-artifact-upload.mjs
@@ -3843,7 +3923,7 @@ import { existsSync as existsSync5, mkdirSync as mkdirSync4, realpathSync as rea
3843
3923
  import { basename as basename4, join as join6, isAbsolute as isAbsolute4, relative as relative4, resolve as resolve4 } from "node:path";
3844
3924
 
3845
3925
  // src/amaster-runtime-daemon/workspace-manifest.mjs
3846
- import { existsSync as existsSync4, readFileSync as readFileSync5, renameSync, rmSync as rmSync3, writeFileSync as writeFileSync4 } from "node:fs";
3926
+ import { existsSync as existsSync4, readFileSync as readFileSync5, renameSync as renameSync2, rmSync as rmSync4, writeFileSync as writeFileSync4 } from "node:fs";
3847
3927
  import { basename as basename3, dirname as dirname4, join as join5 } from "node:path";
3848
3928
  var WORKSPACE_MANIFEST_FILENAME = ".amaster-runtime.json";
3849
3929
  function nowIso() {
@@ -3872,9 +3952,9 @@ function writeWorkspaceManifest(manifestPath, manifest) {
3872
3952
  try {
3873
3953
  writeFileSync4(tempPath, `${JSON.stringify(manifest, null, 2)}
3874
3954
  `, { mode: 384 });
3875
- renameSync(tempPath, manifestPath);
3955
+ renameSync2(tempPath, manifestPath);
3876
3956
  } catch (err) {
3877
- rmSync3(tempPath, { force: true });
3957
+ rmSync4(tempPath, { force: true });
3878
3958
  throw err;
3879
3959
  }
3880
3960
  return manifest;
@@ -3891,6 +3971,7 @@ function createWorkspaceManifest(workspace, input = {}) {
3891
3971
  executorKind: input.executorKind ?? null,
3892
3972
  executorHome: workspace.executorHome ?? null,
3893
3973
  commandId: input.commandId ?? null,
3974
+ generation: input.generation ?? null,
3894
3975
  runId: input.runId ?? null,
3895
3976
  issueId: input.issueId ?? null,
3896
3977
  workspaceKey: workspace.workspaceKey ?? null,
@@ -4004,6 +4085,7 @@ function resolveExecutionWorkspace(config, command, opts = {}) {
4004
4085
  createWorkspaceManifest(workspace, {
4005
4086
  executorKind,
4006
4087
  commandId: readString(command.commandId) ?? null,
4088
+ generation: Number.isInteger(command.generation) ? command.generation : null,
4007
4089
  runId,
4008
4090
  issueId
4009
4091
  });
@@ -4581,7 +4663,7 @@ function readWorkspaceStatus(cwd, opts = {}) {
4581
4663
  }
4582
4664
 
4583
4665
  // src/amaster-runtime-daemon.mjs
4584
- var CONNECTOR_VERSION = "0.1.0-beta.21";
4666
+ var CONNECTOR_VERSION = "0.1.0-beta.23";
4585
4667
  var CONNECTOR_CONTRACT_VERSION = "2026-06-04.v1";
4586
4668
  var MAX_CHECKPOINT_BYTES = 20 * 1024 * 1024;
4587
4669
  var CHECKPOINT_TTL_MS = 24 * 60 * 60 * 1e3;
@@ -4662,6 +4744,40 @@ function resultOutboxActiveRunCommands(config) {
4662
4744
  }
4663
4745
  return entries;
4664
4746
  }
4747
+ function resultOutboxFailedRunCommands(config) {
4748
+ const dir = resultOutboxInvalidDir(config);
4749
+ if (!existsSync9(dir)) return [];
4750
+ const entries = [];
4751
+ for (const file of readdirSync6(dir).filter((name) => name.endsWith(".json")).sort().slice(-100)) {
4752
+ let entry;
4753
+ try {
4754
+ entry = asRecord(JSON.parse(readFileSync7(join10(dir, file), "utf8")));
4755
+ } catch {
4756
+ continue;
4757
+ }
4758
+ const failureReason = readString(entry.invalidReason);
4759
+ if (!failureReason || !["server_rejected_terminal", "retry_exhausted"].includes(failureReason)) continue;
4760
+ const activeRun = asRecord(entry.activeRun);
4761
+ const commandId = readString(activeRun.commandId) ?? readString(entry.commandId);
4762
+ if (!commandId) continue;
4763
+ entries.push({
4764
+ commandId,
4765
+ ...readString(activeRun.runId) ? { runId: readString(activeRun.runId) } : {},
4766
+ ...readString(activeRun.issueId) ? { issueId: readString(activeRun.issueId) } : {},
4767
+ ...readString(activeRun.executorKind) ? { executorKind: readString(activeRun.executorKind) } : {},
4768
+ ...readString(activeRun.workspacePath) ? { workspacePath: readString(activeRun.workspacePath) } : {},
4769
+ ...readString(activeRun.startedAt) ? { startedAt: readString(activeRun.startedAt) } : {},
4770
+ ...readString(activeRun.executorCompletedAt) ? { executorCompletedAt: readString(activeRun.executorCompletedAt) } : {},
4771
+ phase: "result_outbox_failed",
4772
+ outboxPending: 0,
4773
+ evidenceDeliveryStatus: "failed",
4774
+ evidenceFailureReason: failureReason,
4775
+ ...typeof entry.httpStatus === "number" ? { evidenceHttpStatus: entry.httpStatus } : {},
4776
+ ...readString(entry.invalidAt) ? { evidenceFailedAt: readString(entry.invalidAt) } : {}
4777
+ });
4778
+ }
4779
+ return entries;
4780
+ }
4665
4781
  function clearDeliveredResultOutboxCommand(config, entry) {
4666
4782
  const commandId = readString(entry?.commandId);
4667
4783
  if (commandId) pendingResultOutboxRunCommands.delete(commandId);
@@ -4964,8 +5080,10 @@ function buildHeartbeatPayload(config, options = {}) {
4964
5080
  const orphanReaperSummary = asRecord(options.orphanReaper ?? lastOrphanReaperSummary);
4965
5081
  const activeRunCommandById = /* @__PURE__ */ new Map();
4966
5082
  for (const entry of [
5083
+ ...Array.from(localDispatchCommandIds, (commandId) => ({ commandId, phase: "preparing" })),
4967
5084
  ...Array.from(activeRunCommands.values()),
4968
5085
  ...Array.from(pendingResultOutboxRunCommands.values()),
5086
+ ...resultOutboxFailedRunCommands(config),
4969
5087
  ...resultOutboxActiveRunCommands(config)
4970
5088
  ]) {
4971
5089
  const commandId = readString(entry.commandId);
@@ -5534,7 +5652,7 @@ function writeJsonFileAtomic(filePath, value) {
5534
5652
  const tmpPath = `${filePath}.tmp.${process.pid}.${Date.now()}`;
5535
5653
  writeFileSync5(tmpPath, `${JSON.stringify(value, null, 2)}
5536
5654
  `, { mode: 384 });
5537
- renameSync2(tmpPath, filePath);
5655
+ renameSync3(tmpPath, filePath);
5538
5656
  }
5539
5657
  function isPlainRecord(value) {
5540
5658
  return value && typeof value === "object" && !Array.isArray(value);
@@ -5956,7 +6074,7 @@ function buildExecutorInvocation(executor, command = {}, workspace = null) {
5956
6074
  }
5957
6075
  return { command: executor.command, args: [], stdin: "prompt" };
5958
6076
  }
5959
- async function executeModelCallCommand(config, command) {
6077
+ async function executeModelCallCommand(config, command, signal) {
5960
6078
  const executor = selectExecutor(config, command);
5961
6079
  const payload = asRecord(command.payload);
5962
6080
  const prompt = readString(payload.prompt) ?? "";
@@ -5972,6 +6090,7 @@ async function executeModelCallCommand(config, command) {
5972
6090
  config.executorMaxOutputBytes,
5973
6091
  readNumber(payload.maxOutputBytes, 512 * 1024)
5974
6092
  ));
6093
+ await ackCommand(config, command, "spawned");
5975
6094
  await ingestLog(config, command, "system", "info", `Starting ${executor.kind} runtime model call`, {
5976
6095
  executorKind: executor.kind,
5977
6096
  args: invocation.args,
@@ -5984,7 +6103,8 @@ async function executeModelCallCommand(config, command) {
5984
6103
  stdin: invocation.stdin === "prompt" ? prompt : "",
5985
6104
  timeoutSeconds,
5986
6105
  maxOutputBytes,
5987
- executorKind: executor.kind
6106
+ executorKind: executor.kind,
6107
+ signal
5988
6108
  });
5989
6109
  if (execution.stdout) {
5990
6110
  await ingestLog(config, command, "stdout", "info", truncateText(execution.stdout, 4e3));
@@ -6022,6 +6142,17 @@ async function executeModelCallCommand(config, command) {
6022
6142
  } : {}
6023
6143
  }, error ?? void 0);
6024
6144
  }
6145
+ async function executeTrackedModelCallCommand(config, command) {
6146
+ const abortController = new AbortController();
6147
+ const forgetActiveModelCall = rememberActiveRunCommand(command, {});
6148
+ const stopActiveModelCallHeartbeats = startActiveRunHeartbeats(config, command, abortController);
6149
+ try {
6150
+ return await executeModelCallCommand(config, command, abortController.signal);
6151
+ } finally {
6152
+ stopActiveModelCallHeartbeats();
6153
+ forgetActiveModelCall();
6154
+ }
6155
+ }
6025
6156
  function redactProtectedText(value, protectedValues = []) {
6026
6157
  let text = String(value ?? "");
6027
6158
  for (const protectedValue of protectedValues) {
@@ -6268,6 +6399,7 @@ function realOrResolvedPath(value) {
6268
6399
  return resolve8(value);
6269
6400
  }
6270
6401
  }
6402
+ var LSOF_COMMAND = process.platform === "darwin" && existsSync9("/usr/sbin/lsof") ? "/usr/sbin/lsof" : "lsof";
6271
6403
  function processCwdForPid(pid) {
6272
6404
  if (process.platform === "linux") {
6273
6405
  try {
@@ -6277,7 +6409,7 @@ function processCwdForPid(pid) {
6277
6409
  }
6278
6410
  }
6279
6411
  if (process.platform === "darwin") {
6280
- const result2 = spawnSync5("lsof", ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
6412
+ const result2 = spawnSync5(LSOF_COMMAND, ["-a", "-p", String(pid), "-d", "cwd", "-Fn"], {
6281
6413
  encoding: "utf8",
6282
6414
  stdio: ["ignore", "pipe", "ignore"]
6283
6415
  });
@@ -6307,7 +6439,7 @@ function allProcessCwdsByPid() {
6307
6439
  return cwds;
6308
6440
  }
6309
6441
  if (process.platform === "darwin") {
6310
- const result2 = spawnSync5("lsof", ["-nP", "-d", "cwd", "-Fp", "-Fn"], {
6442
+ const result2 = spawnSync5(LSOF_COMMAND, ["-nP", "-d", "cwd", "-Fp", "-Fn"], {
6311
6443
  encoding: "utf8",
6312
6444
  stdio: ["ignore", "pipe", "ignore"]
6313
6445
  });
@@ -6525,8 +6657,19 @@ function runExecutor(command, args, options) {
6525
6657
  let timer = null;
6526
6658
  let rssTimer = null;
6527
6659
  let stopKillTimer = null;
6528
- let stoppedExitDrainTimer = null;
6660
+ let quiescenceTimer = null;
6661
+ let stoppedResidentTimer = null;
6529
6662
  let completionOutputDrainTimer = null;
6663
+ let stopReason = null;
6664
+ let childExited = false;
6665
+ let childClosed = false;
6666
+ let closeCode = null;
6667
+ let closeSignal = null;
6668
+ let spawnError = null;
6669
+ let outputDrainForcedClosed = false;
6670
+ let killedWorkspaceResidents = [];
6671
+ let stoppedResidentDrainUntil = 0;
6672
+ const residentSignalledAt = /* @__PURE__ */ new Map();
6530
6673
  let piCompletionOutputLineBuffer = "";
6531
6674
  const outputBytes = { stdout: 0, stderr: 0 };
6532
6675
  const maxOutputBytes = parsePositiveInteger(options.maxOutputBytes, 50 * 1024 * 1024);
@@ -6545,11 +6688,10 @@ function runExecutor(command, args, options) {
6545
6688
  if (timer) clearTimeout(timer);
6546
6689
  if (rssTimer) clearInterval(rssTimer);
6547
6690
  if (stopKillTimer) clearTimeout(stopKillTimer);
6548
- if (stoppedExitDrainTimer) clearTimeout(stoppedExitDrainTimer);
6691
+ if (quiescenceTimer) clearTimeout(quiescenceTimer);
6692
+ if (stoppedResidentTimer) clearTimeout(stoppedResidentTimer);
6549
6693
  if (completionOutputDrainTimer) clearTimeout(completionOutputDrainTimer);
6550
6694
  options.signal?.removeEventListener?.("abort", abort);
6551
- const shouldCleanupWorkspaceResidents = Boolean(aborted || outputFlood || memoryLimit || result2.timedOut || completionOutputType);
6552
- const killedWorkspaceResidents = shouldCleanupWorkspaceResidents ? killWorkspaceResidentProcesses(options.cwd, processGroupId) : [];
6553
6695
  resolveRun({
6554
6696
  stdout,
6555
6697
  stderr,
@@ -6558,25 +6700,77 @@ function runExecutor(command, args, options) {
6558
6700
  memoryLimit,
6559
6701
  completionOutputType,
6560
6702
  killedWorkspaceResidents,
6703
+ ...outputDrainForcedClosed ? { outputDrainForcedClosed: true } : {},
6561
6704
  ...result2
6562
6705
  });
6563
6706
  };
6564
6707
  const scheduleStopKill = () => {
6565
- if (stopKillTimer) clearTimeout(stopKillTimer);
6708
+ if (stopKillTimer) return;
6566
6709
  stopKillTimer = setTimeout(() => signalExecutorProcess(child, "SIGKILL", processGroupId), 2e3);
6567
- stopKillTimer.unref?.();
6568
6710
  };
6569
- const destroyExecutorOutputPipes = () => {
6570
- child.stdout.destroy();
6571
- child.stderr.destroy();
6711
+ const requestStop = (reason) => {
6712
+ if (settled || stopReason) return;
6713
+ stopReason = reason;
6714
+ signalExecutorProcess(child, "SIGTERM", processGroupId);
6715
+ scheduleStopKill();
6716
+ };
6717
+ const reapStoppedWorkspaceResidents = () => {
6718
+ if (settled || !stopReason || stopReason === "completion_cleanup") return;
6719
+ const residents = listWorkspaceResidentProcesses(options.cwd, processGroupId, {
6720
+ processRows: allProcessRows(),
6721
+ processCwdsByPid: allProcessCwdsByPid()
6722
+ });
6723
+ const seen = new Set(killedWorkspaceResidents.map((resident) => resident.pid));
6724
+ const now = Date.now();
6725
+ for (const resident of residents) {
6726
+ if (!seen.has(resident.pid)) killedWorkspaceResidents.push(resident);
6727
+ if (!residentSignalledAt.has(resident.pid)) {
6728
+ residentSignalledAt.set(resident.pid, now);
6729
+ try {
6730
+ process.kill(resident.pid, "SIGTERM");
6731
+ } catch {
6732
+ }
6733
+ }
6734
+ }
6735
+ for (const resident of killedWorkspaceResidents) {
6736
+ const signalledAt = residentSignalledAt.get(resident.pid) ?? now;
6737
+ if (now - signalledAt < 2e3 || !isProcessIdAlive(resident.pid)) continue;
6738
+ try {
6739
+ process.kill(resident.pid, "SIGKILL");
6740
+ } catch {
6741
+ }
6742
+ }
6743
+ if (isExecutorProcessAlive(child, processGroupId) || now < stoppedResidentDrainUntil || killedWorkspaceResidents.some((resident) => isProcessIdAlive(resident.pid))) {
6744
+ stoppedResidentTimer = setTimeout(reapStoppedWorkspaceResidents, 100);
6745
+ } else {
6746
+ stoppedResidentTimer = null;
6747
+ }
6748
+ maybeFinishQuiescent();
6572
6749
  };
6573
- const finishStoppedExecutorAfterExit = (code, signal) => {
6574
- if (settled || !aborted && !outputFlood && !memoryLimit && !(completionOutputType && signal === "SIGTERM") || stoppedExitDrainTimer) return;
6575
- stoppedExitDrainTimer = setTimeout(() => {
6576
- destroyExecutorOutputPipes();
6577
- finish({ exitCode: code, signal, timedOut: false, spawnError: null, cancelled: aborted });
6578
- }, 100);
6579
- stoppedExitDrainTimer.unref?.();
6750
+ const maybeFinishQuiescent = () => {
6751
+ if (settled || !childExited && !childClosed) return;
6752
+ if (isExecutorProcessAlive(child, processGroupId) || !childClosed && Date.now() < stoppedResidentDrainUntil || killedWorkspaceResidents.some((resident) => isProcessIdAlive(resident.pid))) {
6753
+ requestStop("completion_cleanup");
6754
+ if (!quiescenceTimer) {
6755
+ quiescenceTimer = setTimeout(() => {
6756
+ quiescenceTimer = null;
6757
+ maybeFinishQuiescent();
6758
+ }, 25);
6759
+ }
6760
+ return;
6761
+ }
6762
+ if (!childClosed) {
6763
+ outputDrainForcedClosed = true;
6764
+ child.stdout.destroy();
6765
+ child.stderr.destroy();
6766
+ }
6767
+ finish({
6768
+ exitCode: closeCode,
6769
+ signal: closeSignal,
6770
+ timedOut: stopReason === "timeout",
6771
+ spawnError,
6772
+ cancelled: aborted
6773
+ });
6580
6774
  };
6581
6775
  const maybeSchedulePiCompletionDrain = (text) => {
6582
6776
  if (settled || outputFlood || options.executorKind !== "pi" || completionOutputType) return;
@@ -6601,8 +6795,7 @@ function runExecutor(command, args, options) {
6601
6795
  completionOutputType = stopType;
6602
6796
  completionOutputDrainTimer = setTimeout(() => {
6603
6797
  if (settled) return;
6604
- signalExecutorProcess(child, "SIGTERM", processGroupId);
6605
- scheduleStopKill();
6798
+ requestStop("completion_output");
6606
6799
  }, PI_COMPLETION_OUTPUT_GRACE_MS);
6607
6800
  completionOutputDrainTimer.unref?.();
6608
6801
  return;
@@ -6618,8 +6811,7 @@ function runExecutor(command, args, options) {
6618
6811
  bytes: outputBytes[stream],
6619
6812
  limitBytes: maxOutputBytes
6620
6813
  };
6621
- signalExecutorProcess(child, "SIGTERM", processGroupId);
6622
- scheduleStopKill();
6814
+ requestStop("output_flood");
6623
6815
  };
6624
6816
  const killForMemoryLimit = (rssBytes) => {
6625
6817
  if (settled || memoryLimit) return;
@@ -6627,23 +6819,17 @@ function runExecutor(command, args, options) {
6627
6819
  rssBytes,
6628
6820
  limitBytes: maxRssBytes
6629
6821
  };
6630
- signalExecutorProcess(child, "SIGTERM", processGroupId);
6631
- scheduleStopKill();
6822
+ requestStop("memory_limit");
6632
6823
  };
6633
6824
  const abort = () => {
6634
6825
  if (settled) return;
6635
6826
  aborted = true;
6636
- signalExecutorProcess(child, "SIGTERM", processGroupId);
6637
- scheduleStopKill();
6827
+ requestStop("cancel");
6638
6828
  };
6639
6829
  options.signal?.addEventListener?.("abort", abort, { once: true });
6640
6830
  if (options.signal?.aborted) abort();
6641
6831
  timer = setTimeout(() => {
6642
- signalExecutorProcess(child, "SIGTERM", processGroupId);
6643
- const timeoutKillTimer = setTimeout(() => signalExecutorProcess(child, "SIGKILL", processGroupId), 2e3);
6644
- timeoutKillTimer.unref?.();
6645
- destroyExecutorOutputPipes();
6646
- finish({ exitCode: null, signal: "SIGTERM", timedOut: true, spawnError: null });
6832
+ requestStop("timeout");
6647
6833
  }, options.timeoutSeconds * 1e3);
6648
6834
  if (maxRssMb > 0 && processGroupId !== null) {
6649
6835
  rssTimer = setInterval(() => {
@@ -6670,13 +6856,27 @@ function runExecutor(command, args, options) {
6670
6856
  if (outputBytes.stderr > maxOutputBytes) killForOutputFlood("stderr");
6671
6857
  });
6672
6858
  child.on("error", (err) => {
6673
- finish({ exitCode: null, signal: null, timedOut: false, spawnError: err.message });
6859
+ spawnError = err.message;
6674
6860
  });
6675
6861
  child.on("exit", (code, signal) => {
6676
- finishStoppedExecutorAfterExit(code, signal);
6862
+ childExited = true;
6863
+ closeCode = code;
6864
+ closeSignal = signal;
6865
+ stoppedResidentDrainUntil = Date.now() + 100;
6866
+ if (!stopReason && isExecutorProcessAlive(child, processGroupId)) {
6867
+ requestStop("completion_cleanup");
6868
+ }
6869
+ if (stopReason && stopReason !== "completion_cleanup") {
6870
+ stoppedResidentDrainUntil = Date.now() + 100;
6871
+ reapStoppedWorkspaceResidents();
6872
+ }
6873
+ maybeFinishQuiescent();
6677
6874
  });
6678
6875
  child.on("close", (code, signal) => {
6679
- finish({ exitCode: code, signal, timedOut: false, spawnError: null, cancelled: aborted });
6876
+ childClosed = true;
6877
+ closeCode = code;
6878
+ closeSignal = signal;
6879
+ maybeFinishQuiescent();
6680
6880
  });
6681
6881
  if (options.stdin) child.stdin.end(options.stdin);
6682
6882
  else child.stdin.end();
@@ -6869,7 +7069,7 @@ function moveResultOutboxEntryToInvalid(config, file, fullPath, reason, detail,
6869
7069
  const invalidPath = join10(invalidDir, file);
6870
7070
  if (original === void 0) {
6871
7071
  try {
6872
- renameSync2(fullPath, invalidPath);
7072
+ renameSync3(fullPath, invalidPath);
6873
7073
  } catch {
6874
7074
  copyFileSync2(fullPath, invalidPath);
6875
7075
  unlinkSync(fullPath);
@@ -7240,7 +7440,7 @@ async function materializeRequiredArtifactInputs(config, command, workspace) {
7240
7440
  throw new Error("artifact_input_manifest_invalid: expected version 1 entries array");
7241
7441
  }
7242
7442
  const targetRoot = join10(workspace.cwd, "input-artifacts");
7243
- rmSync4(targetRoot, { recursive: true, force: true });
7443
+ rmSync5(targetRoot, { recursive: true, force: true });
7244
7444
  mkdirSync5(targetRoot, { recursive: true });
7245
7445
  const usedPaths = /* @__PURE__ */ new Set();
7246
7446
  const materialized = [];
@@ -7316,7 +7516,7 @@ function issueCheckpointDir(workspace) {
7316
7516
  async function clearIssueCheckpoint(config, command, workspace, reason) {
7317
7517
  const checkpointDir = issueCheckpointDir(workspace);
7318
7518
  if (!existsSync9(checkpointDir)) return false;
7319
- rmSync4(checkpointDir, { recursive: true, force: true });
7519
+ rmSync5(checkpointDir, { recursive: true, force: true });
7320
7520
  await ingestLog(config, command, "system", "info", "Cleared issue continuation checkpoint", { reason });
7321
7521
  return true;
7322
7522
  }
@@ -7339,12 +7539,12 @@ async function materializeIssueCheckpoint(config, command, workspace) {
7339
7539
  try {
7340
7540
  manifest = asRecord(JSON.parse(readFileSync7(manifestPath, "utf8")));
7341
7541
  } catch (err) {
7342
- rmSync4(checkpointDir, { recursive: true, force: true });
7542
+ rmSync5(checkpointDir, { recursive: true, force: true });
7343
7543
  throw new Error(`invalid issue checkpoint manifest: ${err instanceof Error ? err.message : String(err)}`);
7344
7544
  }
7345
7545
  const expiresAt = Date.parse(readString(manifest.expiresAt) ?? "");
7346
7546
  if (!Number.isFinite(expiresAt) || expiresAt <= Date.now() || readString(manifest.issueId) !== commandIssueId(command)) {
7347
- rmSync4(checkpointDir, { recursive: true, force: true });
7547
+ rmSync5(checkpointDir, { recursive: true, force: true });
7348
7548
  return [];
7349
7549
  }
7350
7550
  try {
@@ -7418,7 +7618,7 @@ async function materializeIssueCheckpoint(config, command, workspace) {
7418
7618
  async function saveIssueCheckpoint(config, command, workspace, candidates) {
7419
7619
  const checkpointDir = issueCheckpointDir(workspace);
7420
7620
  const filesDir = join10(checkpointDir, "files");
7421
- rmSync4(checkpointDir, { recursive: true, force: true });
7621
+ rmSync5(checkpointDir, { recursive: true, force: true });
7422
7622
  mkdirSync5(filesDir, { recursive: true });
7423
7623
  const files = [];
7424
7624
  let totalBytes = 0;
@@ -7441,7 +7641,7 @@ async function saveIssueCheckpoint(config, command, workspace, candidates) {
7441
7641
  files.push({ path: relativePath, byteSize, sha256: hashFileSha256(ownedSource) });
7442
7642
  }
7443
7643
  if (files.length === 0) {
7444
- rmSync4(checkpointDir, { recursive: true, force: true });
7644
+ rmSync5(checkpointDir, { recursive: true, force: true });
7445
7645
  return null;
7446
7646
  }
7447
7647
  const manifest = {
@@ -7479,10 +7679,12 @@ function piOutputValidationError(parsed, options = {}) {
7479
7679
  }
7480
7680
  return null;
7481
7681
  }
7482
- async function ackCommand(config, command) {
7682
+ async function ackCommand(config, command, phase = "preparing") {
7483
7683
  const connectorId = requireConnectorId(config);
7484
- return await postJson(config, `/api/amaster/runtime-connectors/${connectorId}/commands/${command.commandId}/ack`, {
7485
- leaseId: command.leaseId
7684
+ return await postJsonWithRetry(config, `/api/amaster/runtime-connectors/${connectorId}/commands/${command.commandId}/ack`, {
7685
+ leaseId: command.leaseId,
7686
+ generation: command.generation,
7687
+ phase
7486
7688
  });
7487
7689
  }
7488
7690
  async function executeRunCommand(config, command) {
@@ -7571,6 +7773,7 @@ async function executeRunCommand(config, command) {
7571
7773
  presentationKind: "context_manifest",
7572
7774
  contextManifest
7573
7775
  });
7776
+ await ackCommand(config, command, "spawned");
7574
7777
  await ingestLog(config, command, "system", "info", `Starting ${executor.kind} executor`, {
7575
7778
  executorKind: executor.kind,
7576
7779
  cwd,
@@ -7759,6 +7962,7 @@ async function executeRunCommand(config, command) {
7759
7962
  });
7760
7963
  }
7761
7964
  const result2 = {
7965
+ evidenceContract: { version: 1 },
7762
7966
  executorKind: executor.kind,
7763
7967
  command: invocation.command,
7764
7968
  args: invocation.args,
@@ -7768,6 +7972,7 @@ async function executeRunCommand(config, command) {
7768
7972
  exitCode: execution.exitCode,
7769
7973
  signal: execution.signal,
7770
7974
  timedOut: execution.timedOut,
7975
+ ...execution.outputDrainForcedClosed ? { outputDrainForcedClosed: true } : {},
7771
7976
  ...readString(execution.completionOutputType) ? { completionOutputType: readString(execution.completionOutputType) } : {},
7772
7977
  ...cancelled ? { cancelledByControlPlane: true } : {},
7773
7978
  ...invocation.nativeSession ? { nativeSession: invocation.nativeSession } : {},
@@ -7863,7 +8068,7 @@ async function processCommand(config, command) {
7863
8068
  return;
7864
8069
  }
7865
8070
  if (command.commandType === "model_call") {
7866
- await executeModelCallCommand(config, command);
8071
+ await executeTrackedModelCallCommand(config, command);
7867
8072
  return;
7868
8073
  }
7869
8074
  await completeCommand(config, command, "succeeded", {
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
- import { closeSync, existsSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
3
  import { spawn, spawnSync } from "node:child_process";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { homedir, hostname } from "node:os";
6
6
  import { fileURLToPath } from "node:url";
7
7
 
8
- const CONNECTOR_VERSION = "0.1.0-beta.21";
8
+ const CONNECTOR_VERSION = "0.1.0-beta.23";
9
9
 
10
10
  const CAPABILITIES = [
11
11
  "remote_registration",
@@ -277,8 +277,26 @@ function readState(config) {
277
277
  const statePath = config.AMASTER_DAEMON_STATE_FILE || join(configDir, "runtime-connector-state.json");
278
278
  if (!existsSync(statePath)) return {};
279
279
  try {
280
- return JSON.parse(readFileSync(statePath, "utf8"));
280
+ const state = JSON.parse(readFileSync(statePath, "utf8"));
281
+ if (!state || typeof state !== "object" || Array.isArray(state)) {
282
+ throw new TypeError("runtime connector state must be a JSON object");
283
+ }
284
+ return state;
281
285
  } catch {
286
+ const timestamp = new Date().toISOString().replace(/\D/g, "").slice(0, 14);
287
+ const base = `${statePath}.corrupt-${timestamp}Z`;
288
+ let quarantinePath = base;
289
+ for (let suffix = 1; existsSync(quarantinePath); suffix += 1) {
290
+ quarantinePath = `${base}.${suffix}`;
291
+ }
292
+ try {
293
+ renameSync(statePath, quarantinePath);
294
+ process.stderr.write(`AMaster runtime state was invalid and quarantined at ${quarantinePath}\n`);
295
+ } catch (error) {
296
+ process.stderr.write(
297
+ `AMaster runtime state was invalid but could not be quarantined (${error?.code ?? "unknown error"})\n`,
298
+ );
299
+ }
282
300
  return {};
283
301
  }
284
302
  }
@@ -286,7 +304,16 @@ function readState(config) {
286
304
  function writeState(config, state) {
287
305
  const statePath = withDefaults(config).AMASTER_DAEMON_STATE_FILE || join(configDir, "runtime-connector-state.json");
288
306
  mkdirSync(dirname(statePath), { recursive: true });
289
- writeFileSync(statePath, `${JSON.stringify(state, null, 2)}\n`);
307
+ const temporaryPath = `${statePath}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
308
+ try {
309
+ writeFileSync(temporaryPath, `${JSON.stringify(state, null, 2)}\n`, {
310
+ flag: "wx",
311
+ mode: 0o600,
312
+ });
313
+ renameSync(temporaryPath, statePath);
314
+ } finally {
315
+ rmSync(temporaryPath, { force: true });
316
+ }
290
317
  }
291
318
 
292
319
  function readPidInfo() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amaster.ai/employee-runtime-connector",
3
- "version": "0.1.0-beta.21",
3
+ "version": "0.1.0-beta.23",
4
4
  "description": "AMaster Employee runtime connector CLI and daemon",
5
5
  "license": "MIT",
6
6
  "type": "module",