wayang-ai 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1082 -228
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -156,7 +156,11 @@ var METHOD = {
156
156
  GET_SNAPSHOT: "get_snapshot",
157
157
  SUBSCRIBE: "subscribe",
158
158
  UNSUBSCRIBE: "unsubscribe",
159
- SHUTDOWN: "shutdown"
159
+ SHUTDOWN: "shutdown",
160
+ /** Read-only: list turns of the controller conversation (limit/offset). */
161
+ CONVERSATION_LIST_TURNS: "conversation_list_turns",
162
+ /** Read-only: one turn of the controller conversation by turnId. */
163
+ CONVERSATION_GET_TURN: "conversation_get_turn"
160
164
  };
161
165
  var NOTIFY = {
162
166
  READY: "ready",
@@ -819,14 +823,8 @@ function ToolResultRow({ section: section2 }) {
819
823
  import { Box as Box3, Text as Text3 } from "ink";
820
824
 
821
825
  // src/types/conversation.ts
822
- function isSystemEntry(entry) {
823
- return entry.type === "system" /* System */;
824
- }
825
- function getEntryContent(entry) {
826
- if (entry.type === "signal" /* Signal */ || entry.type === "system" /* System */) {
827
- return entry.content;
828
- }
829
- return entry.message.content;
826
+ function isCompactMarker(entry) {
827
+ return entry.type === "system" /* System */ && entry.subtype === "compact" /* Compact */ && entry.compactMeta !== void 0;
830
828
  }
831
829
 
832
830
  // src/tui/components/chat-area/signal-row.tsx
@@ -868,12 +866,19 @@ function SignalRow({ item }) {
868
866
  // src/tui/components/chat-area/system-row.tsx
869
867
  import { Box as Box4, Text as Text4 } from "ink";
870
868
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
869
+ function isCompactSystemItem(item) {
870
+ return item.subtype === "compact" /* Compact */ && item.compactMeta !== void 0;
871
+ }
871
872
  function SystemRow({ item }) {
872
- const isError = item.subtype === "error";
873
- const isCompact = item.subtype === "compact";
873
+ const isError = item.subtype === "error" /* Error */;
874
+ const isCompact = isCompactSystemItem(item);
875
+ const isTaskResult = item.subtype === "task_result" /* TaskResult */;
874
876
  if (isCompact) {
875
877
  return /* @__PURE__ */ jsx5(Box4, { children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: "\u224B Conversation has been compacted" }) });
876
878
  }
879
+ if (isTaskResult) {
880
+ return /* @__PURE__ */ jsx5(Box4, { children: /* @__PURE__ */ jsx5(Text4, { dimColor: true, children: item.content }) });
881
+ }
877
882
  return /* @__PURE__ */ jsxs4(Box4, { children: [
878
883
  /* @__PURE__ */ jsxs4(Text4, { color: isError ? theme.baseToken.color.error : theme.meta.color, children: [
879
884
  isError ? "\u2718" : theme.meta.prefix,
@@ -1396,7 +1401,7 @@ function entryToDisplayItem(entry) {
1396
1401
  case "signal" /* Signal */:
1397
1402
  return { ...base, role: "signal", subtype: entry.subtype, content: entry.content, taskId: entry.taskId, taskTitle: entry.taskTitle, workerId: entry.workerId, workerType: entry.workerType, emoji: entry.emoji };
1398
1403
  case "system" /* System */:
1399
- return { ...base, role: "system", subtype: entry.subtype, content: entry.content };
1404
+ return { ...base, role: "system", subtype: entry.subtype, content: entry.content, compactMeta: entry.compactMeta };
1400
1405
  }
1401
1406
  }
1402
1407
  var signalSubtypeMap = {
@@ -1404,7 +1409,8 @@ var signalSubtypeMap = {
1404
1409
  failed: "worker_failed" /* WorkerFailed */,
1405
1410
  progress: "worker_progress" /* WorkerProgress */,
1406
1411
  heartbeat: "heartbeat" /* Heartbeat */,
1407
- cancelled: "worker_failed" /* WorkerFailed */
1412
+ cancelled: "worker_failed" /* WorkerFailed */,
1413
+ task_cancelled: "task_cancelled" /* TaskCancelled */
1408
1414
  };
1409
1415
  function signalToDisplayItem(signal) {
1410
1416
  const base = {
@@ -1570,7 +1576,7 @@ function AppContent() {
1570
1576
  // package.json
1571
1577
  var package_default = {
1572
1578
  name: "wayang-ai",
1573
- version: "0.5.0",
1579
+ version: "0.5.1",
1574
1580
  description: "Multi-agent orchestration platform",
1575
1581
  type: "module",
1576
1582
  license: "MIT",
@@ -2034,6 +2040,21 @@ import { join as join5 } from "path";
2034
2040
 
2035
2041
  // src/infra/state/base-state.ts
2036
2042
  import { get, set, cloneDeep } from "lodash-es";
2043
+
2044
+ // src/infra/state/conversation-envelope.ts
2045
+ var ENVELOPE_VERSION = 1;
2046
+ function applyAppendEnvelope(entry, conversation) {
2047
+ if (entry === null || typeof entry !== "object") return entry;
2048
+ if (entry.v === void 0) entry.v = ENVELOPE_VERSION;
2049
+ if (entry.parentId === void 0) {
2050
+ const last = conversation.length > 0 ? conversation[conversation.length - 1] : void 0;
2051
+ const lastIsMarker = last !== null && typeof last === "object" && isCompactMarker(last);
2052
+ entry.parentId = !last || lastIsMarker || isCompactMarker(entry) || typeof last.uuid !== "string" ? null : last.uuid;
2053
+ }
2054
+ return entry;
2055
+ }
2056
+
2057
+ // src/infra/state/base-state.ts
2037
2058
  var BaseWayangState = class {
2038
2059
  data;
2039
2060
  persistMappings;
@@ -2069,9 +2090,10 @@ var BaseWayangState = class {
2069
2090
  if (!Array.isArray(arr)) {
2070
2091
  throw new Error(`State.append: path "${path7}" is not an array`);
2071
2092
  }
2072
- arr.push(entry);
2093
+ const stamped = path7 === "conversation" ? applyAppendEnvelope(entry, arr) : entry;
2094
+ arr.push(stamped);
2073
2095
  set(this.data, path7, [...arr]);
2074
- this.persist(path7, "append", entry);
2096
+ this.persist(path7, "append", stamped);
2075
2097
  this.notify(path7, "append", void 0);
2076
2098
  }
2077
2099
  remove(path7, idx) {
@@ -2714,6 +2736,18 @@ function formatTimestamp(date = /* @__PURE__ */ new Date()) {
2714
2736
  return `${Y}${M}${D}-${h}${m}${s}`;
2715
2737
  }
2716
2738
 
2739
+ // src/utils/time.ts
2740
+ function nowISO() {
2741
+ const now = /* @__PURE__ */ new Date();
2742
+ const off = -now.getTimezoneOffset();
2743
+ const sign = off >= 0 ? "+" : "-";
2744
+ const absOff = Math.abs(off);
2745
+ const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
2746
+ const mm = String(absOff % 60).padStart(2, "0");
2747
+ const pad = (n, len = 2) => String(n).padStart(len, "0");
2748
+ return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}${sign}${hh}:${mm}`;
2749
+ }
2750
+
2717
2751
  // src/services/agents/base-agent.ts
2718
2752
  var BaseAgent = class {
2719
2753
  id;
@@ -2742,9 +2776,18 @@ var BaseAgent = class {
2742
2776
  }
2743
2777
  }
2744
2778
  let startTime;
2779
+ let firstChunkAt;
2780
+ let stepStartedAt;
2781
+ const onStep = opts.onStep ? (event) => {
2782
+ const meta = { startedAt: stepStartedAt, firstTokenAt: firstChunkAt };
2783
+ firstChunkAt = void 0;
2784
+ stepStartedAt = nowISO();
2785
+ opts.onStep(event, meta);
2786
+ } : void 0;
2745
2787
  try {
2746
2788
  this.hooks.beforeLLM?.({ messageCount: opts.messages.length, messages: opts.messages });
2747
2789
  startTime = Date.now();
2790
+ stepStartedAt = nowISO();
2748
2791
  stream = streamText({
2749
2792
  model: this.model,
2750
2793
  system: opts.system,
@@ -2753,7 +2796,7 @@ var BaseAgent = class {
2753
2796
  toolChoice: opts.toolChoice,
2754
2797
  stopWhen: stopConditions.length > 0 ? stopConditions : void 0,
2755
2798
  abortSignal: this.abortController.signal,
2756
- onStepFinish: opts.onStep,
2799
+ onStepFinish: onStep,
2757
2800
  onError: ({ error }) => {
2758
2801
  opts.onStreamError?.(error);
2759
2802
  },
@@ -2770,6 +2813,7 @@ var BaseAgent = class {
2770
2813
  }
2771
2814
  try {
2772
2815
  for await (const chunk of stream.textStream) {
2816
+ if (chunk && firstChunkAt === void 0) firstChunkAt = nowISO();
2773
2817
  yield chunk;
2774
2818
  }
2775
2819
  } catch (err) {
@@ -2913,37 +2957,30 @@ function estimateTokens(text) {
2913
2957
  if (!text) return 0;
2914
2958
  return Math.ceil(text.length / 4);
2915
2959
  }
2960
+ function estimateEntryTokens(entry) {
2961
+ if (!entry) return 0;
2962
+ const content = entry.message?.content ?? entry.content ?? "";
2963
+ let tokens = estimateTokens(String(content ?? ""));
2964
+ if (entry.toolCalls?.length) {
2965
+ for (const tc of entry.toolCalls) {
2966
+ tokens += estimateTokens(tc.arguments ?? "");
2967
+ }
2968
+ }
2969
+ if (entry.toolResults?.length) {
2970
+ for (const tr of entry.toolResults) {
2971
+ tokens += estimateTokens(tr.output?.value ?? "");
2972
+ }
2973
+ }
2974
+ return tokens;
2975
+ }
2916
2976
  function estimateConversationTokens(entries) {
2917
2977
  let total = 0;
2918
2978
  for (const entry of entries) {
2919
- if (entry.message?.content) {
2920
- total += estimateTokens(entry.message.content);
2921
- }
2922
- if (entry.arguments) {
2923
- total += estimateTokens(typeof entry.arguments === "string" ? entry.arguments : JSON.stringify(entry.arguments));
2924
- }
2925
- if (entry.result) {
2926
- total += estimateTokens(entry.result);
2927
- }
2928
- if (entry.content) {
2929
- total += estimateTokens(entry.content);
2930
- }
2979
+ total += estimateEntryTokens(entry);
2931
2980
  }
2932
2981
  return total;
2933
2982
  }
2934
2983
 
2935
- // src/utils/time.ts
2936
- function nowISO() {
2937
- const now = /* @__PURE__ */ new Date();
2938
- const off = -now.getTimezoneOffset();
2939
- const sign = off >= 0 ? "+" : "-";
2940
- const absOff = Math.abs(off);
2941
- const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
2942
- const mm = String(absOff % 60).padStart(2, "0");
2943
- const pad = (n, len = 2) => String(n).padStart(len, "0");
2944
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}T${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}${sign}${hh}:${mm}`;
2945
- }
2946
-
2947
2984
  // src/services/agents/context-manager.ts
2948
2985
  var DEFAULT_MAX_TOKENS = 1e5;
2949
2986
  var COMPACT_THRESHOLD_RATIO = 0.9;
@@ -2959,13 +2996,22 @@ var ContextManager = class {
2959
2996
  staticPrompt;
2960
2997
  maxTokens;
2961
2998
  /**
2962
- * Cached compact state — updated by compact() and restore(), read by getMessages().
2963
- * Null means no compaction has occurred.
2999
+ * Cached compact state — updated by compact() and restoreCompact(), read by
3000
+ * getMessages()/getSystemPrompt(). Null means no compaction is active.
2964
3001
  */
2965
3002
  compactSummary = null;
2966
3003
  /**
2967
- * Index into the conversation array: entries from this index onward are the
2968
- * "recent" entries that should be sent to the LLM alongside the summary.
3004
+ * Index into the conversation array: entries from this index onward form the
3005
+ * "verbatim zone" — entries sent to the LLM as-is alongside the summary.
3006
+ *
3007
+ * Semantics (unified):
3008
+ * - After compact(): conversation.length. Only entries appended AFTER the
3009
+ * compaction belong to the verbatim zone; pre-compaction entries live on
3010
+ * in the summary (and in the append-only JSONL history).
3011
+ * - After restoreCompact(): 0. The state layer has already sliced the
3012
+ * in-memory conversation to the restore window, so the whole array IS
3013
+ * the verbatim zone.
3014
+ *
2969
3015
  * Updated together with compactSummary.
2970
3016
  */
2971
3017
  compactStartIdx = 0;
@@ -3004,6 +3050,22 @@ ${this.compactSummary}` },
3004
3050
  const convTokens = estimateConversationTokens(this.getMessages());
3005
3051
  return promptTokens + convTokens > this.maxTokens * COMPACT_THRESHOLD_RATIO;
3006
3052
  }
3053
+ /**
3054
+ * Restore compact state from a summary computed by the state layer.
3055
+ *
3056
+ * Call this after the state layer's restore() has already sliced the
3057
+ * in-memory conversation to the restore window (entries before the cut are
3058
+ * gone from memory). The window itself is the verbatim zone, so
3059
+ * compactStartIdx resets to 0 — see the field's semantics note.
3060
+ */
3061
+ restoreCompact(summary) {
3062
+ this.compactSummary = summary;
3063
+ this.compactStartIdx = 0;
3064
+ }
3065
+ /** Get the cached compaction summary, or null if no compaction is active. */
3066
+ getCompactSummary() {
3067
+ return this.compactSummary;
3068
+ }
3007
3069
  /**
3008
3070
  * Compact the conversation by summarizing all current entries.
3009
3071
  *
@@ -3012,72 +3074,40 @@ ${this.compactSummary}` },
3012
3074
  *
3013
3075
  * The summarizer receives ALL entries and returns a summary.
3014
3076
  * After compaction, getMessages() returns [summary] + entries appended after the marker.
3077
+ *
3078
+ * @param meta Optional provenance recorded on the marker (model name).
3015
3079
  */
3016
- async compact(summarizer) {
3080
+ async compact(summarizer, meta) {
3017
3081
  const conversation = this.state.get("conversation");
3018
3082
  if (conversation.length < 4) return;
3083
+ const fromEntryId = conversation[0]?.uuid;
3084
+ const toEntryId = conversation[conversation.length - 1]?.uuid;
3019
3085
  const summary = await summarizer(conversation);
3020
3086
  this.compactSummary = summary;
3021
3087
  this.compactStartIdx = conversation.length;
3088
+ const markerTs = nowISO();
3022
3089
  this.state.append("conversation", {
3023
3090
  type: "system" /* System */,
3024
3091
  uuid: generateId("compact"),
3025
3092
  parentUuid: null,
3026
3093
  sessionId: this.state.get("runtimeState.session.id") ?? "",
3027
- timestamp: nowISO(),
3094
+ timestamp: markerTs,
3028
3095
  subtype: "compact" /* Compact */,
3029
3096
  content: summary,
3097
+ // System entries are point-in-time events: startedAt/endedAt share the
3098
+ // marker's creation moment.
3099
+ startedAt: markerTs,
3100
+ endedAt: markerTs,
3030
3101
  compactMeta: {
3031
- summarizedTokenCount: estimateConversationTokens(conversation)
3102
+ summarizedTokenCount: estimateConversationTokens(conversation),
3103
+ ...fromEntryId !== void 0 && { fromEntryId },
3104
+ ...toEntryId !== void 0 && { toEntryId },
3105
+ ...meta?.model !== void 0 && { model: meta.model },
3106
+ createdAt: markerTs
3032
3107
  }
3033
3108
  });
3034
3109
  }
3035
- /**
3036
- * Restore compact state from persisted conversation.
3037
- *
3038
- * Scans for the latest compact marker, then loads recent entries within
3039
- * the token budget. If loaded entries are fewer than RECENT_TOKEN_BUDGET,
3040
- * continues loading past the marker (overlap with summary is acceptable).
3041
- */
3042
- restoreFromEntries(entries) {
3043
- const compactIdx = entries.findLastIndex(
3044
- (e) => e.type === "system" /* System */ && e.subtype === "compact" /* Compact */
3045
- );
3046
- if (compactIdx === -1) {
3047
- this.compactSummary = null;
3048
- this.compactStartIdx = 0;
3049
- return;
3050
- }
3051
- const summary = entries[compactIdx].content;
3052
- let budget = RECENT_TOKEN_BUDGET;
3053
- let cutIdx = 0;
3054
- for (let i = compactIdx - 1; i >= 0; i--) {
3055
- const tokens = estimateEntryTokens(entries[i]);
3056
- budget -= tokens;
3057
- if (budget <= 0) {
3058
- cutIdx = i + 1;
3059
- break;
3060
- }
3061
- }
3062
- this.compactSummary = summary;
3063
- this.compactStartIdx = compactIdx - cutIdx;
3064
- }
3065
3110
  };
3066
- function estimateEntryTokens(entry) {
3067
- const content = entry.message?.content ?? entry.content ?? "";
3068
- let tokens = estimateTokens(String(content));
3069
- if (entry.toolCalls?.length) {
3070
- for (const tc of entry.toolCalls) {
3071
- tokens += estimateTokens(tc.arguments ?? "");
3072
- }
3073
- }
3074
- if (entry.toolResults?.length) {
3075
- for (const tr of entry.toolResults) {
3076
- tokens += estimateTokens(tr.output?.value ?? "");
3077
- }
3078
- }
3079
- return tokens;
3080
- }
3081
3111
 
3082
3112
  // src/services/agents/worker-state.ts
3083
3113
  import { join as join7 } from "path";
@@ -3110,33 +3140,67 @@ var WorkerState = class extends BaseWayangState {
3110
3140
  if (runtimeState) {
3111
3141
  this.data.runtimeState = runtimeState;
3112
3142
  }
3113
- const conversation = this.jsonlFile.read();
3114
- if (conversation.length > 0) {
3115
- this.data.conversation = conversation;
3143
+ const allEntries = this.jsonlFile.read();
3144
+ const compactIdx = allEntries.findLastIndex(isCompactMarker);
3145
+ if (compactIdx === -1) {
3146
+ if (allEntries.length > 0) {
3147
+ this.data.conversation = allEntries;
3148
+ }
3149
+ } else {
3150
+ let budget = RECENT_TOKEN_BUDGET;
3151
+ let cutIdx = 0;
3152
+ for (let i = compactIdx - 1; i >= 0; i--) {
3153
+ budget -= estimateEntryTokens(allEntries[i]);
3154
+ if (budget <= 0) {
3155
+ cutIdx = i + 1;
3156
+ break;
3157
+ }
3158
+ }
3159
+ this.data.conversation = allEntries.slice(cutIdx);
3116
3160
  }
3117
3161
  this.logger.info("WorkerState restored");
3118
3162
  }
3119
3163
  };
3120
3164
 
3121
3165
  // src/services/agents/utils/build-assistant-entry.ts
3122
- function buildAssistantEntry(event, sessionId, uuid) {
3166
+ function buildAssistantEntry(event, sessionId, uuid, extras) {
3123
3167
  const ts = nowISO();
3124
3168
  const text = event.text ?? "";
3125
3169
  const reasoningText = event.reasoningText?.trim() || // Fallback: join per-part reasoning when reasoningText is not populated
3126
3170
  (event.reasoning ?? []).map((r) => r.text).filter(Boolean).join("\n") || void 0;
3127
3171
  const reasoning = reasoningText && reasoningText !== "[]" ? reasoningText : void 0;
3128
- const toolCalls = event.toolCalls?.length ? event.toolCalls.map((tc) => ({
3129
- toolCallId: tc.toolCallId,
3130
- toolName: tc.toolName,
3131
- arguments: (
3132
- // SDK OnStepFinishEvent type incomplete
3133
- typeof tc.args === "string" ? tc.args : JSON.stringify(tc.args ?? tc.input ?? {})
3134
- )
3135
- })) : [];
3172
+ const lookupRecord = extras?.recordedToolCalls;
3173
+ const toolCalls = event.toolCalls?.length ? event.toolCalls.map((tc) => {
3174
+ const rec = lookupRecord?.(tc.toolCallId);
3175
+ return {
3176
+ toolCallId: tc.toolCallId,
3177
+ toolName: tc.toolName,
3178
+ arguments: (
3179
+ // SDK OnStepFinishEvent type incomplete
3180
+ typeof tc.args === "string" ? tc.args : JSON.stringify(tc.args ?? tc.input ?? {})
3181
+ ),
3182
+ ...rec?.taskId !== void 0 && { taskId: rec.taskId },
3183
+ ...rec?.status !== void 0 && { status: rec.status },
3184
+ ...rec?.startedAt !== void 0 && { startedAt: rec.startedAt },
3185
+ ...rec?.endedAt !== void 0 && { endedAt: rec.endedAt },
3186
+ ...rec?.errorType !== void 0 && { errorType: rec.errorType }
3187
+ };
3188
+ }) : [];
3136
3189
  const results = [];
3190
+ const withRecordMeta = (r) => {
3191
+ const rec = lookupRecord?.(r.toolCallId);
3192
+ if (!rec) return r;
3193
+ return {
3194
+ ...r,
3195
+ ...rec.status !== void 0 && { status: rec.status },
3196
+ ...rec.errorType !== void 0 && { errorType: rec.errorType },
3197
+ ...rec.startedAt !== void 0 && { startedAt: rec.startedAt },
3198
+ ...rec.endedAt !== void 0 && { endedAt: rec.endedAt }
3199
+ };
3200
+ };
3137
3201
  if (event.toolResults?.length) {
3138
3202
  results.push(
3139
- ...event.toolResults.map((tr) => ({
3203
+ ...event.toolResults.map((tr) => withRecordMeta({
3140
3204
  toolCallId: tr.toolCallId,
3141
3205
  toolName: tr.toolName,
3142
3206
  output: {
@@ -3153,20 +3217,49 @@ function buildAssistantEntry(event, sessionId, uuid) {
3153
3217
  if (Array.isArray(content)) {
3154
3218
  for (const part of content) {
3155
3219
  if (part.type === "tool-error" && part.toolCallId) {
3156
- results.push({
3220
+ results.push(withRecordMeta({
3157
3221
  toolCallId: part.toolCallId,
3158
3222
  toolName: part.toolName ?? "unknown",
3159
3223
  output: {
3160
3224
  type: "text",
3161
3225
  value: part.error?.message ?? part.error ?? "Tool input validation failed"
3162
3226
  },
3163
- isError: true
3164
- });
3227
+ isError: true,
3228
+ status: "failed",
3229
+ errorType: "ToolInputValidationError"
3230
+ }));
3165
3231
  }
3166
3232
  }
3167
3233
  }
3168
3234
  const toolResults = results;
3169
- const usage = event.usage ? { inputTokens: event.usage.inputTokens ?? 0, outputTokens: event.usage.outputTokens ?? 0 } : void 0;
3235
+ const rawUsage = event.usage;
3236
+ const usage = rawUsage ? {
3237
+ inputTokens: rawUsage.inputTokens ?? 0,
3238
+ outputTokens: rawUsage.outputTokens ?? 0,
3239
+ ...cachedInputOf(rawUsage) !== void 0 && { cachedInputTokens: cachedInputOf(rawUsage) },
3240
+ ...reasoningTokensOf(rawUsage) !== void 0 && { reasoningTokens: reasoningTokensOf(rawUsage) },
3241
+ ...rawUsage.totalTokens !== void 0 && { totalTokens: rawUsage.totalTokens }
3242
+ } : void 0;
3243
+ const modelInfo = event.model;
3244
+ const model = modelInfo && (modelInfo.provider !== void 0 || modelInfo.modelId !== void 0) || extras?.temperature !== void 0 || extras?.maxOutputTokens !== void 0 ? {
3245
+ ...modelInfo?.provider !== void 0 && { provider: modelInfo.provider },
3246
+ ...modelInfo?.modelId !== void 0 && { model: modelInfo.modelId },
3247
+ ...extras?.temperature !== void 0 && { temperature: extras.temperature },
3248
+ ...extras?.maxOutputTokens !== void 0 && { maxOutputTokens: extras.maxOutputTokens }
3249
+ } : void 0;
3250
+ const stepStartedAt = toIso(event.response?.timestamp) ?? extras?.startedAt;
3251
+ const endedAt = extras?.endedAt ?? ts;
3252
+ const durationMs = stepStartedAt !== void 0 ? Math.max(0, new Date(endedAt).getTime() - new Date(stepStartedAt).getTime()) : void 0;
3253
+ const firstTokenAt = extras?.firstTokenAt;
3254
+ const timing = stepStartedAt !== void 0 || firstTokenAt !== void 0 ? {
3255
+ ...stepStartedAt !== void 0 && { startedAt: stepStartedAt },
3256
+ ...firstTokenAt !== void 0 && { firstTokenAt },
3257
+ endedAt,
3258
+ ...durationMs !== void 0 && { durationMs }
3259
+ } : { endedAt };
3260
+ const responseId = event.response?.id;
3261
+ const requestId = typeof responseId === "string" && responseId.length > 0 ? responseId : void 0;
3262
+ const stepNumber = event.stepNumber;
3170
3263
  return {
3171
3264
  type: "assistant" /* Assistant */,
3172
3265
  uuid: uuid ?? generateId("step"),
@@ -3181,9 +3274,129 @@ function buildAssistantEntry(event, sessionId, uuid) {
3181
3274
  },
3182
3275
  toolCalls: toolCalls.length > 0 ? toolCalls : void 0,
3183
3276
  toolResults: toolResults.length > 0 ? toolResults : void 0,
3184
- finishReason: event.finishReason
3277
+ finishReason: event.finishReason,
3278
+ ...typeof stepNumber === "number" && { stepIndex: stepNumber },
3279
+ ...model !== void 0 && { model },
3280
+ ...timing !== void 0 && { timing },
3281
+ ...requestId !== void 0 && { requestId },
3282
+ ...extras?.traceId !== void 0 && { traceId: extras.traceId },
3283
+ ...extras?.spanId !== void 0 && { spanId: extras.spanId }
3185
3284
  };
3186
3285
  }
3286
+ function cachedInputOf(usage) {
3287
+ if (usage.cachedInputTokens !== void 0) return usage.cachedInputTokens;
3288
+ return usage.inputTokenDetails?.cacheReadTokens;
3289
+ }
3290
+ function reasoningTokensOf(usage) {
3291
+ if (usage.reasoningTokens !== void 0) return usage.reasoningTokens;
3292
+ return usage.outputTokenDetails?.reasoningTokens;
3293
+ }
3294
+ function toIso(value) {
3295
+ if (value instanceof Date) {
3296
+ return Number.isNaN(value.getTime()) ? void 0 : value.toISOString();
3297
+ }
3298
+ if (typeof value === "string" && value.length > 0) {
3299
+ const d = new Date(value);
3300
+ return Number.isNaN(d.getTime()) ? void 0 : d.toISOString();
3301
+ }
3302
+ if (typeof value === "number" && Number.isFinite(value)) {
3303
+ return new Date(value).toISOString();
3304
+ }
3305
+ return void 0;
3306
+ }
3307
+
3308
+ // src/services/agents/utils/entry-envelope.ts
3309
+ function applyTurnEnvelope(entry, env) {
3310
+ if (!env) return entry;
3311
+ if (entry.turnId === void 0) entry.turnId = env.turnId;
3312
+ if (entry.rootTurnId === void 0 && env.rootTurnId !== void 0) entry.rootTurnId = env.rootTurnId;
3313
+ if (entry.causationId === void 0 && env.causationId !== void 0) entry.causationId = env.causationId;
3314
+ if (entry.actor === void 0 && env.actor !== void 0) entry.actor = env.actor;
3315
+ return entry;
3316
+ }
3317
+ function syncStepTimestamps(entry) {
3318
+ if (entry.type !== "assistant" /* Assistant */) return;
3319
+ if (entry.timing?.startedAt !== void 0) entry.startedAt = entry.timing.startedAt;
3320
+ if (entry.timing?.endedAt !== void 0) entry.endedAt = entry.timing.endedAt;
3321
+ }
3322
+
3323
+ // src/services/agents/utils/tool-call-recorder.ts
3324
+ var ToolCallRecorder = class {
3325
+ /**
3326
+ * @param logger Optional logger — used for debug diagnostics (e.g. an
3327
+ * attachTask call that finds no live record). Absent in unit tests.
3328
+ */
3329
+ constructor(logger) {
3330
+ this.logger = logger;
3331
+ }
3332
+ logger;
3333
+ records = /* @__PURE__ */ new Map();
3334
+ /** Clear records from previous runs. Call at the start of each agent run. */
3335
+ beginTurn() {
3336
+ this.records.clear();
3337
+ }
3338
+ start(toolCallId, toolName) {
3339
+ this.records.set(toolCallId, { toolCallId, toolName, status: "running", startedAt: nowISO() });
3340
+ }
3341
+ finish(toolCallId, status, errorType) {
3342
+ const rec = this.records.get(toolCallId);
3343
+ if (!rec) return;
3344
+ rec.status = status;
3345
+ rec.endedAt = nowISO();
3346
+ if (errorType) rec.errorType = errorType;
3347
+ }
3348
+ /**
3349
+ * Attach an async task dispatched by this call (e.g. add_task) and mark the
3350
+ * call as 'running': the tool returned, but the dispatched task is what
3351
+ * stays in flight. Terminal state arrives later via signal entries —
3352
+ * append-only storage means this record is never updated afterwards.
3353
+ */
3354
+ attachTask(toolCallId, taskId) {
3355
+ const rec = this.records.get(toolCallId);
3356
+ if (!rec) {
3357
+ this.logger?.debug({ toolCallId, taskId }, "attachTask: no live tool-call record for toolCallId");
3358
+ return;
3359
+ }
3360
+ rec.taskId = taskId;
3361
+ rec.status = "running";
3362
+ rec.endedAt = nowISO();
3363
+ }
3364
+ /** Look up a live record (used by the assistant-entry builder). */
3365
+ lookup(toolCallId) {
3366
+ return this.records.get(toolCallId);
3367
+ }
3368
+ };
3369
+ var TOOL_ERROR_PREFIX = "[ERROR]";
3370
+ function recordToolCalls(tools, recorder) {
3371
+ const wrapped = {};
3372
+ for (const [name, def] of Object.entries(tools)) {
3373
+ if (!def || typeof def.execute !== "function") {
3374
+ wrapped[name] = def;
3375
+ continue;
3376
+ }
3377
+ const original = def.execute;
3378
+ wrapped[name] = {
3379
+ ...def,
3380
+ execute: async (args, options) => {
3381
+ const toolCallId = options?.toolCallId ?? `${name}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
3382
+ recorder.start(toolCallId, name);
3383
+ try {
3384
+ const result = await original(args, options);
3385
+ if (typeof result === "string" && result.startsWith(TOOL_ERROR_PREFIX)) {
3386
+ recorder.finish(toolCallId, "failed", "ToolError");
3387
+ } else {
3388
+ recorder.finish(toolCallId, "succeeded");
3389
+ }
3390
+ return result;
3391
+ } catch (err) {
3392
+ recorder.finish(toolCallId, "failed", err?.constructor?.name ?? "Error");
3393
+ throw err;
3394
+ }
3395
+ }
3396
+ };
3397
+ }
3398
+ return wrapped;
3399
+ }
3187
3400
 
3188
3401
  // src/services/agents/constants.ts
3189
3402
  var WORKER_AGENT_MAX_STEP = 1e3;
@@ -3204,9 +3417,9 @@ function truncate(result) {
3204
3417
  return result.slice(0, MAX_RESULT_CHARS) + "\n...(truncated)";
3205
3418
  }
3206
3419
  function safeExecute(name, fn) {
3207
- return async (args, _options) => {
3420
+ return async (args, options) => {
3208
3421
  try {
3209
- const result = await fn(args);
3422
+ const result = await fn(args, options);
3210
3423
  return truncate(typeof result === "string" ? result : JSON.stringify(result));
3211
3424
  } catch (err) {
3212
3425
  return `[ERROR] ${name}: ${err.message ?? String(err)}`;
@@ -3762,10 +3975,14 @@ var WorkerAgent = class extends BaseAgent {
3762
3975
  _terminalResult = null;
3763
3976
  workspaceDir;
3764
3977
  toolChoice;
3978
+ provider;
3765
3979
  contextManager;
3766
3980
  ctx;
3981
+ /** Live tool execution records — merged into entries at step finish. */
3982
+ recorder = new ToolCallRecorder();
3767
3983
  constructor(provider, ctx, skills, toolChoice) {
3768
3984
  super(provider);
3985
+ this.provider = provider;
3769
3986
  this.ctx = ctx;
3770
3987
  this.logger = ctx.logger;
3771
3988
  this.workspaceDir = ctx.workspaceDir;
@@ -3788,20 +4005,31 @@ var WorkerAgent = class extends BaseAgent {
3788
4005
  this.logger.debug({ task }, "WorkerAgent run called");
3789
4006
  this.state.set("runtimeState.task", { id: task.id, description: task.description });
3790
4007
  this._terminalResult = null;
3791
- this.state.append("conversation", {
4008
+ const turnEnv = {
4009
+ turnId: task.executionTurnId ?? generateId("turn"),
4010
+ ...(task.rootTurnId ?? task.turnId) !== void 0 && { rootTurnId: task.rootTurnId ?? task.turnId },
4011
+ causationId: task.id,
4012
+ actor: { kind: "worker", id: task.workerId, taskId: task.id }
4013
+ };
4014
+ this.recorder.beginTurn();
4015
+ const promptTs = nowISO();
4016
+ this.state.append("conversation", applyTurnEnvelope({
3792
4017
  type: "user" /* User */,
3793
4018
  uuid: generateId("u"),
3794
4019
  parentUuid: null,
3795
- sessionId: this.id,
3796
- timestamp: nowISO(),
4020
+ sessionId: this.ctx.sessionId,
4021
+ timestamp: promptTs,
4022
+ actor: { kind: "system", id: "engine", taskId: task.id },
4023
+ // Delivery of the task prompt completes at write time.
4024
+ endedAt: promptTs,
3797
4025
  message: { role: "user", content: task.description }
3798
- });
3799
- const allTools = {
4026
+ }, turnEnv));
4027
+ const allTools = recordToolCalls({
3800
4028
  ...tools,
3801
4029
  check_controller_messages: checkControllerMessagesTool({
3802
4030
  drainInbox: () => this.drainInbox()
3803
4031
  })
3804
- };
4032
+ }, this.recorder);
3805
4033
  const result = await this.collectLoop({
3806
4034
  system: this.contextManager.getSystemPrompt(),
3807
4035
  messages: this.contextManager.getMessages(),
@@ -3812,8 +4040,16 @@ var WorkerAgent = class extends BaseAgent {
3812
4040
  onStreamError: (err) => {
3813
4041
  this.logger.error({ workerId: this.id, err }, "LLM stream error");
3814
4042
  },
3815
- onStep: (event) => {
3816
- const entry = buildAssistantEntry(event, this.id);
4043
+ onStep: (event, meta) => {
4044
+ const entry = buildAssistantEntry(event, this.ctx.sessionId, void 0, {
4045
+ startedAt: meta?.startedAt,
4046
+ firstTokenAt: meta?.firstTokenAt,
4047
+ temperature: this.provider.temperature,
4048
+ maxOutputTokens: this.provider.maxOutputTokens,
4049
+ recordedToolCalls: (toolCallId) => this.recorder.lookup(toolCallId)
4050
+ });
4051
+ applyTurnEnvelope(entry, turnEnv);
4052
+ syncStepTimestamps(entry);
3817
4053
  this.state.append("conversation", entry);
3818
4054
  const toolNames = entry.toolCalls?.map((tc) => tc.toolName) ?? [];
3819
4055
  const hasError = entry.toolResults?.some((tr) => tr.isError);
@@ -3932,6 +4168,23 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
3932
4168
  // Signal context set during run()
3933
4169
  _signalQueue = null;
3934
4170
  _runContext = null;
4171
+ /** Turn lineage for permission-request signals emitted by this worker. */
4172
+ _turnLineage = {};
4173
+ /** Turn lineage stamped on entries written by the run in flight. */
4174
+ _currentTurnEnv = null;
4175
+ /**
4176
+ * Turn identity: one worker run = one turn. The engine passes the task's
4177
+ * execution turn id; rootTurnId is inherited from the task; the task is
4178
+ * the direct cause of this activation.
4179
+ */
4180
+ buildTurnEnv(task) {
4181
+ return {
4182
+ turnId: task.executionTurnId ?? generateId("turn"),
4183
+ ...(task.rootTurnId ?? task.turnId) !== void 0 && { rootTurnId: task.rootTurnId ?? task.turnId },
4184
+ causationId: task.id,
4185
+ actor: { kind: "worker", id: task.workerId, taskId: task.id }
4186
+ };
4187
+ }
3935
4188
  /**
3936
4189
  * Progress debounce mechanism.
3937
4190
  *
@@ -3959,14 +4212,20 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
3959
4212
  async run(task, _tools, onProgress) {
3960
4213
  this._terminalResult = null;
3961
4214
  this.state.set("runtimeState.task", { id: task.id, description: task.description });
3962
- this.state.append("conversation", {
4215
+ const turnEnv = this.buildTurnEnv(task);
4216
+ this._currentTurnEnv = turnEnv;
4217
+ const promptTs = nowISO();
4218
+ this.state.append("conversation", applyTurnEnvelope({
3963
4219
  type: "user" /* User */,
3964
4220
  uuid: generateId("u"),
3965
4221
  parentUuid: null,
3966
- sessionId: this.id,
3967
- timestamp: nowISO(),
4222
+ sessionId: this.ctx.sessionId,
4223
+ timestamp: promptTs,
4224
+ actor: { kind: "system", id: "engine", taskId: task.id },
4225
+ // Delivery of the task prompt completes at write time.
4226
+ endedAt: promptTs,
3968
4227
  message: { role: "user", content: task.description }
3969
- });
4228
+ }, turnEnv));
3970
4229
  try {
3971
4230
  const options = {
3972
4231
  cwd: this.workspaceDir,
@@ -4005,21 +4264,25 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
4005
4264
  }
4006
4265
  this.permissionHandler.cleanup();
4007
4266
  this.flushMessageQueue();
4008
- this.state.append("conversation", {
4267
+ const resultTs = nowISO();
4268
+ this.state.append("conversation", applyTurnEnvelope({
4009
4269
  type: "system" /* System */,
4010
4270
  uuid: generateId("s"),
4011
4271
  parentUuid: null,
4012
- sessionId: this.id,
4013
- timestamp: nowISO(),
4014
- subtype: this._terminalResult.status === "completed" ? "compact" : "error",
4015
- content: this._terminalResult.summary ?? this._terminalResult.error ?? ""
4016
- });
4272
+ sessionId: this.ctx.sessionId,
4273
+ timestamp: resultTs,
4274
+ subtype: this._terminalResult.status === "completed" ? "task_result" /* TaskResult */ : "error" /* Error */,
4275
+ content: this._terminalResult.summary ?? this._terminalResult.error ?? "",
4276
+ startedAt: resultTs,
4277
+ endedAt: resultTs
4278
+ }, turnEnv));
4017
4279
  return this._terminalResult;
4018
4280
  }
4019
4281
  /** Set signal context for permission requests. Called by engine before run(). */
4020
4282
  setSignalContext(deps) {
4021
4283
  this._signalQueue = deps.signalQueue;
4022
4284
  this._runContext = deps;
4285
+ this._turnLineage = deps.turnLineage ?? {};
4023
4286
  }
4024
4287
  abort() {
4025
4288
  this.abortController.abort();
@@ -4076,7 +4339,8 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
4076
4339
  toolName,
4077
4340
  toolArgs: input,
4078
4341
  description
4079
- }
4342
+ },
4343
+ ...this._turnLineage
4080
4344
  });
4081
4345
  return new Promise((resolve11) => {
4082
4346
  const timer = setTimeout(() => {
@@ -4150,21 +4414,24 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
4150
4414
  }
4151
4415
  const text = textParts.join("\n");
4152
4416
  if (!text) return;
4153
- this.state.append("conversation", {
4417
+ this.state.append("conversation", applyTurnEnvelope({
4154
4418
  type: "assistant" /* Assistant */,
4155
4419
  uuid: generateId("a"),
4156
4420
  parentUuid: null,
4157
- sessionId: this.id,
4421
+ sessionId: this.ctx.sessionId,
4158
4422
  timestamp: nowISO(),
4159
4423
  message: {
4160
4424
  role: "assistant",
4161
4425
  content: text,
4162
4426
  usage: {
4163
4427
  inputTokens: msg.message.usage?.input_tokens ?? 0,
4164
- outputTokens: msg.message.usage?.output_tokens ?? 0
4428
+ outputTokens: msg.message.usage?.output_tokens ?? 0,
4429
+ ...msg.message.usage?.cache_read_input_tokens != null && {
4430
+ cachedInputTokens: msg.message.usage.cache_read_input_tokens
4431
+ }
4165
4432
  }
4166
4433
  }
4167
- });
4434
+ }, this._currentTurnEnv ?? void 0));
4168
4435
  if (onProgress) {
4169
4436
  this._pendingProgress = text.slice(0, 200);
4170
4437
  if (this._progressTimer) clearTimeout(this._progressTimer);
@@ -4235,7 +4502,7 @@ function addTaskTool(deps) {
4235
4502
  "Dispatch this task to an existing idle worker (the workerId returned from a prior multiStage task). The worker resumes with its accumulated context. Omit for a fresh worker."
4236
4503
  )
4237
4504
  }),
4238
- execute: safeExecute("add_task", async ({ title, description, priority, workerType, multiStage, assignToWorker }) => {
4505
+ execute: safeExecute("add_task", async ({ title, description, priority, workerType, multiStage, assignToWorker }, options) => {
4239
4506
  const resolvedType = workerType || "puppet";
4240
4507
  if (deps.validateWorkerType) {
4241
4508
  const err = deps.validateWorkerType(resolvedType);
@@ -4248,6 +4515,8 @@ function addTaskTool(deps) {
4248
4515
  const err = deps.validateIdleWorker(assignToWorker);
4249
4516
  if (err) return err;
4250
4517
  }
4518
+ const toolCallId = options?.toolCallId;
4519
+ const turnCtx = deps.getTurnContext?.();
4251
4520
  const task = {
4252
4521
  id: generateId("t"),
4253
4522
  title,
@@ -4257,14 +4526,19 @@ function addTaskTool(deps) {
4257
4526
  status: "pending",
4258
4527
  createdAt: Date.now(),
4259
4528
  multiStage,
4260
- assignToWorker
4529
+ assignToWorker,
4530
+ ...turnCtx?.turnId !== void 0 && { turnId: turnCtx.turnId },
4531
+ ...turnCtx?.rootTurnId !== void 0 && { rootTurnId: turnCtx.rootTurnId },
4532
+ ...(toolCallId ?? turnCtx?.causationId) !== void 0 && { causationId: toolCallId ?? turnCtx?.causationId }
4261
4533
  };
4262
4534
  if (assignToWorker && deps.assignToWorker) {
4263
4535
  const ok = deps.assignToWorker(assignToWorker, task);
4264
- if (!ok) return `Worker ${assignToWorker} could not accept the task (no longer idle?).`;
4536
+ if (ok) deps.onTaskDispatched?.({ toolCallId, taskId: task.id });
4537
+ else return `Worker ${assignToWorker} could not accept the task (no longer idle?).`;
4265
4538
  return `Task ${task.id} assigned to worker ${assignToWorker} (reusing its context, priority: ${priority}, worker: ${resolvedType}). You will be notified when the task completes.`;
4266
4539
  }
4267
4540
  deps.addTask(task);
4541
+ deps.onTaskDispatched?.({ toolCallId, taskId: task.id });
4268
4542
  const stageNote = multiStage ? " [multi-stage: worker will idle after completion]" : "";
4269
4543
  return `Task ${task.id} added (priority: ${priority}, worker: ${task.workerType})${stageNote}. You will be notified when the task completes.`;
4270
4544
  })
@@ -4801,7 +5075,9 @@ function createControllerTools(deps) {
4801
5075
  addTask: deps.addTask,
4802
5076
  validateWorkerType: deps.validateWorkerType,
4803
5077
  assignToWorker: deps.assignToWorker,
4804
- validateIdleWorker: deps.validateIdleWorker
5078
+ validateIdleWorker: deps.validateIdleWorker,
5079
+ getTurnContext: deps.getTurnContext,
5080
+ onTaskDispatched: deps.onTaskDispatched
4805
5081
  }),
4806
5082
  list_tasks: listTasksTool({ listTasks: deps.listTasks }),
4807
5083
  cancel_task: cancelTaskTool({
@@ -4877,7 +5153,8 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
4877
5153
  toolName,
4878
5154
  toolArgs: args,
4879
5155
  description
4880
- }
5156
+ },
5157
+ ...deps.turnLineage ?? {}
4881
5158
  });
4882
5159
  const result = await new Promise((resolve11) => {
4883
5160
  const timer = setTimeout(() => {
@@ -5035,6 +5312,19 @@ var TaskExecuteEngine = class {
5035
5312
  workerTaskMap = /* @__PURE__ */ new Map();
5036
5313
  /** Permission middleware resolvers keyed by workerId. */
5037
5314
  permissionResolvers = /* @__PURE__ */ new Map();
5315
+ /**
5316
+ * Turn lineage of each active worker's current run: the execution turn id
5317
+ * (also handed to the worker for stamping its entries), the chain's root
5318
+ * turn, and the task id (used as signals' causationId).
5319
+ */
5320
+ workerTurnLineage = /* @__PURE__ */ new Map();
5321
+ /**
5322
+ * Running tasks cancelled via cancel(): their (aborted) worker resolution
5323
+ * must not re-classify the task or emit a worker_failed signal — the
5324
+ * task_cancelled signal was already emitted at cancel time. Cleared when
5325
+ * the worker's run promise settles (handleDone/handleFail).
5326
+ */
5327
+ cancelledTaskIds = /* @__PURE__ */ new Set();
5038
5328
  /** Idle-reaping timers keyed by workerId (multi-stage workers only). */
5039
5329
  idleTimers = /* @__PURE__ */ new Map();
5040
5330
  /** Idle TTL (ms). Idle workers are disposed after this if not reused. */
@@ -5086,8 +5376,12 @@ var TaskExecuteEngine = class {
5086
5376
  const running = this.taskState.get("tasks.running");
5087
5377
  const runIdx = running.findIndex((t) => t.id === id);
5088
5378
  if (runIdx !== -1) {
5379
+ const task = running[runIdx];
5380
+ const workerId = [...this.workerTaskMap.entries()].find(([, tId]) => tId === id)?.[0];
5089
5381
  this.abortByTaskId(id);
5090
5382
  this.moveToHistory(id, "cancelled", {});
5383
+ this.cancelledTaskIds.add(id);
5384
+ this.emitTaskCancelled(task, workerId);
5091
5385
  this.logger.info({ taskId: id }, "Running task cancelled");
5092
5386
  this.ctx.hooks.emit("task:cancelled", { taskId: id });
5093
5387
  return true;
@@ -5102,6 +5396,7 @@ var TaskExecuteEngine = class {
5102
5396
  status: "cancelled",
5103
5397
  completedAt: Date.now()
5104
5398
  });
5399
+ this.emitTaskCancelled(task);
5105
5400
  this.logger.info({ taskId: id }, "Pending task cancelled");
5106
5401
  this.ctx.hooks.emit("task:cancelled", { taskId: id });
5107
5402
  return true;
@@ -5109,6 +5404,34 @@ var TaskExecuteEngine = class {
5109
5404
  this.logger.warn({ taskId: id }, "Task not found for cancel");
5110
5405
  return false;
5111
5406
  }
5407
+ /**
5408
+ * Announce a task cancellation on the signal queue (subtype
5409
+ * task_cancelled — terminal for the dispatched task, but not a failure).
5410
+ * Running tasks carry their worker's lineage; pending tasks fall back to
5411
+ * the task's own chain fields.
5412
+ */
5413
+ emitTaskCancelled(task, workerId) {
5414
+ const workerType = task.workerType ?? PUPPET_WORKER_TYPE;
5415
+ const meta = getWorkerMeta(workerType, this.workerConfigs);
5416
+ const payload = {
5417
+ taskId: task.id,
5418
+ ...workerId !== void 0 && { workerId },
5419
+ taskTitle: task.title,
5420
+ workerType,
5421
+ emoji: meta.emoji
5422
+ };
5423
+ if (workerId !== void 0) {
5424
+ this.enqueueWorkerSignal(workerId, { source: "system", type: "task_cancelled", payload });
5425
+ return;
5426
+ }
5427
+ this.signalQueue.enqueue({
5428
+ source: "system",
5429
+ type: "task_cancelled",
5430
+ payload,
5431
+ ...(task.rootTurnId ?? task.turnId) !== void 0 && { rootTurnId: task.rootTurnId ?? task.turnId },
5432
+ causationId: task.id
5433
+ });
5434
+ }
5112
5435
  updatePending(id, updates) {
5113
5436
  const pending = [...this.taskState.get("tasks.pending")];
5114
5437
  const idx = pending.findIndex((t) => t.id === id);
@@ -5179,6 +5502,39 @@ var TaskExecuteEngine = class {
5179
5502
  return this.taskState.get("tasks.running").length;
5180
5503
  }
5181
5504
  // ---------------------------------------------------------------------------
5505
+ // Worker turn lineage
5506
+ // ---------------------------------------------------------------------------
5507
+ /**
5508
+ * Register the execution turn for a worker run and return it. The id is
5509
+ * handed to the worker (stamped on its entries) and reused on every signal
5510
+ * the engine emits for this worker, so the worker's conversation file and
5511
+ * the controller's signal records share the same turn id.
5512
+ */
5513
+ registerWorkerTurn(workerId, task) {
5514
+ const executionTurnId = generateId("turn");
5515
+ this.workerTurnLineage.set(workerId, {
5516
+ turnId: executionTurnId,
5517
+ ...task.rootTurnId !== void 0 && { rootTurnId: task.rootTurnId },
5518
+ ...task.rootTurnId === void 0 && task.turnId !== void 0 && { rootTurnId: task.turnId },
5519
+ taskId: task.id
5520
+ });
5521
+ return executionTurnId;
5522
+ }
5523
+ /** Turn lineage to attach to signals emitted for a worker. */
5524
+ workerLineage(workerId) {
5525
+ const lin = this.workerTurnLineage.get(workerId);
5526
+ if (!lin) return {};
5527
+ return {
5528
+ turnId: lin.turnId,
5529
+ ...lin.rootTurnId !== void 0 && { rootTurnId: lin.rootTurnId },
5530
+ causationId: lin.taskId
5531
+ };
5532
+ }
5533
+ /** Enqueue a worker signal with the worker's turn lineage attached. */
5534
+ enqueueWorkerSignal(workerId, sig) {
5535
+ this.signalQueue.enqueue({ ...sig, ...this.workerLineage(workerId) });
5536
+ }
5537
+ // ---------------------------------------------------------------------------
5182
5538
  // Scheduling (direct calls, no hooks)
5183
5539
  // ---------------------------------------------------------------------------
5184
5540
  /** Attempt to schedule pending tasks up to maxConcurrency. */
@@ -5382,7 +5738,7 @@ var TaskExecuteEngine = class {
5382
5738
  const worker = this.workers.get(workerId);
5383
5739
  worker?.abort();
5384
5740
  this.clearIdleTimer(workerId);
5385
- this.signalQueue.enqueue({
5741
+ this.enqueueWorkerSignal(workerId, {
5386
5742
  source: "system",
5387
5743
  type: "cancelled",
5388
5744
  payload: {
@@ -5424,10 +5780,11 @@ var TaskExecuteEngine = class {
5424
5780
  const worker = this.createWorker(workerType);
5425
5781
  this.workers.set(workerId, worker);
5426
5782
  this.workerTaskMap.set(workerId, task.id);
5783
+ const executionTurnId = this.registerWorkerTurn(workerId, task);
5427
5784
  const onProgress = this.buildProgressEmitter(workerId, task, workerType);
5428
5785
  const tools = this.wireWorker(worker, task, workerId, workerType);
5429
- this.logger.info({ workerId, taskId: task.id, workerType }, "Worker starting");
5430
- return worker.run({ ...task, workerId }, tools, onProgress);
5786
+ this.logger.info({ workerId, taskId: task.id, workerType, executionTurnId }, "Worker starting");
5787
+ return worker.run({ ...task, workerId, executionTurnId }, tools, onProgress);
5431
5788
  }
5432
5789
  /**
5433
5790
  * Dispatch a follow-up task to an existing idle (multi-stage) worker.
@@ -5457,6 +5814,7 @@ var TaskExecuteEngine = class {
5457
5814
  };
5458
5815
  this.taskState.append("tasks.running", taskWithOrigin);
5459
5816
  this.workerTaskMap.set(workerId, task.id);
5817
+ const executionTurnId = this.registerWorkerTurn(workerId, taskWithOrigin);
5460
5818
  this.updateActiveWorker(workerId, {
5461
5819
  status: "running",
5462
5820
  taskId: task.id,
@@ -5469,8 +5827,8 @@ var TaskExecuteEngine = class {
5469
5827
  });
5470
5828
  const onProgress = this.buildProgressEmitter(workerId, task, workerType);
5471
5829
  const tools = this.wireWorker(worker, task, workerId, workerType);
5472
- this.logger.info({ workerId, taskId: task.id, workerType }, "Idle worker reused");
5473
- worker.run({ ...taskWithOrigin, workerId }, tools, onProgress).then((result) => this.handleDone(workerId, taskWithOrigin, result)).catch((err2) => this.handleFail(workerId, taskWithOrigin, formatLlmError(err2)));
5830
+ this.logger.info({ workerId, taskId: task.id, workerType, executionTurnId }, "Idle worker reused");
5831
+ worker.run({ ...taskWithOrigin, workerId, executionTurnId }, tools, onProgress).then((result) => this.handleDone(workerId, taskWithOrigin, result)).catch((err2) => this.handleFail(workerId, taskWithOrigin, formatLlmError(err2)));
5474
5832
  return true;
5475
5833
  }
5476
5834
  /**
@@ -5480,7 +5838,7 @@ var TaskExecuteEngine = class {
5480
5838
  buildProgressEmitter(workerId, task, workerType) {
5481
5839
  const meta = getWorkerMeta(workerType, this.workerConfigs);
5482
5840
  return (msg) => {
5483
- this.signalQueue.enqueue({
5841
+ this.enqueueWorkerSignal(workerId, {
5484
5842
  source: "worker",
5485
5843
  type: "progress",
5486
5844
  payload: { workerId, taskId: task.id, taskTitle: task.title, workerType, emoji: meta.emoji, message: msg }
@@ -5500,7 +5858,7 @@ var TaskExecuteEngine = class {
5500
5858
  tavilyApiKey: this.ctx.config.tavilyApiKey,
5501
5859
  registry: this.skills,
5502
5860
  reportProgress: (msg, _percent) => {
5503
- this.signalQueue.enqueue({
5861
+ this.enqueueWorkerSignal(workerId, {
5504
5862
  source: "worker",
5505
5863
  type: "progress",
5506
5864
  payload: { workerId, taskId: task.id, taskTitle: task.title, workerType, emoji: workerMeta.emoji, message: msg }
@@ -5541,7 +5899,8 @@ var TaskExecuteEngine = class {
5541
5899
  taskTitle: task.title,
5542
5900
  workerType,
5543
5901
  emoji: workerMeta.emoji,
5544
- signalQueue: this.signalQueue
5902
+ signalQueue: this.signalQueue,
5903
+ turnLineage: this.workerLineage(workerId)
5545
5904
  });
5546
5905
  tools = middleware.wrappedTools;
5547
5906
  this.permissionResolvers.set(workerId, middleware);
@@ -5553,18 +5912,25 @@ var TaskExecuteEngine = class {
5553
5912
  taskId: task.id,
5554
5913
  taskTitle: task.title,
5555
5914
  workerType,
5556
- emoji: workerMeta.emoji
5915
+ emoji: workerMeta.emoji,
5916
+ turnLineage: this.workerLineage(workerId)
5557
5917
  });
5558
5918
  this.permissionResolvers.set(workerId, worker.permissionHandler);
5559
5919
  }
5560
5920
  return tools;
5561
5921
  }
5562
5922
  handleDone(workerId, task, result) {
5923
+ if (this.consumeCancelled(task.id)) {
5924
+ this.removeActiveWorker(workerId);
5925
+ this.removeWorkerTracking(workerId);
5926
+ this.scheduleNext();
5927
+ return;
5928
+ }
5563
5929
  const workerType = task.workerType ?? PUPPET_WORKER_TYPE;
5564
5930
  const meta = getWorkerMeta(workerType, this.workerConfigs);
5565
5931
  if (result.status !== "completed") {
5566
5932
  this.failTask(task.id, result.error ?? "Unknown error");
5567
- this.signalQueue.enqueue({
5933
+ this.enqueueWorkerSignal(workerId, {
5568
5934
  source: "worker",
5569
5935
  type: "failed",
5570
5936
  payload: {
@@ -5583,7 +5949,7 @@ var TaskExecuteEngine = class {
5583
5949
  }
5584
5950
  if (task.multiStage) {
5585
5951
  this.completeTask(task.id, result.summary ?? "");
5586
- this.signalQueue.enqueue({
5952
+ this.enqueueWorkerSignal(workerId, {
5587
5953
  source: "worker",
5588
5954
  type: "completed",
5589
5955
  payload: {
@@ -5600,7 +5966,7 @@ var TaskExecuteEngine = class {
5600
5966
  return;
5601
5967
  }
5602
5968
  this.completeTask(task.id, result.summary ?? "");
5603
- this.signalQueue.enqueue({
5969
+ this.enqueueWorkerSignal(workerId, {
5604
5970
  source: "worker",
5605
5971
  type: "completed",
5606
5972
  payload: {
@@ -5617,10 +5983,16 @@ var TaskExecuteEngine = class {
5617
5983
  this.scheduleNext();
5618
5984
  }
5619
5985
  handleFail(workerId, task, error) {
5986
+ if (this.consumeCancelled(task.id)) {
5987
+ this.removeActiveWorker(workerId);
5988
+ this.removeWorkerTracking(workerId);
5989
+ this.scheduleNext();
5990
+ return;
5991
+ }
5620
5992
  const workerType = task.workerType ?? PUPPET_WORKER_TYPE;
5621
5993
  const meta = getWorkerMeta(workerType, this.workerConfigs);
5622
5994
  this.failTask(task.id, error);
5623
- this.signalQueue.enqueue({
5995
+ this.enqueueWorkerSignal(workerId, {
5624
5996
  source: "worker",
5625
5997
  type: "failed",
5626
5998
  payload: {
@@ -5636,8 +6008,16 @@ var TaskExecuteEngine = class {
5636
6008
  this.removeWorkerTracking(workerId);
5637
6009
  this.scheduleNext();
5638
6010
  }
6011
+ /** Whether the task was cancelled; clears the marker (one-shot). */
6012
+ consumeCancelled(taskId) {
6013
+ if (!this.cancelledTaskIds.has(taskId)) return false;
6014
+ this.cancelledTaskIds.delete(taskId);
6015
+ this.logger.info({ taskId }, "Worker resolution for cancelled task suppressed");
6016
+ return true;
6017
+ }
5639
6018
  removeWorkerTracking(workerId) {
5640
6019
  this.clearIdleTimer(workerId);
6020
+ this.workerTurnLineage.delete(workerId);
5641
6021
  setTimeout(() => {
5642
6022
  this.permissionResolvers.get(workerId)?.cleanup();
5643
6023
  this.permissionResolvers.delete(workerId);
@@ -5874,10 +6254,223 @@ var SessionManager = class _SessionManager {
5874
6254
 
5875
6255
  // src/services/agents/controller-state.ts
5876
6256
  import { join as join10 } from "path";
6257
+
6258
+ // src/services/conversation/conversation-index.ts
6259
+ function emptyUsage() {
6260
+ return { inputTokens: 0, outputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, totalTokens: 0 };
6261
+ }
6262
+ function newTurn(turnId) {
6263
+ const view = {
6264
+ turnId,
6265
+ status: "running",
6266
+ steps: [],
6267
+ signals: [],
6268
+ toolCalls: [],
6269
+ usage: emptyUsage(),
6270
+ entryIds: []
6271
+ };
6272
+ return { view, unfinishedTaskIds: /* @__PURE__ */ new Set(), hasSyncRunning: false, hasFailure: false, hasCancelled: false, toolCallById: /* @__PURE__ */ new Map() };
6273
+ }
6274
+ function trackTimeBounds(view, iso, pick) {
6275
+ if (iso === void 0) return;
6276
+ const t = Date.parse(iso);
6277
+ if (Number.isNaN(t)) return;
6278
+ const current = pick === "start" ? view.startedAt : view.endedAt;
6279
+ if (current === void 0) {
6280
+ if (pick === "start") view.startedAt = iso;
6281
+ else view.endedAt = iso;
6282
+ return;
6283
+ }
6284
+ const cur = Date.parse(current);
6285
+ if (Number.isNaN(cur)) return;
6286
+ if (pick === "start" ? t < cur : t > cur) {
6287
+ if (pick === "start") view.startedAt = iso;
6288
+ else view.endedAt = iso;
6289
+ }
6290
+ }
6291
+ function terminalSignalForTask(subtype) {
6292
+ if (subtype === "worker_failed" /* WorkerFailed */) return "terminal-failed";
6293
+ if (subtype === "task_cancelled" /* TaskCancelled */) return "terminal-cancelled";
6294
+ if (subtype === "worker_completed" /* WorkerCompleted */ || subtype === "worker_disposed" /* WorkerDisposed */) return "terminal-ok";
6295
+ return null;
6296
+ }
6297
+ var ConversationIndexBuilder = class {
6298
+ accs = [];
6299
+ byTurnId = /* @__PURE__ */ new Map();
6300
+ byEntryIdMap = /* @__PURE__ */ new Map();
6301
+ /** taskId → turn that dispatched it (for terminal-signal resolution). */
6302
+ taskDispatchTurn = /* @__PURE__ */ new Map();
6303
+ /** Sequence for derived legacy turn ids. */
6304
+ legacySeq = 0;
6305
+ /** Turn id derived for the most recent entry lacking an explicit turnId. */
6306
+ currentDerivedTurnId;
6307
+ constructor(entries) {
6308
+ for (const entry of entries ?? []) this.onAppend(entry);
6309
+ }
6310
+ /**
6311
+ * Feed one more entry. O(1) amortized (a terminal signal also updates the
6312
+ * dispatching turn). Duplicate uuids are ignored, making append feeds
6313
+ * idempotent.
6314
+ */
6315
+ onAppend(entry) {
6316
+ if (entry.uuid !== void 0 && this.byEntryIdMap.has(entry.uuid)) return;
6317
+ const turnId = this.resolveTurnId(entry);
6318
+ let acc = this.byTurnId.get(turnId);
6319
+ if (!acc) {
6320
+ acc = newTurn(turnId);
6321
+ this.accs.push(acc);
6322
+ this.byTurnId.set(turnId, acc);
6323
+ }
6324
+ const { view } = acc;
6325
+ this.byEntryIdMap.set(entry.uuid, view);
6326
+ view.entryIds.push(entry.uuid);
6327
+ trackTimeBounds(view, entry.startedAt ?? entry.timestamp, "start");
6328
+ trackTimeBounds(view, entry.endedAt ?? entry.timestamp, "end");
6329
+ if (view.rootTurnId === void 0 && entry.rootTurnId !== void 0) {
6330
+ view.rootTurnId = entry.rootTurnId;
6331
+ }
6332
+ if (entry.actor !== void 0) {
6333
+ if (acc.firstActor === void 0) acc.firstActor = entry.actor;
6334
+ if (acc.agentActor === void 0 && (entry.type === "assistant" /* Assistant */ || entry.type === "system" /* System */)) {
6335
+ acc.agentActor = entry.actor;
6336
+ }
6337
+ }
6338
+ view.actor = acc.agentActor ?? acc.firstActor;
6339
+ switch (entry.type) {
6340
+ case "user" /* User */:
6341
+ this.applyUserEntry(acc, entry);
6342
+ break;
6343
+ case "assistant" /* Assistant */:
6344
+ this.applyAssistantEntry(acc, entry);
6345
+ break;
6346
+ case "signal" /* Signal */:
6347
+ this.applySignalEntry(acc, entry);
6348
+ break;
6349
+ case "system" /* System */:
6350
+ this.applySystemEntry(acc, entry);
6351
+ break;
6352
+ }
6353
+ this.recomputeStatus(acc);
6354
+ }
6355
+ /** Read-only index snapshot (live objects — callers must not mutate). */
6356
+ getIndex() {
6357
+ return { turns: this.accs.map((a) => a.view), byEntryId: this.byEntryIdMap };
6358
+ }
6359
+ /** Look up a turn by id. */
6360
+ getTurn(turnId) {
6361
+ return this.byTurnId.get(turnId)?.view;
6362
+ }
6363
+ /** Look up the turn an entry belongs to. */
6364
+ getTurnByEntryId(entryId) {
6365
+ return this.byEntryIdMap.get(entryId);
6366
+ }
6367
+ get turnCount() {
6368
+ return this.accs.length;
6369
+ }
6370
+ // --- Internals ---
6371
+ /**
6372
+ * Resolve the turn an entry belongs to. New data carries `turnId`; legacy
6373
+ * data is backfilled: a user entry starts a new derived turn, anything else
6374
+ * attaches to the most recent turn.
6375
+ */
6376
+ resolveTurnId(entry) {
6377
+ if (entry.turnId !== void 0 && entry.turnId !== null) {
6378
+ this.currentDerivedTurnId = entry.turnId;
6379
+ return entry.turnId;
6380
+ }
6381
+ if (entry.type === "user" /* User */ || this.currentDerivedTurnId === void 0) {
6382
+ this.currentDerivedTurnId = `legacy-turn-${this.legacySeq++}`;
6383
+ }
6384
+ return this.currentDerivedTurnId;
6385
+ }
6386
+ applyUserEntry(acc, entry) {
6387
+ if (acc.view.userEntry === void 0) acc.view.userEntry = entry;
6388
+ }
6389
+ applyAssistantEntry(acc, entry) {
6390
+ const { view } = acc;
6391
+ view.steps.push(entry);
6392
+ const u = entry.message?.usage;
6393
+ if (u) {
6394
+ view.usage.inputTokens += u.inputTokens ?? 0;
6395
+ view.usage.outputTokens += u.outputTokens ?? 0;
6396
+ view.usage.cachedInputTokens += u.cachedInputTokens ?? 0;
6397
+ view.usage.reasoningTokens += u.reasoningTokens ?? 0;
6398
+ view.usage.totalTokens += u.totalTokens ?? (u.inputTokens ?? 0) + (u.outputTokens ?? 0);
6399
+ }
6400
+ if (entry.toolCalls) {
6401
+ for (const tc of entry.toolCalls) {
6402
+ const vtc = {
6403
+ toolCallId: tc.toolCallId,
6404
+ toolName: tc.toolName,
6405
+ ...tc.taskId !== void 0 && { taskId: tc.taskId },
6406
+ ...tc.status !== void 0 && { status: tc.status },
6407
+ ...tc.startedAt !== void 0 && { startedAt: tc.startedAt },
6408
+ ...tc.endedAt !== void 0 && { endedAt: tc.endedAt }
6409
+ };
6410
+ view.toolCalls.push(vtc);
6411
+ acc.toolCallById.set(tc.toolCallId, vtc);
6412
+ if (tc.status === "failed") acc.hasFailure = true;
6413
+ if (tc.status === "pending" || tc.status === "running") {
6414
+ if (tc.taskId !== void 0) {
6415
+ acc.unfinishedTaskIds.add(tc.taskId);
6416
+ this.taskDispatchTurn.set(tc.taskId, acc);
6417
+ } else {
6418
+ acc.hasSyncRunning = true;
6419
+ }
6420
+ }
6421
+ }
6422
+ }
6423
+ if (entry.toolResults) {
6424
+ for (const tr of entry.toolResults) {
6425
+ const status = tr.status ?? (tr.isError ? "failed" : "succeeded");
6426
+ const vtc = acc.toolCallById.get(tr.toolCallId);
6427
+ if (vtc && status !== void 0) vtc.resultStatus = status;
6428
+ if (tr.isError || status === "failed") acc.hasFailure = true;
6429
+ }
6430
+ }
6431
+ }
6432
+ applySignalEntry(acc, entry) {
6433
+ const { view } = acc;
6434
+ view.signals.push(entry);
6435
+ const terminal = terminalSignalForTask(entry.subtype);
6436
+ if (terminal !== null && entry.taskId !== void 0) {
6437
+ if (terminal === "terminal-failed") acc.hasFailure = true;
6438
+ const dispatchAcc = this.taskDispatchTurn.get(entry.taskId);
6439
+ if (dispatchAcc) {
6440
+ dispatchAcc.unfinishedTaskIds.delete(entry.taskId);
6441
+ if (terminal === "terminal-failed") dispatchAcc.hasFailure = true;
6442
+ if (terminal === "terminal-cancelled") dispatchAcc.hasCancelled = true;
6443
+ this.recomputeStatus(dispatchAcc);
6444
+ }
6445
+ }
6446
+ }
6447
+ applySystemEntry(acc, entry) {
6448
+ if (!isCompactMarker(entry) && entry.subtype === "error" /* Error */) {
6449
+ acc.hasFailure = true;
6450
+ }
6451
+ }
6452
+ recomputeStatus(acc) {
6453
+ let status;
6454
+ if (acc.unfinishedTaskIds.size > 0 || acc.hasSyncRunning) {
6455
+ status = "running";
6456
+ } else if (acc.hasFailure) {
6457
+ status = "failed";
6458
+ } else if (acc.hasCancelled) {
6459
+ status = "cancelled";
6460
+ } else {
6461
+ status = "completed";
6462
+ }
6463
+ acc.view.status = status;
6464
+ }
6465
+ };
6466
+
6467
+ // src/services/agents/controller-state.ts
5877
6468
  var ControllerAgentState = class extends BaseWayangState {
5878
6469
  jsonFile;
5879
6470
  jsonlFile;
5880
6471
  logger;
6472
+ /** In-memory turn index — built in restore(), fed incrementally on append. */
6473
+ convIndex = null;
5881
6474
  /** In-memory resolve function for the current inquiry — never persisted. */
5882
6475
  inquiryResolver = null;
5883
6476
  constructor(ctx) {
@@ -5891,7 +6484,7 @@ var ControllerAgentState = class extends BaseWayangState {
5891
6484
  },
5892
6485
  conversation: [],
5893
6486
  compactSummary: null,
5894
- dynamicState: { streamingEntries: [], busy: false }
6487
+ dynamicState: { streamingEntries: [], busy: false, currentTurn: null }
5895
6488
  };
5896
6489
  super(initialData, [
5897
6490
  { path: "runtimeState", helper: jsonFile },
@@ -5901,36 +6494,59 @@ var ControllerAgentState = class extends BaseWayangState {
5901
6494
  this.jsonlFile = jsonlFile;
5902
6495
  this.logger = ctx.logger;
5903
6496
  }
6497
+ /**
6498
+ * Append with turn-index feeding: mechanical envelope fields (v / parentId,
6499
+ * compaction-boundary aware) are stamped by BaseWayangState.append via the
6500
+ * shared conversation-envelope module — the single source of truth. This
6501
+ * override only feeds the in-memory turn index incrementally.
6502
+ */
6503
+ append(path7, entry) {
6504
+ super.append(path7, entry);
6505
+ if (path7 === "conversation" && this.convIndex) {
6506
+ this.convIndex.onAppend(entry);
6507
+ }
6508
+ }
6509
+ /** Read the full persisted conversation (all JSONL lines, ignoring the in-memory window). */
6510
+ readFullConversation() {
6511
+ return this.jsonlFile.read();
6512
+ }
6513
+ /** Read-only turn index (live view; callers must not mutate). */
6514
+ getConversationIndex() {
6515
+ return this.convIndex?.getIndex() ?? { turns: [], byEntryId: /* @__PURE__ */ new Map() };
6516
+ }
6517
+ /** Look up one turn by id. */
6518
+ getTurn(turnId) {
6519
+ return this.convIndex?.getTurn(turnId);
6520
+ }
5904
6521
  async restore() {
5905
6522
  const runtimeState = this.jsonFile.read();
5906
6523
  if (runtimeState) {
5907
6524
  this.data.runtimeState = runtimeState;
5908
6525
  }
5909
6526
  const allEntries = this.jsonlFile.read();
5910
- const compactIdx = allEntries.findLastIndex(
5911
- (e) => isSystemEntry(e) && e.subtype === "compact" /* Compact */
5912
- );
6527
+ const compactIdx = allEntries.findLastIndex(isCompactMarker);
5913
6528
  if (compactIdx === -1) {
5914
6529
  this.data.conversation = allEntries;
6530
+ this.data.compactSummary = null;
5915
6531
  } else {
5916
- const RECENT_TOKEN_BUDGET2 = 2e4;
5917
- let budget = RECENT_TOKEN_BUDGET2;
6532
+ let budget = RECENT_TOKEN_BUDGET;
5918
6533
  let cutIdx = 0;
5919
6534
  for (let i = compactIdx - 1; i >= 0; i--) {
5920
- const entry = allEntries[i];
5921
- const content = getEntryContent(entry);
5922
- budget -= Math.ceil(String(content).length / 4);
6535
+ budget -= estimateEntryTokens(allEntries[i]);
5923
6536
  if (budget <= 0) {
5924
6537
  cutIdx = i + 1;
5925
6538
  break;
5926
6539
  }
5927
6540
  }
5928
6541
  this.data.conversation = allEntries.slice(cutIdx);
6542
+ const marker = allEntries[compactIdx];
6543
+ this.data.compactSummary = marker.content;
5929
6544
  }
5930
6545
  this.logger.info({
5931
6546
  entryCount: this.data.conversation.length,
5932
6547
  totalEntries: allEntries.length
5933
6548
  }, "ControllerAgentState restored");
6549
+ this.convIndex = new ConversationIndexBuilder(allEntries);
5934
6550
  }
5935
6551
  /** Set a pending inquiry and return a Promise that resolves when the user answers. */
5936
6552
  askInquiry(question) {
@@ -5957,11 +6573,18 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
5957
6573
  tools;
5958
6574
  logger;
5959
6575
  ctx;
5960
- constructor(ctx, state, provider, tools, skills) {
6576
+ provider;
6577
+ /** Live tool execution records — merged into entries at step finish. */
6578
+ recorder;
6579
+ /** Turn id of the most recent run — used to stamp late error entries. */
6580
+ lastTurnId = null;
6581
+ constructor(ctx, state, provider, tools, skills, recorder = new ToolCallRecorder()) {
5961
6582
  super(provider);
5962
6583
  this.ctx = ctx;
5963
6584
  this.state = state;
5964
- this.tools = tools;
6585
+ this.provider = provider;
6586
+ this.recorder = recorder;
6587
+ this.tools = recordToolCalls(tools, recorder);
5965
6588
  this.logger = ctx.logger;
5966
6589
  this.contextManager = new ContextManager(
5967
6590
  state,
@@ -5984,6 +6607,7 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
5984
6607
  static create(opts) {
5985
6608
  const { ctx, provider, config, engine, signalQueue, skills } = opts;
5986
6609
  const state = new ControllerAgentState(ctx);
6610
+ const recorder = new ToolCallRecorder(ctx.logger);
5987
6611
  const tools = createControllerTools({
5988
6612
  addTask: (task) => engine.add(task),
5989
6613
  validateWorkerType: (type) => engine.validateWorkerType(type),
@@ -6006,9 +6630,16 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
6006
6630
  sendMessageToWorker: (workerId, message) => engine.sendMessageToWorker(workerId, message),
6007
6631
  resolvePermission: (requestId, approved, reason) => engine.resolvePermission(requestId, approved, reason),
6008
6632
  registry: skills,
6009
- disposeWorker: (workerId) => engine.abortByWorkerId(workerId)
6633
+ disposeWorker: (workerId) => engine.abortByWorkerId(workerId),
6634
+ // Turn lineage for tasks created by add_task (read from dynamicState,
6635
+ // which run() sets at the start of every controller activation).
6636
+ getTurnContext: () => state.get("dynamicState.currentTurn") ?? void 0,
6637
+ // toolCall→task audit link, consumed when the step entry is built.
6638
+ onTaskDispatched: ({ toolCallId, taskId }) => {
6639
+ if (toolCallId) recorder.attachTask(toolCallId, taskId);
6640
+ }
6010
6641
  });
6011
- return new _ControllerAgent(ctx, state, provider, tools, skills);
6642
+ return new _ControllerAgent(ctx, state, provider, tools, skills, recorder);
6012
6643
  }
6013
6644
  /** Run the controller agent — streams text chunks while updating streamingEntries in state. */
6014
6645
  async run(signals) {
@@ -6021,8 +6652,23 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
6021
6652
  }
6022
6653
  async _run(signals) {
6023
6654
  const ts = nowISO();
6655
+ const turnId = generateId("turn");
6656
+ const hasUserInput = signals.some((s) => s.type === "input");
6657
+ const rootTurnId = hasUserInput ? turnId : signals.find((s) => s.rootTurnId !== void 0)?.rootTurnId;
6658
+ const causationId = signals[0]?.id;
6659
+ const turnEnv = {
6660
+ turnId,
6661
+ ...rootTurnId !== void 0 && { rootTurnId },
6662
+ ...causationId !== void 0 && { causationId },
6663
+ actor: { kind: "controller", id: this.id }
6664
+ };
6665
+ this.state.set("dynamicState.currentTurn", turnEnv);
6666
+ this.lastTurnId = turnId;
6667
+ this.recorder.beginTurn();
6024
6668
  for (const sig of signals) {
6025
- this.state.append("conversation", signalToEntry(sig, ts));
6669
+ const entry = signalToEntry(sig, ts, this.ctx.sessionId);
6670
+ applyTurnEnvelope(entry, turnEnv);
6671
+ this.state.append("conversation", entry);
6026
6672
  }
6027
6673
  this.state.set("dynamicState.streamingEntries", []);
6028
6674
  let streamingEntry = this.createStreamingEntry();
@@ -6035,10 +6681,18 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
6035
6681
  onStreamError: (err) => {
6036
6682
  this.logger.error({ agent: "controller", err }, "LLM stream error");
6037
6683
  },
6038
- onStep: (event) => {
6684
+ onStep: (event, meta) => {
6039
6685
  const streamingEntries = this.state.get("dynamicState.streamingEntries");
6040
6686
  const streamingUuid = streamingEntries?.[0]?.uuid;
6041
- const entry = buildAssistantEntry(event, this.id, streamingUuid);
6687
+ const entry = buildAssistantEntry(event, this.ctx.sessionId, streamingUuid, {
6688
+ startedAt: meta?.startedAt,
6689
+ firstTokenAt: meta?.firstTokenAt,
6690
+ temperature: this.provider.temperature,
6691
+ maxOutputTokens: this.provider.maxOutputTokens,
6692
+ recordedToolCalls: (toolCallId) => this.recorder.lookup(toolCallId)
6693
+ });
6694
+ applyTurnEnvelope(entry, turnEnv);
6695
+ syncStepTimestamps(entry);
6042
6696
  this.state.append("conversation", entry);
6043
6697
  this.state.set("dynamicState.streamingEntries", []);
6044
6698
  }
@@ -6063,7 +6717,7 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
6063
6717
  type: "assistant" /* Assistant */,
6064
6718
  uuid: generateId("s"),
6065
6719
  parentUuid: null,
6066
- sessionId: "controller",
6720
+ sessionId: this.ctx.sessionId,
6067
6721
  timestamp: nowISO(),
6068
6722
  message: { role: "assistant", content: "" }
6069
6723
  };
@@ -6071,6 +6725,10 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
6071
6725
  /** Restore persisted state (conversation, runtime). Called by Supervisor. */
6072
6726
  async restore() {
6073
6727
  await this.state.restore();
6728
+ const summary = this.state.get("compactSummary");
6729
+ if (summary) {
6730
+ this.contextManager.restoreCompact(summary);
6731
+ }
6074
6732
  }
6075
6733
  /** Initialize session metadata. Called by Supervisor at start. */
6076
6734
  initSession(session) {
@@ -6083,15 +6741,29 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
6083
6741
  /** Surface an error in the UI as a system conversation entry. */
6084
6742
  reportError(message) {
6085
6743
  this.state.set("dynamicState.streamingEntries", []);
6086
- this.state.append("conversation", {
6744
+ const errTs = nowISO();
6745
+ const entry = {
6087
6746
  type: "system" /* System */,
6088
6747
  uuid: generateId("err"),
6089
6748
  parentUuid: null,
6090
- sessionId: "controller",
6091
- timestamp: nowISO(),
6749
+ sessionId: this.ctx.sessionId,
6750
+ timestamp: errTs,
6092
6751
  subtype: "error" /* Error */,
6093
- content: message
6094
- });
6752
+ content: message,
6753
+ // Point-in-time event: startedAt/endedAt share the recording moment.
6754
+ startedAt: errTs,
6755
+ endedAt: errTs
6756
+ };
6757
+ applyTurnEnvelope(entry, this.lastTurnId !== null ? { turnId: this.lastTurnId, actor: { kind: "controller", id: this.id } } : void 0);
6758
+ this.state.append("conversation", entry);
6759
+ }
6760
+ /** Read-only turn index over the controller conversation. */
6761
+ getConversationIndex() {
6762
+ return this.state.getConversationIndex();
6763
+ }
6764
+ /** Look up one turn by id. */
6765
+ getTurn(turnId) {
6766
+ return this.state.getTurn(turnId);
6095
6767
  }
6096
6768
  // --- Subscribable ---
6097
6769
  subscribe(path7, callback) {
@@ -6112,28 +6784,36 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
6112
6784
  */
6113
6785
  async performCompaction() {
6114
6786
  try {
6115
- await this.contextManager.compact(async (entries) => {
6116
- const entryTexts = entries.map((e, i) => {
6117
- const content = e.message?.content ?? e.content ?? "";
6118
- return `[${i}] (${e.type}) ${String(content).slice(0, 300)}`;
6119
- });
6120
- let lastError;
6121
- for (let attempt = 0; attempt <= COMPACT_MAX_RETRIES; attempt++) {
6122
- try {
6123
- const { text } = await generateText({
6124
- model: this.model,
6125
- // WayangModel opaque boundary
6126
- system: SUMMARIZER_SYSTEM_PROMPT,
6127
- prompt: buildSummarizerPrompt(entryTexts)
6128
- });
6129
- return text;
6130
- } catch (err) {
6131
- lastError = err;
6132
- this.logger.warn({ attempt: attempt + 1, error: String(err) }, "Compaction summarizer failed");
6787
+ const previousSummary = this.contextManager.getCompactSummary();
6788
+ await this.contextManager.compact(
6789
+ async (entries) => {
6790
+ const entryTexts = entries.map((e, i) => {
6791
+ const content = e.message?.content ?? e.content ?? "";
6792
+ return `[${i}] (${e.type}) ${String(content).slice(0, 300)}`;
6793
+ });
6794
+ if (previousSummary) {
6795
+ entryTexts.unshift(`[Previous Summary]
6796
+ ${previousSummary}`);
6133
6797
  }
6134
- }
6135
- throw lastError;
6136
- });
6798
+ let lastError;
6799
+ for (let attempt = 0; attempt <= COMPACT_MAX_RETRIES; attempt++) {
6800
+ try {
6801
+ const { text } = await generateText({
6802
+ model: this.model,
6803
+ // WayangModel opaque boundary
6804
+ system: SUMMARIZER_SYSTEM_PROMPT,
6805
+ prompt: buildSummarizerPrompt(entryTexts)
6806
+ });
6807
+ return text;
6808
+ } catch (err) {
6809
+ lastError = err;
6810
+ this.logger.warn({ attempt: attempt + 1, error: String(err) }, "Compaction summarizer failed");
6811
+ }
6812
+ }
6813
+ throw lastError;
6814
+ },
6815
+ { model: this.provider.modelName }
6816
+ );
6137
6817
  } catch (err) {
6138
6818
  this.logger.warn({ error: String(err) }, "Compaction failed, falling back to half-truncation");
6139
6819
  const conversation = this.state.get("conversation");
@@ -6145,28 +6825,36 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
6145
6825
  }
6146
6826
  }
6147
6827
  };
6148
- function signalBase(ts) {
6828
+ function signalBase(ts, sessionId) {
6149
6829
  return {
6150
6830
  type: "signal" /* Signal */,
6151
6831
  uuid: generateId("sig"),
6152
6832
  parentUuid: null,
6153
- sessionId: "controller",
6833
+ sessionId,
6154
6834
  timestamp: ts
6155
6835
  };
6156
6836
  }
6837
+ function isoMs(ms) {
6838
+ if (typeof ms !== "number" || !Number.isFinite(ms)) return void 0;
6839
+ return new Date(ms).toISOString();
6840
+ }
6157
6841
  var signalConverters = {
6158
- input: (sig, ts) => ({
6842
+ input: (sig, ts, sessionId) => ({
6159
6843
  type: "user" /* User */,
6160
6844
  uuid: generateId("u"),
6161
6845
  parentUuid: null,
6162
- sessionId: "controller",
6846
+ sessionId,
6163
6847
  timestamp: ts,
6848
+ actor: { kind: "user", id: "user" },
6849
+ startedAt: isoMs(sig.timestamp),
6850
+ // Delivery/processing of the input is complete when it is recorded here.
6851
+ endedAt: ts,
6164
6852
  message: { role: "user", content: sig.payload.text }
6165
6853
  }),
6166
- completed: (sig, ts) => {
6854
+ completed: (sig, ts, sessionId) => {
6167
6855
  const p = sig.payload;
6168
6856
  return {
6169
- ...signalBase(ts),
6857
+ ...signalBase(ts, sessionId),
6170
6858
  subtype: "worker_completed" /* WorkerCompleted */,
6171
6859
  workerId: p.workerId,
6172
6860
  workerType: p.workerType,
@@ -6176,10 +6864,10 @@ var signalConverters = {
6176
6864
  content: p.summary ?? JSON.stringify(sig.payload)
6177
6865
  };
6178
6866
  },
6179
- failed: (sig, ts) => {
6867
+ failed: (sig, ts, sessionId) => {
6180
6868
  const p = sig.payload;
6181
6869
  return {
6182
- ...signalBase(ts),
6870
+ ...signalBase(ts, sessionId),
6183
6871
  subtype: "worker_failed" /* WorkerFailed */,
6184
6872
  workerId: p.workerId,
6185
6873
  workerType: p.workerType,
@@ -6189,10 +6877,10 @@ var signalConverters = {
6189
6877
  content: p.error
6190
6878
  };
6191
6879
  },
6192
- progress: (sig, ts) => {
6880
+ progress: (sig, ts, sessionId) => {
6193
6881
  const p = sig.payload;
6194
6882
  return {
6195
- ...signalBase(ts),
6883
+ ...signalBase(ts, sessionId),
6196
6884
  subtype: "worker_progress" /* WorkerProgress */,
6197
6885
  workerId: p.workerId,
6198
6886
  workerType: p.workerType,
@@ -6202,11 +6890,11 @@ var signalConverters = {
6202
6890
  content: p.message
6203
6891
  };
6204
6892
  },
6205
- heartbeat: (sig, ts) => {
6893
+ heartbeat: (sig, ts, sessionId) => {
6206
6894
  const p = sig.payload;
6207
6895
  const workerSummary = p.workers.map((w) => `${w.taskTitle} (${w.workerType}, running ${Math.round(w.runningForMs / 1e3)}s)`).join(", ");
6208
6896
  return {
6209
- ...signalBase(ts),
6897
+ ...signalBase(ts, sessionId),
6210
6898
  subtype: "heartbeat" /* Heartbeat */,
6211
6899
  taskTitle: "heartbeat",
6212
6900
  content: `[HEARTBEAT] ${p.reason}
@@ -6215,10 +6903,10 @@ Workers: ${workerSummary}
6215
6903
  Pending tasks: ${p.pendingTaskCount}`
6216
6904
  };
6217
6905
  },
6218
- permission_request: (sig, ts) => {
6906
+ permission_request: (sig, ts, sessionId) => {
6219
6907
  const p = sig.payload;
6220
6908
  return {
6221
- ...signalBase(ts),
6909
+ ...signalBase(ts, sessionId),
6222
6910
  subtype: "permission_request" /* PermissionRequest */,
6223
6911
  workerId: p.workerId,
6224
6912
  workerType: p.workerType,
@@ -6231,10 +6919,10 @@ Request ID: ${p.requestId}
6231
6919
  Use respond_permission to approve or deny.`
6232
6920
  };
6233
6921
  },
6234
- cancelled: (sig, ts) => {
6922
+ cancelled: (sig, ts, sessionId) => {
6235
6923
  const p = sig.payload;
6236
6924
  return {
6237
- ...signalBase(ts),
6925
+ ...signalBase(ts, sessionId),
6238
6926
  subtype: "worker_disposed" /* WorkerDisposed */,
6239
6927
  workerId: p.workerId,
6240
6928
  workerType: p.workerType,
@@ -6244,11 +6932,24 @@ Use respond_permission to approve or deny.`
6244
6932
  content: `[WORKER DISPOSED] Idle (multi-stage) worker ${p.workerId} was disposed (reaped by timeout or manually). No task failed \u2014 its last stage already completed. If you still need this worker's context, create a fresh multi-stage task instead.`
6245
6933
  };
6246
6934
  },
6247
- previous_session_tasks: (sig, ts) => {
6935
+ task_cancelled: (sig, ts, sessionId) => {
6936
+ const p = sig.payload;
6937
+ return {
6938
+ ...signalBase(ts, sessionId),
6939
+ subtype: "task_cancelled" /* TaskCancelled */,
6940
+ workerId: p.workerId,
6941
+ workerType: p.workerType,
6942
+ emoji: p.emoji,
6943
+ taskId: p.taskId,
6944
+ taskTitle: p.taskTitle ?? "task_cancelled",
6945
+ content: `[TASK CANCELLED] Task ${p.taskId}${p.taskTitle ? ` (${p.taskTitle})` : ""} was cancelled by the user/controller. This is not a failure \u2014 the task terminated as cancelled.`
6946
+ };
6947
+ },
6948
+ previous_session_tasks: (sig, ts, sessionId) => {
6248
6949
  const p = sig.payload;
6249
6950
  const lines = p.tasks.map((t) => `- [${t.status}] ${t.title} (id: ${t.taskId}): ${t.description}`);
6250
6951
  return {
6251
- ...signalBase(ts),
6952
+ ...signalBase(ts, sessionId),
6252
6953
  subtype: "previous_session_tasks" /* PreviousSessionTasks */,
6253
6954
  taskTitle: "previous_session_tasks",
6254
6955
  content: `[PREVIOUS SESSION] ${p.tasks.length} unfinished task(s) from session ${p.sessionId} (last active ${new Date(p.lastActiveAt).toLocaleString()}):
@@ -6257,20 +6958,43 @@ Workers were NOT resumed. Decide: re-dispatch with add_task, treat as stale, or
6257
6958
  };
6258
6959
  }
6259
6960
  };
6260
- function signalToEntry(sig, ts) {
6961
+ function signalToEntry(sig, ts, sessionId) {
6261
6962
  const converter = signalConverters[sig.type];
6262
- if (converter) return converter(sig, ts);
6263
- const p = sig.payload;
6264
- return {
6265
- ...signalBase(ts),
6266
- subtype: "worker_failed" /* WorkerFailed */,
6267
- workerId: p.workerId,
6268
- workerType: p.workerType,
6269
- emoji: p.emoji,
6270
- taskId: p.taskId,
6271
- taskTitle: p.taskTitle ?? sig.type,
6272
- content: JSON.stringify(sig.payload)
6273
- };
6963
+ let entry;
6964
+ if (converter) {
6965
+ entry = converter(sig, ts, sessionId);
6966
+ } else {
6967
+ const p = sig.payload;
6968
+ entry = {
6969
+ ...signalBase(ts, sessionId),
6970
+ subtype: "worker_failed" /* WorkerFailed */,
6971
+ workerId: p.workerId,
6972
+ workerType: p.workerType,
6973
+ emoji: p.emoji,
6974
+ taskId: p.taskId,
6975
+ taskTitle: p.taskTitle ?? sig.type,
6976
+ content: JSON.stringify(sig.payload)
6977
+ };
6978
+ }
6979
+ if (entry.type === "signal" /* Signal */) {
6980
+ const s = entry;
6981
+ s.signalId = sig.id;
6982
+ if (sig.rootTurnId !== void 0) s.rootTurnId = sig.rootTurnId;
6983
+ if (sig.causationId !== void 0) s.causationId = sig.causationId;
6984
+ s.startedAt = isoMs(sig.timestamp);
6985
+ s.endedAt = ts;
6986
+ const payload = sig.payload;
6987
+ if (sig.source === "worker" && typeof payload.workerId === "string") {
6988
+ s.actor = {
6989
+ kind: "worker",
6990
+ id: payload.workerId,
6991
+ ...typeof payload.taskId === "string" && { taskId: payload.taskId }
6992
+ };
6993
+ } else if (sig.source === "system") {
6994
+ s.actor = { kind: "system", id: "system" };
6995
+ }
6996
+ }
6997
+ return entry;
6274
6998
  }
6275
6999
 
6276
7000
  // src/services/skills/registry.ts
@@ -6545,6 +7269,15 @@ var Supervisor = class {
6545
7269
  resolveInquiry(answer) {
6546
7270
  this.controllerAgent.resolveInquiry(answer);
6547
7271
  }
7272
+ // --- Read-only conversation index (WebUI / audit) ---
7273
+ /** Grouped turn view over the controller conversation. */
7274
+ getConversationIndex() {
7275
+ return this.controllerAgent.getConversationIndex();
7276
+ }
7277
+ /** Look up one turn of the controller conversation by id. */
7278
+ getTurn(turnId) {
7279
+ return this.controllerAgent.getTurn(turnId);
7280
+ }
6548
7281
  // --- Shutdown ---
6549
7282
  async shutdown() {
6550
7283
  this.controllerLoop.shutdown();
@@ -6591,6 +7324,96 @@ var RpcServer = class {
6591
7324
  }
6592
7325
  };
6593
7326
 
7327
+ // src/services/conversation/turn-serialize.ts
7328
+ var DEFAULT_TURN_TEXT_LIMIT = 2e3;
7329
+ function cut(text, limit) {
7330
+ if (text === void 0) return { value: "", truncated: false };
7331
+ if (text.length <= limit) return { value: text, truncated: false };
7332
+ return { value: `${text.slice(0, limit)}\u2026[truncated]`, truncated: true };
7333
+ }
7334
+ function serializeTurnView(view, limits = {}) {
7335
+ const textLimit = limits.maxTextLength ?? DEFAULT_TURN_TEXT_LIMIT;
7336
+ return {
7337
+ turnId: view.turnId,
7338
+ ...view.rootTurnId !== void 0 && { rootTurnId: view.rootTurnId },
7339
+ ...view.actor !== void 0 && { actor: view.actor },
7340
+ ...view.startedAt !== void 0 && { startedAt: view.startedAt },
7341
+ ...view.endedAt !== void 0 && { endedAt: view.endedAt },
7342
+ status: view.status,
7343
+ ...view.userEntry !== void 0 && (() => {
7344
+ const u = cut(view.userEntry.message?.content, textLimit);
7345
+ return {
7346
+ userEntry: {
7347
+ uuid: view.userEntry.uuid,
7348
+ timestamp: view.userEntry.timestamp,
7349
+ content: u.value,
7350
+ contentTruncated: u.truncated
7351
+ }
7352
+ };
7353
+ })(),
7354
+ steps: view.steps.map((step) => {
7355
+ const content = cut(step.message?.content, textLimit);
7356
+ const reasoning = step.message?.reasoning !== void 0 ? cut(step.message.reasoning, textLimit) : void 0;
7357
+ return {
7358
+ uuid: step.uuid,
7359
+ ...step.stepIndex !== void 0 && { stepIndex: step.stepIndex },
7360
+ timestamp: step.timestamp,
7361
+ content: content.value,
7362
+ contentTruncated: content.truncated,
7363
+ ...reasoning !== void 0 && {
7364
+ reasoning: reasoning.value,
7365
+ reasoningTruncated: reasoning.truncated
7366
+ },
7367
+ ...step.finishReason !== void 0 && { finishReason: step.finishReason },
7368
+ ...step.model !== void 0 && { model: step.model },
7369
+ ...step.timing !== void 0 && { timing: step.timing },
7370
+ ...step.requestId !== void 0 && { requestId: step.requestId },
7371
+ ...step.message?.usage !== void 0 && { usage: step.message.usage },
7372
+ toolCallCount: step.toolCalls?.length ?? 0,
7373
+ toolResultCount: step.toolResults?.length ?? 0
7374
+ };
7375
+ }),
7376
+ signals: view.signals.map((sig) => {
7377
+ const c = cut(sig.content, textLimit);
7378
+ return {
7379
+ uuid: sig.uuid,
7380
+ subtype: sig.subtype,
7381
+ ...sig.signalId !== void 0 && { signalId: sig.signalId },
7382
+ ...sig.workerId !== void 0 && { workerId: sig.workerId },
7383
+ ...sig.taskId !== void 0 && { taskId: sig.taskId },
7384
+ taskTitle: sig.taskTitle,
7385
+ timestamp: sig.timestamp,
7386
+ content: c.value,
7387
+ contentTruncated: c.truncated
7388
+ };
7389
+ }),
7390
+ toolCalls: view.toolCalls,
7391
+ usage: view.usage,
7392
+ entryIds: view.entryIds
7393
+ };
7394
+ }
7395
+ function summarizeTurn(view, limits = {}) {
7396
+ const previewLimit = Math.min(limits.maxTextLength ?? 200, 200);
7397
+ const preview = view.userEntry !== void 0 ? cut(view.userEntry.message?.content, previewLimit) : void 0;
7398
+ return {
7399
+ turnId: view.turnId,
7400
+ ...view.rootTurnId !== void 0 && { rootTurnId: view.rootTurnId },
7401
+ ...view.actor !== void 0 && { actor: view.actor },
7402
+ ...view.startedAt !== void 0 && { startedAt: view.startedAt },
7403
+ ...view.endedAt !== void 0 && { endedAt: view.endedAt },
7404
+ status: view.status,
7405
+ usage: view.usage,
7406
+ stepCount: view.steps.length,
7407
+ signalCount: view.signals.length,
7408
+ toolCallCount: view.toolCalls.length,
7409
+ entryCount: view.entryIds.length,
7410
+ ...preview !== void 0 && {
7411
+ userPreview: preview.value,
7412
+ userPreviewTruncated: preview.truncated
7413
+ }
7414
+ };
7415
+ }
7416
+
6594
7417
  // src/server/wayang-server.ts
6595
7418
  async function runWayangServer(options, io = process) {
6596
7419
  const config = await loadConfig(options.configPath, { nonInteractive: true });
@@ -6703,6 +7526,24 @@ async function runWayangServer(options, io = process) {
6703
7526
  const data = target.getSnapshot(requiredString(params, "path"));
6704
7527
  return { data: data === void 0 ? null : data };
6705
7528
  });
7529
+ rpc.register(METHOD.CONVERSATION_LIST_TURNS, (params) => {
7530
+ const index = supervisor.getConversationIndex();
7531
+ const limit = optionalInt(params, "limit", 50, 1, 500);
7532
+ const offset = optionalInt(params, "offset", 0, 0, Number.MAX_SAFE_INTEGER);
7533
+ const turns = index.turns.slice(offset, offset + limit).map((view) => summarizeTurn(view, { maxTextLength: optionalInt(params, "previewLength", 200, 1, 2e3) }));
7534
+ return { total: index.turns.length, offset, limit, turns };
7535
+ });
7536
+ rpc.register(METHOD.CONVERSATION_GET_TURN, (params) => {
7537
+ const turnId = requiredString(params, "turnId");
7538
+ const view = supervisor.getTurn(turnId);
7539
+ if (!view) {
7540
+ throw new RpcError(RPC_ERROR.INVALID_PARAMS, `Unknown turnId: ${turnId}`);
7541
+ }
7542
+ return serializeTurnView(view, {
7543
+ // Optional — falls back to the serializer's DEFAULT_TURN_TEXT_LIMIT.
7544
+ maxTextLength: optionalInt(params, "maxTextLength", DEFAULT_TURN_TEXT_LIMIT, 1, 1e5)
7545
+ });
7546
+ });
6706
7547
  rpc.register(METHOD.SHUTDOWN, () => {
6707
7548
  setImmediate(() => {
6708
7549
  void supervisor.shutdown().catch(() => {
@@ -6727,6 +7568,19 @@ function requiredString(params, key) {
6727
7568
  }
6728
7569
  return value;
6729
7570
  }
7571
+ function optionalInt(params, key, defaultValue, min, max) {
7572
+ const raw = params[key];
7573
+ if (raw === void 0 || raw === null) {
7574
+ if (defaultValue === void 0) {
7575
+ throw new RpcError(RPC_ERROR.INVALID_PARAMS, `Missing required number param: ${key}`);
7576
+ }
7577
+ return defaultValue;
7578
+ }
7579
+ if (typeof raw !== "number" || !Number.isInteger(raw) || raw < min || raw > max) {
7580
+ throw new RpcError(RPC_ERROR.INVALID_PARAMS, `Param ${key} must be an integer in [${min}, ${max}]`);
7581
+ }
7582
+ return raw;
7583
+ }
6730
7584
 
6731
7585
  // src/server/main.ts
6732
7586
  async function serverMain(options) {