@slock-ai/daemon 0.54.1 → 0.54.2

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.
@@ -1950,6 +1950,7 @@ function unregisterAgentCredentialProxyForLaunch(input) {
1950
1950
  var shellSingleQuote = (value) => `'${value.replace(/'/g, `'\\''`)}'`;
1951
1951
  var powershellSingleQuote = (value) => `'${value.replace(/'/g, "''")}'`;
1952
1952
  var DEFAULT_ACTIVE_CAPABILITIES = "send,read,mentions,tasks,reactions,server,channels";
1953
+ var LOOPBACK_NO_PROXY = "127.0.0.1,localhost";
1953
1954
  var safePathPart = (value) => value.replace(/[^a-zA-Z0-9_.-]/g, "_");
1954
1955
  var RAW_CREDENTIAL_ENV_DENYLIST = [
1955
1956
  "SLOCK_AGENT_CREDENTIAL_KEY"
@@ -1996,6 +1997,34 @@ function windowsUtf8Env() {
1996
1997
  LC_ALL: "C.UTF-8"
1997
1998
  };
1998
1999
  }
2000
+ function posixLoopbackNoProxyPrelude() {
2001
+ return [
2002
+ `SLOCK_LOOPBACK_NO_PROXY=${shellSingleQuote(LOOPBACK_NO_PROXY)}`,
2003
+ `SLOCK_EXISTING_NO_PROXY="\${NO_PROXY:-}"`,
2004
+ `if [ -n "\${no_proxy:-}" ]; then SLOCK_EXISTING_NO_PROXY="\${SLOCK_EXISTING_NO_PROXY:+$SLOCK_EXISTING_NO_PROXY,}$no_proxy"; fi`,
2005
+ `NO_PROXY="\${SLOCK_LOOPBACK_NO_PROXY}\${SLOCK_EXISTING_NO_PROXY:+,$SLOCK_EXISTING_NO_PROXY}"`,
2006
+ `no_proxy="$NO_PROXY"`,
2007
+ "export NO_PROXY no_proxy"
2008
+ ].join("\n");
2009
+ }
2010
+ function cmdLoopbackNoProxyLines() {
2011
+ return [
2012
+ `set "SLOCK_LOOPBACK_NO_PROXY=${LOOPBACK_NO_PROXY}"`,
2013
+ `set "SLOCK_EXISTING_NO_PROXY=%NO_PROXY%"`,
2014
+ `if defined no_proxy (if defined SLOCK_EXISTING_NO_PROXY (set "SLOCK_EXISTING_NO_PROXY=%SLOCK_EXISTING_NO_PROXY%,%no_proxy%") else set "SLOCK_EXISTING_NO_PROXY=%no_proxy%")`,
2015
+ `if defined SLOCK_EXISTING_NO_PROXY (set "NO_PROXY=%SLOCK_LOOPBACK_NO_PROXY%,%SLOCK_EXISTING_NO_PROXY%") else set "NO_PROXY=%SLOCK_LOOPBACK_NO_PROXY%"`,
2016
+ `set "no_proxy=%NO_PROXY%"`
2017
+ ];
2018
+ }
2019
+ function powershellLoopbackNoProxyLines() {
2020
+ return [
2021
+ `$loopbackNoProxy = ${powershellSingleQuote(LOOPBACK_NO_PROXY)}`,
2022
+ "$existingNoProxy = @($env:NO_PROXY, $env:no_proxy) | Where-Object { $_ }",
2023
+ `if ($existingNoProxy.Count -gt 0) { $mergedNoProxy = "$loopbackNoProxy,$($existingNoProxy -join ',')" } else { $mergedNoProxy = $loopbackNoProxy }`,
2024
+ "$env:NO_PROXY = $mergedNoProxy",
2025
+ "$env:no_proxy = $mergedNoProxy"
2026
+ ];
2027
+ }
1999
2028
  function runtimeContextEnv(config) {
2000
2029
  const ctx = config.runtimeContext;
2001
2030
  if (!ctx) return {};
@@ -2045,6 +2074,7 @@ async function prepareCliTransport(ctx, extraEnv = {}, platform = process.platfo
2045
2074
  const posixWrapper = path2.join(slockDir, "slock");
2046
2075
  const posixCredentialPrefix = agentCredentialProxy ? `SLOCK_AGENT_PROXY_URL=${shellSingleQuote(agentCredentialProxy.proxyUrl)} SLOCK_AGENT_PROXY_TOKEN_FILE=${shellSingleQuote(agentCredentialProxyTokenFile)} SLOCK_AGENT_ACTIVE_CAPABILITIES=${shellSingleQuote(DEFAULT_ACTIVE_CAPABILITIES)} ` : "";
2047
2076
  const posixBody = `#!/usr/bin/env bash
2077
+ ${posixLoopbackNoProxyPrelude()}
2048
2078
  ${posixCredentialPrefix}exec ${shellSingleQuote(process.execPath)} ${shellSingleQuote(ctx.slockCliPath)} "$@"
2049
2079
  `;
2050
2080
  writeFileSync(posixWrapper, posixBody, { mode: 493 });
@@ -2061,6 +2091,7 @@ set "SLOCK_AGENT_ACTIVE_CAPABILITIES=${DEFAULT_ACTIVE_CAPABILITIES}"\r
2061
2091
  "set LANG=C.UTF-8",
2062
2092
  "set LC_ALL=C.UTF-8",
2063
2093
  "chcp 65001 >NUL 2>NUL",
2094
+ ...cmdLoopbackNoProxyLines(),
2064
2095
  cmdCredentialLine.trimEnd(),
2065
2096
  `"${process.execPath}" "${ctx.slockCliPath}" %*`,
2066
2097
  ""
@@ -2081,6 +2112,7 @@ set "SLOCK_AGENT_ACTIVE_CAPABILITIES=${DEFAULT_ACTIVE_CAPABILITIES}"\r
2081
2112
  "$env:PYTHONUTF8 = '1'",
2082
2113
  "$env:LANG = 'C.UTF-8'",
2083
2114
  "$env:LC_ALL = 'C.UTF-8'",
2115
+ ...powershellLoopbackNoProxyLines(),
2084
2116
  ...psCredentialLines,
2085
2117
  `$node = ${powershellSingleQuote(process.execPath)}`,
2086
2118
  `$cli = ${powershellSingleQuote(ctx.slockCliPath)}`,
@@ -2715,6 +2747,114 @@ import { spawn as spawn2, execFileSync as execFileSync2, execSync } from "child_
2715
2747
  import { existsSync as existsSync4, readFileSync as readFileSync3 } from "fs";
2716
2748
  import os3 from "os";
2717
2749
  import path5 from "path";
2750
+
2751
+ // src/runtimeTurnState.ts
2752
+ var RuntimeTurnState = class {
2753
+ currentTurnId = null;
2754
+ /**
2755
+ * Post-tool window where the app-server may not yet accept stdin steering.
2756
+ * Gate busy-mode delivery until turn/completed or next progress.
2757
+ */
2758
+ steeringGateActive = false;
2759
+ reset() {
2760
+ this.currentTurnId = null;
2761
+ this.steeringGateActive = false;
2762
+ }
2763
+ get activeTurnId() {
2764
+ return this.currentTurnId;
2765
+ }
2766
+ get canSteerBusy() {
2767
+ return Boolean(this.currentTurnId && !this.steeringGateActive);
2768
+ }
2769
+ markTurnStarted(turnId) {
2770
+ if (turnId !== void 0 && turnId !== null) {
2771
+ this.currentTurnId = turnId;
2772
+ }
2773
+ this.steeringGateActive = false;
2774
+ }
2775
+ adoptTurnId(turnId) {
2776
+ this.currentTurnId = turnId;
2777
+ }
2778
+ markProgress() {
2779
+ this.steeringGateActive = false;
2780
+ }
2781
+ markToolBoundary() {
2782
+ this.steeringGateActive = true;
2783
+ }
2784
+ markTurnCompleted() {
2785
+ this.currentTurnId = null;
2786
+ this.steeringGateActive = false;
2787
+ }
2788
+ };
2789
+
2790
+ // src/drivers/codexTelemetrySidecar.ts
2791
+ function finiteNumber(value) {
2792
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2793
+ }
2794
+ function finiteString(value) {
2795
+ return typeof value === "string" && value.length > 0 ? value : void 0;
2796
+ }
2797
+ function ratio(numerator, denominator) {
2798
+ if (numerator === void 0 || denominator === void 0 || denominator <= 0) return void 0;
2799
+ return Number((numerator / denominator).toFixed(6));
2800
+ }
2801
+ function withDefined(attrs) {
2802
+ return Object.fromEntries(Object.entries(attrs).filter(([, value]) => value !== void 0));
2803
+ }
2804
+ function parseTokenUsageTelemetry(message) {
2805
+ const usage = message.params?.tokenUsage;
2806
+ const total = usage?.total;
2807
+ if (!total || typeof total !== "object") return null;
2808
+ const inputTokens = finiteNumber(total.inputTokens);
2809
+ const cachedInputTokens = finiteNumber(total.cachedInputTokens);
2810
+ const totalTokens = finiteNumber(total.totalTokens);
2811
+ const modelContextWindow = finiteNumber(usage.modelContextWindow);
2812
+ const attrs = withDefined({
2813
+ totalTokens,
2814
+ inputTokens,
2815
+ cachedInputTokens,
2816
+ outputTokens: finiteNumber(total.outputTokens),
2817
+ reasoningOutputTokens: finiteNumber(total.reasoningOutputTokens),
2818
+ modelContextWindow,
2819
+ cachedInputRatio: ratio(cachedInputTokens, inputTokens),
2820
+ contextUtilization: ratio(totalTokens, modelContextWindow)
2821
+ });
2822
+ if (Object.keys(attrs).length === 0) return null;
2823
+ return { kind: "telemetry", name: "token_usage", attrs };
2824
+ }
2825
+ function parseRateLimitTelemetry(message) {
2826
+ const rateLimits = message.params?.rateLimits;
2827
+ const primary = rateLimits?.primary;
2828
+ if (!rateLimits || typeof rateLimits !== "object" || !primary || typeof primary !== "object") return null;
2829
+ const attrs = withDefined({
2830
+ limitId: finiteString(rateLimits.limitId),
2831
+ planType: finiteString(rateLimits.planType),
2832
+ usedPercent: finiteNumber(primary.usedPercent),
2833
+ windowDurationMins: finiteNumber(primary.windowDurationMins),
2834
+ resetsAt: finiteNumber(primary.resetsAt)
2835
+ });
2836
+ if (Object.keys(attrs).length === 0) return null;
2837
+ return { kind: "telemetry", name: "rate_limits", attrs };
2838
+ }
2839
+ function parseCodexTelemetryEvent(message) {
2840
+ switch (message.method) {
2841
+ case "thread/tokenUsage/updated":
2842
+ return parseTokenUsageTelemetry(message);
2843
+ case "account/rateLimits/updated":
2844
+ return parseRateLimitTelemetry(message);
2845
+ default:
2846
+ return null;
2847
+ }
2848
+ }
2849
+
2850
+ // src/drivers/codexEventNormalizer.ts
2851
+ function parseCodexJsonRpcLine(line) {
2852
+ try {
2853
+ return JSON.parse(line);
2854
+ } catch {
2855
+ return null;
2856
+ }
2857
+ }
2718
2858
  function getCodexNotificationErrorMessage(params) {
2719
2859
  const topLevelMessage = params?.message;
2720
2860
  if (typeof topLevelMessage === "string" && topLevelMessage.trim()) {
@@ -2726,6 +2866,246 @@ function getCodexNotificationErrorMessage(params) {
2726
2866
  }
2727
2867
  return null;
2728
2868
  }
2869
+ function joinReasoningText(item) {
2870
+ const summary = Array.isArray(item.summary) ? item.summary.filter((entry) => typeof entry === "string") : [];
2871
+ const content = Array.isArray(item.content) ? item.content.filter((entry) => typeof entry === "string") : [];
2872
+ return [...summary, ...content].join("\n").trim();
2873
+ }
2874
+ function rawResponseItemProgressEvent(message) {
2875
+ if (message.method !== "rawResponseItem/completed") return null;
2876
+ const item = message.params?.item ?? message.params?.responseItem ?? message.params?.rawItem ?? message.params;
2877
+ if (!item || typeof item !== "object") return null;
2878
+ const itemType = typeof item.type === "string" ? item.type : void 0;
2879
+ let payloadBytes;
2880
+ try {
2881
+ payloadBytes = Buffer.byteLength(JSON.stringify(item), "utf8");
2882
+ } catch {
2883
+ payloadBytes = void 0;
2884
+ }
2885
+ return {
2886
+ kind: "internal_progress",
2887
+ source: "codex_raw_response_item",
2888
+ itemType,
2889
+ payloadBytes
2890
+ };
2891
+ }
2892
+ var CodexEventNormalizer = class {
2893
+ mcpToolPrefix;
2894
+ currentThreadId = null;
2895
+ sessionAnnounced = false;
2896
+ streamedAgentMessageIds = /* @__PURE__ */ new Set();
2897
+ streamedReasoningIds = /* @__PURE__ */ new Set();
2898
+ turnState = new RuntimeTurnState();
2899
+ constructor(opts) {
2900
+ this.mcpToolPrefix = opts.mcpToolPrefix;
2901
+ }
2902
+ reset(opts = {}) {
2903
+ this.currentThreadId = opts.threadId ?? null;
2904
+ this.turnState.reset();
2905
+ this.sessionAnnounced = false;
2906
+ this.streamedAgentMessageIds.clear();
2907
+ this.streamedReasoningIds.clear();
2908
+ }
2909
+ get threadId() {
2910
+ return this.currentThreadId;
2911
+ }
2912
+ get activeTurnId() {
2913
+ return this.turnState.activeTurnId;
2914
+ }
2915
+ get canSteerBusy() {
2916
+ return this.turnState.canSteerBusy;
2917
+ }
2918
+ adoptThreadId(threadId) {
2919
+ this.currentThreadId = threadId;
2920
+ }
2921
+ normalizeMessage(message) {
2922
+ const events = [];
2923
+ if (message.result) {
2924
+ const thread = message.result.thread;
2925
+ if (thread && typeof thread.id === "string") {
2926
+ return this.handleThreadReady(thread.id, events);
2927
+ }
2928
+ const turn = message.result.turn;
2929
+ if (turn && typeof turn.id === "string") {
2930
+ this.turnState.adoptTurnId(turn.id);
2931
+ return { events };
2932
+ }
2933
+ if (typeof message.result.turnId === "string") {
2934
+ this.turnState.adoptTurnId(message.result.turnId);
2935
+ return { events };
2936
+ }
2937
+ }
2938
+ if (message.error) {
2939
+ events.push({ kind: "error", message: message.error.message || "Codex app-server request failed" });
2940
+ return { events };
2941
+ }
2942
+ const telemetry = parseCodexTelemetryEvent(message);
2943
+ if (telemetry) {
2944
+ events.push(telemetry);
2945
+ return { events };
2946
+ }
2947
+ const rawProgress = rawResponseItemProgressEvent(message);
2948
+ if (rawProgress) {
2949
+ events.push(rawProgress);
2950
+ return { events };
2951
+ }
2952
+ switch (message.method) {
2953
+ case "thread/started": {
2954
+ const threadId = message.params?.thread?.id;
2955
+ if (typeof threadId === "string") {
2956
+ return this.handleThreadReady(threadId, events);
2957
+ }
2958
+ break;
2959
+ }
2960
+ case "turn/started": {
2961
+ const turnId = message.params?.turn?.id;
2962
+ this.turnState.markTurnStarted(typeof turnId === "string" ? turnId : null);
2963
+ events.push({ kind: "thinking", text: "" });
2964
+ break;
2965
+ }
2966
+ case "item/agentMessage/delta": {
2967
+ const delta = message.params?.delta;
2968
+ const itemId = message.params?.itemId;
2969
+ if (typeof itemId === "string") {
2970
+ this.streamedAgentMessageIds.add(itemId);
2971
+ }
2972
+ if (typeof delta === "string" && delta.length > 0) {
2973
+ this.turnState.markProgress();
2974
+ events.push({ kind: "text", text: delta });
2975
+ }
2976
+ break;
2977
+ }
2978
+ case "item/reasoning/summaryTextDelta":
2979
+ case "item/reasoning/textDelta": {
2980
+ const delta = message.params?.delta;
2981
+ const itemId = message.params?.itemId;
2982
+ if (typeof itemId === "string") {
2983
+ this.streamedReasoningIds.add(itemId);
2984
+ }
2985
+ if (typeof delta === "string" && delta.length > 0) {
2986
+ this.turnState.markProgress();
2987
+ events.push({ kind: "thinking", text: delta });
2988
+ }
2989
+ break;
2990
+ }
2991
+ case "item/started":
2992
+ case "item/completed": {
2993
+ const item = message.params?.item;
2994
+ if (!item || typeof item !== "object" || typeof item.type !== "string") break;
2995
+ const itemType = item.type;
2996
+ const isStarted = message.method === "item/started";
2997
+ const isCompleted = message.method === "item/completed";
2998
+ switch (itemType) {
2999
+ case "reasoning":
3000
+ if (isCompleted && typeof item.id === "string" && !this.streamedReasoningIds.has(item.id)) {
3001
+ const text = joinReasoningText(item);
3002
+ if (text) {
3003
+ this.turnState.markProgress();
3004
+ events.push({ kind: "thinking", text });
3005
+ }
3006
+ }
3007
+ if (isCompleted && typeof item.id === "string") {
3008
+ this.streamedReasoningIds.delete(item.id);
3009
+ }
3010
+ break;
3011
+ case "agentMessage":
3012
+ if (isCompleted && typeof item.id === "string" && !this.streamedAgentMessageIds.has(item.id) && typeof item.text === "string" && item.text.length > 0) {
3013
+ this.turnState.markProgress();
3014
+ events.push({ kind: "text", text: item.text });
3015
+ }
3016
+ if (isCompleted && typeof item.id === "string") {
3017
+ this.streamedAgentMessageIds.delete(item.id);
3018
+ }
3019
+ break;
3020
+ case "commandExecution":
3021
+ if (isStarted && typeof item.command === "string") {
3022
+ events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
3023
+ }
3024
+ if (isCompleted) {
3025
+ events.push({ kind: "tool_output", name: "shell" });
3026
+ this.turnState.markToolBoundary();
3027
+ }
3028
+ break;
3029
+ case "contextCompaction":
3030
+ if (isStarted) {
3031
+ events.push({ kind: "compaction_started" });
3032
+ }
3033
+ if (isCompleted) {
3034
+ events.push({ kind: "compaction_finished" });
3035
+ }
3036
+ break;
3037
+ case "fileChange":
3038
+ if (isStarted && Array.isArray(item.changes)) {
3039
+ for (const change of item.changes) {
3040
+ events.push({
3041
+ kind: "tool_call",
3042
+ name: "file_change",
3043
+ input: { path: change?.path, kind: change?.kind }
3044
+ });
3045
+ }
3046
+ }
3047
+ break;
3048
+ case "mcpToolCall":
3049
+ if (isStarted) {
3050
+ const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
3051
+ events.push({ kind: "tool_call", name: toolName, input: item.arguments });
3052
+ }
3053
+ if (isCompleted) {
3054
+ const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
3055
+ events.push({ kind: "tool_output", name: toolName });
3056
+ this.turnState.markToolBoundary();
3057
+ }
3058
+ break;
3059
+ case "collabAgentToolCall":
3060
+ if (isStarted) {
3061
+ events.push({ kind: "tool_call", name: "collab_tool_call", input: { tool: item.tool, prompt: item.prompt } });
3062
+ }
3063
+ if (isCompleted) {
3064
+ this.turnState.markToolBoundary();
3065
+ }
3066
+ break;
3067
+ case "webSearch":
3068
+ if (isStarted) {
3069
+ events.push({ kind: "tool_call", name: "web_search", input: { query: item.query } });
3070
+ }
3071
+ if (isCompleted) {
3072
+ this.turnState.markToolBoundary();
3073
+ }
3074
+ break;
3075
+ }
3076
+ break;
3077
+ }
3078
+ case "turn/completed": {
3079
+ const turn = message.params?.turn;
3080
+ if (turn?.status === "failed" && turn?.error?.message) {
3081
+ events.push({ kind: "error", message: turn.error.message });
3082
+ }
3083
+ this.turnState.markTurnCompleted();
3084
+ this.streamedAgentMessageIds.clear();
3085
+ this.streamedReasoningIds.clear();
3086
+ events.push({ kind: "turn_end", sessionId: this.currentThreadId || void 0 });
3087
+ break;
3088
+ }
3089
+ case "error":
3090
+ events.push({
3091
+ kind: "error",
3092
+ message: getCodexNotificationErrorMessage(message.params) || "Unknown Codex app-server error"
3093
+ });
3094
+ break;
3095
+ }
3096
+ return { events };
3097
+ }
3098
+ handleThreadReady(threadId, events) {
3099
+ this.currentThreadId = threadId;
3100
+ if (!this.sessionAnnounced) {
3101
+ events.push({ kind: "session_init", sessionId: threadId });
3102
+ this.sessionAnnounced = true;
3103
+ }
3104
+ return { events, threadReady: threadId };
3105
+ }
3106
+ };
3107
+
3108
+ // src/drivers/codex.ts
2729
3109
  function ensureGitRepoForCodex(workingDirectory, deps = {}) {
2730
3110
  const existsSyncFn = deps.existsSyncFn ?? existsSync4;
2731
3111
  const execSyncFn = deps.execSyncFn ?? execSync;
@@ -2839,11 +3219,6 @@ function resolveCodexSpawn(commandArgs, deps = {}) {
2839
3219
  "Cannot resolve Codex CLI entry point on Windows. Install Codex Desktop or install @openai/codex globally via npm (npm i -g @openai/codex). Ignoring .codex/.sandbox-bin/codex-command-runner because it is a sandbox helper, not the Codex CLI."
2840
3220
  );
2841
3221
  }
2842
- function joinReasoningText(item) {
2843
- const summary = Array.isArray(item.summary) ? item.summary.filter((entry) => typeof entry === "string") : [];
2844
- const content = Array.isArray(item.content) ? item.content.filter((entry) => typeof entry === "string") : [];
2845
- return [...summary, ...content].join("\n").trim();
2846
- }
2847
3222
  var CodexDriver = class {
2848
3223
  id = "codex";
2849
3224
  lifecycle = {
@@ -2919,7 +3294,10 @@ var CodexDriver = class {
2919
3294
  cwd: ctx.workingDirectory,
2920
3295
  approvalPolicy: "never",
2921
3296
  sandbox: "danger-full-access",
2922
- developerInstructions: ctx.standingPrompt
3297
+ developerInstructions: ctx.standingPrompt,
3298
+ // Raw response items are used only as payload-free liveness signals in
3299
+ // the daemon. They replace the previous transcript-mtime heuristic.
3300
+ experimentalRawEvents: true
2923
3301
  };
2924
3302
  if (ctx.config.model) {
2925
3303
  threadParams.model = ctx.config.model;
@@ -2943,35 +3321,21 @@ var CodexDriver = class {
2943
3321
  }
2944
3322
  process = null;
2945
3323
  requestId = 0;
2946
- threadId = null;
2947
- activeTurnId = null;
2948
3324
  pendingInitialPrompt = null;
2949
3325
  initializeRequestId = null;
2950
3326
  pendingThreadRequest = null;
2951
3327
  initialTurnStarted = false;
2952
- sessionAnnounced = false;
2953
- streamedAgentMessageIds = /* @__PURE__ */ new Set();
2954
- streamedReasoningIds = /* @__PURE__ */ new Set();
2955
- /**
2956
- * Post-tool window where the app-server may not yet accept stdin steering.
2957
- * Gate busy-mode delivery until turn/completed or next progress.
2958
- */
2959
- steeringGateActive = false;
3328
+ normalizer = new CodexEventNormalizer({ mcpToolPrefix: this.mcpToolPrefix });
2960
3329
  async spawn(ctx) {
2961
3330
  ensureGitRepoForCodex(ctx.workingDirectory);
2962
3331
  const { spawnEnv } = await prepareCliTransport(ctx, { NO_COLOR: "1" });
2963
3332
  this.process = null;
2964
3333
  this.requestId = 0;
2965
- this.threadId = ctx.config.sessionId || null;
2966
- this.activeTurnId = null;
2967
3334
  this.pendingInitialPrompt = ctx.prompt;
2968
3335
  this.initializeRequestId = null;
2969
3336
  this.pendingThreadRequest = null;
2970
3337
  this.initialTurnStarted = false;
2971
- this.sessionAnnounced = false;
2972
- this.streamedAgentMessageIds.clear();
2973
- this.streamedReasoningIds.clear();
2974
- this.steeringGateActive = false;
3338
+ this.normalizer.reset({ threadId: ctx.config.sessionId || null });
2975
3339
  const args = ["app-server", "--listen", "stdio://"];
2976
3340
  args.push(...this.buildRuntimeActionsConfigArgs(ctx));
2977
3341
  const { command, args: spawnArgs } = resolveCodexSpawn(args);
@@ -2992,10 +3356,8 @@ var CodexDriver = class {
2992
3356
  return { process: proc };
2993
3357
  }
2994
3358
  parseLine(line) {
2995
- let message;
2996
- try {
2997
- message = JSON.parse(line);
2998
- } catch {
3359
+ const message = parseCodexJsonRpcLine(line);
3360
+ if (!message) {
2999
3361
  return [];
3000
3362
  }
3001
3363
  const events = [];
@@ -3009,194 +3371,34 @@ var CodexDriver = class {
3009
3371
  }
3010
3372
  return events;
3011
3373
  }
3012
- const thread = message.result.thread;
3013
- if (thread && typeof thread.id === "string") {
3014
- this.handleThreadReady(thread.id, events);
3015
- return events;
3016
- }
3017
- const turn = message.result.turn;
3018
- if (turn && typeof turn.id === "string") {
3019
- this.activeTurnId = turn.id;
3020
- return events;
3021
- }
3022
- if (typeof message.result.turnId === "string") {
3023
- this.activeTurnId = message.result.turnId;
3024
- return events;
3025
- }
3026
3374
  }
3027
- if (message.error) {
3028
- if (message.id === this.initializeRequestId) {
3029
- this.initializeRequestId = null;
3030
- this.pendingThreadRequest = null;
3031
- }
3375
+ if (message.error && message.id === this.initializeRequestId) {
3376
+ this.initializeRequestId = null;
3377
+ this.pendingThreadRequest = null;
3032
3378
  events.push({ kind: "error", message: message.error.message || "Codex app-server request failed" });
3033
3379
  return events;
3034
3380
  }
3035
- switch (message.method) {
3036
- case "thread/started": {
3037
- const threadId = message.params?.thread?.id;
3038
- if (typeof threadId === "string") {
3039
- this.handleThreadReady(threadId, events);
3040
- }
3041
- break;
3042
- }
3043
- case "turn/started": {
3044
- const turnId = message.params?.turn?.id;
3045
- if (typeof turnId === "string") {
3046
- this.activeTurnId = turnId;
3047
- }
3048
- this.steeringGateActive = false;
3049
- events.push({ kind: "thinking", text: "" });
3050
- break;
3051
- }
3052
- case "item/agentMessage/delta": {
3053
- const delta = message.params?.delta;
3054
- const itemId = message.params?.itemId;
3055
- if (typeof itemId === "string") {
3056
- this.streamedAgentMessageIds.add(itemId);
3057
- }
3058
- if (typeof delta === "string" && delta.length > 0) {
3059
- this.steeringGateActive = false;
3060
- events.push({ kind: "text", text: delta });
3061
- }
3062
- break;
3063
- }
3064
- case "item/reasoning/summaryTextDelta":
3065
- case "item/reasoning/textDelta": {
3066
- const delta = message.params?.delta;
3067
- const itemId = message.params?.itemId;
3068
- if (typeof itemId === "string") {
3069
- this.streamedReasoningIds.add(itemId);
3070
- }
3071
- if (typeof delta === "string" && delta.length > 0) {
3072
- this.steeringGateActive = false;
3073
- events.push({ kind: "thinking", text: delta });
3074
- }
3075
- break;
3076
- }
3077
- case "item/started":
3078
- case "item/completed": {
3079
- const item = message.params?.item;
3080
- if (!item || typeof item !== "object" || typeof item.type !== "string") break;
3081
- const itemType = item.type;
3082
- const isStarted = message.method === "item/started";
3083
- const isCompleted = message.method === "item/completed";
3084
- switch (itemType) {
3085
- case "reasoning":
3086
- if (isCompleted && typeof item.id === "string" && !this.streamedReasoningIds.has(item.id)) {
3087
- const text = joinReasoningText(item);
3088
- if (text) {
3089
- this.steeringGateActive = false;
3090
- events.push({ kind: "thinking", text });
3091
- }
3092
- }
3093
- if (isCompleted && typeof item.id === "string") {
3094
- this.streamedReasoningIds.delete(item.id);
3095
- }
3096
- break;
3097
- case "agentMessage":
3098
- if (isCompleted && typeof item.id === "string" && !this.streamedAgentMessageIds.has(item.id) && typeof item.text === "string" && item.text.length > 0) {
3099
- this.steeringGateActive = false;
3100
- events.push({ kind: "text", text: item.text });
3101
- }
3102
- if (isCompleted && typeof item.id === "string") {
3103
- this.streamedAgentMessageIds.delete(item.id);
3104
- }
3105
- break;
3106
- case "commandExecution":
3107
- if (isStarted && typeof item.command === "string") {
3108
- events.push({ kind: "tool_call", name: "shell", input: { command: item.command } });
3109
- }
3110
- if (isCompleted) {
3111
- events.push({ kind: "tool_output", name: "shell" });
3112
- this.steeringGateActive = true;
3113
- }
3114
- break;
3115
- case "contextCompaction":
3116
- if (isStarted) {
3117
- events.push({ kind: "compaction_started" });
3118
- }
3119
- if (isCompleted) {
3120
- events.push({ kind: "compaction_finished" });
3121
- }
3122
- break;
3123
- case "fileChange":
3124
- if (isStarted && Array.isArray(item.changes)) {
3125
- for (const change of item.changes) {
3126
- events.push({
3127
- kind: "tool_call",
3128
- name: "file_change",
3129
- input: { path: change?.path, kind: change?.kind }
3130
- });
3131
- }
3132
- }
3133
- break;
3134
- case "mcpToolCall":
3135
- if (isStarted) {
3136
- const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
3137
- events.push({ kind: "tool_call", name: toolName, input: item.arguments });
3138
- }
3139
- if (isCompleted) {
3140
- const toolName = item.server === "chat" ? `${this.mcpToolPrefix}${item.tool}` : `${this.mcpToolPrefix.replace(/_$/, "")}_${item.server}_${item.tool}`;
3141
- events.push({ kind: "tool_output", name: toolName });
3142
- this.steeringGateActive = true;
3143
- }
3144
- break;
3145
- case "collabAgentToolCall":
3146
- if (isStarted) {
3147
- events.push({ kind: "tool_call", name: "collab_tool_call", input: { tool: item.tool, prompt: item.prompt } });
3148
- }
3149
- if (isCompleted) {
3150
- this.steeringGateActive = true;
3151
- }
3152
- break;
3153
- case "webSearch":
3154
- if (isStarted) {
3155
- events.push({ kind: "tool_call", name: "web_search", input: { query: item.query } });
3156
- }
3157
- if (isCompleted) {
3158
- this.steeringGateActive = true;
3159
- }
3160
- break;
3161
- }
3162
- break;
3163
- }
3164
- case "turn/completed": {
3165
- const turn = message.params?.turn;
3166
- if (turn?.status === "failed" && turn?.error?.message) {
3167
- events.push({ kind: "error", message: turn.error.message });
3168
- }
3169
- this.activeTurnId = null;
3170
- this.streamedAgentMessageIds.clear();
3171
- this.streamedReasoningIds.clear();
3172
- this.steeringGateActive = false;
3173
- events.push({ kind: "turn_end", sessionId: this.threadId || void 0 });
3174
- break;
3175
- }
3176
- case "error":
3177
- events.push({
3178
- kind: "error",
3179
- message: getCodexNotificationErrorMessage(message.params) || "Unknown Codex app-server error"
3180
- });
3181
- break;
3381
+ const result = this.normalizer.normalizeMessage(message);
3382
+ if (result.threadReady) {
3383
+ this.startInitialTurn();
3182
3384
  }
3183
- return events;
3385
+ return result.events;
3184
3386
  }
3185
3387
  encodeStdinMessage(text, sessionId, opts) {
3186
- if (!this.threadId && sessionId) {
3187
- this.threadId = sessionId;
3388
+ if (!this.normalizer.threadId && sessionId) {
3389
+ this.normalizer.adoptThreadId(sessionId);
3188
3390
  }
3189
- if (!this.threadId) return null;
3391
+ if (!this.normalizer.threadId) return null;
3190
3392
  const mode = opts?.mode || "busy";
3191
3393
  if (mode === "busy") {
3192
- if (!this.activeTurnId || this.steeringGateActive) return null;
3394
+ if (!this.normalizer.canSteerBusy) return null;
3193
3395
  return JSON.stringify({
3194
3396
  jsonrpc: "2.0",
3195
3397
  id: this.nextRequestId(),
3196
3398
  method: "turn/steer",
3197
3399
  params: {
3198
- threadId: this.threadId,
3199
- expectedTurnId: this.activeTurnId,
3400
+ threadId: this.normalizer.threadId,
3401
+ expectedTurnId: this.normalizer.activeTurnId,
3200
3402
  input: [{ type: "text", text }]
3201
3403
  }
3202
3404
  });
@@ -3206,7 +3408,7 @@ var CodexDriver = class {
3206
3408
  id: this.nextRequestId(),
3207
3409
  method: "turn/start",
3208
3410
  params: {
3209
- threadId: this.threadId,
3411
+ threadId: this.normalizer.threadId,
3210
3412
  input: [{ type: "text", text }]
3211
3413
  }
3212
3414
  });
@@ -3226,21 +3428,13 @@ var CodexDriver = class {
3226
3428
  this.requestId += 1;
3227
3429
  return this.requestId;
3228
3430
  }
3229
- handleThreadReady(threadId, events) {
3230
- this.threadId = threadId;
3231
- if (!this.sessionAnnounced) {
3232
- events.push({ kind: "session_init", sessionId: threadId });
3233
- this.sessionAnnounced = true;
3234
- }
3235
- this.startInitialTurn();
3236
- }
3237
3431
  startInitialTurn() {
3238
- if (this.initialTurnStarted || !this.pendingInitialPrompt || !this.threadId) return;
3432
+ if (this.initialTurnStarted || !this.pendingInitialPrompt || !this.normalizer.threadId) return;
3239
3433
  this.initialTurnStarted = true;
3240
3434
  const prompt = this.pendingInitialPrompt;
3241
3435
  this.pendingInitialPrompt = null;
3242
3436
  this.sendRequest("turn/start", {
3243
- threadId: this.threadId,
3437
+ threadId: this.normalizer.threadId,
3244
3438
  input: [{ type: "text", text: prompt }]
3245
3439
  });
3246
3440
  }
@@ -5079,6 +5273,87 @@ function redactUrlQuery(value) {
5079
5273
  }
5080
5274
  }
5081
5275
 
5276
+ // src/runtimeProgressState.ts
5277
+ var RuntimeProgressState = class {
5278
+ lastEventAtMs;
5279
+ lastEventKindValue = null;
5280
+ staleSinceMs = null;
5281
+ constructor(nowMs = Date.now()) {
5282
+ this.lastEventAtMs = nowMs;
5283
+ }
5284
+ get lastEventAt() {
5285
+ return this.lastEventAtMs;
5286
+ }
5287
+ get lastEventKind() {
5288
+ return this.lastEventKindValue;
5289
+ }
5290
+ get staleSince() {
5291
+ return this.staleSinceMs;
5292
+ }
5293
+ get isStale() {
5294
+ return this.staleSinceMs !== null;
5295
+ }
5296
+ ageMs(nowMs = Date.now()) {
5297
+ return nowMs - this.lastEventAtMs;
5298
+ }
5299
+ noteRuntimeEvent(eventKind, nowMs = Date.now()) {
5300
+ this.lastEventAtMs = nowMs;
5301
+ this.lastEventKindValue = eventKind ?? null;
5302
+ this.staleSinceMs = null;
5303
+ }
5304
+ noteInternalProgress(observedAtMs = Date.now()) {
5305
+ this.lastEventAtMs = observedAtMs;
5306
+ this.staleSinceMs = null;
5307
+ }
5308
+ markStale(nowMs = Date.now()) {
5309
+ this.staleSinceMs ??= nowMs;
5310
+ return this.staleSinceMs;
5311
+ }
5312
+ };
5313
+
5314
+ // src/runtimeNotificationState.ts
5315
+ var RuntimeNotificationState = class {
5316
+ timerValue = null;
5317
+ pendingCountValue = 0;
5318
+ get pendingCount() {
5319
+ return this.pendingCountValue;
5320
+ }
5321
+ get timer() {
5322
+ return this.timerValue;
5323
+ }
5324
+ get hasTimer() {
5325
+ return this.timerValue !== null;
5326
+ }
5327
+ add(count = 1) {
5328
+ this.pendingCountValue += count;
5329
+ return this.pendingCountValue;
5330
+ }
5331
+ clearPending() {
5332
+ this.pendingCountValue = 0;
5333
+ }
5334
+ clearTimer() {
5335
+ if (this.timerValue) {
5336
+ clearTimeout(this.timerValue);
5337
+ this.timerValue = null;
5338
+ }
5339
+ }
5340
+ clear() {
5341
+ this.clearPending();
5342
+ this.clearTimer();
5343
+ }
5344
+ schedule(callback, delayMs) {
5345
+ if (this.timerValue) return false;
5346
+ this.timerValue = setTimeout(callback, delayMs);
5347
+ return true;
5348
+ }
5349
+ takePendingAndClearTimer() {
5350
+ const count = this.pendingCountValue;
5351
+ this.pendingCountValue = 0;
5352
+ this.clearTimer();
5353
+ return count;
5354
+ }
5355
+ };
5356
+
5082
5357
  // src/agentProcessManager.ts
5083
5358
  var DEFAULT_MAX_CONCURRENT_AGENT_STARTS = 5;
5084
5359
  var DEFAULT_AGENT_START_INTERVAL_MS = 500;
@@ -5297,6 +5572,36 @@ function resolveRuntimeSessionRef(runtime, sessionId, homeDir = os6.homedir(), f
5297
5572
  }
5298
5573
  return ref;
5299
5574
  }
5575
+ function classifySpawnFailure(error) {
5576
+ const detail = error instanceof Error ? error.message : String(error);
5577
+ const lower = detail.toLowerCase();
5578
+ if (lower.includes("agent credential proxy") && lower.includes("failed to bind")) {
5579
+ return {
5580
+ reason: "agent_proxy_bind_failed",
5581
+ detail,
5582
+ userMessage: "Local agent proxy could not start. Check if another daemon or service is using the required local port."
5583
+ };
5584
+ }
5585
+ if (lower.includes("runner_credential_mint") || error instanceof RunnerCredentialMintError) {
5586
+ return {
5587
+ reason: "runner_credential_mint_failed",
5588
+ detail,
5589
+ userMessage: "Runner credential mint failed. Ensure the server is deployed and the daemon binary is compatible."
5590
+ };
5591
+ }
5592
+ if (lower.includes("enoent") || lower.includes("cannot resolve") || lower.includes("not found")) {
5593
+ return {
5594
+ reason: "runtime_not_found",
5595
+ detail,
5596
+ userMessage: "Runtime executable not found. Ensure the required CLI is installed and available on PATH."
5597
+ };
5598
+ }
5599
+ return {
5600
+ reason: "runtime_spawn_failed",
5601
+ detail,
5602
+ userMessage: `Start failed: ${detail}`
5603
+ };
5604
+ }
5300
5605
  function formatSenderHandle(message) {
5301
5606
  return message.sender_description ? `@${message.sender_name} \u2014 ${message.sender_description}` : `@${message.sender_name}`;
5302
5607
  }
@@ -5945,26 +6250,29 @@ function hasDirectStdinRecoveryEvidence(ap) {
5945
6250
  (text) => /write_stdin failed|stdin is closed|closed for this session|session.*closed/i.test(text)
5946
6251
  );
5947
6252
  }
5948
- function isMissingResumeSession(ap) {
5949
- if (!ap.sessionId) return false;
6253
+ function resumeSessionRecoveryReason(ap) {
6254
+ if (!ap.sessionId) return null;
5950
6255
  const candidates = [
5951
6256
  ap.lastRuntimeError,
5952
6257
  ...ap.recentStderr
5953
6258
  ].filter((value) => !!value);
5954
6259
  if (ap.driver.id === "claude") {
5955
- return candidates.some((text) => /No conversation found with session ID/i.test(text));
6260
+ return candidates.some((text) => /No conversation found with session ID/i.test(text)) ? "missing" : null;
5956
6261
  }
5957
6262
  if (ap.driver.id === "opencode") {
5958
- return candidates.some(
5959
- (text) => /Session not found/i.test(text) && text.includes(ap.sessionId)
5960
- );
6263
+ if (candidates.some((text) => /Session not found/i.test(text) && text.includes(ap.sessionId))) return "missing";
6264
+ if (candidates.some(isOpenCodeReplayRejectedByProvider)) return "provider_replay_rejected";
6265
+ return null;
5961
6266
  }
5962
6267
  if (ap.driver.id === "gemini") {
5963
6268
  return candidates.some(
5964
6269
  (text) => /Error resuming session:\s*Invalid session identifier/i.test(text) && text.includes(ap.sessionId)
5965
- );
6270
+ ) ? "missing" : null;
5966
6271
  }
5967
- return false;
6272
+ return null;
6273
+ }
6274
+ function isOpenCodeReplayRejectedByProvider(text) {
6275
+ return /Invalid request:\s*the message at position \d+ with role ['"]?assistant['"]? must not be empty/i.test(text);
5968
6276
  }
5969
6277
  function resumeSessionRuntimeLabel(runtimeId) {
5970
6278
  if (runtimeId === "opencode") return "OpenCode";
@@ -6021,7 +6329,7 @@ function buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes) {
6021
6329
  launchId: ap.launchId || void 0,
6022
6330
  sessionIdPresent: Boolean(ap.sessionId),
6023
6331
  inboxCount: ap.inbox.length,
6024
- pendingNotificationCount: ap.pendingNotificationCount,
6332
+ pendingNotificationCount: ap.notifications.pendingCount,
6025
6333
  processPidPresent: typeof ap.process.pid === "number",
6026
6334
  busyDeliveryMode: ap.driver.busyDeliveryMode,
6027
6335
  supportsStdinNotification: ap.driver.supportsStdinNotification,
@@ -6035,7 +6343,7 @@ function buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes) {
6035
6343
  };
6036
6344
  }
6037
6345
  function classifyRuntimeStallReason(ap) {
6038
- if (ap.lastRuntimeEventKind === "tool_output" && ap.gatedSteering.outstandingToolUses === 0) {
6346
+ if (ap.runtimeProgress.lastEventKind === "tool_output" && ap.gatedSteering.outstandingToolUses === 0) {
6039
6347
  return "harness_post_tool_silent_wedge";
6040
6348
  }
6041
6349
  return "no_runtime_events";
@@ -6766,16 +7074,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6766
7074
  startupUnreadSummary: unreadSummary,
6767
7075
  startupResumePrompt: resumePrompt,
6768
7076
  isIdle: false,
6769
- notificationTimer: null,
6770
- pendingNotificationCount: 0,
7077
+ notifications: new RuntimeNotificationState(),
6771
7078
  activityHeartbeat: null,
6772
7079
  startupTimeoutTimer: null,
6773
7080
  startupTimedOut: false,
6774
7081
  compactionWatchdog: null,
6775
7082
  compactionStartedAt: null,
6776
- lastRuntimeEventAt: Date.now(),
6777
- lastRuntimeEventKind: null,
6778
- runtimeProgressStaleSince: null,
7083
+ runtimeProgress: new RuntimeProgressState(Date.now()),
6779
7084
  runtimeTraceSpan: null,
6780
7085
  runtimeTraceCounters: createRuntimeTraceCounters(),
6781
7086
  lastActivity: "",
@@ -6884,7 +7189,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6884
7189
  clean_exit: code === 0,
6885
7190
  runtime_trace_active: Boolean(current?.runtimeTraceSpan),
6886
7191
  inbox_count: current?.inbox.length ?? 0,
6887
- pending_notification_count: current?.pendingNotificationCount ?? 0,
7192
+ pending_notification_count: current?.notifications.pendingCount ?? 0,
6888
7193
  ...this.processExitTraceAttrs.get(proc)
6889
7194
  });
6890
7195
  logger.info(`[Agent ${agentId}] Process exited with code ${code}${signal ? ` (signal ${signal})` : ""}`);
@@ -6893,9 +7198,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6893
7198
  if (this.agents.has(agentId)) {
6894
7199
  const ap = this.agents.get(agentId);
6895
7200
  if (ap.process !== proc) return;
6896
- if (ap.notificationTimer) {
6897
- clearTimeout(ap.notificationTimer);
6898
- }
7201
+ ap.notifications.clearTimer();
6899
7202
  if (ap.pendingTrajectory?.timer) {
6900
7203
  clearTimeout(ap.pendingTrajectory.timer);
6901
7204
  }
@@ -6909,7 +7212,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6909
7212
  const expectedTermination = Boolean(ap.expectedTerminationReason) || ap.startupTimedOut;
6910
7213
  const processEndedCleanly = finalCode === 0 || expectedTermination && !ap.lastRuntimeError;
6911
7214
  const terminalFailureDetail = processEndedCleanly ? null : classifyTerminalFailure(ap);
6912
- const missingResumeSession = isMissingResumeSession(ap);
7215
+ const resumeRecoveryReason = resumeSessionRecoveryReason(ap);
7216
+ const shouldColdStartResumeSession = resumeRecoveryReason !== null;
6913
7217
  const summary = summarizeCrash(finalCode, finalSignal);
6914
7218
  this.endRuntimeTrace(ap, processEndedCleanly ? "ok" : "error", {
6915
7219
  outcome: processEndedCleanly ? "process-exit" : "process-crash",
@@ -6923,20 +7227,22 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
6923
7227
  cleanupAgentCredentialProxy(agentId, ap.launchId);
6924
7228
  this.revokeManagedRunnerCredential(agentId, ap.config, ap.launchId);
6925
7229
  this.agents.delete(agentId);
6926
- if (missingResumeSession) {
7230
+ if (shouldColdStartResumeSession) {
6927
7231
  const staleSessionId = ap.sessionId;
6928
7232
  const runtimeLabel = resumeSessionRuntimeLabel(ap.driver.id);
6929
7233
  const restartConfig = { ...stripManagedRunnerCredential(ap.config), sessionId: null };
7234
+ const reasonText = resumeRecoveryReason === "provider_replay_rejected" ? "was rejected by the provider during replay" : "is unavailable locally";
7235
+ const activityText = resumeRecoveryReason === "provider_replay_rejected" ? `Stored ${runtimeLabel} session replay rejected; cold-starting a new session\u2026` : `Stored ${runtimeLabel} session missing; cold-starting a new session\u2026`;
6930
7236
  logger.warn(
6931
- `[Agent ${agentId}] Stored ${runtimeLabel} session ${staleSessionId} is unavailable locally; falling back to cold start`
7237
+ `[Agent ${agentId}] Stored ${runtimeLabel} session ${staleSessionId} ${reasonText}; falling back to cold start`
6932
7238
  );
6933
7239
  this.broadcastActivity(
6934
7240
  agentId,
6935
7241
  "working",
6936
- `Stored ${runtimeLabel} session missing; cold-starting a new session\u2026`,
7242
+ activityText,
6937
7243
  [{
6938
7244
  kind: "text",
6939
- text: `Stored ${runtimeLabel} session ${staleSessionId} was not found locally. Falling back to a cold start; earlier runtime context may not be restored.`
7245
+ text: `Stored ${runtimeLabel} session ${staleSessionId} ${reasonText}. Falling back to a cold start; earlier runtime context may not be restored.`
6940
7246
  }]
6941
7247
  );
6942
7248
  this.startAgent(
@@ -7205,15 +7511,15 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7205
7511
  enqueueRuntimeProfileNotification(agentId, ap, message, kind, key) {
7206
7512
  ap.inbox.push(message);
7207
7513
  if (ap.driver.supportsStdinNotification && ap.sessionId) {
7208
- ap.pendingNotificationCount++;
7514
+ ap.notifications.add();
7209
7515
  if (ap.driver.busyDeliveryMode === "gated") {
7210
7516
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
7211
7517
  reason: "runtime_profile",
7212
7518
  kind,
7213
7519
  pendingMessages: ap.inbox.length
7214
7520
  });
7215
- } else if (!ap.notificationTimer) {
7216
- ap.notificationTimer = setTimeout(() => {
7521
+ } else if (!ap.notifications.hasTimer) {
7522
+ ap.notifications.schedule(() => {
7217
7523
  this.sendStdinNotification(agentId);
7218
7524
  }, 3e3);
7219
7525
  }
@@ -7228,7 +7534,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7228
7534
  session_id_present: Boolean(ap.sessionId),
7229
7535
  launchId: ap.launchId || void 0,
7230
7536
  inbox_count: ap.inbox.length,
7231
- pending_notification_count: ap.pendingNotificationCount,
7537
+ pending_notification_count: ap.notifications.pendingCount,
7232
7538
  busy_delivery_mode: ap.driver.busyDeliveryMode,
7233
7539
  supports_stdin_notification: ap.driver.supportsStdinNotification
7234
7540
  });
@@ -7266,9 +7572,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7266
7572
  }
7267
7573
  return;
7268
7574
  }
7269
- if (ap.notificationTimer) {
7270
- clearTimeout(ap.notificationTimer);
7271
- }
7575
+ ap.notifications.clearTimer();
7272
7576
  if (ap.activityHeartbeat) {
7273
7577
  clearInterval(ap.activityHeartbeat);
7274
7578
  }
@@ -7449,11 +7753,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7449
7753
  return true;
7450
7754
  }
7451
7755
  if (ap.gatedSteering.compacting) {
7452
- ap.pendingNotificationCount++;
7453
- if (ap.notificationTimer) {
7454
- clearTimeout(ap.notificationTimer);
7455
- ap.notificationTimer = null;
7456
- }
7756
+ ap.notifications.add();
7757
+ ap.notifications.clearTimer();
7457
7758
  if (ap.driver.busyDeliveryMode === "gated") {
7458
7759
  this.recordGatedSteeringEvent(agentId, ap, "buffer", {
7459
7760
  reason: "compaction_boundary",
@@ -7461,7 +7762,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7461
7762
  });
7462
7763
  }
7463
7764
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.compaction_boundary.delivery_buffered", {
7464
- pendingNotificationCount: ap.pendingNotificationCount,
7765
+ pendingNotificationCount: ap.notifications.pendingCount,
7465
7766
  pendingMessages: ap.inbox.length,
7466
7767
  busyDeliveryMode: ap.driver.busyDeliveryMode
7467
7768
  });
@@ -7473,16 +7774,16 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7473
7774
  session_id_present: true,
7474
7775
  launchId: ap.launchId || void 0,
7475
7776
  inbox_count: ap.inbox.length,
7476
- pending_notification_count: ap.pendingNotificationCount,
7777
+ pending_notification_count: ap.notifications.pendingCount,
7477
7778
  busy_delivery_mode: ap.driver.busyDeliveryMode,
7478
7779
  notification_timer_present: false
7479
7780
  }));
7480
7781
  return true;
7481
7782
  }
7482
7783
  if (ap.driver.busyDeliveryMode === "gated") {
7483
- ap.pendingNotificationCount++;
7484
- if (!ap.notificationTimer) {
7485
- ap.notificationTimer = setTimeout(() => {
7784
+ ap.notifications.add();
7785
+ if (!ap.notifications.hasTimer) {
7786
+ ap.notifications.schedule(() => {
7486
7787
  this.sendStdinNotification(agentId);
7487
7788
  }, 3e3);
7488
7789
  }
@@ -7498,14 +7799,14 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7498
7799
  session_id_present: true,
7499
7800
  launchId: ap.launchId || void 0,
7500
7801
  inbox_count: ap.inbox.length,
7501
- pending_notification_count: ap.pendingNotificationCount,
7502
- notification_timer_present: Boolean(ap.notificationTimer)
7802
+ pending_notification_count: ap.notifications.pendingCount,
7803
+ notification_timer_present: ap.notifications.hasTimer
7503
7804
  }));
7504
7805
  return true;
7505
7806
  }
7506
- ap.pendingNotificationCount++;
7507
- if (!ap.notificationTimer) {
7508
- ap.notificationTimer = setTimeout(() => {
7807
+ ap.notifications.add();
7808
+ if (!ap.notifications.hasTimer) {
7809
+ ap.notifications.schedule(() => {
7509
7810
  this.sendStdinNotification(agentId);
7510
7811
  }, 3e3);
7511
7812
  }
@@ -7516,8 +7817,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
7516
7817
  runtime: ap.config.runtime,
7517
7818
  session_id_present: true,
7518
7819
  inbox_count: ap.inbox.length,
7519
- pending_notification_count: ap.pendingNotificationCount,
7520
- notification_timer_present: Boolean(ap.notificationTimer)
7820
+ pending_notification_count: ap.notifications.pendingCount,
7821
+ notification_timer_present: ap.notifications.hasTimer
7521
7822
  }));
7522
7823
  return true;
7523
7824
  }
@@ -8312,40 +8613,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8312
8613
  this.startRuntimeTrace(agentId, ap, "runtime-progress").addEvent(name, attrs);
8313
8614
  }
8314
8615
  noteRuntimeProgress(ap, eventKind) {
8315
- ap.lastRuntimeEventAt = Date.now();
8316
- ap.lastRuntimeEventKind = eventKind ?? null;
8317
- ap.runtimeProgressStaleSince = null;
8318
- }
8319
- observeRuntimeTranscriptProgress(agentId, ap, staleForMs, source) {
8320
- if (ap.config.runtime !== "codex" || !ap.sessionId) return false;
8321
- const ref = resolveRuntimeSessionRef(ap.config.runtime, ap.sessionId, this.runtimeSessionHomeDir);
8322
- if (!ref.reachable || !ref.path) return false;
8323
- let mtimeMs;
8324
- try {
8325
- const stats = statSync2(ref.path);
8326
- if (!stats.isFile()) return false;
8327
- mtimeMs = stats.mtimeMs;
8328
- } catch {
8329
- return false;
8330
- }
8331
- if (mtimeMs <= ap.lastRuntimeEventAt) return false;
8332
- const now = Date.now();
8333
- const transcriptAgeMs = Math.max(0, now - mtimeMs);
8334
- if (transcriptAgeMs >= RUNTIME_PROGRESS_STALE_MS) return false;
8335
- ap.lastRuntimeEventAt = mtimeMs;
8336
- ap.runtimeProgressStaleSince = null;
8337
- this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.internal_observed", {
8338
- turn_outcome: "held",
8339
- turn_subtype: "runtime_progress",
8340
- turn_reason: "internal_activity_observed",
8341
- signal: "runtime_transcript_mtime",
8342
- source,
8343
- runtime: ap.config.runtime,
8344
- sessionRefReachable: true,
8345
- transcriptAgeMs,
8346
- previousStaleForMs: staleForMs
8347
- });
8348
- return true;
8616
+ ap.runtimeProgress.noteRuntimeEvent(eventKind);
8349
8617
  }
8350
8618
  recordGatedSteeringEvent(agentId, ap, event, attrs = {}) {
8351
8619
  if (ap.driver.busyDeliveryMode !== "gated") return;
@@ -8376,13 +8644,9 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8376
8644
  return this.sendStdinNotification(agentId);
8377
8645
  }
8378
8646
  const pendingMessages = ap.inbox.length;
8379
- const pendingNotificationCount = ap.pendingNotificationCount;
8647
+ const pendingNotificationCount = ap.notifications.pendingCount;
8380
8648
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
8381
- ap.pendingNotificationCount = 0;
8382
- if (ap.notificationTimer) {
8383
- clearTimeout(ap.notificationTimer);
8384
- ap.notificationTimer = null;
8385
- }
8649
+ ap.notifications.clear();
8386
8650
  ap.gatedSteering.lastFlushReason = reason;
8387
8651
  if (reason !== "turn_end") {
8388
8652
  ap.gatedSteering.inFlightBatch = {
@@ -8405,16 +8669,13 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8405
8669
  if (reason !== "turn_end") {
8406
8670
  ap.gatedSteering.inFlightBatch = null;
8407
8671
  }
8408
- ap.pendingNotificationCount += pendingNotificationCount || pendingMessages;
8672
+ ap.notifications.add(pendingNotificationCount || pendingMessages);
8409
8673
  return false;
8410
8674
  }
8411
8675
  flushCompactionBoundaryMessages(agentId, ap) {
8412
8676
  if (!ap.sessionId || !ap.driver.supportsStdinNotification || ap.inbox.length === 0) return false;
8413
- if (ap.pendingNotificationCount > 0) {
8414
- if (ap.notificationTimer) {
8415
- clearTimeout(ap.notificationTimer);
8416
- ap.notificationTimer = null;
8417
- }
8677
+ if (ap.notifications.pendingCount > 0) {
8678
+ ap.notifications.clearTimer();
8418
8679
  this.sendStdinNotification(agentId);
8419
8680
  return true;
8420
8681
  }
@@ -8442,7 +8703,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8442
8703
  handleRuntimeStartupTimeout(agentId, ap, timeoutMs) {
8443
8704
  const current = this.agents.get(agentId);
8444
8705
  if (current !== ap) return;
8445
- if (ap.lastRuntimeEventKind) {
8706
+ if (ap.runtimeProgress.lastEventKind) {
8446
8707
  this.clearRuntimeStartupTimeout(ap);
8447
8708
  return;
8448
8709
  }
@@ -8451,8 +8712,8 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8451
8712
  const detail = terminalFailureDetail?.detail ?? formatRuntimeStartTimeoutMessage(ap.driver.id);
8452
8713
  ap.startupTimedOut = true;
8453
8714
  ap.lastRuntimeError = detail;
8454
- ap.runtimeProgressStaleSince = Date.now();
8455
- const staleForMs = Math.max(timeoutMs, Date.now() - ap.lastRuntimeEventAt);
8715
+ ap.runtimeProgress.markStale();
8716
+ const staleForMs = Math.max(timeoutMs, ap.runtimeProgress.ageMs());
8456
8717
  const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, Math.max(1, Math.floor(staleForMs / 6e4)));
8457
8718
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.start.timeout", {
8458
8719
  turn_outcome: "failed",
@@ -8491,7 +8752,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8491
8752
  const batch = ap.gatedSteering.inFlightBatch;
8492
8753
  ap.gatedSteering.inFlightBatch = null;
8493
8754
  ap.inbox.unshift(...batch.messages);
8494
- ap.pendingNotificationCount += batch.messages.length;
8755
+ ap.notifications.add(batch.messages.length);
8495
8756
  this.recordGatedSteeringEvent(agentId, ap, "requeue", {
8496
8757
  reason,
8497
8758
  originalFlushReason: batch.reason,
@@ -8503,11 +8764,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8503
8764
  }
8504
8765
  markRuntimeProgressStaleIfNeeded(agentId, ap) {
8505
8766
  if (ap.lastActivity !== "working" && ap.lastActivity !== "thinking") return false;
8506
- if (ap.runtimeProgressStaleSince) return true;
8507
- const staleForMs = Date.now() - ap.lastRuntimeEventAt;
8767
+ if (ap.runtimeProgress.isStale) return true;
8768
+ const staleForMs = ap.runtimeProgress.ageMs();
8508
8769
  if (staleForMs < RUNTIME_PROGRESS_STALE_MS) return false;
8509
- if (this.observeRuntimeTranscriptProgress(agentId, ap, staleForMs, "activity_heartbeat")) return false;
8510
- ap.runtimeProgressStaleSince = Date.now();
8770
+ ap.runtimeProgress.markStale();
8511
8771
  const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
8512
8772
  const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
8513
8773
  const turnReason = classifyRuntimeStallReason(ap);
@@ -8540,11 +8800,10 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8540
8800
  const canRestartDirectStdinProcess = directStdinRuntime && Boolean(ap.sessionId) && (ap.gatedSteering.outstandingToolUses === 0 || hasDirectStdinRecoveryEvidence(ap));
8541
8801
  const canRestartStalledProcess = !ap.driver.supportsStdinNotification || canRestartDirectStdinProcess;
8542
8802
  if (!canRestartStalledProcess) return false;
8543
- const staleForMs = Date.now() - ap.lastRuntimeEventAt;
8544
- if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgressStaleSince) return false;
8545
- if (this.observeRuntimeTranscriptProgress(agentId, ap, staleForMs, "queued_recovery")) return false;
8803
+ const staleForMs = ap.runtimeProgress.ageMs();
8804
+ if (staleForMs < RUNTIME_PROGRESS_STALE_MS && !ap.runtimeProgress.isStale) return false;
8546
8805
  const staleForMinutes = Math.max(1, Math.floor(staleForMs / 6e4));
8547
- ap.runtimeProgressStaleSince ??= Date.now();
8806
+ ap.runtimeProgress.markStale();
8548
8807
  const diagnostic = buildRuntimeStallDiagnostic(ap, staleForMs, staleForMinutes);
8549
8808
  const turnReason = classifyRuntimeStallReason(ap);
8550
8809
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.stalled", {
@@ -8593,14 +8852,38 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8593
8852
  /** Handle a single ParsedEvent from any runtime driver */
8594
8853
  handleParsedEvent(agentId, event, driver) {
8595
8854
  const ap = this.agents.get(agentId);
8855
+ if (event.kind === "telemetry") {
8856
+ if (ap) this.recordRuntimeTelemetry(agentId, ap, event);
8857
+ return;
8858
+ }
8596
8859
  if (ap) {
8597
- const wasStalled = Boolean(ap.runtimeProgressStaleSince);
8860
+ const wasStalled = ap.runtimeProgress.isStale;
8598
8861
  this.clearRuntimeStartupTimeout(ap);
8599
8862
  this.noteRuntimeTraceCounter(ap, event);
8600
- this.recordRuntimeTraceEvent(agentId, ap, "runtime.event.received", { kind: event.kind });
8863
+ const eventAttrs = event.kind === "internal_progress" ? {
8864
+ kind: event.kind,
8865
+ source: event.source,
8866
+ itemType: event.itemType,
8867
+ payloadBytes: event.payloadBytes
8868
+ } : { kind: event.kind };
8869
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.event.received", eventAttrs);
8601
8870
  if (wasStalled) {
8602
8871
  this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.observed", { afterStall: true });
8603
8872
  }
8873
+ if (event.kind === "internal_progress") {
8874
+ ap.runtimeProgress.noteInternalProgress();
8875
+ this.recordRuntimeTraceEvent(agentId, ap, "runtime.progress.internal_observed", {
8876
+ turn_outcome: "held",
8877
+ turn_subtype: "runtime_progress",
8878
+ turn_reason: "internal_activity_observed",
8879
+ signal: event.source,
8880
+ source: "runtime_event",
8881
+ runtime: ap.config.runtime,
8882
+ itemType: event.itemType,
8883
+ payloadBytes: event.payloadBytes
8884
+ });
8885
+ return;
8886
+ }
8604
8887
  this.noteRuntimeProgress(ap, event.kind);
8605
8888
  }
8606
8889
  switch (event.kind) {
@@ -8704,11 +8987,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8704
8987
  this.setGatedSteeringPhase(agentId, ap, "idle", { event: "turn_end" });
8705
8988
  if (ap.inbox.length > 0 && ap.driver.supportsStdinNotification && ap.sessionId) {
8706
8989
  const nextMessages = ap.inbox.splice(0, ap.inbox.length);
8707
- ap.pendingNotificationCount = 0;
8708
- if (ap.notificationTimer) {
8709
- clearTimeout(ap.notificationTimer);
8710
- ap.notificationTimer = null;
8711
- }
8990
+ ap.notifications.clear();
8712
8991
  if (ap.driver.busyDeliveryMode === "gated") {
8713
8992
  ap.gatedSteering.lastFlushReason = "turn_end";
8714
8993
  this.recordGatedSteeringEvent(agentId, ap, "flush", {
@@ -8802,11 +9081,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8802
9081
  }
8803
9082
  } else {
8804
9083
  ap.isIdle = true;
8805
- ap.pendingNotificationCount = 0;
8806
- if (ap.notificationTimer) {
8807
- clearTimeout(ap.notificationTimer);
8808
- ap.notificationTimer = null;
8809
- }
9084
+ ap.notifications.clear();
8810
9085
  logger.info(`[Agent ${agentId}] Marked ${ap.driver.id} wakeable after terminal runtime error`);
8811
9086
  }
8812
9087
  }
@@ -8818,6 +9093,18 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8818
9093
  }
8819
9094
  }
8820
9095
  }
9096
+ recordRuntimeTelemetry(agentId, ap, event) {
9097
+ const attrs = {
9098
+ agentId,
9099
+ launchId: ap.launchId || void 0,
9100
+ runtime: ap.config.runtime,
9101
+ model: ap.config.model,
9102
+ telemetry_name: event.name,
9103
+ ...event.attrs
9104
+ };
9105
+ ap.runtimeTraceSpan?.addEvent(`runtime.telemetry.${event.name}`, event.attrs);
9106
+ this.recordDaemonTrace(`daemon.runtime.telemetry.${event.name}`, attrs);
9107
+ }
8821
9108
  sendAgentStatus(agentId, status, launchId) {
8822
9109
  this.sendToServer({ type: "agent:status", agentId, status, launchId: launchId || void 0 });
8823
9110
  }
@@ -8871,12 +9158,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8871
9158
  sendStdinNotification(agentId) {
8872
9159
  const ap = this.agents.get(agentId);
8873
9160
  if (!ap) return false;
8874
- const count = ap.pendingNotificationCount;
8875
- ap.pendingNotificationCount = 0;
8876
- if (ap.notificationTimer) {
8877
- clearTimeout(ap.notificationTimer);
8878
- ap.notificationTimer = null;
8879
- }
9161
+ const count = ap.notifications.takePendingAndClearTimer();
8880
9162
  if (count === 0) return false;
8881
9163
  if (ap.isIdle) return false;
8882
9164
  if (!ap.sessionId) return false;
@@ -8886,7 +9168,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8886
9168
  pendingMessages: ap.inbox.length,
8887
9169
  busyDeliveryMode: ap.driver.busyDeliveryMode
8888
9170
  });
8889
- ap.pendingNotificationCount += count;
9171
+ ap.notifications.add(count);
8890
9172
  logger.info(
8891
9173
  `[Agent ${agentId}] Suppressing stdin delivery until context compaction finishes; pending=${ap.inbox.length}`
8892
9174
  );
@@ -8912,7 +9194,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8912
9194
  });
8913
9195
  return true;
8914
9196
  } else {
8915
- ap.pendingNotificationCount += count;
9197
+ ap.notifications.add(count);
8916
9198
  this.recordDaemonTrace("daemon.agent.stdin_notification", {
8917
9199
  agentId,
8918
9200
  runtime: ap.config.runtime,
@@ -8963,7 +9245,7 @@ Use ${communicationCommand(driver, "read_history")} to catch up on the channels
8963
9245
  messages_count: messages.length,
8964
9246
  session_id_present: Boolean(ap.sessionId),
8965
9247
  inbox_count: ap.inbox.length,
8966
- pending_notification_count: ap.pendingNotificationCount,
9248
+ pending_notification_count: ap.notifications.pendingCount,
8967
9249
  busy_delivery_mode: ap.driver.busyDeliveryMode,
8968
9250
  supports_stdin_notification: ap.driver.supportsStdinNotification,
8969
9251
  ...this.messagesTraceAttrs(messages)
@@ -10443,10 +10725,19 @@ var DaemonCore = class {
10443
10725
  case "agent:start":
10444
10726
  logger.info(`[Agent ${msg.agentId}] Start requested (runtime=${msg.config.runtime}, model=${msg.config.model}, session=${msg.config.sessionId || "new"}${msg.wakeMessage ? ", wake=true" : ""})`);
10445
10727
  this.startAgentFromMessage(msg).catch((err) => {
10446
- const reason = err instanceof Error ? err.message : String(err);
10447
- logger.error(`[Agent ${msg.agentId}] Start failed (${reason})`);
10728
+ const classification = classifySpawnFailure(err);
10729
+ logger.error(`[Agent ${msg.agentId}] Start failed (${classification.reason}): ${classification.detail}`);
10730
+ this.recordDaemonTrace("daemon.agent.spawn.failed", {
10731
+ agentId: msg.agentId,
10732
+ launchId: msg.launchId,
10733
+ runtime: msg.config.runtime,
10734
+ model: msg.config.model,
10735
+ failure_reason: classification.reason,
10736
+ failure_detail: classification.detail,
10737
+ session_id_present: Boolean(msg.config.sessionId)
10738
+ }, "error");
10448
10739
  this.connection.send({ type: "agent:status", agentId: msg.agentId, status: "inactive", launchId: msg.launchId });
10449
- this.connection.send({ type: "agent:activity", agentId: msg.agentId, activity: "offline", detail: `Start failed: ${reason}`, launchId: msg.launchId });
10740
+ this.connection.send({ type: "agent:activity", agentId: msg.agentId, activity: "offline", detail: classification.userMessage, launchId: msg.launchId });
10450
10741
  });
10451
10742
  break;
10452
10743
  case "agent:stop":