wayang-ai 0.6.3 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.js +1993 -537
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -192,9 +192,9 @@ var RpcClient = class {
192
192
  return Promise.reject(new Error("RPC transport closed: cannot send request"));
193
193
  }
194
194
  const id = this.nextId++;
195
- return new Promise((resolve11, reject) => {
195
+ return new Promise((resolve12, reject) => {
196
196
  this.pending.set(id, {
197
- resolve: (v) => resolve11(v),
197
+ resolve: (v) => resolve12(v),
198
198
  reject
199
199
  });
200
200
  this.transport.send({ id, method, params });
@@ -398,8 +398,8 @@ async function connectRemoteServer(options) {
398
398
  child.stderr?.on("data", (chunk) => {
399
399
  stderrTail = (stderrTail + chunk).slice(-4e3);
400
400
  });
401
- const exited = new Promise((resolve11) => {
402
- child.on("exit", (code) => resolve11(code));
401
+ const exited = new Promise((resolve12) => {
402
+ child.on("exit", (code) => resolve12(code));
403
403
  });
404
404
  const transport = createStreamTransport(child.stdout, child.stdin);
405
405
  const rpc = new RpcClient(transport);
@@ -418,10 +418,10 @@ async function connectRemoteServer(options) {
418
418
  }
419
419
  const exitOrKill = Promise.race([
420
420
  exited,
421
- new Promise((resolve11) => {
421
+ new Promise((resolve12) => {
422
422
  setTimeout(() => {
423
423
  child.kill("SIGKILL");
424
- resolve11(null);
424
+ resolve12(null);
425
425
  }, SHUTDOWN_GRACE_MS);
426
426
  })
427
427
  ]);
@@ -439,7 +439,7 @@ ${stderrTail}`);
439
439
  return { remote, stop };
440
440
  }
441
441
  function waitForReady(child, rpc, exited, getStderrTail, timeoutMs) {
442
- return new Promise((resolve11, reject) => {
442
+ return new Promise((resolve12, reject) => {
443
443
  let settled = false;
444
444
  const finish = (err) => {
445
445
  if (settled) return;
@@ -447,7 +447,7 @@ function waitForReady(child, rpc, exited, getStderrTail, timeoutMs) {
447
447
  clearTimeout(timer);
448
448
  offReady();
449
449
  if (err) reject(err);
450
- else resolve11();
450
+ else resolve12();
451
451
  };
452
452
  const timer = setTimeout(() => {
453
453
  child.kill("SIGKILL");
@@ -604,7 +604,7 @@ import React6 from "react";
604
604
  import { render } from "ink";
605
605
 
606
606
  // src/tui/app.tsx
607
- import { useRef as useRef2 } from "react";
607
+ import { useRef as useRef3 } from "react";
608
608
 
609
609
  // src/tui/hooks/use-memoized-fn.ts
610
610
  import { useRef, useCallback } from "react";
@@ -1372,6 +1372,7 @@ function WorkerMessageRow({ section: section2, result, lookups }) {
1372
1372
  const worker = summary.workerId ? lookups.workers.get(summary.workerId) : void 0;
1373
1373
  const target = formatWorkerTag(worker ?? { workerId: summary.workerId }) || "(\u672A\u77E5 worker)";
1374
1374
  const iconColor = summary.outcome === "failed" ? theme.action.failColor : theme.action.color;
1375
+ const delivered = summary.delivery === "queued" ? "\u5DF2\u6392\u961F" : summary.delivery === "delivered" ? "\u5DF2\u9001\u8FBE" : "\u5DF2\u53D1\u9001";
1375
1376
  return /* @__PURE__ */ jsxs2(Box2, { flexDirection: "column", children: [
1376
1377
  /* @__PURE__ */ jsxs2(Box2, { children: [
1377
1378
  /* @__PURE__ */ jsxs2(Text2, { color: iconColor, bold: true, children: [
@@ -1382,7 +1383,7 @@ function WorkerMessageRow({ section: section2, result, lookups }) {
1382
1383
  " ",
1383
1384
  target
1384
1385
  ] }),
1385
- /* @__PURE__ */ jsx3(OutcomeMark, { outcome: summary.outcome, ok: summary.delivery === "queued" ? "\u5DF2\u6392\u961F" : "\u5DF2\u9001\u8FBE", failed: "\u672A\u9001\u8FBE" })
1386
+ /* @__PURE__ */ jsx3(OutcomeMark, { outcome: summary.outcome, ok: delivered, failed: "\u672A\u9001\u8FBE" })
1386
1387
  ] }),
1387
1388
  /* @__PURE__ */ jsx3(PreviewLines, { lines: summary.lines, hidden: summary.hiddenLines }),
1388
1389
  summary.failure && /* @__PURE__ */ jsx3(SecondaryLine, { color: theme.action.failColor, children: summary.failure })
@@ -1654,7 +1655,7 @@ var ChatArea = React2.memo(function ChatArea2({ items, busy, streamingItems, con
1654
1655
  });
1655
1656
 
1656
1657
  // src/tui/components/input-area.tsx
1657
- import { useState as useState2, useMemo as useMemo2 } from "react";
1658
+ import { useState as useState2, useMemo as useMemo2, useRef as useRef2 } from "react";
1658
1659
 
1659
1660
  // src/tui/utils/format-tokens.ts
1660
1661
  function formatTokenCount(tokens) {
@@ -1801,21 +1802,302 @@ function useSlashCommands(supervisor) {
1801
1802
 
1802
1803
  // src/tui/components/input-area.tsx
1803
1804
  import { Box as Box6, Text as Text7, useInput } from "ink";
1804
- import { jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
1805
- function InputArea({ onSubmit, onExit, onEscape, busy }) {
1805
+
1806
+ // src/tui/utils/input-display.ts
1807
+ function normalizeInputText(s) {
1808
+ return s.replace(/\r\n?/g, "\n");
1809
+ }
1810
+ function isHighSurrogate(codeUnit) {
1811
+ return codeUnit >= 55296 && codeUnit <= 56319;
1812
+ }
1813
+ function isLowSurrogate(codeUnit) {
1814
+ return codeUnit >= 56320 && codeUnit <= 57343;
1815
+ }
1816
+ function prevCodePointLength(s, index) {
1817
+ if (index <= 0) return 0;
1818
+ if (index >= 2 && isHighSurrogate(s.charCodeAt(index - 2)) && isLowSurrogate(s.charCodeAt(index - 1))) {
1819
+ return 2;
1820
+ }
1821
+ return 1;
1822
+ }
1823
+ function nextCodePointLength(s, index) {
1824
+ if (index < 0 || index >= s.length) return 0;
1825
+ if (index + 1 < s.length && isHighSurrogate(s.charCodeAt(index)) && isLowSurrogate(s.charCodeAt(index + 1))) {
1826
+ return 2;
1827
+ }
1828
+ return 1;
1829
+ }
1830
+ function wrapByDisplayWidth(line, maxWidth) {
1831
+ const limit = Math.max(1, Math.floor(maxWidth));
1832
+ if (stringWidth(line) <= limit) return [line];
1833
+ const segments = [];
1834
+ let current = "";
1835
+ let width = 0;
1836
+ for (const ch of line) {
1837
+ const cw = charWidth(ch.codePointAt(0) ?? 0);
1838
+ if (current !== "" && width + cw > limit) {
1839
+ segments.push(current);
1840
+ current = ch;
1841
+ width = cw;
1842
+ } else {
1843
+ current += ch;
1844
+ width += cw;
1845
+ }
1846
+ }
1847
+ if (current !== "") segments.push(current);
1848
+ return segments.length > 0 ? segments : [""];
1849
+ }
1850
+ function splitLineBySelection(line, selection) {
1851
+ const lo = line.offset;
1852
+ const hi = line.offset + line.text.length;
1853
+ const s = Math.max(lo, Math.min(hi, selection.start));
1854
+ const e = Math.max(lo, Math.min(hi, selection.end));
1855
+ if (s >= e) return [{ text: line.text, selected: false }];
1856
+ const out = [];
1857
+ if (s > lo) out.push({ text: line.text.slice(0, s - lo), selected: false });
1858
+ out.push({ text: line.text.slice(s - lo, e - lo), selected: true });
1859
+ if (e < hi) out.push({ text: line.text.slice(e - lo), selected: false });
1860
+ return out.filter((seg) => seg.text.length > 0);
1861
+ }
1862
+ function layoutInput(value, cursor, maxWidth) {
1863
+ const c = Math.max(0, Math.min(value.length, cursor));
1864
+ const logicalLines = value.split("\n");
1865
+ const lines = [];
1866
+ let cursorLine = 0;
1867
+ let cursorCol = 0;
1868
+ let offset = 0;
1869
+ for (const lineText of logicalLines) {
1870
+ const col = c - offset;
1871
+ const ownsCursor = col >= 0 && col <= lineText.length;
1872
+ const segments = wrapByDisplayWidth(lineText, maxWidth);
1873
+ for (const segment of segments) {
1874
+ lines.push({ text: segment, offset });
1875
+ offset += segment.length;
1876
+ }
1877
+ if (ownsCursor) {
1878
+ let base = 0;
1879
+ for (let i = 0; i < segments.length; i++) {
1880
+ const segment = segments[i];
1881
+ const segEnd = base + segment.length;
1882
+ const isLastSegment = i === segments.length - 1;
1883
+ if (col < segEnd || col === segEnd && isLastSegment) {
1884
+ cursorLine = lines.length - segments.length + i;
1885
+ cursorCol = col - base;
1886
+ break;
1887
+ }
1888
+ base = segEnd;
1889
+ }
1890
+ }
1891
+ offset += 1;
1892
+ }
1893
+ return { lines, cursorLine, cursorCol };
1894
+ }
1895
+
1896
+ // src/tui/utils/input-editing.ts
1897
+ function clampIndex(value, index) {
1898
+ return Math.max(0, Math.min(value.length, index));
1899
+ }
1900
+ function normalizeCursorIndex(value, index) {
1901
+ const i = clampIndex(value, index);
1902
+ if (i > 0 && i < value.length && (value.codePointAt(i - 1) ?? 0) > 65535) {
1903
+ return i + 1;
1904
+ }
1905
+ return i;
1906
+ }
1907
+ var CJK_RANGES = [
1908
+ // Hiragana + Katakana, incl. the prolonged sound mark ー (the middle dot ・
1909
+ // is punctuation and filtered out before this check).
1910
+ [12352, 12543],
1911
+ // CJK Extension A + Unified Ideographs.
1912
+ [13312, 40959],
1913
+ // CJK compatibility ideographs.
1914
+ [63744, 64255],
1915
+ // CJK Extension B and beyond.
1916
+ [131072, 262143]
1917
+ ];
1918
+ var WHITESPACE = /\s/u;
1919
+ var PUNCTUATION = new RegExp("\\p{P}", "u");
1920
+ function classifyCodePoint(cp) {
1921
+ if (cp === 95) return "word";
1922
+ const ch = String.fromCodePoint(cp);
1923
+ if (WHITESPACE.test(ch)) return "separator";
1924
+ if (PUNCTUATION.test(ch)) return "separator";
1925
+ for (const [lo, hi] of CJK_RANGES) {
1926
+ if (cp >= lo && cp <= hi) return "cjk";
1927
+ }
1928
+ return "word";
1929
+ }
1930
+ function classBefore(value, index) {
1931
+ if (index <= 0) return "separator";
1932
+ const len = prevCodePointLength(value, index);
1933
+ return classifyCodePoint(value.codePointAt(index - len) ?? 0);
1934
+ }
1935
+ function classAt(value, index) {
1936
+ return classifyCodePoint(value.codePointAt(index) ?? 0);
1937
+ }
1938
+ function wordLeft(value, index) {
1939
+ let i = normalizeCursorIndex(value, index);
1940
+ while (i > 0 && classBefore(value, i) === "separator") {
1941
+ i -= prevCodePointLength(value, i);
1942
+ }
1943
+ if (i === 0) return 0;
1944
+ if (classBefore(value, i) === "cjk") {
1945
+ return i - prevCodePointLength(value, i);
1946
+ }
1947
+ while (i > 0 && classBefore(value, i) === "word") {
1948
+ i -= prevCodePointLength(value, i);
1949
+ }
1950
+ return i;
1951
+ }
1952
+ function wordRight(value, index) {
1953
+ const len = value.length;
1954
+ let i = normalizeCursorIndex(value, index);
1955
+ while (i < len && classAt(value, i) === "separator") {
1956
+ i += nextCodePointLength(value, i);
1957
+ }
1958
+ if (i === len) return len;
1959
+ if (classAt(value, i) === "cjk") {
1960
+ return i + nextCodePointLength(value, i);
1961
+ }
1962
+ while (i < len && classAt(value, i) === "word") {
1963
+ i += nextCodePointLength(value, i);
1964
+ }
1965
+ return i;
1966
+ }
1967
+ function lineStart(value, index) {
1968
+ let i = normalizeCursorIndex(value, index);
1969
+ while (i > 0 && value.charCodeAt(i - 1) !== 10) i -= 1;
1970
+ return i;
1971
+ }
1972
+ function lineEnd(value, index) {
1973
+ let i = normalizeCursorIndex(value, index);
1974
+ while (i < value.length && value.charCodeAt(i) !== 10) i += 1;
1975
+ return i;
1976
+ }
1977
+ function killToLineEnd(value, index) {
1978
+ const cursor = normalizeCursorIndex(value, index);
1979
+ const end = lineEnd(value, cursor);
1980
+ return { value: value.slice(0, cursor) + value.slice(end), cursor };
1981
+ }
1982
+ function killToLineStart(value, index) {
1983
+ const cursor = normalizeCursorIndex(value, index);
1984
+ const start = lineStart(value, cursor);
1985
+ return { value: value.slice(0, start) + value.slice(cursor), cursor: start };
1986
+ }
1987
+ function killWordBefore(value, index) {
1988
+ const cursor = normalizeCursorIndex(value, index);
1989
+ const start = wordLeft(value, cursor);
1990
+ return { value: value.slice(0, start) + value.slice(cursor), cursor: start };
1991
+ }
1992
+ function normalizeSelection(value, anchor, cursor) {
1993
+ const c = normalizeCursorIndex(value, cursor);
1994
+ const a = anchor === null ? c : normalizeCursorIndex(value, anchor);
1995
+ return a <= c ? { start: a, end: c } : { start: c, end: a };
1996
+ }
1997
+ function replaceRange(value, start, end, insert) {
1998
+ let a = clampIndex(value, start);
1999
+ let b = clampIndex(value, end);
2000
+ if (a > b) [a, b] = [b, a];
2001
+ return { value: value.slice(0, a) + insert + value.slice(b), cursor: a + insert.length };
2002
+ }
2003
+
2004
+ // src/tui/utils/input-history.ts
2005
+ var MERGE_WINDOW_MS = 500;
2006
+ var MAX_ENTRIES = 100;
2007
+ function initInputHistory(value, ts = Date.now()) {
2008
+ return {
2009
+ entries: [{ value, cursor: value.length, anchor: null, ts, kind: "snap" }],
2010
+ pointer: 0,
2011
+ rewound: false
2012
+ };
2013
+ }
2014
+ function continuesRun(prev, edit) {
2015
+ if (prev.kind === "insert" && edit.kind === "insert") {
2016
+ const lenDiff = edit.value.length - prev.value.length;
2017
+ return (lenDiff === 1 || lenDiff === 2) && edit.cursor === prev.cursor + lenDiff;
2018
+ }
2019
+ if (prev.kind === "delete" && edit.kind === "delete") {
2020
+ const diff = prev.value.length - edit.value.length;
2021
+ return (diff === 1 || diff === 2) && edit.cursor === prev.cursor - diff;
2022
+ }
2023
+ return false;
2024
+ }
2025
+ function commitEdit(history, edit) {
2026
+ const { entries, pointer, rewound } = history;
2027
+ const prev = entries[pointer];
2028
+ const atTail = pointer === entries.length - 1;
2029
+ if (prev && atTail && !rewound && edit.ts - prev.ts < MERGE_WINDOW_MS && continuesRun(prev, edit)) {
2030
+ const merged = entries.slice();
2031
+ merged[pointer] = { ...edit };
2032
+ return { entries: merged, pointer, rewound: false };
2033
+ }
2034
+ const next = [...entries.slice(0, pointer + 1), { ...edit }];
2035
+ if (next.length > MAX_ENTRIES) {
2036
+ const trimmed = next.slice(next.length - MAX_ENTRIES);
2037
+ return { entries: trimmed, pointer: trimmed.length - 1, rewound: false };
2038
+ }
2039
+ return { entries: next, pointer: next.length - 1, rewound: false };
2040
+ }
2041
+ function undoEdit(history) {
2042
+ if (history.pointer === 0) return null;
2043
+ return { entries: history.entries, pointer: history.pointer - 1, rewound: true };
2044
+ }
2045
+ function redoEdit(history) {
2046
+ if (history.pointer === history.entries.length - 1) return null;
2047
+ return { entries: history.entries, pointer: history.pointer + 1, rewound: true };
2048
+ }
2049
+
2050
+ // src/tui/components/input-area.tsx
2051
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
2052
+ var PREFIX2 = `${theme.user.prefix} `;
2053
+ var PREFIX_WIDTH2 = stringWidth(PREFIX2);
2054
+ function InputArea({ onSubmit, onExit, onEscape, busy, width }) {
1806
2055
  const [input, setInput] = useState2("");
1807
2056
  const [cursor, setCursor] = useState2(0);
2057
+ const [anchor, setAnchor] = useState2(null);
1808
2058
  const [selectedIndex, setSelectedIndex] = useState2(0);
2059
+ const historyRef = useRef2(initInputHistory(""));
1809
2060
  const suggestions = useMemo2(() => {
1810
2061
  if (!input.startsWith("/") || input.includes(" ")) return [];
1811
2062
  const typed = input.slice(1).toLowerCase();
1812
2063
  return SLASH_COMMANDS.filter((c) => c.name.startsWith(typed));
1813
2064
  }, [input]);
1814
2065
  const showSuggestions = suggestions.length > 0;
2066
+ const sel = useMemo2(
2067
+ () => normalizeSelection(input, anchor, cursor),
2068
+ [input, anchor, cursor]
2069
+ );
2070
+ const hasSelection = sel.start < sel.end;
2071
+ const visibleSelection = hasSelection && input.slice(sel.start, sel.end).replace(/\n/g, "").length > 0;
1815
2072
  const clampedIndex = showSuggestions ? Math.min(selectedIndex, suggestions.length - 1) : 0;
1816
2073
  const reset = useMemoizedFn(() => {
1817
2074
  setInput("");
1818
2075
  setCursor(0);
2076
+ setAnchor(null);
2077
+ setSelectedIndex(0);
2078
+ historyRef.current = initInputHistory("");
2079
+ });
2080
+ const applyEdit = useMemoizedFn((r, kind) => {
2081
+ setInput(r.value);
2082
+ setCursor(r.cursor);
2083
+ setAnchor(null);
2084
+ setSelectedIndex(0);
2085
+ if (r.value === input && r.cursor === cursor) return;
2086
+ historyRef.current = commitEdit(historyRef.current, {
2087
+ value: r.value,
2088
+ cursor: r.cursor,
2089
+ anchor: null,
2090
+ ts: Date.now(),
2091
+ kind
2092
+ });
2093
+ });
2094
+ const applySnapshot = useMemoizedFn((history) => {
2095
+ const entry = history.entries[history.pointer];
2096
+ if (!entry) return;
2097
+ historyRef.current = history;
2098
+ setInput(entry.value);
2099
+ setCursor(entry.cursor);
2100
+ setAnchor(entry.anchor);
1819
2101
  setSelectedIndex(0);
1820
2102
  });
1821
2103
  const applySuggestion = useMemoizedFn((index) => {
@@ -1823,9 +2105,7 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
1823
2105
  if (!cmd) return;
1824
2106
  const text = `/${cmd.name}`;
1825
2107
  const filled = "args" in cmd ? `${text} ` : text;
1826
- setInput(filled);
1827
- setCursor(filled.length);
1828
- setSelectedIndex(0);
2108
+ applyEdit(replaceRange(input, 0, input.length, filled), "replace");
1829
2109
  });
1830
2110
  useInput((ch, key) => {
1831
2111
  if (key.return) {
@@ -1875,39 +2155,133 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
1875
2155
  return;
1876
2156
  }
1877
2157
  if (key.escape) {
2158
+ if (hasSelection) {
2159
+ setAnchor(null);
2160
+ return;
2161
+ }
1878
2162
  if (showSuggestions) {
1879
- setInput("");
1880
- setCursor(0);
1881
- setSelectedIndex(0);
2163
+ reset();
1882
2164
  } else {
1883
2165
  onEscape?.();
1884
2166
  reset();
1885
2167
  }
1886
2168
  return;
1887
2169
  }
2170
+ if (key.ctrl && ch === "a") {
2171
+ setAnchor(0);
2172
+ setCursor(input.length);
2173
+ return;
2174
+ }
2175
+ if (key.ctrl && ch === "e") {
2176
+ setAnchor(null);
2177
+ setCursor(lineEnd(input, cursor));
2178
+ return;
2179
+ }
2180
+ if (key.ctrl && ch === "k") {
2181
+ if (hasSelection) {
2182
+ applyEdit(replaceRange(input, sel.start, sel.end, ""), "replace");
2183
+ } else {
2184
+ applyEdit(killToLineEnd(input, cursor), "kill");
2185
+ }
2186
+ return;
2187
+ }
2188
+ if (key.ctrl && ch === "u") {
2189
+ if (hasSelection) {
2190
+ applyEdit(replaceRange(input, sel.start, sel.end, ""), "replace");
2191
+ } else {
2192
+ applyEdit(killToLineStart(input, cursor), "kill");
2193
+ }
2194
+ return;
2195
+ }
2196
+ if (key.ctrl && ch === "w") {
2197
+ if (hasSelection) {
2198
+ applyEdit(replaceRange(input, sel.start, sel.end, ""), "replace");
2199
+ } else {
2200
+ applyEdit(killWordBefore(input, cursor), "kill");
2201
+ }
2202
+ return;
2203
+ }
2204
+ const moveWordLeft = () => {
2205
+ if (key.shift) setAnchor((a) => a ?? cursor);
2206
+ else setAnchor(null);
2207
+ setCursor(wordLeft(input, cursor));
2208
+ };
2209
+ const moveWordRight = () => {
2210
+ if (key.shift) setAnchor((a) => a ?? cursor);
2211
+ else setAnchor(null);
2212
+ setCursor(wordRight(input, cursor));
2213
+ };
2214
+ if (key.ctrl && key.leftArrow) {
2215
+ moveWordLeft();
2216
+ return;
2217
+ }
2218
+ if (key.ctrl && key.rightArrow) {
2219
+ moveWordRight();
2220
+ return;
2221
+ }
2222
+ if (key.meta && !key.escape && ch === "b") {
2223
+ moveWordLeft();
2224
+ return;
2225
+ }
2226
+ if (key.meta && !key.escape && ch === "f") {
2227
+ moveWordRight();
2228
+ return;
2229
+ }
2230
+ if (key.ctrl && ch === "z") {
2231
+ const next = undoEdit(historyRef.current);
2232
+ if (next) applySnapshot(next);
2233
+ return;
2234
+ }
2235
+ if (key.ctrl && ch === "y") {
2236
+ const next = redoEdit(historyRef.current);
2237
+ if (next) applySnapshot(next);
2238
+ return;
2239
+ }
2240
+ if (key.shift && key.leftArrow) {
2241
+ setAnchor((a) => a ?? cursor);
2242
+ setCursor((c) => Math.max(0, c - prevCodePointLength(input, c)));
2243
+ return;
2244
+ }
2245
+ if (key.shift && key.rightArrow) {
2246
+ setAnchor((a) => a ?? cursor);
2247
+ setCursor((c) => Math.min(input.length, c + nextCodePointLength(input, c)));
2248
+ return;
2249
+ }
1888
2250
  if (key.backspace || key.delete) {
2251
+ if (hasSelection) {
2252
+ applyEdit(replaceRange(input, sel.start, sel.end, ""), "delete");
2253
+ return;
2254
+ }
1889
2255
  if (cursor === 0) return;
1890
- setInput((prev) => prev.slice(0, cursor - 1) + prev.slice(cursor));
1891
- setCursor((c) => Math.max(0, c - 1));
1892
- setSelectedIndex(0);
2256
+ const len = prevCodePointLength(input, cursor);
2257
+ applyEdit(
2258
+ { value: input.slice(0, cursor - len) + input.slice(cursor), cursor: cursor - len },
2259
+ "delete"
2260
+ );
1893
2261
  return;
1894
2262
  }
1895
2263
  if (key.leftArrow) {
1896
- setCursor((c) => Math.max(0, c - 1));
2264
+ setAnchor(null);
2265
+ setCursor((c) => Math.max(0, c - prevCodePointLength(input, c)));
1897
2266
  return;
1898
2267
  }
1899
2268
  if (key.rightArrow) {
1900
- setCursor((c) => Math.min(input.length, c + 1));
2269
+ setAnchor(null);
2270
+ setCursor((c) => Math.min(input.length, c + nextCodePointLength(input, c)));
1901
2271
  return;
1902
2272
  }
1903
2273
  if (ch && !key.ctrl && !key.meta) {
1904
- setInput((prev) => prev.slice(0, cursor) + ch + prev.slice(cursor));
1905
- setCursor((c) => c + ch.length);
1906
- setSelectedIndex(0);
2274
+ const text = normalizeInputText(ch);
2275
+ if (text.length === 0) return;
2276
+ const r = replaceRange(input, sel.start, sel.end, text);
2277
+ applyEdit(r, [...text].length === 1 ? "insert" : "paste");
1907
2278
  }
1908
2279
  });
1909
- const before = input.slice(0, cursor);
1910
- const after = input.slice(cursor);
2280
+ const textBudget = Math.max(1, width - 2 - PREFIX_WIDTH2);
2281
+ const layout = useMemo2(
2282
+ () => layoutInput(input, cursor, textBudget),
2283
+ [input, cursor, textBudget]
2284
+ );
1911
2285
  return /* @__PURE__ */ jsxs6(Box6, { flexDirection: "column", children: [
1912
2286
  showSuggestions && /* @__PURE__ */ jsx9(Box6, { flexDirection: "column", paddingX: 1, marginBottom: 0, children: suggestions.map((cmd, i) => {
1913
2287
  const isSelected = i === clampedIndex;
@@ -1921,7 +2295,7 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
1921
2295
  ] })
1922
2296
  ] }, cmd.name);
1923
2297
  }) }),
1924
- /* @__PURE__ */ jsxs6(
2298
+ /* @__PURE__ */ jsx9(
1925
2299
  Box6,
1926
2300
  {
1927
2301
  borderStyle: "single",
@@ -1929,16 +2303,26 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
1929
2303
  borderLeft: false,
1930
2304
  borderRight: false,
1931
2305
  paddingX: 1,
1932
- children: [
1933
- /* @__PURE__ */ jsx9(Text7, { color: theme.user.prefixColor, bold: true, children: theme.user.prefix }),
1934
- /* @__PURE__ */ jsxs6(Text7, { children: [
1935
- " ",
1936
- before
1937
- ] }),
1938
- /* @__PURE__ */ jsx9(Text7, { color: "white", inverse: true, children: after.length > 0 ? after[0] : " " }),
1939
- /* @__PURE__ */ jsx9(Text7, { children: after.slice(1) }),
1940
- /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: busy ? " (thinking...)" : "" })
1941
- ]
2306
+ children: /* @__PURE__ */ jsx9(Box6, { flexDirection: "column", children: layout.lines.map((line, i) => {
2307
+ const isCursorLine = i === layout.cursorLine;
2308
+ const col = layout.cursorCol;
2309
+ const charLen = isCursorLine ? nextCodePointLength(line.text, col) : 0;
2310
+ const before = isCursorLine ? line.text.slice(0, col) : line.text;
2311
+ const cursorCell = isCursorLine && charLen > 0 ? line.text.slice(col, col + charLen) : " ";
2312
+ const after = isCursorLine ? line.text.slice(col + charLen) : "";
2313
+ return /* @__PURE__ */ jsxs6(Box6, { children: [
2314
+ i === 0 ? /* @__PURE__ */ jsx9(Text7, { color: theme.user.prefixColor, bold: true, children: theme.user.prefix }) : /* @__PURE__ */ jsx9(Text7, { children: " ".repeat(PREFIX_WIDTH2) }),
2315
+ visibleSelection ? /* @__PURE__ */ jsxs6(Fragment3, { children: [
2316
+ i === 0 && /* @__PURE__ */ jsx9(Text7, { children: " " }),
2317
+ splitLineBySelection(line, sel).map((seg, j) => /* @__PURE__ */ jsx9(Text7, { inverse: seg.selected, children: seg.text }, j))
2318
+ ] }) : /* @__PURE__ */ jsxs6(Fragment3, { children: [
2319
+ /* @__PURE__ */ jsx9(Text7, { children: i === 0 ? ` ${before}` : before }),
2320
+ isCursorLine && /* @__PURE__ */ jsx9(Text7, { color: "white", inverse: true, children: cursorCell }),
2321
+ /* @__PURE__ */ jsx9(Text7, { children: after })
2322
+ ] }),
2323
+ isCursorLine && busy && /* @__PURE__ */ jsx9(Text7, { dimColor: true, children: " (thinking...)" })
2324
+ ] }, i);
2325
+ }) })
1942
2326
  }
1943
2327
  )
1944
2328
  ] });
@@ -1947,13 +2331,59 @@ function InputArea({ onSubmit, onExit, onEscape, busy }) {
1947
2331
  // src/tui/components/inquiry-prompt.tsx
1948
2332
  import React4, { useState as useState3 } from "react";
1949
2333
  import { Box as Box7, Text as Text8, useInput as useInput2 } from "ink";
2334
+
2335
+ // src/types/config.ts
2336
+ function validateConfig(config) {
2337
+ for (const [name, provider] of Object.entries(config.providers)) {
2338
+ if (!provider.endpoint) return `providers.${name}.endpoint is required`;
2339
+ if (!provider.apiKey) return `providers.${name}.apiKey is required (set WAYANG_LLM_API_KEY or configure in config file)`;
2340
+ if (!provider.modelName) return `providers.${name}.modelName is required`;
2341
+ const window = provider.contextWindowTokens;
2342
+ if (window !== void 0 && !(Number.isInteger(window) && window > 0)) {
2343
+ return `providers.${name}.contextWindowTokens must be a positive integer (got ${JSON.stringify(window)})`;
2344
+ }
2345
+ }
2346
+ const cp = config.controller.provider;
2347
+ if (!config.providers[cp]) return `controller.provider "${cp}" not found`;
2348
+ const wp = config.worker.provider;
2349
+ if (!config.providers[wp]) return `worker.provider "${wp}" not found`;
2350
+ return null;
2351
+ }
2352
+
2353
+ // src/types/signal.ts
2354
+ function getSignalField(sig, field) {
2355
+ return sig.payload[field];
2356
+ }
2357
+
2358
+ // src/types/inquiry.ts
2359
+ var INQUIRE_OTHER_OPTION = "\u5176\u4ED6\uFF08\u81EA\u884C\u8F93\u5165\uFF09";
2360
+ var INQUIRE_CUSTOM_ANSWER_PREFIX = "[Other] ";
2361
+ function buildSelectChoices(options, allowOther) {
2362
+ return allowOther === false ? options : [...options, INQUIRE_OTHER_OPTION];
2363
+ }
2364
+ function isOtherChoice(choice) {
2365
+ return choice === INQUIRE_OTHER_OPTION;
2366
+ }
2367
+ function formatCustomAnswer(text) {
2368
+ return `${INQUIRE_CUSTOM_ANSWER_PREFIX}${text}`;
2369
+ }
2370
+
2371
+ // src/tui/components/inquiry-prompt.tsx
1950
2372
  import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
1951
2373
  function InquiryPrompt({ inquiry, onAnswer }) {
1952
2374
  if (inquiry.type === "confirm") {
1953
2375
  return /* @__PURE__ */ jsx10(ConfirmPrompt, { message: inquiry.message, defaultVal: inquiry.default, onAnswer });
1954
2376
  }
1955
2377
  if (inquiry.type === "select") {
1956
- return /* @__PURE__ */ jsx10(SelectPrompt, { message: inquiry.message, options: inquiry.options ?? [], onAnswer });
2378
+ return /* @__PURE__ */ jsx10(
2379
+ SelectPrompt,
2380
+ {
2381
+ message: inquiry.message,
2382
+ options: inquiry.options ?? [],
2383
+ allowOther: inquiry.allowOther,
2384
+ onAnswer
2385
+ }
2386
+ );
1957
2387
  }
1958
2388
  return /* @__PURE__ */ jsx10(TextPrompt, { message: inquiry.message, onAnswer });
1959
2389
  }
@@ -1977,25 +2407,53 @@ function ConfirmPrompt({ message, defaultVal, onAnswer }) {
1977
2407
  ] }, opt)) })
1978
2408
  ] });
1979
2409
  }
1980
- function SelectPrompt({ message, options, onAnswer }) {
2410
+ function SelectPrompt({ message, options, allowOther, onAnswer }) {
2411
+ const [awaitingCustom, setAwaitingCustom] = useState3(false);
2412
+ if (awaitingCustom) {
2413
+ return /* @__PURE__ */ jsx10(
2414
+ TextPrompt,
2415
+ {
2416
+ message,
2417
+ inputLabel: INQUIRE_OTHER_OPTION,
2418
+ onAnswer: (text) => onAnswer(formatCustomAnswer(text))
2419
+ }
2420
+ );
2421
+ }
2422
+ return /* @__PURE__ */ jsx10(
2423
+ SelectList,
2424
+ {
2425
+ message,
2426
+ options,
2427
+ allowOther,
2428
+ onAnswer,
2429
+ onRequestCustom: () => setAwaitingCustom(true)
2430
+ }
2431
+ );
2432
+ }
2433
+ function SelectList({ message, options, allowOther, onAnswer, onRequestCustom }) {
2434
+ const choices = buildSelectChoices(options, allowOther);
1981
2435
  const [selected, setSelected] = useState3(0);
1982
2436
  useInput2((_ch, key) => {
1983
- if (key.upArrow) setSelected((i) => (i - 1 + options.length) % options.length);
1984
- if (key.downArrow) setSelected((i) => (i + 1) % options.length);
1985
- if (key.return && options.length > 0) onAnswer(options[selected]);
2437
+ if (key.upArrow && choices.length > 0) setSelected((i) => (i - 1 + choices.length) % choices.length);
2438
+ if (key.downArrow && choices.length > 0) setSelected((i) => (i + 1) % choices.length);
2439
+ if (key.return && choices.length > 0) {
2440
+ const choice = choices[selected];
2441
+ if (isOtherChoice(choice)) onRequestCustom();
2442
+ else onAnswer(choice);
2443
+ }
1986
2444
  });
1987
2445
  return /* @__PURE__ */ jsxs7(Box7, { flexDirection: "column", children: [
1988
2446
  /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, borderStyle: "single", borderColor: "gray", borderLeft: false, borderRight: false, children: [
1989
2447
  /* @__PURE__ */ jsx10(Text8, { color: "cyan", children: "? " }),
1990
2448
  /* @__PURE__ */ jsx10(Text8, { bold: true, children: message })
1991
2449
  ] }),
1992
- /* @__PURE__ */ jsx10(Box7, { flexDirection: "column", paddingX: 1, children: options.map((opt, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
2450
+ /* @__PURE__ */ jsx10(Box7, { flexDirection: "column", paddingX: 1, children: choices.map((choice, i) => /* @__PURE__ */ jsxs7(Box7, { children: [
1993
2451
  /* @__PURE__ */ jsx10(Text8, { color: i === selected ? theme.baseToken.color.accent : void 0, children: i === selected ? "\u25B8 " : " " }),
1994
- /* @__PURE__ */ jsx10(Text8, { bold: i === selected, color: i === selected ? theme.baseToken.color.accent : void 0, children: opt })
2452
+ /* @__PURE__ */ jsx10(Text8, { bold: i === selected, color: i === selected ? theme.baseToken.color.accent : void 0, dimColor: isOtherChoice(choice) && i !== selected, children: choice })
1995
2453
  ] }, i)) })
1996
2454
  ] });
1997
2455
  }
1998
- function TextPrompt({ message, onAnswer }) {
2456
+ function TextPrompt({ message, onAnswer, inputLabel }) {
1999
2457
  const [input, setInput] = useState3("");
2000
2458
  const [cursor, setCursor] = useState3(0);
2001
2459
  useInput2((ch, key) => {
@@ -2031,10 +2489,12 @@ function TextPrompt({ message, onAnswer }) {
2031
2489
  /* @__PURE__ */ jsx10(Text8, { bold: true, children: message })
2032
2490
  ] }),
2033
2491
  /* @__PURE__ */ jsxs7(Box7, { paddingX: 1, borderStyle: "single", borderColor: "gray", borderLeft: false, borderRight: false, children: [
2034
- /* @__PURE__ */ jsxs7(Text8, { children: [
2492
+ inputLabel ? /* @__PURE__ */ jsxs7(Text8, { dimColor: true, children: [
2035
2493
  " ",
2036
- before
2037
- ] }),
2494
+ inputLabel,
2495
+ ": "
2496
+ ] }) : /* @__PURE__ */ jsx10(Text8, { children: " " }),
2497
+ /* @__PURE__ */ jsx10(Text8, { children: before }),
2038
2498
  /* @__PURE__ */ jsx10(Text8, { color: "white", inverse: true, children: after.length > 0 ? after[0] : " " }),
2039
2499
  /* @__PURE__ */ jsx10(Text8, { children: after.slice(1) })
2040
2500
  ] })
@@ -2173,11 +2633,6 @@ function OverlayPanel({ overlay }) {
2173
2633
  ] });
2174
2634
  }
2175
2635
 
2176
- // src/types/signal.ts
2177
- function getSignalField(sig, field) {
2178
- return sig.payload[field];
2179
- }
2180
-
2181
2636
  // src/tui/types/display-item.ts
2182
2637
  function entryToDisplayItem(entry) {
2183
2638
  const base = {
@@ -2405,7 +2860,7 @@ function ControllerPage({ onExit }) {
2405
2860
  }
2406
2861
  ) }),
2407
2862
  overlay && /* @__PURE__ */ jsx13(OverlayPanel, { overlay }),
2408
- pendingInquiry ? /* @__PURE__ */ jsx13(InquiryPrompt, { inquiry: pendingInquiry, onAnswer: (answer) => supervisor.resolveInquiry(answer) }) : /* @__PURE__ */ jsx13(InputArea, { onSubmit: handleSubmit, onExit, onEscape: dismissOverlay, busy }),
2863
+ pendingInquiry ? /* @__PURE__ */ jsx13(InquiryPrompt, { inquiry: pendingInquiry, onAnswer: (answer) => supervisor.resolveInquiry(answer) }) : /* @__PURE__ */ jsx13(InputArea, { onSubmit: handleSubmit, onExit, onEscape: dismissOverlay, busy, width: terminalCols }),
2409
2864
  /* @__PURE__ */ jsx13(
2410
2865
  StatusBar,
2411
2866
  {
@@ -2427,7 +2882,7 @@ function AppContent() {
2427
2882
  const { exit } = useApp();
2428
2883
  const supervisor = useSupervisor();
2429
2884
  const busy = useWayangState(supervisor.controllerAgent, "dynamicState.busy") ?? false;
2430
- const lastCtrlC = useRef2(0);
2885
+ const lastCtrlC = useRef3(0);
2431
2886
  const handleExit = useMemoizedFn(() => {
2432
2887
  exit();
2433
2888
  });
@@ -2451,7 +2906,7 @@ function AppContent() {
2451
2906
  // package.json
2452
2907
  var package_default = {
2453
2908
  name: "wayang-ai",
2454
- version: "0.6.3",
2909
+ version: "0.7.0",
2455
2910
  description: "Multi-agent orchestration platform",
2456
2911
  type: "module",
2457
2912
  license: "MIT",
@@ -2553,13 +3008,13 @@ function writeWelcome(workspace) {
2553
3008
 
2554
3009
  // src/tui/render.ts
2555
3010
  function renderInkUI(supervisor) {
2556
- return new Promise((resolve11) => {
3011
+ return new Promise((resolve12) => {
2557
3012
  process.stdout.write("\x1B[2J\x1B[H");
2558
3013
  writeWelcome(supervisor.ctx.workspaceDir);
2559
3014
  const { waitUntilExit } = render(
2560
3015
  React6.createElement(App, { supervisor })
2561
3016
  );
2562
- waitUntilExit().then(() => resolve11());
3017
+ waitUntilExit().then(() => resolve12());
2563
3018
  });
2564
3019
  }
2565
3020
 
@@ -2568,26 +3023,6 @@ import * as fs3 from "fs";
2568
3023
  import * as path3 from "path";
2569
3024
  import * as childProcess from "child_process";
2570
3025
  import prompts from "prompts";
2571
-
2572
- // src/types/config.ts
2573
- function validateConfig(config) {
2574
- for (const [name, provider] of Object.entries(config.providers)) {
2575
- if (!provider.endpoint) return `providers.${name}.endpoint is required`;
2576
- if (!provider.apiKey) return `providers.${name}.apiKey is required (set WAYANG_LLM_API_KEY or configure in config file)`;
2577
- if (!provider.modelName) return `providers.${name}.modelName is required`;
2578
- const window = provider.contextWindowTokens;
2579
- if (window !== void 0 && !(Number.isInteger(window) && window > 0)) {
2580
- return `providers.${name}.contextWindowTokens must be a positive integer (got ${JSON.stringify(window)})`;
2581
- }
2582
- }
2583
- const cp = config.controller.provider;
2584
- if (!config.providers[cp]) return `controller.provider "${cp}" not found`;
2585
- const wp = config.worker.provider;
2586
- if (!config.providers[wp]) return `worker.provider "${wp}" not found`;
2587
- return null;
2588
- }
2589
-
2590
- // src/onboard.ts
2591
3026
  function fillApiKeys(config) {
2592
3027
  const envKey = process.env.WAYANG_LLM_API_KEY || "";
2593
3028
  for (const provider of Object.values(config.providers)) {
@@ -3511,10 +3946,10 @@ var SignalQueue = class {
3511
3946
  waitForSignal() {
3512
3947
  const unread = this.state.get("signals").filter((s) => s.status === "unread");
3513
3948
  if (unread.length > 0) return Promise.resolve();
3514
- return new Promise((resolve11) => {
3949
+ return new Promise((resolve12) => {
3515
3950
  this.resolveWait = () => {
3516
3951
  this.logger.debug("waitForSignal resolved");
3517
- resolve11();
3952
+ resolve12();
3518
3953
  };
3519
3954
  });
3520
3955
  }
@@ -3574,267 +4009,12 @@ var SignalQueue = class {
3574
4009
  }
3575
4010
  };
3576
4011
 
3577
- // src/services/task/task-pool-state.ts
3578
- import { join as join6 } from "path";
3579
- var TaskPoolState = class extends BaseWayangState {
3580
- jsonFile;
3581
- logger;
3582
- constructor(ctx) {
3583
- const jsonFile = new JSONFileHelper(join6(ctx.sessionDir, "tasks.json"));
3584
- super(
3585
- {
3586
- tasks: {
3587
- pending: [],
3588
- running: [],
3589
- history: []
3590
- }
3591
- },
3592
- [{ path: "tasks", helper: jsonFile }]
3593
- );
3594
- this.jsonFile = jsonFile;
3595
- this.logger = ctx.logger;
3596
- }
3597
- async restore() {
3598
- const data = this.jsonFile.read();
3599
- if (data) {
3600
- this.data.tasks = data;
3601
- }
3602
- this.logger.info("TaskPoolState restored");
3603
- }
3604
- };
3605
-
3606
- // src/services/agents/base-agent.ts
3607
- import { streamText, stepCountIs, hasToolCall } from "ai";
3608
-
3609
- // src/services/agents/model-factory.ts
3610
- import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
3611
- function createModel(provider) {
3612
- const compatible = createOpenAICompatible({
3613
- name: "wayang",
3614
- baseURL: provider.endpoint,
3615
- apiKey: provider.apiKey
3616
- });
3617
- return compatible.chatModel(provider.modelName);
3618
- }
3619
-
3620
- // src/utils/id.ts
3621
- function generateId(prefix) {
3622
- return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
3623
- }
3624
- function formatTimestamp(date = /* @__PURE__ */ new Date()) {
3625
- const Y = date.getFullYear();
3626
- const M = String(date.getMonth() + 1).padStart(2, "0");
3627
- const D = String(date.getDate()).padStart(2, "0");
3628
- const h = String(date.getHours()).padStart(2, "0");
3629
- const m = String(date.getMinutes()).padStart(2, "0");
3630
- const s = String(date.getSeconds()).padStart(2, "0");
3631
- return `${Y}${M}${D}-${h}${m}${s}`;
3632
- }
4012
+ // src/services/worker-transcript.ts
4013
+ import { closeSync as closeSync2, existsSync as existsSync5, openSync as openSync2, readSync, statSync } from "fs";
4014
+ import { join as join7 } from "path";
3633
4015
 
3634
- // src/utils/time.ts
3635
- function nowISO() {
3636
- const now = /* @__PURE__ */ new Date();
3637
- const off = -now.getTimezoneOffset();
3638
- const sign = off >= 0 ? "+" : "-";
3639
- const absOff = Math.abs(off);
3640
- const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
3641
- const mm = String(absOff % 60).padStart(2, "0");
3642
- const pad = (n, len = 2) => String(n).padStart(len, "0");
3643
- 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}`;
3644
- }
3645
-
3646
- // src/services/agents/base-agent.ts
3647
- var DEFAULT_LLM_IDLE_TIMEOUT_MS = 3e5;
3648
- var BaseAgent = class {
3649
- id;
3650
- abortController;
3651
- model;
3652
- hooks = {};
3653
- constructor(provider) {
3654
- this.id = generateId("agent");
3655
- this.abortController = new AbortController();
3656
- this.model = createModel(provider);
3657
- }
3658
- /** Set lifecycle hooks. */
3659
- setHooks(hooks) {
3660
- this.hooks = hooks;
3661
- }
3662
- /** Core streaming primitive — yields text chunks, returns final result.
3663
- * Catches abort signal internally so callers see a clean completion. */
3664
- async *streamLoop(opts) {
3665
- const step = 0;
3666
- const idleTimeoutMs = opts.llmIdleTimeoutMs ?? DEFAULT_LLM_IDLE_TIMEOUT_MS;
3667
- const callAbort = new AbortController();
3668
- const forwardAbort = () => callAbort.abort();
3669
- if (this.abortController.signal.aborted) {
3670
- callAbort.abort();
3671
- } else {
3672
- this.abortController.signal.addEventListener("abort", forwardAbort);
3673
- }
3674
- let watchdog = null;
3675
- let timedOut = false;
3676
- let toolsInFlight = 0;
3677
- let finished = false;
3678
- const clearWatchdog = () => {
3679
- if (watchdog !== null) {
3680
- clearTimeout(watchdog);
3681
- watchdog = null;
3682
- }
3683
- };
3684
- const armWatchdog = () => {
3685
- if (idleTimeoutMs <= 0 || finished || toolsInFlight > 0) return;
3686
- clearWatchdog();
3687
- watchdog = setTimeout(() => {
3688
- timedOut = true;
3689
- callAbort.abort();
3690
- }, idleTimeoutMs);
3691
- };
3692
- const tools = pauseWatchdogDuringTools(
3693
- opts.tools,
3694
- () => {
3695
- toolsInFlight++;
3696
- clearWatchdog();
3697
- },
3698
- () => {
3699
- toolsInFlight--;
3700
- if (toolsInFlight === 0) armWatchdog();
3701
- }
3702
- );
3703
- let stream;
3704
- const stopConditions = [];
3705
- if (opts.maxSteps) stopConditions.push(stepCountIs(opts.maxSteps));
3706
- if (opts.stopTools?.length) {
3707
- for (const name of opts.stopTools) {
3708
- stopConditions.push(hasToolCall(name));
3709
- }
3710
- }
3711
- let startTime;
3712
- let firstChunkAt;
3713
- let stepStartedAt;
3714
- const onStep = opts.onStep ? (event) => {
3715
- armWatchdog();
3716
- const meta = { startedAt: stepStartedAt, firstTokenAt: firstChunkAt };
3717
- firstChunkAt = void 0;
3718
- stepStartedAt = nowISO();
3719
- opts.onStep(event, meta);
3720
- } : void 0;
3721
- try {
3722
- this.hooks.beforeLLM?.({ messageCount: opts.messages.length, messages: opts.messages });
3723
- startTime = Date.now();
3724
- stepStartedAt = nowISO();
3725
- armWatchdog();
3726
- stream = streamText({
3727
- model: this.model,
3728
- system: opts.system,
3729
- messages: opts.messages,
3730
- tools,
3731
- toolChoice: opts.toolChoice,
3732
- stopWhen: stopConditions.length > 0 ? stopConditions : void 0,
3733
- abortSignal: callAbort.signal,
3734
- ...opts.prepareStep && { prepareStep: opts.prepareStep },
3735
- onStepFinish: onStep,
3736
- onError: ({ error }) => {
3737
- opts.onStreamError?.(error);
3738
- },
3739
- // SignalEntry entries are trusted system-generated notifications emitted
3740
- // as `system`-role messages (in-band, order-sensitive). AI SDK v6 warns
3741
- // on any system message inside `messages` unless this is opted in.
3742
- allowSystemInMessages: true
3743
- });
3744
- } catch (err) {
3745
- finished = true;
3746
- clearWatchdog();
3747
- this.abortController.signal.removeEventListener("abort", forwardAbort);
3748
- if (this.abortController.signal.aborted) {
3749
- return { text: "", toolResults: [], usage: void 0 };
3750
- }
3751
- throw err;
3752
- }
3753
- try {
3754
- for await (const chunk of stream.textStream) {
3755
- armWatchdog();
3756
- if (chunk && firstChunkAt === void 0) firstChunkAt = nowISO();
3757
- yield chunk;
3758
- }
3759
- } catch (err) {
3760
- if (this.abortController.signal.aborted) {
3761
- return { text: "", toolResults: [], usage: void 0 };
3762
- }
3763
- if (timedOut) {
3764
- throw new Error(
3765
- `LLM call idle timeout: no stream progress (text chunk or step finish) for ${idleTimeoutMs}ms`
3766
- );
3767
- }
3768
- if (this.abortController.signal.aborted) {
3769
- return { text: "", toolResults: [], usage: void 0 };
3770
- }
3771
- throw err;
3772
- } finally {
3773
- finished = true;
3774
- clearWatchdog();
3775
- this.abortController.signal.removeEventListener("abort", forwardAbort);
3776
- }
3777
- const duration = Date.now() - startTime;
3778
- const text = await stream.text;
3779
- const reasoningText = await stream.reasoningText;
3780
- const toolResults = await stream.toolResults;
3781
- const rawUsage = await stream.usage;
3782
- const finishReason = await stream.finishReason;
3783
- const steps = await stream.steps;
3784
- const usage = rawUsage ? { inputTokens: rawUsage.inputTokens ?? 0, outputTokens: rawUsage.outputTokens ?? 0 } : void 0;
3785
- this.hooks.afterLLM?.({ step, usage, durationMs: duration });
3786
- return {
3787
- text,
3788
- reasoningText,
3789
- toolResults: toolResults?.map((tr) => ({
3790
- toolCallId: tr.toolCallId,
3791
- result: tr.result,
3792
- isError: tr.isError
3793
- })),
3794
- usage,
3795
- finishReason,
3796
- steps: steps?.length
3797
- };
3798
- }
3799
- /** Batch wrapper — iterates streamLoop and returns the final result. */
3800
- async collectLoop(opts) {
3801
- const gen = this.streamLoop(opts);
3802
- let result;
3803
- while (true) {
3804
- const { done, value } = await gen.next();
3805
- if (done) {
3806
- result = value;
3807
- break;
3808
- }
3809
- }
3810
- return result;
3811
- }
3812
- abort() {
3813
- this.abortController.abort();
3814
- }
3815
- };
3816
- function pauseWatchdogDuringTools(tools, onStart, onEnd) {
3817
- const wrapped = {};
3818
- for (const [name, def] of Object.entries(tools ?? {})) {
3819
- const original = def?.execute;
3820
- if (typeof original !== "function") {
3821
- wrapped[name] = def;
3822
- continue;
3823
- }
3824
- wrapped[name] = {
3825
- ...def,
3826
- execute: async (...args) => {
3827
- onStart();
3828
- try {
3829
- return await original(...args);
3830
- } finally {
3831
- onEnd();
3832
- }
3833
- }
3834
- };
3835
- }
3836
- return wrapped;
3837
- }
4016
+ // src/services/agents/worker-state.ts
4017
+ import { join as join6 } from "path";
3838
4018
 
3839
4019
  // src/services/agents/utils/conversation-to-sdk-messages.ts
3840
4020
  function conversationToSdkMessages(entries) {
@@ -4008,6 +4188,32 @@ function toolOutputText(output) {
4008
4188
  return toolPayloadText(output);
4009
4189
  }
4010
4190
 
4191
+ // src/utils/id.ts
4192
+ function generateId(prefix) {
4193
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
4194
+ }
4195
+ function formatTimestamp(date = /* @__PURE__ */ new Date()) {
4196
+ const Y = date.getFullYear();
4197
+ const M = String(date.getMonth() + 1).padStart(2, "0");
4198
+ const D = String(date.getDate()).padStart(2, "0");
4199
+ const h = String(date.getHours()).padStart(2, "0");
4200
+ const m = String(date.getMinutes()).padStart(2, "0");
4201
+ const s = String(date.getSeconds()).padStart(2, "0");
4202
+ return `${Y}${M}${D}-${h}${m}${s}`;
4203
+ }
4204
+
4205
+ // src/utils/time.ts
4206
+ function nowISO() {
4207
+ const now = /* @__PURE__ */ new Date();
4208
+ const off = -now.getTimezoneOffset();
4209
+ const sign = off >= 0 ? "+" : "-";
4210
+ const absOff = Math.abs(off);
4211
+ const hh = String(Math.floor(absOff / 60)).padStart(2, "0");
4212
+ const mm = String(absOff % 60).padStart(2, "0");
4213
+ const pad = (n, len = 2) => String(n).padStart(len, "0");
4214
+ 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}`;
4215
+ }
4216
+
4011
4217
  // src/services/agents/context-manager.ts
4012
4218
  var DEFAULT_CONTEXT_WINDOW_TOKENS = 256e3;
4013
4219
  var COMPACT_THRESHOLD_RATIO = 0.9;
@@ -4247,12 +4453,14 @@ ${this.compactSummary}` : "";
4247
4453
  };
4248
4454
 
4249
4455
  // src/services/agents/worker-state.ts
4250
- import { join as join7 } from "path";
4456
+ function workerDir(sessionDir, workerId) {
4457
+ return join6(sessionDir, "workers", workerId);
4458
+ }
4251
4459
  var WorkerState = class extends BaseWayangState {
4252
4460
  constructor(sessionDir, workerId, logger) {
4253
- const workerDir = join7(sessionDir, "workers", workerId);
4254
- const jsonFile = new JSONFileHelper(join7(workerDir, "runtime-state.json"));
4255
- const jsonlFile = new JSONLFileHelper(join7(workerDir, "conversation.jsonl"));
4461
+ const dir = workerDir(sessionDir, workerId);
4462
+ const jsonFile = new JSONFileHelper(join6(dir, "runtime-state.json"));
4463
+ const jsonlFile = new JSONLFileHelper(join6(dir, "conversation.jsonl"));
4256
4464
  const initialData = {
4257
4465
  runtimeState: {
4258
4466
  session: { id: workerId, startedAt: Date.now() },
@@ -4299,6 +4507,524 @@ var WorkerState = class extends BaseWayangState {
4299
4507
  }
4300
4508
  };
4301
4509
 
4510
+ // src/services/worker-transcript.ts
4511
+ var WORKER_TRANSCRIPT_FILE = "conversation.jsonl";
4512
+ var MAX_TRANSCRIPT_RESULT_CHARS = 4e4;
4513
+ var WORKER_ID_PATTERN = /^[A-Za-z0-9_-]+$/;
4514
+ function isValidWorkerId(workerId) {
4515
+ return WORKER_ID_PATTERN.test(workerId) && workerId !== "." && workerId !== "..";
4516
+ }
4517
+ function workerTranscriptPath(sessionDir, workerId) {
4518
+ return join7(workerDir(sessionDir, workerId), WORKER_TRANSCRIPT_FILE);
4519
+ }
4520
+ function readWorkerTranscript(sessionDir, workerId) {
4521
+ if (!isValidWorkerId(workerId)) return null;
4522
+ const path7 = workerTranscriptPath(sessionDir, workerId);
4523
+ if (!existsSync5(path7)) return null;
4524
+ return new JSONLFileHelper(path7).read();
4525
+ }
4526
+ var MAX_TRANSCRIPT_WINDOW_BYTES = 2 * 1024 * 1024;
4527
+ var MIN_WINDOW_BYTES = 64 * 1024;
4528
+ var ENTRY_BYTES_ESTIMATE = 1024;
4529
+ function parseWindow(text, startsAtFileHead) {
4530
+ let body = text;
4531
+ if (!startsAtFileHead) {
4532
+ const nl = body.indexOf("\n");
4533
+ if (nl === -1) return { entries: [], incompleteTail: false };
4534
+ body = body.slice(nl + 1);
4535
+ }
4536
+ const endsComplete = body.endsWith("\n");
4537
+ const lines = body.split("\n");
4538
+ if (endsComplete) lines.pop();
4539
+ const entries = [];
4540
+ let incompleteTail = false;
4541
+ for (let i = 0; i < lines.length; i++) {
4542
+ const line = lines[i];
4543
+ if (line.length === 0) continue;
4544
+ try {
4545
+ entries.push(JSON.parse(line));
4546
+ } catch {
4547
+ if (i === lines.length - 1 && !endsComplete) incompleteTail = true;
4548
+ }
4549
+ }
4550
+ return { entries, incompleteTail };
4551
+ }
4552
+ function readWorkerTranscriptWindow(sessionDir, workerId, opts = {}) {
4553
+ if (!isValidWorkerId(workerId)) return null;
4554
+ const path7 = workerTranscriptPath(sessionDir, workerId);
4555
+ let size;
4556
+ try {
4557
+ size = statSync(path7).size;
4558
+ } catch {
4559
+ return null;
4560
+ }
4561
+ if (size === 0) {
4562
+ return { entries: [], incompleteTail: false, windowTruncated: false, coversFile: true };
4563
+ }
4564
+ const matches = opts.matches ?? (() => true);
4565
+ const need = opts.need ?? Number.POSITIVE_INFINITY;
4566
+ let windowBytes = need === Number.POSITIVE_INFINITY ? MAX_TRANSCRIPT_WINDOW_BYTES : Math.min(MAX_TRANSCRIPT_WINDOW_BYTES, Math.max(MIN_WINDOW_BYTES, Math.ceil(need) * ENTRY_BYTES_ESTIMATE));
4567
+ for (; ; ) {
4568
+ const readLen = Math.min(size, windowBytes);
4569
+ const start = size - readLen;
4570
+ const chunk = Buffer.alloc(readLen);
4571
+ const fd = openSync2(path7, "r");
4572
+ try {
4573
+ readSync(fd, chunk, 0, readLen, start);
4574
+ } finally {
4575
+ closeSync2(fd);
4576
+ }
4577
+ const parsed = parseWindow(chunk.toString("utf8"), start === 0);
4578
+ const enough = parsed.entries.filter(matches).length >= need;
4579
+ const canGrow = start > 0 && windowBytes < MAX_TRANSCRIPT_WINDOW_BYTES;
4580
+ if (enough || !canGrow) {
4581
+ return {
4582
+ entries: parsed.entries,
4583
+ incompleteTail: parsed.incompleteTail,
4584
+ windowTruncated: start > 0,
4585
+ coversFile: start === 0
4586
+ };
4587
+ }
4588
+ windowBytes = Math.min(MAX_TRANSCRIPT_WINDOW_BYTES, windowBytes * 2);
4589
+ }
4590
+ }
4591
+ function blockText(block) {
4592
+ if (typeof block === "string") return block;
4593
+ if (block && typeof block === "object") {
4594
+ const b = block;
4595
+ if (b.type === "text" && typeof b.text === "string") return b.text;
4596
+ return JSON.stringify(block);
4597
+ }
4598
+ return String(block ?? "");
4599
+ }
4600
+ function entryText(entry) {
4601
+ const raw = entry;
4602
+ const content = raw.message?.content ?? raw.content ?? "";
4603
+ if (typeof content === "string") return content;
4604
+ if (Array.isArray(content)) return content.map(blockText).join("\n");
4605
+ return content === null || content === void 0 ? "" : String(content);
4606
+ }
4607
+ function entryToolCalls(entry) {
4608
+ const calls = entry.toolCalls;
4609
+ if (!Array.isArray(calls)) return [];
4610
+ return calls.filter((c) => typeof c?.toolName === "string").map((c) => ({ toolName: c.toolName, arguments: c.arguments }));
4611
+ }
4612
+ function entryToolResults(entry) {
4613
+ const results = entry.toolResults;
4614
+ if (!Array.isArray(results)) return [];
4615
+ return results.map((r) => ({
4616
+ output: toolOutputText(r.output),
4617
+ isError: r.isError
4618
+ }));
4619
+ }
4620
+ function entryLabel(entry) {
4621
+ const subtype = entry.subtype;
4622
+ return subtype ? `${entry.type}/${subtype}` : entry.type;
4623
+ }
4624
+ var TERMINAL_SUBTYPES = /* @__PURE__ */ new Set(["task_result" /* TaskResult */, "error" /* Error */]);
4625
+ function entryTimestamp(entry) {
4626
+ const e = entry;
4627
+ for (const raw of [e.timestamp, e.endedAt, e.startedAt]) {
4628
+ if (typeof raw === "string") {
4629
+ const ms = Date.parse(raw);
4630
+ if (Number.isFinite(ms)) return ms;
4631
+ }
4632
+ }
4633
+ return null;
4634
+ }
4635
+ function summarizeTranscript(entries) {
4636
+ let lastActivityAt = null;
4637
+ for (let i = entries.length - 1; i >= 0; i--) {
4638
+ lastActivityAt = entryTimestamp(entries[i]);
4639
+ if (lastActivityAt !== null) break;
4640
+ }
4641
+ let currentTool = null;
4642
+ let terminalSubtype = null;
4643
+ for (let i = entries.length - 1; i >= 0 && (currentTool === null || terminalSubtype === null); i--) {
4644
+ const entry = entries[i];
4645
+ if (currentTool === null) {
4646
+ const calls = entryToolCalls(entry);
4647
+ if (calls.length > 0) currentTool = calls[calls.length - 1].toolName;
4648
+ }
4649
+ if (terminalSubtype === null && entry.type === "system" /* System */ && TERMINAL_SUBTYPES.has(String(entry.subtype))) {
4650
+ terminalSubtype = String(entry.subtype);
4651
+ }
4652
+ }
4653
+ return {
4654
+ totalEntries: entries.length,
4655
+ lastActivityAt,
4656
+ lastEntryType: entries.length > 0 ? entries[entries.length - 1].type : null,
4657
+ currentTool,
4658
+ hasTerminalEntry: terminalSubtype !== null,
4659
+ terminalSubtype
4660
+ };
4661
+ }
4662
+ function formatDuration(ms) {
4663
+ if (!Number.isFinite(ms)) return "unknown";
4664
+ const abs = Math.max(0, ms);
4665
+ if (abs < 1e3) return `${Math.round(abs)}ms`;
4666
+ if (abs < 6e4) return `${(abs / 1e3).toFixed(1)}s`;
4667
+ if (abs < 36e5) {
4668
+ const m2 = Math.floor(abs / 6e4);
4669
+ const s = Math.floor(abs % 6e4 / 1e3);
4670
+ return `${m2}m ${String(s).padStart(2, "0")}s`;
4671
+ }
4672
+ const h = Math.floor(abs / 36e5);
4673
+ const m = Math.floor(abs % 36e5 / 6e4);
4674
+ return `${h}h ${String(m).padStart(2, "0")}m`;
4675
+ }
4676
+ function formatLastActivity(lastActivityAt, now) {
4677
+ if (lastActivityAt === null) return "unknown";
4678
+ return `${new Date(lastActivityAt).toISOString()} (${formatDuration(now - lastActivityAt)} ago)`;
4679
+ }
4680
+ function selectWindow(entries, offset, limit) {
4681
+ const total = entries.length;
4682
+ const safeOffset = Math.max(0, Number.isFinite(offset) ? Math.trunc(offset) : 0);
4683
+ const safeLimit = Math.max(0, Number.isFinite(limit) ? Math.trunc(limit) : 0);
4684
+ const end = Math.max(0, total - safeOffset);
4685
+ const start = safeLimit === 0 ? end : Math.max(0, end - safeLimit);
4686
+ const window = entries.slice(start, end);
4687
+ return {
4688
+ entries: window,
4689
+ totalEntries: total,
4690
+ returnedEntries: window.length,
4691
+ offset: safeOffset,
4692
+ limit: safeLimit,
4693
+ startIndex: window.length > 0 ? start + 1 : 0,
4694
+ endIndex: window.length > 0 ? end : 0,
4695
+ windowTruncated: window.length < total
4696
+ };
4697
+ }
4698
+ function formatTranscriptEntry(entry, position, opts) {
4699
+ const body = entryText(entry).replace(/\r/g, "");
4700
+ const toolNames = entryToolCalls(entry).map((c) => c.toolName);
4701
+ const toolNote = toolNames.length > 0 ? ` (tools: ${toolNames.join(", ")})` : "";
4702
+ const head = ` #${position} [${entryLabel(entry)}]${toolNote}`;
4703
+ const cap = opts.maxChars;
4704
+ const cut2 = cap !== null && body.length > cap;
4705
+ const shown = cut2 ? body.slice(0, cap) + "\u2026" : body;
4706
+ const marker = cut2 ? ` (truncated: ${cap}/${body.length} chars)` : "";
4707
+ const lines = [];
4708
+ if (shown.length === 0) {
4709
+ lines.push(`${head}${marker} (empty)`);
4710
+ } else if (!shown.includes("\n")) {
4711
+ lines.push(`${head}${marker} ${shown}`);
4712
+ } else {
4713
+ lines.push(`${head}${marker}`);
4714
+ for (const line of shown.split("\n")) lines.push(` ${line}`);
4715
+ }
4716
+ if (opts.includeToolDetail) {
4717
+ for (const call of entryToolCalls(entry)) {
4718
+ const args = call.arguments ?? "";
4719
+ const argsShown = cap !== null && args.length > cap ? args.slice(0, cap) + "\u2026" : args;
4720
+ lines.push(` \u21B3 ${call.toolName} ${argsShown}`.trimEnd());
4721
+ }
4722
+ for (const result of entryToolResults(entry)) {
4723
+ const output = result.output ?? "";
4724
+ const outputShown = cap !== null && output.length > cap ? output.slice(0, cap) + "\u2026" : output;
4725
+ lines.push(` \u21B3 result${result.isError ? " [error]" : ""}: ${outputShown}`.trimEnd());
4726
+ }
4727
+ }
4728
+ return { lines, truncated: cut2, originalChars: body.length };
4729
+ }
4730
+ function shrinkWindow(win, dropFromStart) {
4731
+ const drop = Math.max(0, Math.min(dropFromStart, win.entries.length));
4732
+ if (drop === 0) return win;
4733
+ const entries = win.entries.slice(drop);
4734
+ return {
4735
+ ...win,
4736
+ entries,
4737
+ returnedEntries: entries.length,
4738
+ startIndex: win.startIndex + drop,
4739
+ windowTruncated: true
4740
+ };
4741
+ }
4742
+ function renderEntriesFitting(entries, startIndex, opts, budgetChars) {
4743
+ const formatted = entries.map((entry, i) => formatTranscriptEntry(entry, startIndex + i, opts));
4744
+ let used = 0;
4745
+ let first = formatted.length;
4746
+ for (let i = formatted.length - 1; i >= 0; i--) {
4747
+ const size = formatted[i].lines.reduce((n, line) => n + line.length + 1, 0);
4748
+ if (used + size > budgetChars && first < formatted.length) break;
4749
+ used += size;
4750
+ first = i;
4751
+ }
4752
+ const kept = formatted.slice(first);
4753
+ return {
4754
+ lines: kept.flatMap((f) => f.lines),
4755
+ dropped: first,
4756
+ shortened: kept.filter((f) => f.truncated).length
4757
+ };
4758
+ }
4759
+ function buildWindowMeta(win, opts) {
4760
+ return {
4761
+ totalEntries: win.totalEntries,
4762
+ returnedEntries: win.returnedEntries,
4763
+ offset: win.offset,
4764
+ limit: win.limit,
4765
+ maxChars: opts.maxChars,
4766
+ full: opts.full,
4767
+ truncated: win.windowTruncated || opts.shortenedEntries > 0,
4768
+ windowTruncated: win.windowTruncated,
4769
+ startIndex: win.startIndex,
4770
+ endIndex: win.endIndex,
4771
+ shortenedEntries: opts.shortenedEntries
4772
+ };
4773
+ }
4774
+ function buildTruncationNotice(meta) {
4775
+ const perEntry = meta.maxChars === null ? "none" : `${meta.maxChars}`;
4776
+ const shortened = meta.shortenedEntries > 0 ? ` (${meta.shortenedEntries} entries shortened)` : "";
4777
+ if (meta.windowTruncated) {
4778
+ const how = meta.full ? "output cap reached \u2014 page further back with offset" : "pass offset/limit or full=true";
4779
+ return `[truncated: showing ${meta.returnedEntries} of ${meta.totalEntries} entries (${meta.startIndex}-${meta.endIndex}), per-entry limit ${perEntry} chars${shortened}; ${how}]`;
4780
+ }
4781
+ if (meta.shortenedEntries > 0) {
4782
+ return `[truncated: showing all ${meta.totalEntries} entries, per-entry limit ${perEntry} chars${shortened}; pass full=true for complete content]`;
4783
+ }
4784
+ return null;
4785
+ }
4786
+ function buildTranscriptHeader(meta) {
4787
+ if (meta.totalEntries === 0) return "Conversation (0 entries):";
4788
+ if (!meta.windowTruncated) return `Conversation (${meta.totalEntries} entries):`;
4789
+ return `Conversation (${meta.totalEntries} entries total, showing ${meta.startIndex}-${meta.endIndex} by offset=${meta.offset} limit=${meta.limit}):`;
4790
+ }
4791
+
4792
+ // src/services/task/task-pool-state.ts
4793
+ import { join as join8 } from "path";
4794
+ var TaskPoolState = class extends BaseWayangState {
4795
+ jsonFile;
4796
+ logger;
4797
+ constructor(ctx) {
4798
+ const jsonFile = new JSONFileHelper(join8(ctx.sessionDir, "tasks.json"));
4799
+ super(
4800
+ {
4801
+ tasks: {
4802
+ pending: [],
4803
+ running: [],
4804
+ history: []
4805
+ }
4806
+ },
4807
+ [{ path: "tasks", helper: jsonFile }]
4808
+ );
4809
+ this.jsonFile = jsonFile;
4810
+ this.logger = ctx.logger;
4811
+ }
4812
+ async restore() {
4813
+ const data = this.jsonFile.read();
4814
+ if (data) {
4815
+ this.data.tasks = data;
4816
+ }
4817
+ this.logger.info("TaskPoolState restored");
4818
+ }
4819
+ };
4820
+
4821
+ // src/services/agents/base-agent.ts
4822
+ import { streamText, stepCountIs, hasToolCall } from "ai";
4823
+
4824
+ // src/services/agents/model-factory.ts
4825
+ import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
4826
+ function createModel(provider) {
4827
+ const compatible = createOpenAICompatible({
4828
+ name: "wayang",
4829
+ baseURL: provider.endpoint,
4830
+ apiKey: provider.apiKey
4831
+ });
4832
+ return compatible.chatModel(provider.modelName);
4833
+ }
4834
+
4835
+ // src/services/agents/base-agent.ts
4836
+ var DEFAULT_LLM_IDLE_TIMEOUT_MS = 3e5;
4837
+ var BaseAgent = class {
4838
+ id;
4839
+ abortController;
4840
+ model;
4841
+ hooks = {};
4842
+ constructor(provider) {
4843
+ this.id = generateId("agent");
4844
+ this.abortController = new AbortController();
4845
+ this.model = createModel(provider);
4846
+ }
4847
+ /** Set lifecycle hooks. */
4848
+ setHooks(hooks) {
4849
+ this.hooks = hooks;
4850
+ }
4851
+ /** Core streaming primitive — yields text chunks, returns final result.
4852
+ * Catches abort signal internally so callers see a clean completion. */
4853
+ async *streamLoop(opts) {
4854
+ const step = 0;
4855
+ const idleTimeoutMs = opts.llmIdleTimeoutMs ?? DEFAULT_LLM_IDLE_TIMEOUT_MS;
4856
+ const callAbort = new AbortController();
4857
+ const forwardAbort = () => callAbort.abort();
4858
+ if (this.abortController.signal.aborted) {
4859
+ callAbort.abort();
4860
+ } else {
4861
+ this.abortController.signal.addEventListener("abort", forwardAbort);
4862
+ }
4863
+ let watchdog = null;
4864
+ let timedOut = false;
4865
+ let toolsInFlight = 0;
4866
+ let finished = false;
4867
+ const clearWatchdog = () => {
4868
+ if (watchdog !== null) {
4869
+ clearTimeout(watchdog);
4870
+ watchdog = null;
4871
+ }
4872
+ };
4873
+ const armWatchdog = () => {
4874
+ if (idleTimeoutMs <= 0 || finished || toolsInFlight > 0) return;
4875
+ clearWatchdog();
4876
+ watchdog = setTimeout(() => {
4877
+ timedOut = true;
4878
+ callAbort.abort();
4879
+ }, idleTimeoutMs);
4880
+ };
4881
+ const tools = pauseWatchdogDuringTools(
4882
+ opts.tools,
4883
+ () => {
4884
+ toolsInFlight++;
4885
+ clearWatchdog();
4886
+ },
4887
+ () => {
4888
+ toolsInFlight--;
4889
+ if (toolsInFlight === 0) armWatchdog();
4890
+ }
4891
+ );
4892
+ let stream;
4893
+ const stopConditions = [];
4894
+ if (opts.maxSteps) stopConditions.push(stepCountIs(opts.maxSteps));
4895
+ if (opts.stopTools?.length) {
4896
+ for (const name of opts.stopTools) {
4897
+ stopConditions.push(hasToolCall(name));
4898
+ }
4899
+ }
4900
+ let startTime;
4901
+ let firstChunkAt;
4902
+ let stepStartedAt;
4903
+ const onStep = opts.onStep ? (event) => {
4904
+ armWatchdog();
4905
+ const meta = { startedAt: stepStartedAt, firstTokenAt: firstChunkAt };
4906
+ firstChunkAt = void 0;
4907
+ stepStartedAt = nowISO();
4908
+ opts.onStep(event, meta);
4909
+ } : void 0;
4910
+ try {
4911
+ this.hooks.beforeLLM?.({ messageCount: opts.messages.length, messages: opts.messages });
4912
+ startTime = Date.now();
4913
+ stepStartedAt = nowISO();
4914
+ armWatchdog();
4915
+ stream = streamText({
4916
+ model: this.model,
4917
+ system: opts.system,
4918
+ messages: opts.messages,
4919
+ tools,
4920
+ toolChoice: opts.toolChoice,
4921
+ stopWhen: stopConditions.length > 0 ? stopConditions : void 0,
4922
+ abortSignal: callAbort.signal,
4923
+ ...opts.prepareStep && { prepareStep: opts.prepareStep },
4924
+ onStepFinish: onStep,
4925
+ onError: ({ error }) => {
4926
+ opts.onStreamError?.(error);
4927
+ },
4928
+ // SignalEntry entries are trusted system-generated notifications emitted
4929
+ // as `system`-role messages (in-band, order-sensitive). AI SDK v6 warns
4930
+ // on any system message inside `messages` unless this is opted in.
4931
+ allowSystemInMessages: true
4932
+ });
4933
+ } catch (err) {
4934
+ finished = true;
4935
+ clearWatchdog();
4936
+ this.abortController.signal.removeEventListener("abort", forwardAbort);
4937
+ if (this.abortController.signal.aborted) {
4938
+ return { text: "", toolResults: [], usage: void 0 };
4939
+ }
4940
+ throw err;
4941
+ }
4942
+ try {
4943
+ for await (const chunk of stream.textStream) {
4944
+ armWatchdog();
4945
+ if (chunk && firstChunkAt === void 0) firstChunkAt = nowISO();
4946
+ yield chunk;
4947
+ }
4948
+ } catch (err) {
4949
+ if (this.abortController.signal.aborted) {
4950
+ return { text: "", toolResults: [], usage: void 0 };
4951
+ }
4952
+ if (timedOut) {
4953
+ throw new Error(
4954
+ `LLM call idle timeout: no stream progress (text chunk or step finish) for ${idleTimeoutMs}ms`
4955
+ );
4956
+ }
4957
+ if (this.abortController.signal.aborted) {
4958
+ return { text: "", toolResults: [], usage: void 0 };
4959
+ }
4960
+ throw err;
4961
+ } finally {
4962
+ finished = true;
4963
+ clearWatchdog();
4964
+ this.abortController.signal.removeEventListener("abort", forwardAbort);
4965
+ }
4966
+ const duration = Date.now() - startTime;
4967
+ const text = await stream.text;
4968
+ const reasoningText = await stream.reasoningText;
4969
+ const toolResults = await stream.toolResults;
4970
+ const rawUsage = await stream.usage;
4971
+ const finishReason = await stream.finishReason;
4972
+ const steps = await stream.steps;
4973
+ const usage = rawUsage ? { inputTokens: rawUsage.inputTokens ?? 0, outputTokens: rawUsage.outputTokens ?? 0 } : void 0;
4974
+ this.hooks.afterLLM?.({ step, usage, durationMs: duration });
4975
+ return {
4976
+ text,
4977
+ reasoningText,
4978
+ toolResults: toolResults?.map((tr) => ({
4979
+ toolCallId: tr.toolCallId,
4980
+ result: tr.result,
4981
+ isError: tr.isError
4982
+ })),
4983
+ usage,
4984
+ finishReason,
4985
+ steps: steps?.length
4986
+ };
4987
+ }
4988
+ /** Batch wrapper — iterates streamLoop and returns the final result. */
4989
+ async collectLoop(opts) {
4990
+ const gen = this.streamLoop(opts);
4991
+ let result;
4992
+ while (true) {
4993
+ const { done, value } = await gen.next();
4994
+ if (done) {
4995
+ result = value;
4996
+ break;
4997
+ }
4998
+ }
4999
+ return result;
5000
+ }
5001
+ abort() {
5002
+ this.abortController.abort();
5003
+ }
5004
+ };
5005
+ function pauseWatchdogDuringTools(tools, onStart, onEnd) {
5006
+ const wrapped = {};
5007
+ for (const [name, def] of Object.entries(tools ?? {})) {
5008
+ const original = def?.execute;
5009
+ if (typeof original !== "function") {
5010
+ wrapped[name] = def;
5011
+ continue;
5012
+ }
5013
+ wrapped[name] = {
5014
+ ...def,
5015
+ execute: async (...args) => {
5016
+ onStart();
5017
+ try {
5018
+ return await original(...args);
5019
+ } finally {
5020
+ onEnd();
5021
+ }
5022
+ }
5023
+ };
5024
+ }
5025
+ return wrapped;
5026
+ }
5027
+
4302
5028
  // src/services/agents/utils/build-assistant-entry.ts
4303
5029
  function buildAssistantEntry(event, sessionId, uuid, extras) {
4304
5030
  const ts = nowISO();
@@ -4590,15 +5316,16 @@ function isInsideWorkspace(resolved, workspace) {
4590
5316
  const rel = relative(workspace, resolved);
4591
5317
  return !rel.startsWith("..") && resolve(workspace, rel) === resolved;
4592
5318
  }
4593
- function truncate(result) {
4594
- if (result.length <= MAX_RESULT_CHARS) return result;
4595
- return result.slice(0, MAX_RESULT_CHARS) + "\n...(truncated)";
5319
+ function truncate(result, maxChars = MAX_RESULT_CHARS) {
5320
+ if (result.length <= maxChars) return result;
5321
+ return result.slice(0, maxChars) + "\n...(truncated)";
4596
5322
  }
4597
- function safeExecute(name, fn) {
4598
- return async (args, options) => {
5323
+ function safeExecute(name, fn, options) {
5324
+ const maxChars = options?.maxResultChars ?? MAX_RESULT_CHARS;
5325
+ return async (args, callOptions) => {
4599
5326
  try {
4600
- const result = await fn(args, options);
4601
- return truncate(typeof result === "string" ? result : JSON.stringify(result));
5327
+ const result = await fn(args, callOptions);
5328
+ return truncate(typeof result === "string" ? result : JSON.stringify(result), maxChars);
4602
5329
  } catch (err) {
4603
5330
  return `[ERROR] ${name}: ${err.message ?? String(err)}`;
4604
5331
  }
@@ -4752,12 +5479,12 @@ Check the "Available workers" section below for current options.
4752
5479
 
4753
5480
  ## How to write a good task description
4754
5481
 
4755
- The worker only sees the task description \u2014 it has NO access to your conversation.
4756
5482
  Write it as a **self-contained instruction**:
4757
5483
  1. What to do (specific, unambiguous)
4758
5484
  2. All context/data the worker needs
4759
5485
  3. Expected output (file path, format, etc.)
4760
5486
  4. Constraints (language, length, style)
5487
+ 5. Acceptance criteria \u2014 what counts as done
4761
5488
 
4762
5489
  Bad: "Write the essay the user asked for"
4763
5490
  Good: "Write a 200-word Chinese prose essay about autumn. Save to sanwen.txt in UTF-8."
@@ -4823,7 +5550,29 @@ Use get_task_detail when you need more information about a specific task:
4823
5550
  - You need to understand what a worker actually did before deciding next steps
4824
5551
  - Checking the error details of a failed task before retrying
4825
5552
 
4826
- The tool shows task metadata (status, result, error) and recent worker conversation entries (tool calls and outputs).
5553
+ The tool shows task metadata (status, result, error) and the worker conversation. The conversation comes from the worker's live in-memory state while it runs, and from its on-disk transcript after the worker ended (marked [from transcript]). The conversation is paginated and ALWAYS says what was cut:
5554
+ - Pagination semantics: \`offset\` skips N entries counted back from the newest; output is ordered oldest\u2192newest; type filtering is applied BEFORE pagination.
5555
+ - \`limit\` (default 10, counted back from the newest), \`offset\` (default 0, skip the newest N), \`maxChars\` (per-entry cap, default 200), \`full=true\` (complete text, all entries).
5556
+ - A meta line reports \`totalEntries / returnedEntries / offset / truncated\`; a \`[truncated: ...]\` line appears whenever anything was cut, and each cut entry carries \`(truncated: 200/4642 chars)\`.
5557
+ - NEVER conclude that a worker "stopped mid-sentence" from a truncated entry. If an entry is marked truncated, re-read it with full=true (or page back with offset) before judging.
5558
+
5559
+ ## Reading a worker's full transcript (read_worker_conversation)
5560
+
5561
+ read_worker_conversation(workerId, limit?, offset?, maxChars?, full?, type?) reads a worker's conversation with a layered source: while the engine still tracks the worker (running or idle), its live in-memory conversation is returned (meta \`source="memory"\`); after the worker finished/failed/was cancelled or disposed, its append-only on-disk \`conversation.jsonl\` is read instead (meta \`source="disk"\`). So it works for running AND already-finished workers, including after cancel_task.
5562
+ - Pagination semantics: \`offset\` skips N entries counted back from the newest; output is ordered oldest\u2192newest; type filtering is applied BEFORE pagination.
5563
+ - Same per-entry truncation markers as get_task_detail; entries print in chronological order.
5564
+ - \`type\` filters: user / assistant / system / signal / tool ("tool" = assistant entries that made tool calls).
5565
+ - A \`[note: last entry is incomplete \u2014 the transcript is still being written]\` line means the newest entry was mid-append when you read it: re-read in a moment instead of treating the tail as the final state. \`readWindowTruncated\` in the meta means older entries exist beyond the read window's byte cap \u2014 page back with offset.
5566
+ - Use it to see what a worker actually produced before you report, retry, or cancel.
5567
+
5568
+ ## Checking whether a worker is alive (get_worker_status)
5569
+
5570
+ get_worker_status(workerId?) \u2014 with an id returns that worker's status object, without one returns all current workers.
5571
+ Fields: workerId, type, taskId, status, runningForMs, lastActivityAt (epoch ms of the newest transcript entry) with lastActivityAgoMs (how long ago that was), currentTool, streamEnded, resultReceived, idleSince.
5572
+ Use it to tell "still working" from "stuck / already finished but not released":
5573
+ - Many minutes without a new transcript entry and no current tool \u2192 likely stuck: inspect with read_worker_conversation, then cancel_task.
5574
+ - resultReceived=true but streamEnded=false / status still running \u2192 the worker is in its wrap-up; wait or inspect instead of cancelling.
5575
+ - Unknown worker ids return a not-found message (never an exception).
4827
5576
 
4828
5577
  ## Sending messages to workers (chat_worker)
4829
5578
 
@@ -4831,6 +5580,7 @@ Use chat_worker(workerId, message) to send guidance to a running worker.
4831
5580
  The worker reads it at its next step (puppet) or turn boundary (third-party) \u2014 a tool call in progress is NEVER interrupted; an idle multi-stage worker reads it when its next stage starts.
4832
5581
  Delivery is blind: "queued" only means the message sits in the worker's inbox until that boundary. A stuck worker never reads it.
4833
5582
  A third-party worker that already finished its session rejects the message with an explicit [ERROR] \u2014 inspect its output instead of retrying.
5583
+ Before chatting, check get_worker_status; if the worker is stuck, cancel_task is the honest action (a queued message will not revive it).
4834
5584
  Use sparingly \u2014 only when you need to:
4835
5585
  - Correct a worker that is going in the wrong direction
4836
5586
  - Provide additional context the worker did not have
@@ -4867,7 +5617,8 @@ Types:
4867
5617
  Guidelines:
4868
5618
  - Prefer confirm and select over text. Less typing = better experience.
4869
5619
  - Keep messages short and specific.
4870
- - For select, provide 2-5 clear options. Not too many.`
5620
+ - For select, provide 2-5 clear preset options \u2014 an "Other" choice is appended automatically so the user can always type a custom answer, so never add your own catch-all option. Custom answers come back prefixed with "[Other] ".
5621
+ - Set allowOther=false only when the answer must be one of the preset options.`
4871
5622
  );
4872
5623
  var SIGNAL_HANDLING = section(
4873
5624
  "Handling signals",
@@ -4903,8 +5654,9 @@ Explain the error. Suggest a retry or alternative.
4903
5654
  ## [HEARTBEAT] \u2014 periodic check-in while workers are running
4904
5655
 
4905
5656
  You were woken up because workers are running but no events arrived for a while.
4906
- Review the worker status in the heartbeat message:
4907
- - A worker has been running unusually long \u2192 inform the user proactively
5657
+ Review the worker status in the heartbeat message (runningForMs, last activity, current tool):
5658
+ - A worker has been running unusually long AND its last activity is old (or it shows no tool) \u2192 inspect it: get_worker_status then read_worker_conversation. If it is genuinely stuck, cancel_task and report honestly.
5659
+ - A worker has been running unusually long but activity is recent \u2192 inform the user proactively
4908
5660
  - Everything looks normal \u2192 call skip_reply()
4909
5661
 
4910
5662
  ## [PERMISSION REQUEST ...] \u2014 worker needs approval
@@ -4927,6 +5679,39 @@ You resumed a previous session that had unfinished tasks. Workers were NOT autom
4927
5679
 
4928
5680
  Do NOT blindly re-create everything. The user may have moved on. Prefer to summarize and ask unless the intent is obvious.`
4929
5681
  );
5682
+ var WORKING_PRINCIPLES = section(
5683
+ "Working principles",
5684
+ `\u3010\u4EFB\u52A1\u62C6\u5206\u3011
5685
+ - \u4E00\u4E2A\u4EFB\u52A1\u53EA\u4EA4\u4ED8\u4E00\u4E2A\u53EF\u72EC\u7ACB\u9A8C\u6536\u7684\u7ED3\u679C\uFF1B\u4E0D\u8981\u628A\u8BBE\u8BA1\u3001\u5B9E\u73B0\u3001\u6D4B\u8BD5\u585E\u8FDB\u540C\u4E00\u6BB5\u3002
5686
+ - \u5148\u505A\u80FD\u88AB\u5355\u6D4B\u65AD\u8A00\u7684\u6838\u5FC3\u903B\u8F91\uFF0C\u628A\u8FB9\u754C\uFF08\u5BBD\u5B57\u7B26\u3001\u4EE3\u7406\u5BF9\u3001\u6298\u884C\u4F4D\u7F6E\u4E4B\u7C7B\uFF09\u56FA\u5316\u6210\u7528\u4F8B\uFF0C\u518D\u53E0\u52A0\u96C6\u6210\u4E0E\u4EA4\u4E92\u5C42\u3002
5687
+ - \u4EFB\u52A1\u63CF\u8FF0\u5FC5\u987B\u81EA\u5305\u542B\uFF1A\u80CC\u666F\u3001\u76EE\u6807\u3001\u8F93\u5165\u8F93\u51FA\u3001\u7EA6\u675F\u3001\u9A8C\u6536\u6807\u51C6\u3002\u6267\u884C\u65B9\u770B\u4E0D\u5230\u4F60\u7684\u5BF9\u8BDD\u3002
5688
+
5689
+ \u3010\u5206\u5DE5\u4E0E\u5BA1\u6838\u3011
5690
+ - \u8BBE\u8BA1\u5224\u65AD\u4E0E\u673A\u68B0\u5B9E\u73B0\u5206\u5F00\u6D3E\uFF1A\u8BBE\u8BA1\u65B9\u5148\u5B9E\u6D4B\u5173\u952E\u884C\u4E3A\uFF0C\u4EA7\u51FA\u300C\u6539\u54EA\u4E2A\u6587\u4EF6\u3001\u51FD\u6570\u7B7E\u540D\u3001\u6570\u636E\u7ED3\u6784\u3001\u8FB9\u754C\u60C5\u51B5\u3001\u6D4B\u8BD5\u6E05\u5355\u300D\u7684\u65B9\u6848\uFF1B\u6267\u884C\u65B9\u7167\u65B9\u6848\u7F16\u7801\uFF0C\u4E0D\u505A\u8BBE\u8BA1\u51B3\u7B56\u3002
5691
+ - \u65B9\u6848\u91CC\u8981\u663E\u5F0F\u5199\u51FA\u300C\u4E0D\u8981\u505A\u4EC0\u4E48\u300D\uFF0C\u5426\u5219\u6267\u884C\u65B9\u4F1A\u987A\u624B\u91CD\u6784\u65E0\u5173\u4EE3\u7801\u3002
5692
+ - \u5B9E\u73B0\u5B8C\u6210\u540E\u8BA9\u8BBE\u8BA1\u65B9\u56DE\u5BA1\uFF0C\u53EA\u67E5 diff\u3001\u4E0D\u53D8\u91CF\u4E0E\u56DE\u5F52\uFF0C\u4E0D\u63D0\u98CE\u683C\u504F\u597D\u3002
5693
+
5694
+ \u3010\u5E76\u884C\u3011
5695
+ - \u53EA\u6709\u72EC\u7ACB\u5DE5\u4F5C\u533A\u624D\u80FD\u5E76\u884C\uFF1B\u540C\u4E00\u5DE5\u4F5C\u533A\u5185\u7684\u5199\u5165\u4E0E\u63D0\u4EA4\u5FC5\u987B\u4E32\u884C\u3002
5696
+ - \u8DE8\u53D8\u4F53\u642C\u8FD0\u6539\u52A8\u65F6\u53EA\u642C\u6539\u52A8\u672C\u8EAB\uFF1A\u5265\u79BB\u6765\u6E90\u5206\u652F\u7684\u9644\u5E26\u4F9D\u8D56\uFF0C\u5408\u6D41\u4EE5\u76EE\u6807\u57FA\u5EA7\u7684\u8BED\u4E49\u4E3A\u51C6\u3002
5697
+
5698
+ \u3010\u5224\u65AD\u4E0E\u5904\u7F6E\u3011
5699
+ - \u4E0D\u8981\u51ED\u300C\u6CA1\u6709\u65B0\u8F93\u51FA\u300D\u5224\u65AD\u6B7B\u6D3B\u3002\u5148\u53D6\u8BC1\uFF1A\u6267\u884C\u75D5\u8FF9\u6700\u540E\u4E00\u6761\u662F\u4EC0\u4E48\u3001\u6709\u6CA1\u6709\u60AC\u7A7A\u7684\u8C03\u7528\u3001\u8D44\u6E90\u5360\u7528\u662F\u5426\u5728\u589E\u957F\uFF0C\u518D\u51B3\u5B9A\u7B49\u5F85\u3001\u62C6\u5C0F\u91CD\u6D3E\u8FD8\u662F\u53D6\u6D88\u3002
5700
+ - \u5DF2\u7ECF\u9677\u5165\u5FAA\u73AF\u7684\uFF08\u53CD\u590D\u88AB max_tokens \u622A\u65AD\u3001\u957F\u65F6\u95F4\u65E0\u4EA7\u51FA\uFF09\u7B49\u4E0D\u51FA\u6765\uFF0C\u53CA\u65F6\u62C6\u5C0F\u6216\u6362\u7B56\u7565\u3002
5701
+ - \u8FD0\u884C\u65F6\u7684\u4EA4\u4E92\u8BED\u4E49\u662F\u7248\u672C\u76F8\u5173\u7684\uFF08\u4F8B\u5982\u4E2D\u9014\u6D88\u606F\u662F\u6253\u65AD\u5F53\u524D\u56DE\u5408\u8FD8\u662F\u5728\u8FB9\u754C\u4EA4\u4ED8\uFF09\uFF0C\u4F9D\u8D56\u524D\u5148\u5728\u5F53\u524D\u7248\u672C\u5B9E\u6D4B\u3002
5702
+
5703
+ \u3010\u9A8C\u8BC1\u3011
5704
+ - \u6BCF\u6B21\u6539\u52A8\u8DD1\u540C\u4E00\u5957\u95E8\u69DB\uFF08\u7C7B\u578B\u68C0\u67E5\u3001lint\u3001\u6D4B\u8BD5\u3001\u6784\u5EFA\uFF09\uFF0C\u8303\u56F4\u968F\u5F71\u54CD\u9762\u4F38\u7F29\uFF1A\u5C0F\u6539\u8DD1\u76F8\u5173\u5B50\u96C6\uFF0C\u5927\u6539\u8DD1\u5168\u91CF\u3002
5705
+ - \u91CD\u6392\u5386\u53F2\u3001\u8DE8\u5206\u652F\u642C\u8FD0\u540E\uFF0C\u7528\u7B26\u53F7\u68C0\u7D22\u4E0E tree hash \u8FD9\u7C7B\u673A\u68B0\u8BC1\u636E\u9A8C\u8BC1\u7B49\u4EF7\uFF0C\u4E0D\u8981\u8089\u773C\u590D\u67E5\u3002
5706
+ - \u4EA4\u4E92\u7C7B\u6539\u52A8\u8981\u6709\u4E00\u6B21\u771F\u5B9E\u8FD0\u884C\u7684\u7AEF\u5230\u7AEF\u590D\u6F14\uFF1B\u5355\u6D4B\u5168\u7EFF\u4E0D\u7B49\u4E8E\u5B8C\u6210\u3002
5707
+
5708
+ \u3010\u9519\u8BEF\u6A21\u5F0F\u3011
5709
+ - \u6DF7\u6742\u6D3E\u5DE5 \u2014\u2014 \u628A\u8BBE\u8BA1\u3001\u5B9E\u73B0\u3001\u6D4B\u8BD5\u585E\u8FDB\u4E00\u4E2A\u4EFB\u52A1\uFF0C\u7ED3\u679C\u957F\u65F6\u95F4\u7A7A\u8F6C\u3001\u6700\u7EC8\u4EA4\u4ED8\u4E3A\u7A7A\u3002
5710
+ - \u540C\u533A\u4E92\u5377 \u2014\u2014 \u8BA9\u4E24\u4E2A\u6267\u884C\u8005\u5728\u540C\u4E00\u5DE5\u4F5C\u533A\u5E76\u53D1\u63D0\u4EA4\uFF0C\u4E92\u76F8\u628A\u5BF9\u65B9\u7684\u6539\u52A8\u5377\u8FDB\u81EA\u5DF1\u7684 commit\u3002
5711
+ - \u9759\u9ED8\u5224\u6B7B \u2014\u2014 \u51ED\u8F93\u51FA\u9759\u9ED8\u5224\u65AD\u6B7B\u6D3B\uFF1A\u628A\u5728\u8DD1\u957F\u4EFB\u52A1\u5F53\u6210\u5361\u6B7B\u6740\u6389\uFF0C\u6216\u628A\u6D3B\u9501\u5F53\u6210\u6162\u4EFB\u52A1\u6B7B\u7B49\u3002
5712
+ - \u7EFF\u706F\u5373\u5B8C\u6210 \u2014\u2014 \u5355\u6D4B\u6216\u6D41\u7A0B\u5168\u7EFF\u5C31\u5BA3\u5E03\u5B8C\u6210\uFF1B\u5076\u53D1\u5931\u8D25\u590D\u8DD1\u4E00\u6B21\u901A\u8FC7\u5C31\u5F53\u6CA1\u53D1\u751F\uFF0C\u4E0D\u505A\u8BB0\u5F55\u3002
5713
+ - \u96F6\u788E\u53D6\u8BC1 \u2014\u2014 \u628A\u4E00\u6B21\u53D6\u8BC1\u62C6\u6210\u51E0\u5341\u6761\u96F6\u788E\u53EA\u8BFB\u547D\u4EE4\u9010\u6761\u7B49\u6279\u51C6\uFF0C\u6392\u969C\u88AB\u6D41\u7A0B\u672C\u8EAB\u62D6\u6B7B\u3002`
5714
+ );
4930
5715
  var HARD_CONSTRAINTS = section(
4931
5716
  "Hard constraints",
4932
5717
  `- NEVER execute commands or write files yourself. Delegate via add_task\uFF08worker will execute\uFF09.
@@ -4952,6 +5737,7 @@ function buildControllerSystemPrompt(ctx, skills) {
4952
5737
  TOOL_USAGE,
4953
5738
  PLANNING_TOOLS,
4954
5739
  SIGNAL_HANDLING,
5740
+ WORKING_PRINCIPLES,
4955
5741
  HARD_CONSTRAINTS,
4956
5742
  buildWorkerList(ctx.config.workers),
4957
5743
  buildSkillCatalog(skills),
@@ -4959,6 +5745,54 @@ function buildControllerSystemPrompt(ctx, skills) {
4959
5745
  );
4960
5746
  }
4961
5747
 
5748
+ // src/services/agents/prompts/project-rules.ts
5749
+ import { existsSync as existsSync6, readFileSync as readFileSync6, statSync as statSync2 } from "fs";
5750
+ import { basename as basename2, dirname as dirname4, join as join9, resolve as resolve2 } from "path";
5751
+ var RULE_FILE_NAMES = ["AGENTS.md", "CLAUDE.md"];
5752
+ var MAX_LEVELS = 5;
5753
+ var PROJECT_RULES_MAX_LENGTH = 8e3;
5754
+ function findProjectRules(startDir) {
5755
+ let current = resolve2(startDir);
5756
+ for (let level = 0; level < MAX_LEVELS; level++) {
5757
+ const found = readRuleFile(current);
5758
+ if (found) return found;
5759
+ if (existsSync6(join9(current, ".git"))) break;
5760
+ const parent = dirname4(current);
5761
+ if (parent === current) break;
5762
+ current = parent;
5763
+ }
5764
+ return null;
5765
+ }
5766
+ function readRuleFile(dir) {
5767
+ for (const name of RULE_FILE_NAMES) {
5768
+ const path7 = join9(dir, name);
5769
+ try {
5770
+ if (!statSync2(path7).isFile()) continue;
5771
+ const content = readFileSync6(path7, "utf8");
5772
+ if (!content.trim()) continue;
5773
+ return { path: path7, content };
5774
+ } catch {
5775
+ }
5776
+ }
5777
+ return null;
5778
+ }
5779
+ function buildProjectRulesSection(rules) {
5780
+ if (!rules) return "";
5781
+ const name = basename2(rules.path);
5782
+ const truncated = rules.content.length > PROJECT_RULES_MAX_LENGTH;
5783
+ const content = truncated ? `${rules.content.slice(0, PROJECT_RULES_MAX_LENGTH).trimEnd()}
5784
+
5785
+ ...truncated (rules file exceeded ${PROJECT_RULES_MAX_LENGTH} characters)` : rules.content.trimEnd();
5786
+ return section(
5787
+ `Project rules (${name})`,
5788
+ `Rules from this project's ${name}. They apply to all work on the task \u2014 follow them unless the task description explicitly overrides them.
5789
+
5790
+ Source: ${rules.path}
5791
+
5792
+ ${content}`
5793
+ );
5794
+ }
5795
+
4962
5796
  // src/services/agents/prompts/worker-prompt.ts
4963
5797
  var IDENTITY2 = section(
4964
5798
  "Identity",
@@ -5110,6 +5944,10 @@ function buildWorkerSystemPrompt(ctx, skills) {
5110
5944
  TOOL_USAGE2,
5111
5945
  EXECUTION_STRATEGY,
5112
5946
  PROJECT_CONVENTIONS,
5947
+ // Project rules discovered near the task workspace (AGENTS.md, falling
5948
+ // back to CLAUDE.md). Empty when no rules file exists — assemble() drops
5949
+ // the section, so no empty paragraph leaks into the prompt.
5950
+ buildProjectRulesSection(findProjectRules(ctx.workspaceDir ?? process.cwd())),
5113
5951
  QUALITY_RULES,
5114
5952
  HARD_CONSTRAINTS2,
5115
5953
  CONTROLLER_COMMUNICATION,
@@ -5288,6 +6126,15 @@ var WorkerAgent = class extends BaseAgent {
5288
6126
  getState() {
5289
6127
  return this.state;
5290
6128
  }
6129
+ /**
6130
+ * Live run flags (issue #16). A puppet worker has no SDK result/stream
6131
+ * concept: `resultReceived` means the model already called done/fail, and
6132
+ * `streamEnded` is not tracked (null), so status readers fall back to the
6133
+ * transcript / task state instead of guessing.
6134
+ */
6135
+ getLiveStatus() {
6136
+ return { resultReceived: this._terminalResult !== null, streamEnded: null };
6137
+ }
5291
6138
  /** Get conversation entries. */
5292
6139
  getConversation() {
5293
6140
  return this.state.get("conversation") ?? [];
@@ -5350,6 +6197,56 @@ var WorkerAgent = class extends BaseAgent {
5350
6197
  import { query, createSdkMcpServer } from "@anthropic-ai/claude-agent-sdk";
5351
6198
  import { z as z2 } from "zod";
5352
6199
 
6200
+ // src/services/agents/utils/claude-tool-records.ts
6201
+ var CLAUDE_TOOL_RESULT_STORE_CHARS = 2e3;
6202
+ var CLAUDE_TOOL_ARGS_STORE_CHARS = 8e3;
6203
+ function truncateForStorage(text, maxChars) {
6204
+ if (text.length <= maxChars) return text;
6205
+ return `${text.slice(0, maxChars)}... [truncated, ${text.length} chars]`;
6206
+ }
6207
+ function toolUseBlockToRecord(block, startedAt) {
6208
+ return {
6209
+ toolCallId: block.id,
6210
+ toolName: block.name,
6211
+ arguments: truncateForStorage(toolPayloadText(block.input) || "{}", CLAUDE_TOOL_ARGS_STORE_CHARS),
6212
+ status: "running",
6213
+ startedAt
6214
+ };
6215
+ }
6216
+ function flattenToolResultContent(content) {
6217
+ if (content === void 0 || content === null) return "";
6218
+ if (typeof content === "string") return content;
6219
+ if (!Array.isArray(content)) return toolPayloadText(content);
6220
+ const parts = [];
6221
+ for (const block of content) {
6222
+ if (block && typeof block === "object" && "type" in block) {
6223
+ const b = block;
6224
+ if (b.type === "text" && typeof b.text === "string") {
6225
+ parts.push(b.text);
6226
+ } else {
6227
+ parts.push(`[${String(b.type)} block]`);
6228
+ }
6229
+ } else if (typeof block === "string") {
6230
+ parts.push(block);
6231
+ }
6232
+ }
6233
+ return parts.join("\n");
6234
+ }
6235
+ function toolResultBlockToRecord(block, toolName, endedAt) {
6236
+ const isError = block.is_error === true;
6237
+ return {
6238
+ toolCallId: block.tool_use_id,
6239
+ toolName,
6240
+ output: {
6241
+ type: "text",
6242
+ value: truncateForStorage(flattenToolResultContent(block.content), CLAUDE_TOOL_RESULT_STORE_CHARS)
6243
+ },
6244
+ ...isError && { isError: true, errorType: "ToolError" },
6245
+ status: isError ? "failed" : "succeeded",
6246
+ endedAt
6247
+ };
6248
+ }
6249
+
5353
6250
  // src/infra/blocking-requests.ts
5354
6251
  var BlockingRequestMap = class {
5355
6252
  pending = /* @__PURE__ */ new Map();
@@ -5361,14 +6258,14 @@ var BlockingRequestMap = class {
5361
6258
  if (this.pending.has(id)) {
5362
6259
  throw new Error(`BlockingRequestMap: duplicate wait id "${id}"`);
5363
6260
  }
5364
- return new Promise((resolve11) => {
6261
+ return new Promise((resolve12) => {
5365
6262
  const settle = (value) => {
5366
6263
  this.pending.delete(id);
5367
- resolve11(value);
6264
+ resolve12(value);
5368
6265
  };
5369
6266
  const timer = setTimeout(() => {
5370
6267
  this.pending.delete(id);
5371
- resolve11(makeTimeoutValue());
6268
+ resolve12(makeTimeoutValue());
5372
6269
  }, timeoutMs);
5373
6270
  if (typeof timer === "object" && timer && "unref" in timer) {
5374
6271
  timer.unref();
@@ -5405,6 +6302,8 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5405
6302
  logger;
5406
6303
  abortController = new AbortController();
5407
6304
  _terminalResult = null;
6305
+ /** True once the SDK message stream of the current run has closed (issue #16). */
6306
+ _streamEnded = false;
5408
6307
  // --- Controller messaging via streamInput ---
5409
6308
  _messageQueue = [];
5410
6309
  _messageResolve = null;
@@ -5422,6 +6321,16 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5422
6321
  * explicitly instead of pretending to queue.
5423
6322
  */
5424
6323
  _inputClosed = false;
6324
+ // --- Tool-call recording (see utils/claude-tool-records.ts) ---
6325
+ /**
6326
+ * Assistant steps buffered while their tool results are still in flight.
6327
+ * The SDK stream splits one LLM step across two messages (assistant
6328
+ * tool_use → user tool_result), but the schema stores a step atomically on
6329
+ * one AssistantEntry and storage is append-only — so the entry waits in
6330
+ * memory until its results arrive (or the stream ends). FIFO order is kept:
6331
+ * only the head is flushed, so the file order always matches arrival order.
6332
+ */
6333
+ _pendingToolSteps = [];
5425
6334
  // --- ask_controller via in-process SDK MCP server ---
5426
6335
  _askController = null;
5427
6336
  // --- Permission control via canUseTool ---
@@ -5487,8 +6396,10 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5487
6396
  }
5488
6397
  async run(task, _tools, onProgress) {
5489
6398
  this._terminalResult = null;
6399
+ this._streamEnded = false;
5490
6400
  this._inputClosed = false;
5491
6401
  this._runInFlight = true;
6402
+ this._pendingToolSteps = [];
5492
6403
  this.state.set("runtimeState.task", { id: task.id, description: task.description });
5493
6404
  const turnEnv = this.buildTurnEnv(task);
5494
6405
  this._currentTurnEnv = turnEnv;
@@ -5552,7 +6463,10 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5552
6463
  this.processStreamMessage(message, onProgress);
5553
6464
  }
5554
6465
  } finally {
6466
+ this._streamEnded = true;
5555
6467
  this._inputClosed = true;
6468
+ this.clearPendingProgress();
6469
+ this.flushAllToolSteps();
5556
6470
  }
5557
6471
  } catch (err) {
5558
6472
  this.logger.error({ err, taskId: task.id }, "ClaudeCodeWorker query error");
@@ -5594,6 +6508,7 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5594
6508
  }
5595
6509
  abort() {
5596
6510
  this.abortController.abort();
6511
+ this.clearPendingProgress();
5597
6512
  this.permissionHandler.cleanup();
5598
6513
  this.flushMessageQueue();
5599
6514
  }
@@ -5601,6 +6516,14 @@ var ClaudeCodeWorker = class _ClaudeCodeWorker {
5601
6516
  getState() {
5602
6517
  return this.state;
5603
6518
  }
6519
+ /**
6520
+ * Live run flags (issue #16): `resultReceived` flips as soon as the SDK result
6521
+ * message lands, `streamEnded` when the message loop exits. The gap between
6522
+ * them is exactly the #15 window where the controller saw nothing.
6523
+ */
6524
+ getLiveStatus() {
6525
+ return { resultReceived: this._terminalResult !== null, streamEnded: this._streamEnded };
6526
+ }
5604
6527
  /** Get conversation entries. */
5605
6528
  getConversation() {
5606
6529
  return this.state.get("conversation") ?? [];
@@ -5716,8 +6639,8 @@ ${leftoverText}` : `${buildThirdPartyPrompt(this.skills)}${initialPrompt}`;
5716
6639
  return { value: void 0, done: true };
5717
6640
  }
5718
6641
  } else if (this._messageQueue.length === 0) {
5719
- await new Promise((resolve11) => {
5720
- this._messageResolve = resolve11;
6642
+ await new Promise((resolve12) => {
6643
+ this._messageResolve = resolve12;
5721
6644
  });
5722
6645
  continue;
5723
6646
  }
@@ -5786,25 +6709,49 @@ controller: ${answer}`,
5786
6709
  processStreamMessage(message, onProgress) {
5787
6710
  if (message.type === "assistant") {
5788
6711
  this.handleAssistantMessage(message, onProgress);
6712
+ } else if (message.type === "user") {
6713
+ this.handleUserMessage(message);
5789
6714
  } else if (message.type === "result") {
5790
6715
  this.handleResultMessage(message);
5791
6716
  }
5792
6717
  }
6718
+ /** Append one entry to the conversation under the current turn envelope. */
6719
+ appendConversationEntry(entry) {
6720
+ this.state.append("conversation", applyTurnEnvelope(entry, this._currentTurnEnv ?? void 0));
6721
+ }
6722
+ /**
6723
+ * Drop a debounced progress delivery: cancel the timer and the buffered
6724
+ * text. Called when a result supersedes the progress, when the run is
6725
+ * aborted, and when the stream loop exits (all paths — the run is over, so
6726
+ * nothing may be delivered afterwards).
6727
+ */
6728
+ clearPendingProgress() {
6729
+ if (this._progressTimer) {
6730
+ clearTimeout(this._progressTimer);
6731
+ this._progressTimer = null;
6732
+ }
6733
+ this._pendingProgress = null;
6734
+ }
5793
6735
  handleAssistantMessage(msg, onProgress) {
5794
6736
  const textParts = [];
6737
+ const toolCalls = [];
6738
+ const ts = nowISO();
5795
6739
  for (const block of msg.message.content) {
5796
6740
  if (block.type === "text") {
5797
6741
  textParts.push(block.text);
6742
+ } else if (block.type === "tool_use") {
6743
+ toolCalls.push(toolUseBlockToRecord(block, ts));
5798
6744
  }
5799
6745
  }
5800
6746
  const text = textParts.join("\n");
5801
- if (!text) return;
5802
- this.state.append("conversation", applyTurnEnvelope({
6747
+ if (!text && toolCalls.length === 0) return;
6748
+ const entry = {
5803
6749
  type: "assistant" /* Assistant */,
5804
6750
  uuid: generateId("a"),
5805
6751
  parentUuid: null,
5806
6752
  sessionId: this.ctx.sessionId,
5807
- timestamp: nowISO(),
6753
+ timestamp: ts,
6754
+ startedAt: ts,
5808
6755
  message: {
5809
6756
  role: "assistant",
5810
6757
  content: text,
@@ -5815,26 +6762,112 @@ controller: ${answer}`,
5815
6762
  cachedInputTokens: msg.message.usage.cache_read_input_tokens
5816
6763
  }
5817
6764
  }
5818
- }
5819
- }, this._currentTurnEnv ?? void 0));
5820
- if (onProgress) {
6765
+ },
6766
+ ...toolCalls.length > 0 && { toolCalls }
6767
+ };
6768
+ if (this._pendingToolSteps.length > 0 || toolCalls.length > 0) {
6769
+ this._pendingToolSteps.push(entry);
6770
+ } else {
6771
+ entry.endedAt = ts;
6772
+ this.appendConversationEntry(entry);
6773
+ }
6774
+ if (text && onProgress) {
5821
6775
  this._pendingProgress = text.slice(0, 200);
5822
6776
  if (this._progressTimer) clearTimeout(this._progressTimer);
5823
6777
  this._progressTimer = setTimeout(() => {
5824
- if (this._pendingProgress) {
5825
- onProgress(this._pendingProgress);
6778
+ this._progressTimer = null;
6779
+ if (this._pendingProgress === null || this.abortController.signal.aborted || this._streamEnded || this._terminalResult !== null) {
5826
6780
  this._pendingProgress = null;
6781
+ return;
5827
6782
  }
5828
- this._progressTimer = null;
6783
+ const pending = this._pendingProgress;
6784
+ this._pendingProgress = null;
6785
+ onProgress(pending);
5829
6786
  }, _ClaudeCodeWorker.PROGRESS_DEBOUNCE_MS);
5830
6787
  }
5831
6788
  }
5832
- handleResultMessage(msg) {
5833
- if (this._progressTimer) {
5834
- clearTimeout(this._progressTimer);
5835
- this._progressTimer = null;
5836
- this._pendingProgress = null;
6789
+ /**
6790
+ * SDK user messages carry the tool_result blocks for the preceding
6791
+ * assistant step's tool_use calls. Each result is merged into the buffered
6792
+ * entry that issued the matching id; completed steps are then written,
6793
+ * oldest first.
6794
+ *
6795
+ * User messages WITHOUT tool_result blocks (echoes of the task prompt or of
6796
+ * controller messages this worker itself injected) are ignored — the task
6797
+ * entry is written by run() and controller messages are audited elsewhere,
6798
+ * so writing them here would duplicate entries.
6799
+ */
6800
+ handleUserMessage(msg) {
6801
+ const content = msg.message.content;
6802
+ if (!Array.isArray(content)) return;
6803
+ const ts = nowISO();
6804
+ for (const block of content) {
6805
+ if (!block || block.type !== "tool_result") continue;
6806
+ const resultBlock = block;
6807
+ const owner = this._pendingToolSteps.find(
6808
+ (step) => step.toolCalls?.some((tc) => tc.toolCallId === resultBlock.tool_use_id)
6809
+ );
6810
+ if (!owner) {
6811
+ this.logger.debug(
6812
+ { toolUseId: resultBlock.tool_use_id },
6813
+ "tool_result with no matching pending tool_use \u2014 dropped"
6814
+ );
6815
+ continue;
6816
+ }
6817
+ const call = owner.toolCalls.find((tc) => tc.toolCallId === resultBlock.tool_use_id);
6818
+ const result = toolResultBlockToRecord(resultBlock, call.toolName, ts);
6819
+ (owner.toolResults ??= []).push(result);
6820
+ call.status = result.status;
6821
+ call.endedAt = ts;
6822
+ }
6823
+ this.flushCompletedToolSteps();
6824
+ }
6825
+ /**
6826
+ * Write buffered steps whose tool calls have ALL received results, from the
6827
+ * head of the queue (order-preserving). Steps still missing results stay
6828
+ * buffered — a later user message or the end-of-stream flush resolves them.
6829
+ */
6830
+ flushCompletedToolSteps() {
6831
+ while (this._pendingToolSteps.length > 0) {
6832
+ const head = this._pendingToolSteps[0];
6833
+ const callIds = new Set(head.toolCalls?.map((tc) => tc.toolCallId) ?? []);
6834
+ const resultIds = new Set(head.toolResults?.map((tr) => tr.toolCallId) ?? []);
6835
+ let complete = true;
6836
+ for (const id of callIds) {
6837
+ if (!resultIds.has(id)) {
6838
+ complete = false;
6839
+ break;
6840
+ }
6841
+ }
6842
+ if (!complete) break;
6843
+ this._pendingToolSteps.shift();
6844
+ head.endedAt = nowISO();
6845
+ this.appendConversationEntry(head);
6846
+ }
6847
+ }
6848
+ /**
6849
+ * End-of-stream fallback: write every still-buffered step so nothing is
6850
+ * lost. Tool calls that never received a result (abort, CLI death mid-tool)
6851
+ * are marked 'cancelled' instead of claiming to run forever.
6852
+ */
6853
+ flushAllToolSteps() {
6854
+ if (this._pendingToolSteps.length === 0) return;
6855
+ const ts = nowISO();
6856
+ for (const entry of this._pendingToolSteps) {
6857
+ const resultIds = new Set(entry.toolResults?.map((tr) => tr.toolCallId) ?? []);
6858
+ for (const tc of entry.toolCalls ?? []) {
6859
+ if (!resultIds.has(tc.toolCallId)) {
6860
+ tc.status = "cancelled";
6861
+ if (tc.endedAt === void 0) tc.endedAt = ts;
6862
+ }
6863
+ }
6864
+ entry.endedAt = ts;
6865
+ this.appendConversationEntry(entry);
5837
6866
  }
6867
+ this._pendingToolSteps = [];
6868
+ }
6869
+ handleResultMessage(msg) {
6870
+ this.clearPendingProgress();
5838
6871
  if (msg.subtype === "success") {
5839
6872
  this._terminalResult = {
5840
6873
  status: "completed",
@@ -5847,9 +6880,9 @@ controller: ${answer}`,
5847
6880
  };
5848
6881
  }
5849
6882
  if (this._messageResolve) {
5850
- const resolve11 = this._messageResolve;
6883
+ const resolve12 = this._messageResolve;
5851
6884
  this._messageResolve = null;
5852
- resolve11();
6885
+ resolve12();
5853
6886
  }
5854
6887
  const prev = this.state.get("runtimeState");
5855
6888
  this.state.set("runtimeState", {
@@ -5989,13 +7022,21 @@ function disposeWorkerTool(deps) {
5989
7022
 
5990
7023
  // src/services/tools/get-task-detail.ts
5991
7024
  import { z as z7 } from "zod";
7025
+ var OUTPUT_OVERHEAD_CHARS = 1500;
7026
+ var DEFAULT_LIMIT = 10;
7027
+ var DEFAULT_MAX_CHARS = 200;
5992
7028
  function getTaskDetailTool(deps) {
5993
7029
  return defineTool({
5994
- description: "Get detailed information about a specific task",
7030
+ description: "Get detailed information about a specific task, including its worker conversation. The conversation comes from the worker's live in-memory state while it runs, and from its on-disk transcript after the worker ended (marked [from transcript]). The conversation is paginated and truncation is always marked explicitly: offset skips N entries counted back from the newest; output is ordered oldest\u2192newest; type filtering is applied BEFORE pagination. limit (default 10 entries, counted back from the newest), maxChars (per-entry character cap, default 200) and full=true (complete text, all entries). A machine-readable meta line reports totalEntries / returnedEntries / offset / truncated.",
5995
7031
  parameters: z7.object({
5996
- taskId: z7.string().describe("Task ID")
7032
+ taskId: z7.string().describe("Task ID"),
7033
+ limit: z7.number().int().min(1).optional().describe(`Max entries to return (default ${DEFAULT_LIMIT})`),
7034
+ offset: z7.number().int().min(0).optional().describe("Skip the newest N entries (default 0)"),
7035
+ maxChars: z7.number().int().min(1).optional().describe(`Per-entry character cap (default ${DEFAULT_MAX_CHARS})`),
7036
+ full: z7.boolean().optional().describe("Return complete entry text and all entries (ignores limit/maxChars)")
5997
7037
  }),
5998
- execute: safeExecute("get_task_detail", async ({ taskId }) => {
7038
+ // Full reports can be far longer than the default tool-output cap.
7039
+ execute: safeExecute("get_task_detail", async ({ taskId, limit, offset, maxChars, full }) => {
5999
7040
  const task = deps.getTask(taskId);
6000
7041
  if (!task) return `Task ${taskId} not found`;
6001
7042
  const lines = [
@@ -6010,30 +7051,313 @@ function getTaskDetailTool(deps) {
6010
7051
  if (task.result) lines.push(`Result: ${task.result}`);
6011
7052
  if (task.error) lines.push(`Error: ${task.error}`);
6012
7053
  if (task.workerSessionId) lines.push(`Worker: ${task.workerSessionId}`);
7054
+ const isFull = full === true;
7055
+ const requestedLimit = limit ?? DEFAULT_LIMIT;
7056
+ let entries = [];
7057
+ let source = "none";
7058
+ let incompleteTail = false;
7059
+ let readWindowTruncated = false;
6013
7060
  if (deps.getWorkerConversation) {
6014
- const entries = deps.getWorkerConversation(taskId);
6015
- if (entries.length > 0) {
6016
- lines.push(`
6017
- Conversation (${entries.length} entries):`);
6018
- for (const entry of entries.slice(-10)) {
6019
- lines.push(` [${entry.type}] ${entry.message?.content ?? entry.content ?? ""}`.slice(0, 200));
7061
+ entries = deps.getWorkerConversation(taskId);
7062
+ if (entries.length > 0) source = "memory";
7063
+ }
7064
+ if (entries.length === 0 && deps.readTranscriptWindow) {
7065
+ const diskIds = [task.workerTranscriptId, task.workerSessionId].filter((id) => typeof id === "string");
7066
+ for (const id of [...new Set(diskIds)]) {
7067
+ const win2 = deps.readTranscriptWindow(id, {
7068
+ need: isFull ? void 0 : (offset ?? 0) + requestedLimit
7069
+ });
7070
+ if (win2 && win2.entries.length > 0) {
7071
+ entries = win2.entries;
7072
+ source = "transcript";
7073
+ incompleteTail = win2.incompleteTail;
7074
+ readWindowTruncated = win2.windowTruncated;
7075
+ break;
6020
7076
  }
6021
7077
  }
6022
7078
  }
7079
+ if (entries.length === 0) return lines.join("\n");
7080
+ const perEntryCap = isFull ? null : maxChars ?? DEFAULT_MAX_CHARS;
7081
+ const requested = selectWindow(entries, offset ?? 0, isFull ? entries.length : requestedLimit);
7082
+ const rendered = renderEntriesFitting(
7083
+ requested.entries,
7084
+ requested.startIndex,
7085
+ { maxChars: perEntryCap, includeToolDetail: isFull },
7086
+ MAX_TRANSCRIPT_RESULT_CHARS - OUTPUT_OVERHEAD_CHARS
7087
+ );
7088
+ const win = shrinkWindow(requested, rendered.dropped);
7089
+ const body = rendered.lines;
7090
+ const base = buildWindowMeta(win, { maxChars: perEntryCap, full: isFull, shortenedEntries: rendered.shortened });
7091
+ const meta = {
7092
+ ...base,
7093
+ source,
7094
+ incompleteTail,
7095
+ readWindowTruncated,
7096
+ windowTruncated: base.windowTruncated || readWindowTruncated,
7097
+ truncated: base.truncated || readWindowTruncated
7098
+ };
7099
+ lines.push("");
7100
+ let header = buildTranscriptHeader(meta);
7101
+ if (readWindowTruncated) {
7102
+ header = `Conversation (${meta.totalEntries} entries in read window, showing ${meta.startIndex}-${meta.endIndex}; older entries exist beyond the read-window cap)`;
7103
+ }
7104
+ lines.push(`${header}${source === "transcript" ? " [from transcript]" : ""}`);
7105
+ lines.push(...body);
7106
+ if (incompleteTail) {
7107
+ lines.push("[note: last entry is incomplete \u2014 the transcript is still being written]");
7108
+ }
7109
+ if (readWindowTruncated) {
7110
+ lines.push("[note: transcript read window hit its byte cap \u2014 older entries were not read; page back with offset/limit]");
7111
+ }
7112
+ const notice = buildTruncationNotice(meta);
7113
+ if (notice) lines.push(notice);
7114
+ lines.push(`[conversation-meta] ${JSON.stringify(meta)}`);
6023
7115
  return lines.join("\n");
6024
- })
7116
+ }, { maxResultChars: MAX_TRANSCRIPT_RESULT_CHARS })
6025
7117
  });
6026
7118
  }
6027
7119
 
6028
- // src/services/tools/update-task.ts
7120
+ // src/services/tools/read-worker-conversation.ts
6029
7121
  import { z as z8 } from "zod";
7122
+ var OUTPUT_OVERHEAD_CHARS2 = 1500;
7123
+ var DEFAULT_LIMIT2 = 20;
7124
+ var DEFAULT_MAX_CHARS2 = 200;
7125
+ var FILTER_TYPES = ["user", "assistant", "system", "signal", "tool"];
7126
+ function typeMatches(entry, filter) {
7127
+ if (filter === "tool") {
7128
+ const calls = entry.toolCalls;
7129
+ return Array.isArray(calls) && calls.length > 0;
7130
+ }
7131
+ return entry.type === filter;
7132
+ }
7133
+ function readWorkerConversationTool(deps) {
7134
+ return defineTool({
7135
+ description: `Read a worker's conversation transcript. Layered source: while the engine still tracks the worker (running or idle), its live in-memory conversation is returned (meta source="memory"); once the worker finished/failed/was cancelled or disposed, the append-only on-disk file <sessionDir>/workers/<transcript id>/conversation.jsonl is read instead (meta source="disk") \u2014 so it works for running AND completed/failed/cancelled workers. Pagination: offset skips N entries counted back from the newest; output is ordered oldest\u2192newest; type filtering is applied BEFORE pagination. limit (default 20, max entries), maxChars (per-entry cap, default 200), full=true (complete text, all entries \u2014 still bounded by a 2 MB read window), type (filter: user/assistant/system/signal/tool). Truncation is always marked explicitly and reported as totalEntries / returnedEntries / offset / truncated, plus incompleteTail (last entry still being written) and readWindowTruncated (older entries exist beyond the read-window cap \u2014 page back with offset).`,
7136
+ parameters: z8.object({
7137
+ workerId: z8.string().describe("Worker ID (e.g. w-1793587065421-6h37)"),
7138
+ limit: z8.number().int().min(1).optional().describe(`Max entries to return (default ${DEFAULT_LIMIT2})`),
7139
+ offset: z8.number().int().min(0).optional().describe("Skip the newest N entries (default 0)"),
7140
+ maxChars: z8.number().int().min(1).optional().describe(`Per-entry character cap (default ${DEFAULT_MAX_CHARS2})`),
7141
+ full: z8.boolean().optional().describe("Return complete entry text and all entries (ignores limit/maxChars)"),
7142
+ type: z8.enum(FILTER_TYPES).optional().describe('Only entries of this type; "tool" = assistant entries that made tool calls')
7143
+ }),
7144
+ execute: safeExecute("read_worker_conversation", async ({ workerId, limit, offset, maxChars, full, type }) => {
7145
+ const now = deps.now?.() ?? Date.now();
7146
+ if (!isValidWorkerId(workerId)) {
7147
+ return `Invalid worker id "${workerId}" \u2014 worker ids are alphanumeric with dashes/underscores only.`;
7148
+ }
7149
+ const task = deps.listTasks?.().find((t) => t.workerSessionId === workerId);
7150
+ const isActive = deps.listWorkers?.().some((w) => w.workerId === workerId) ?? false;
7151
+ const liveTranscriptId = deps.getWorkerTranscriptId?.(workerId);
7152
+ const isFull = full === true;
7153
+ const requestedLimit = limit ?? DEFAULT_LIMIT2;
7154
+ const need = isFull ? void 0 : (offset ?? 0) + requestedLimit;
7155
+ const matches = type === void 0 ? void 0 : (e) => typeMatches(e, type);
7156
+ const memory = deps.getWorkerConversationByWorkerId?.(workerId);
7157
+ if (memory !== void 0 && memory.length > 0) {
7158
+ return render2({ source: "memory", entries: memory });
7159
+ }
7160
+ const candidates = [];
7161
+ for (const id of [task?.workerTranscriptId, liveTranscriptId, workerId]) {
7162
+ if (typeof id === "string" && !candidates.includes(id)) candidates.push(id);
7163
+ }
7164
+ let disk = null;
7165
+ for (const id of candidates) {
7166
+ const win = deps.readTranscriptWindow(id, { need, matches });
7167
+ if (win !== null) {
7168
+ disk = { id, win };
7169
+ break;
7170
+ }
7171
+ }
7172
+ if (disk !== null) {
7173
+ return render2({
7174
+ source: "disk",
7175
+ entries: disk.win.entries,
7176
+ transcriptId: disk.id,
7177
+ incompleteTail: disk.win.incompleteTail,
7178
+ readWindowTruncated: disk.win.windowTruncated
7179
+ });
7180
+ }
7181
+ const known = isActive || task !== void 0 || memory !== void 0;
7182
+ if (!known) {
7183
+ return `Unknown worker ${workerId}: no active worker and no task references it.`;
7184
+ }
7185
+ if (isActive) {
7186
+ return `Worker ${workerId} is running but has not written any transcript entries yet.`;
7187
+ }
7188
+ const resolved = task?.workerTranscriptId ?? liveTranscriptId;
7189
+ if (resolved !== void 0) {
7190
+ return `Worker ${workerId} exists but its transcript directory is missing on disk (expected ${workerTranscriptPath(deps.sessionDir, resolved)}).`;
7191
+ }
7192
+ return `Worker ${workerId} exists but its transcript cannot be located on disk (legacy session id layout?).`;
7193
+ function render2(src) {
7194
+ const { source, entries } = src;
7195
+ const incompleteTail = src.incompleteTail ?? false;
7196
+ const readWindowTruncated = src.readWindowTruncated ?? false;
7197
+ if (entries.length === 0) {
7198
+ const emptyMsg = `Transcript for worker ${workerId} exists but contains no complete entries yet.`;
7199
+ return incompleteTail ? `${emptyMsg}
7200
+ [note: last entry is incomplete \u2014 the transcript is still being written]` : emptyMsg;
7201
+ }
7202
+ const filter = type;
7203
+ const population = filter === void 0 ? entries : entries.filter((e) => typeMatches(e, filter));
7204
+ const reqLimit = isFull ? population.length : requestedLimit;
7205
+ const perEntryCap = isFull ? null : maxChars ?? DEFAULT_MAX_CHARS2;
7206
+ const requested = selectWindow(population, offset ?? 0, reqLimit);
7207
+ const rendered = renderEntriesFitting(
7208
+ requested.entries,
7209
+ requested.startIndex,
7210
+ { maxChars: perEntryCap, includeToolDetail: isFull },
7211
+ MAX_TRANSCRIPT_RESULT_CHARS - OUTPUT_OVERHEAD_CHARS2
7212
+ );
7213
+ const win = shrinkWindow(requested, rendered.dropped);
7214
+ const body = rendered.lines;
7215
+ const base = buildWindowMeta(win, {
7216
+ maxChars: perEntryCap,
7217
+ full: isFull,
7218
+ shortenedEntries: rendered.shortened
7219
+ });
7220
+ const meta = {
7221
+ workerId,
7222
+ source,
7223
+ ...src.transcriptId !== void 0 && { transcriptId: src.transcriptId },
7224
+ ...base,
7225
+ incompleteTail,
7226
+ readWindowTruncated,
7227
+ windowTruncated: base.windowTruncated || readWindowTruncated,
7228
+ truncated: base.truncated || readWindowTruncated,
7229
+ ...filter !== void 0 && { filter, totalEntriesBeforeFilter: entries.length }
7230
+ };
7231
+ const summary = summarizeTranscript(entries);
7232
+ const headLine = source === "disk" ? `Worker ${workerId} transcript (source=disk, dir id ${src.transcriptId}): ` + workerTranscriptPath(deps.sessionDir, src.transcriptId ?? workerId) : `Worker ${workerId} conversation (source=memory \u2014 live in-memory state of the tracked worker)`;
7233
+ const lines = [
7234
+ headLine,
7235
+ `Last activity: ${formatLastActivity(summary.lastActivityAt, now)}; currentTool: ${summary.currentTool ?? "-"}; terminal marker: ${summary.terminalSubtype ?? "-"}`
7236
+ ];
7237
+ let header = buildTranscriptHeader(meta);
7238
+ if (readWindowTruncated) {
7239
+ header = `Conversation (${meta.totalEntries} entries in read window, showing ${meta.startIndex}-${meta.endIndex}; older entries exist beyond the read-window cap):`;
7240
+ }
7241
+ lines.push(header + (filter !== void 0 ? ` [type=${filter}; ${entries.length} entries in transcript]` : ""));
7242
+ lines.push(...body);
7243
+ if (incompleteTail) {
7244
+ lines.push("[note: last entry is incomplete \u2014 the transcript is still being written]");
7245
+ }
7246
+ if (readWindowTruncated) {
7247
+ lines.push("[note: transcript read window hit its byte cap \u2014 older entries were not read; page back with offset/limit]");
7248
+ }
7249
+ const notice = buildTruncationNotice(meta);
7250
+ if (notice) lines.push(notice);
7251
+ lines.push(`[transcript-meta] ${JSON.stringify(meta)}`);
7252
+ return lines.join("\n");
7253
+ }
7254
+ }, { maxResultChars: MAX_TRANSCRIPT_RESULT_CHARS })
7255
+ });
7256
+ }
7257
+
7258
+ // src/services/tools/get-worker-status.ts
7259
+ import { z as z9 } from "zod";
7260
+ var SETTLED_TASK_STATUSES = /* @__PURE__ */ new Set(["completed", "failed", "cancelled"]);
7261
+ function taskStatusToWorkerStatus(status) {
7262
+ if (status === "running") return "running";
7263
+ if (SETTLED_TASK_STATUSES.has(status)) return status;
7264
+ return "unknown";
7265
+ }
7266
+ function buildSnapshot(workerId, deps, now) {
7267
+ const active = deps.listWorkers().find((w) => w.workerId === workerId);
7268
+ const tasks = deps.listTasks();
7269
+ const task = active ? tasks.find((t) => t.id === active.taskId) : tasks.find((t) => t.workerSessionId === workerId);
7270
+ const entries = deps.readTranscript(workerId);
7271
+ if (!active && !task && entries === null) return null;
7272
+ const summary = summarizeTranscript(entries ?? []);
7273
+ const live = deps.getLiveStatus?.(workerId);
7274
+ const workerType = active?.workerType ?? task?.workerType ?? "unknown";
7275
+ const settled = task ? SETTLED_TASK_STATUSES.has(task.status) : false;
7276
+ const terminalTracked = workerType !== "puppet";
7277
+ const inferredTerminal = summary.hasTerminalEntry || settled;
7278
+ let runningForMs = null;
7279
+ if (active) {
7280
+ runningForMs = Math.max(0, now - active.startedAt);
7281
+ } else if (task?.startedAt) {
7282
+ runningForMs = Math.max(0, (task.completedAt ?? now) - task.startedAt);
7283
+ }
7284
+ return {
7285
+ workerId,
7286
+ type: workerType,
7287
+ taskId: active?.taskId ?? task?.id ?? null,
7288
+ status: active?.status ?? (task ? taskStatusToWorkerStatus(task.status) : "unknown"),
7289
+ runningForMs,
7290
+ lastActivityAt: summary.lastActivityAt,
7291
+ lastActivityAgoMs: summary.lastActivityAt === null ? null : Math.max(0, now - summary.lastActivityAt),
7292
+ currentTool: summary.currentTool,
7293
+ streamEnded: live?.streamEnded ?? (inferredTerminal ? true : terminalTracked ? false : null),
7294
+ resultReceived: live?.resultReceived ?? (inferredTerminal ? true : terminalTracked ? false : null),
7295
+ idleSince: active?.status === "idle" ? active.idleSinceMs ?? null : null,
7296
+ live: active !== void 0,
7297
+ lastEntryType: summary.lastEntryType,
7298
+ entries: summary.totalEntries
7299
+ };
7300
+ }
7301
+ function boolLabel(v) {
7302
+ return v === null ? "unknown" : String(v);
7303
+ }
7304
+ function formatSnapshot(s, now) {
7305
+ return [
7306
+ `Worker ${s.workerId} (${s.type})`,
7307
+ ` task: ${s.taskId ?? "-"}`,
7308
+ ` status: ${s.status}${s.live ? " (tracked)" : " (no longer tracked)"}`,
7309
+ ` runningForMs: ${s.runningForMs === null ? "unknown" : `${s.runningForMs} (${formatDuration(s.runningForMs)})`}`,
7310
+ ` lastActivityAt: ${formatLastActivity(s.lastActivityAt, now)}`,
7311
+ ` currentTool: ${s.currentTool ?? "-"}`,
7312
+ ` streamEnded: ${boolLabel(s.streamEnded)}`,
7313
+ ` resultReceived: ${boolLabel(s.resultReceived)}`,
7314
+ ` idleSince: ${s.idleSince === null ? "-" : formatLastActivity(s.idleSince, now)}`,
7315
+ ` entries: ${s.entries} (last: ${s.lastEntryType ?? "-"})`
7316
+ ];
7317
+ }
7318
+ function formatSnapshotLine(s) {
7319
+ const activity = s.lastActivityAgoMs === null ? "none" : `${formatDuration(s.lastActivityAgoMs)} ago`;
7320
+ return ` [${s.status}] ${s.workerId} (${s.type}) task=${s.taskId ?? "-"} runningFor=${s.runningForMs === null ? "unknown" : formatDuration(s.runningForMs)} lastActivity=${activity} tool=${s.currentTool ?? "-"} streamEnded=${boolLabel(s.streamEnded)} resultReceived=${boolLabel(s.resultReceived)}`;
7321
+ }
7322
+ function getWorkerStatusTool(deps) {
7323
+ return defineTool({
7324
+ description: 'Get the liveness of one worker (workerId given) or of all current workers (omitted). Reports status, runningForMs, lastActivityAt (epoch ms of the newest transcript entry; lastActivityAgoMs is how long ago that was), currentTool, streamEnded, resultReceived and idleSince \u2014 use it to tell "still working" from "stuck / already finished and just not released" before deciding to wait, chat, or cancel. Unknown worker ids return a not-found message instead of throwing. A machine-readable [worker-status] line carries the same data as JSON.',
7325
+ parameters: z9.object({
7326
+ workerId: z9.string().optional().describe("Worker ID; omit to list all current workers")
7327
+ }),
7328
+ execute: safeExecute("get_worker_status", async ({ workerId }) => {
7329
+ const now = deps.now?.() ?? Date.now();
7330
+ if (workerId !== void 0 && !isValidWorkerId(workerId)) {
7331
+ return `Invalid worker id "${workerId}" \u2014 worker ids are alphanumeric with dashes/underscores only.`;
7332
+ }
7333
+ if (workerId === void 0) {
7334
+ const workers = deps.listWorkers();
7335
+ if (workers.length === 0) {
7336
+ return "No workers are running or idle.\n[worker-status] []";
7337
+ }
7338
+ const snapshots = workers.map((w) => buildSnapshot(w.workerId, deps, now)).filter((s) => s !== null);
7339
+ const lines = [`Workers (${snapshots.length}):`, ...snapshots.map(formatSnapshotLine)];
7340
+ lines.push(`[worker-status] ${JSON.stringify(snapshots)}`);
7341
+ return lines.join("\n");
7342
+ }
7343
+ const snapshot = buildSnapshot(workerId, deps, now);
7344
+ if (!snapshot) {
7345
+ return `Worker ${workerId} not found (not tracked, no task references it, no transcript on disk).`;
7346
+ }
7347
+ return [...formatSnapshot(snapshot, now), `[worker-status] ${JSON.stringify(snapshot)}`].join("\n");
7348
+ }, { maxResultChars: MAX_TRANSCRIPT_RESULT_CHARS })
7349
+ });
7350
+ }
7351
+
7352
+ // src/services/tools/update-task.ts
7353
+ import { z as z10 } from "zod";
6030
7354
  function updateTaskTool(deps) {
6031
7355
  return defineTool({
6032
7356
  description: "Update a pending task description or priority",
6033
- parameters: z8.object({
6034
- taskId: z8.string().describe("Task ID to update"),
6035
- description: z8.string().optional().describe("New task description"),
6036
- priority: z8.enum(["normal", "high"]).optional().describe("New priority")
7357
+ parameters: z10.object({
7358
+ taskId: z10.string().describe("Task ID to update"),
7359
+ description: z10.string().optional().describe("New task description"),
7360
+ priority: z10.enum(["normal", "high"]).optional().describe("New priority")
6037
7361
  }),
6038
7362
  execute: safeExecute("update_task", async ({ taskId, description, priority }) => {
6039
7363
  if (!description && !priority) {
@@ -6047,14 +7371,14 @@ function updateTaskTool(deps) {
6047
7371
  }
6048
7372
 
6049
7373
  // src/services/tools/query-signals.ts
6050
- import { z as z9 } from "zod";
7374
+ import { z as z11 } from "zod";
6051
7375
  function querySignalsTool(deps) {
6052
7376
  return defineTool({
6053
7377
  description: "Query signal history by status, source, or type",
6054
- parameters: z9.object({
6055
- status: z9.enum(["unread", "read", "discarded"]).optional().describe("Filter by status"),
6056
- source: z9.enum(["user", "worker", "system"]).optional().describe("Filter by source"),
6057
- type: z9.enum(["input", "completed", "failed", "progress", "cancelled"]).optional().describe("Filter by type")
7378
+ parameters: z11.object({
7379
+ status: z11.enum(["unread", "read", "discarded"]).optional().describe("Filter by status"),
7380
+ source: z11.enum(["user", "worker", "system"]).optional().describe("Filter by source"),
7381
+ type: z11.enum(["input", "completed", "failed", "progress", "cancelled"]).optional().describe("Filter by type")
6058
7382
  }),
6059
7383
  execute: safeExecute("query_signals", async (filter) => {
6060
7384
  const signals = deps.querySignals(filter);
@@ -6067,11 +7391,11 @@ function querySignalsTool(deps) {
6067
7391
  }
6068
7392
 
6069
7393
  // src/services/tools/skip-reply.ts
6070
- import { z as z10 } from "zod";
7394
+ import { z as z12 } from "zod";
6071
7395
  function skipReplyTool() {
6072
7396
  return defineTool({
6073
7397
  description: "Acknowledge a signal without responding to the user. IMPORTANT: this tool terminates the turn immediately. You MUST NOT output any text in the same response \u2014 your response must contain ONLY this tool call. Any co-emitted text will leak to the user as a broken message.",
6074
- parameters: z10.object({}),
7398
+ parameters: z12.object({}),
6075
7399
  execute: safeExecute("skip_reply", async () => {
6076
7400
  return "(skipped)";
6077
7401
  })
@@ -6079,13 +7403,13 @@ function skipReplyTool() {
6079
7403
  }
6080
7404
 
6081
7405
  // src/services/tools/bash.ts
6082
- import { z as z11 } from "zod";
7406
+ import { z as z13 } from "zod";
6083
7407
  import { execSync as execSync2 } from "child_process";
6084
7408
  function bashTool(deps) {
6085
7409
  return defineTool({
6086
7410
  description: "Execute a shell command and return its output",
6087
- parameters: z11.object({
6088
- command: z11.string().describe("Shell command to execute")
7411
+ parameters: z13.object({
7412
+ command: z13.string().describe("Shell command to execute")
6089
7413
  }),
6090
7414
  execute: safeExecute("bash", async ({ command }) => {
6091
7415
  try {
@@ -6107,22 +7431,22 @@ ${detail}`;
6107
7431
  }
6108
7432
 
6109
7433
  // src/services/tools/read-file.ts
6110
- import { z as z12 } from "zod";
6111
- import { readFileSync as readFileSync6 } from "fs";
6112
- import { resolve as resolve2 } from "path";
7434
+ import { z as z14 } from "zod";
7435
+ import { readFileSync as readFileSync7 } from "fs";
7436
+ import { resolve as resolve3 } from "path";
6113
7437
  var DEFAULT_MAX_LINES = 500;
6114
7438
  function readFileTool(deps = {}) {
6115
7439
  const workspace = deps.cwd ?? process.cwd();
6116
7440
  return defineTool({
6117
7441
  description: `Read file content (with line numbers). Returns at most ${DEFAULT_MAX_LINES} lines by default. Use offset (1-based start line) and limit to read specific sections of large files.`,
6118
- parameters: z12.object({
6119
- path: z12.string().describe("File path (relative to workspace or absolute)"),
6120
- offset: z12.number().int().min(1).optional().describe("Start line number (1-based)"),
6121
- limit: z12.number().int().min(1).optional().describe("Number of lines to read")
7442
+ parameters: z14.object({
7443
+ path: z14.string().describe("File path (relative to workspace or absolute)"),
7444
+ offset: z14.number().int().min(1).optional().describe("Start line number (1-based)"),
7445
+ limit: z14.number().int().min(1).optional().describe("Number of lines to read")
6122
7446
  }),
6123
7447
  execute: safeExecute("read_file", async ({ path: path7, offset, limit }) => {
6124
- const resolved = resolve2(workspace, path7);
6125
- const content = readFileSync6(resolved, "utf-8");
7448
+ const resolved = resolve3(workspace, path7);
7449
+ const content = readFileSync7(resolved, "utf-8");
6126
7450
  const lines = content.split("\n");
6127
7451
  const totalLines = lines.length;
6128
7452
  const start = offset ? offset - 1 : 0;
@@ -6141,23 +7465,23 @@ function readFileTool(deps = {}) {
6141
7465
  }
6142
7466
 
6143
7467
  // src/services/tools/write-file.ts
6144
- import { z as z13 } from "zod";
6145
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
6146
- import { dirname as dirname4, resolve as resolve3 } from "path";
7468
+ import { z as z15 } from "zod";
7469
+ import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync4, existsSync as existsSync7 } from "fs";
7470
+ import { dirname as dirname5, resolve as resolve4 } from "path";
6147
7471
  function writeFileTool(deps = {}) {
6148
7472
  const workspace = deps.cwd ?? process.cwd();
6149
7473
  return defineTool({
6150
7474
  description: "Create a new file with the given content. Creates parent directories as needed. Use edit_file to modify existing files.",
6151
- parameters: z13.object({
6152
- path: z13.string().describe("File path (relative to workspace or absolute)"),
6153
- content: z13.string().describe("File content")
7475
+ parameters: z15.object({
7476
+ path: z15.string().describe("File path (relative to workspace or absolute)"),
7477
+ content: z15.string().describe("File content")
6154
7478
  }),
6155
7479
  execute: safeExecute("write_file", async ({ path: path7, content }) => {
6156
- const resolved = resolve3(workspace, path7);
6157
- if (existsSync5(resolved)) {
7480
+ const resolved = resolve4(workspace, path7);
7481
+ if (existsSync7(resolved)) {
6158
7482
  return `[ERROR] write_file: File already exists: ${path7}. Use edit_file to modify existing files.`;
6159
7483
  }
6160
- mkdirSync4(dirname4(resolved), { recursive: true });
7484
+ mkdirSync4(dirname5(resolved), { recursive: true });
6161
7485
  writeFileSync3(resolved, content, "utf-8");
6162
7486
  return `Written ${content.length} chars to ${resolved}`;
6163
7487
  })
@@ -6165,23 +7489,23 @@ function writeFileTool(deps = {}) {
6165
7489
  }
6166
7490
 
6167
7491
  // src/services/tools/edit-file.ts
6168
- import { readFileSync as readFileSync7, writeFileSync as writeFileSync4 } from "fs";
6169
- import { resolve as resolve4 } from "path";
6170
- import { z as z14 } from "zod";
7492
+ import { readFileSync as readFileSync8, writeFileSync as writeFileSync4 } from "fs";
7493
+ import { resolve as resolve5 } from "path";
7494
+ import { z as z16 } from "zod";
6171
7495
  function editFileTool(deps) {
6172
7496
  const workspace = deps.cwd ?? process.cwd();
6173
7497
  return defineTool({
6174
7498
  description: "Edit a file by replacing a unique string. old_string must appear exactly once in the file \u2014 if it matches multiple times, expand the surrounding context to make it unique.",
6175
- parameters: z14.object({
6176
- path: z14.string().describe("File path (relative to workspace or absolute)"),
6177
- old_string: z14.string().describe("Exact text to find (must be unique in file)"),
6178
- new_string: z14.string().describe("Replacement text")
7499
+ parameters: z16.object({
7500
+ path: z16.string().describe("File path (relative to workspace or absolute)"),
7501
+ old_string: z16.string().describe("Exact text to find (must be unique in file)"),
7502
+ new_string: z16.string().describe("Replacement text")
6179
7503
  }),
6180
7504
  execute: safeExecute("edit_file", async ({ path: path7, old_string, new_string }) => {
6181
- const resolved = resolve4(workspace, path7);
7505
+ const resolved = resolve5(workspace, path7);
6182
7506
  let content;
6183
7507
  try {
6184
- content = readFileSync7(resolved, "utf-8");
7508
+ content = readFileSync8(resolved, "utf-8");
6185
7509
  } catch {
6186
7510
  return `[ERROR] edit_file: File not found: ${path7}`;
6187
7511
  }
@@ -6199,18 +7523,18 @@ function editFileTool(deps) {
6199
7523
 
6200
7524
  // src/services/tools/search-files.ts
6201
7525
  import fg from "fast-glob";
6202
- import { resolve as resolve5 } from "path";
6203
- import { z as z15 } from "zod";
7526
+ import { resolve as resolve6 } from "path";
7527
+ import { z as z17 } from "zod";
6204
7528
  function searchFilesTool(deps) {
6205
7529
  const workspace = deps.cwd ?? process.cwd();
6206
7530
  return defineTool({
6207
7531
  description: "Search for files matching a glob pattern. Returns matching file paths relative to workspace.",
6208
- parameters: z15.object({
6209
- pattern: z15.string().describe('Glob pattern (e.g. "**/*.ts", "src/**/*.test.ts")'),
6210
- path: z15.string().optional().describe("Base directory for search (default: workspace root)")
7532
+ parameters: z17.object({
7533
+ pattern: z17.string().describe('Glob pattern (e.g. "**/*.ts", "src/**/*.test.ts")'),
7534
+ path: z17.string().optional().describe("Base directory for search (default: workspace root)")
6211
7535
  }),
6212
7536
  execute: safeExecute("search_files", async ({ pattern, path: path7 }) => {
6213
- const basePath = path7 ? resolve5(workspace, path7) : workspace;
7537
+ const basePath = path7 ? resolve6(workspace, path7) : workspace;
6214
7538
  const entries = await fg(pattern, {
6215
7539
  cwd: basePath,
6216
7540
  onlyFiles: true,
@@ -6224,8 +7548,8 @@ function searchFilesTool(deps) {
6224
7548
 
6225
7549
  // src/services/tools/search-content.ts
6226
7550
  import { execSync as execSync3 } from "child_process";
6227
- import { resolve as resolve6 } from "path";
6228
- import { z as z16 } from "zod";
7551
+ import { resolve as resolve7 } from "path";
7552
+ import { z as z18 } from "zod";
6229
7553
  function shellQuote(s) {
6230
7554
  return `'${s.replace(/'/g, "'\\''")}'`;
6231
7555
  }
@@ -6233,13 +7557,13 @@ function searchContentTool(deps) {
6233
7557
  const workspace = deps.cwd ?? process.cwd();
6234
7558
  return defineTool({
6235
7559
  description: "Search file contents for a text pattern. Returns matching lines in file:line:content format.",
6236
- parameters: z16.object({
6237
- query: z16.string().describe("Search text or regex pattern"),
6238
- path: z16.string().optional().describe("Directory to search in (default: workspace root)"),
6239
- include: z16.string().optional().describe('File pattern to include (e.g. "*.ts", "*.py")')
7560
+ parameters: z18.object({
7561
+ query: z18.string().describe("Search text or regex pattern"),
7562
+ path: z18.string().optional().describe("Directory to search in (default: workspace root)"),
7563
+ include: z18.string().optional().describe('File pattern to include (e.g. "*.ts", "*.py")')
6240
7564
  }),
6241
7565
  execute: safeExecute("search_content", async ({ query: query2, path: path7, include }) => {
6242
- const searchDir = path7 ? resolve6(workspace, path7) : workspace;
7566
+ const searchDir = path7 ? resolve7(workspace, path7) : workspace;
6243
7567
  const cmd = `grep -rn${include ? ` --include=${shellQuote(include)}` : ""} -- ${shellQuote(query2)} ${shellQuote(searchDir)}`;
6244
7568
  try {
6245
7569
  const output = execSync3(cmd, {
@@ -6258,13 +7582,13 @@ ${err.stderr?.toString() || ""}`;
6258
7582
  }
6259
7583
 
6260
7584
  // src/services/tools/web-search.ts
6261
- import { z as z17 } from "zod";
7585
+ import { z as z19 } from "zod";
6262
7586
  function webSearchTool(deps) {
6263
7587
  const apiKey = deps.tavilyApiKey;
6264
7588
  return defineTool({
6265
7589
  description: "Search the web using Tavily. Returns top results with title, URL, and content snippet.",
6266
- parameters: z17.object({
6267
- query: z17.string().describe("Search query")
7590
+ parameters: z19.object({
7591
+ query: z19.string().describe("Search query")
6268
7592
  }),
6269
7593
  execute: safeExecute("web_search", async ({ query: query2 }) => {
6270
7594
  if (!apiKey) {
@@ -6295,11 +7619,11 @@ function webSearchTool(deps) {
6295
7619
  }
6296
7620
 
6297
7621
  // src/services/tools/notebook.ts
6298
- import { z as z18 } from "zod";
7622
+ import { z as z20 } from "zod";
6299
7623
  function readNotebookTool(deps) {
6300
7624
  return defineTool({
6301
7625
  description: "Read the current notebook content. The notebook is a private scratchpad that persists across context compaction \u2014 use it to store plans, notes, and working context.",
6302
- parameters: z18.object({}),
7626
+ parameters: z20.object({}),
6303
7627
  execute: safeExecute("read_notebook", async () => {
6304
7628
  const content = deps.getNotebook();
6305
7629
  return content || "(notebook is empty)";
@@ -6309,9 +7633,9 @@ function readNotebookTool(deps) {
6309
7633
  function updateNotebookTool(deps) {
6310
7634
  return defineTool({
6311
7635
  description: 'Update the notebook content. Use "replace" to overwrite the entire notebook, or "append" to add to the end. The notebook persists across context compaction.',
6312
- parameters: z18.object({
6313
- content: z18.string().describe("Content to write"),
6314
- mode: z18.enum(["replace", "append"]).optional().describe('Write mode: "replace" overwrites, "append" adds to the end (default: replace)')
7636
+ parameters: z20.object({
7637
+ content: z20.string().describe("Content to write"),
7638
+ mode: z20.enum(["replace", "append"]).optional().describe('Write mode: "replace" overwrites, "append" adds to the end (default: replace)')
6315
7639
  }),
6316
7640
  execute: safeExecute("update_notebook", async ({ content, mode }) => {
6317
7641
  deps.setNotebook(content, mode ?? "replace");
@@ -6321,15 +7645,16 @@ function updateNotebookTool(deps) {
6321
7645
  }
6322
7646
 
6323
7647
  // src/services/tools/inquire.ts
6324
- import { z as z19 } from "zod";
7648
+ import { z as z21 } from "zod";
6325
7649
  function inquireTool(deps) {
6326
7650
  return defineTool({
6327
- description: "Ask the user a structured question and wait for their response. Use this to clarify ambiguous requests, confirm approach, or let the user choose between options. The tool blocks until the user answers.",
6328
- parameters: z19.object({
6329
- message: z19.string().describe("Question to ask the user"),
6330
- type: z19.enum(["confirm", "select", "text"]).describe('Question type: "confirm" for yes/no, "select" for choosing from options, "text" for free-form input'),
6331
- options: z19.array(z19.string()).optional().describe('Available choices (required for "select" type)'),
6332
- default: z19.string().optional().describe("Default value or pre-selected option")
7651
+ description: 'Ask the user a structured question and wait for their response. Use this to clarify ambiguous requests, confirm approach, or let the user choose between options. The tool blocks until the user answers. For "select" questions an "Other" choice is appended automatically so the user can always answer outside the preset options \u2014 do not add your own catch-all option. Answers typed via "Other" come back prefixed with "[Other] ".',
7652
+ parameters: z21.object({
7653
+ message: z21.string().describe("Question to ask the user"),
7654
+ type: z21.enum(["confirm", "select", "text"]).describe('Question type: "confirm" for yes/no, "select" for choosing from options, "text" for free-form input'),
7655
+ options: z21.array(z21.string()).optional().describe('Available choices (required for "select" type). Provide 2-5 clear preset options \u2014 an "Other" choice for free-form input is appended automatically'),
7656
+ default: z21.string().optional().describe("Default value or pre-selected option"),
7657
+ allowOther: z21.boolean().optional().describe('"select" only: append an "Other" choice letting the user type a custom answer (default true). Set false to restrict answers to the preset options')
6333
7658
  }),
6334
7659
  execute: safeExecute("inquire", async (args) => {
6335
7660
  return deps.inquire(args);
@@ -6338,12 +7663,12 @@ function inquireTool(deps) {
6338
7663
  }
6339
7664
 
6340
7665
  // src/services/tools/done.ts
6341
- import { z as z20 } from "zod";
7666
+ import { z as z22 } from "zod";
6342
7667
  function doneTool(deps) {
6343
7668
  return defineTool({
6344
7669
  description: "Report task completion. Worker stops executing after this call.",
6345
- parameters: z20.object({
6346
- summary: z20.string().describe("Task completion summary")
7670
+ parameters: z22.object({
7671
+ summary: z22.string().describe("Task completion summary")
6347
7672
  }),
6348
7673
  execute: safeExecute("done", async ({ summary }) => {
6349
7674
  deps.onComplete(summary);
@@ -6353,12 +7678,12 @@ function doneTool(deps) {
6353
7678
  }
6354
7679
 
6355
7680
  // src/services/tools/fail.ts
6356
- import { z as z21 } from "zod";
7681
+ import { z as z23 } from "zod";
6357
7682
  function failTool(deps) {
6358
7683
  return defineTool({
6359
7684
  description: "Report task failure. Worker stops executing after this call.",
6360
- parameters: z21.object({
6361
- error: z21.string().describe("Error description")
7685
+ parameters: z23.object({
7686
+ error: z23.string().describe("Error description")
6362
7687
  }),
6363
7688
  execute: safeExecute("fail", async ({ error }) => {
6364
7689
  deps.onFail(error);
@@ -6368,13 +7693,13 @@ function failTool(deps) {
6368
7693
  }
6369
7694
 
6370
7695
  // src/services/tools/update-progress.ts
6371
- import { z as z22 } from "zod";
7696
+ import { z as z24 } from "zod";
6372
7697
  function updateProgressTool(deps) {
6373
7698
  return defineTool({
6374
7699
  description: "Report progress for the current task (replaces automatic step-based progress)",
6375
- parameters: z22.object({
6376
- message: z22.string().describe("Progress description"),
6377
- percent: z22.number().min(0).max(100).optional().describe("Completion percentage (0-100)")
7700
+ parameters: z24.object({
7701
+ message: z24.string().describe("Progress description"),
7702
+ percent: z24.number().min(0).max(100).optional().describe("Completion percentage (0-100)")
6378
7703
  }),
6379
7704
  execute: safeExecute("update_progress", async ({ message, percent }) => {
6380
7705
  deps.reportProgress(message, percent);
@@ -6385,24 +7710,24 @@ function updateProgressTool(deps) {
6385
7710
  }
6386
7711
 
6387
7712
  // src/services/tools/chat-worker.ts
6388
- import { z as z23 } from "zod";
7713
+ import { z as z25 } from "zod";
6389
7714
  function chatWorkerTool(deps) {
6390
7715
  return defineTool({
6391
7716
  description: `Send a message to a running worker. Use this to provide additional guidance, correct the worker's approach, or ask the worker to check something. The worker reads it at its next step (puppet) or turn boundary (third-party); idle multi-stage workers read it when their next stage starts. A third-party worker NEVER acts on the message mid-tool-call: it finishes the current turn first, so "queued" can mean a delay of minutes. If the worker has already finished, this tool fails explicitly \u2014 inspect its output instead.`,
6392
- parameters: z23.object({
6393
- workerId: z23.string().describe("ID of the target worker"),
6394
- message: z23.string().describe("Message to send to the worker")
7717
+ parameters: z25.object({
7718
+ workerId: z25.string().describe("ID of the target worker"),
7719
+ message: z25.string().describe("Message to send to the worker")
6395
7720
  }),
6396
7721
  execute: safeExecute("chat_worker", async ({ workerId, message }) => {
6397
7722
  const status = deps.sendMessageToWorker(workerId, message);
6398
7723
  if (status === "not_running") {
6399
- return `[ERROR] Worker ${workerId} is not running (it may have just finished).`;
7724
+ return `[ERROR] Worker ${workerId} is not running (it may have just finished). Use get_worker_status("${workerId}") or read_worker_conversation("${workerId}") to inspect what it produced.`;
6400
7725
  }
6401
7726
  if (status === "finished") {
6402
- return `[ERROR] Worker ${workerId} has already finished; message not delivered. Use get_task_detail to inspect its output.`;
7727
+ return `[ERROR] Worker ${workerId} has already finished; message not delivered. Use read_worker_conversation("${workerId}") / get_task_detail to inspect its output.`;
6403
7728
  }
6404
7729
  if (status === "queued") {
6405
- return `Message queued for worker ${workerId} \u2014 QUEUED only, not delivered yet: the worker reads it at its next step (puppet), turn boundary (third-party \u2014 the current turn, including any running tool call, finishes first) or next stage start (idle). A stuck worker will never reach that point. Verify with get_task_detail before relying on it; if it looks stuck, use cancel_task instead of waiting for this message.`;
7730
+ return `Message queued for worker ${workerId} \u2014 QUEUED only, not delivered yet: the worker reads it at its next step (puppet), turn boundary (third-party \u2014 the current turn, including any running tool call, finishes first) or next stage start (idle). A stuck worker will never reach that point. Verify with get_worker_status("${workerId}") (lastActivityAt / currentTool); if it looks stuck, use cancel_task instead of waiting for this message.`;
6406
7731
  }
6407
7732
  return `Message delivered to worker ${workerId} \u2014 it will be read at the worker's next step.`;
6408
7733
  })
@@ -6410,14 +7735,14 @@ function chatWorkerTool(deps) {
6410
7735
  }
6411
7736
 
6412
7737
  // src/services/tools/respond-permission.ts
6413
- import { z as z24 } from "zod";
7738
+ import { z as z26 } from "zod";
6414
7739
  function respondPermissionTool(deps) {
6415
7740
  return defineTool({
6416
7741
  description: "Respond to a worker's permission request. Approve to allow the operation, deny to reject it.",
6417
- parameters: z24.object({
6418
- requestId: z24.string().describe("The request ID from the permission_request signal"),
6419
- approved: z24.boolean().describe("true to approve, false to deny"),
6420
- reason: z24.string().optional().describe("Reason for denial (shown to worker)")
7742
+ parameters: z26.object({
7743
+ requestId: z26.string().describe("The request ID from the permission_request signal"),
7744
+ approved: z26.boolean().describe("true to approve, false to deny"),
7745
+ reason: z26.string().optional().describe("Reason for denial (shown to worker)")
6421
7746
  }),
6422
7747
  execute: safeExecute("respond_permission", async ({ requestId, approved, reason }) => {
6423
7748
  const handled = deps.respondPermission(requestId, approved, reason);
@@ -6428,13 +7753,13 @@ function respondPermissionTool(deps) {
6428
7753
  }
6429
7754
 
6430
7755
  // src/services/tools/respond-inquiry.ts
6431
- import { z as z25 } from "zod";
7756
+ import { z as z27 } from "zod";
6432
7757
  function respondInquiryTool(deps) {
6433
7758
  return defineTool({
6434
7759
  description: "Answer a worker's question (from a worker_inquiry signal). The blocked worker resumes with your answer as the tool result. If the decision belongs to the user, ask them first via inquire, then answer here.",
6435
- parameters: z25.object({
6436
- inquiryId: z25.string().describe("The inquiryId from the worker_inquiry signal"),
6437
- answer: z25.string().max(4e3).describe("The answer/instruction for the worker. Self-contained \u2014 the worker sees only this text.")
7760
+ parameters: z27.object({
7761
+ inquiryId: z27.string().describe("The inquiryId from the worker_inquiry signal"),
7762
+ answer: z27.string().max(4e3).describe("The answer/instruction for the worker. Self-contained \u2014 the worker sees only this text.")
6438
7763
  }),
6439
7764
  execute: safeExecute("respond_inquiry", async ({ inquiryId, answer }) => {
6440
7765
  const status = deps.respondInquiry(inquiryId, answer);
@@ -6450,7 +7775,7 @@ function respondInquiryTool(deps) {
6450
7775
  }
6451
7776
 
6452
7777
  // src/services/tools/use-skill.ts
6453
- import { z as z26 } from "zod";
7778
+ import { z as z28 } from "zod";
6454
7779
  function formatSkillContent(skill) {
6455
7780
  const header = `# Skill: ${skill.name}
6456
7781
 
@@ -6472,8 +7797,8 @@ ${skill.content}${resources}`;
6472
7797
  function useSkillTool(deps) {
6473
7798
  return defineTool({
6474
7799
  description: `Load a skill's full instructions by name. Skills are on-demand expertise (workflows, conventions, scripts). Use this when a task matches a skill in the "Skills" section. Returns the skill body plus a list of its resource files (invoke those with your shell/file tools, not here).`,
6475
- parameters: z26.object({
6476
- name: z26.string().describe('Skill name, exactly as shown in the "Skills" section')
7800
+ parameters: z28.object({
7801
+ name: z28.string().describe('Skill name, exactly as shown in the "Skills" section')
6477
7802
  }),
6478
7803
  execute: safeExecute("use-skill", async ({ name }) => {
6479
7804
  const content = deps.registry.get(name);
@@ -6505,7 +7830,26 @@ function createControllerTools(deps) {
6505
7830
  }),
6506
7831
  get_task_detail: getTaskDetailTool({
6507
7832
  getTask: deps.getTask,
6508
- getWorkerConversation: deps.getWorkerConversation
7833
+ getWorkerConversation: deps.getWorkerConversation,
7834
+ readTranscriptWindow: deps.readTranscriptWindow
7835
+ }),
7836
+ read_worker_conversation: readWorkerConversationTool({
7837
+ sessionDir: deps.sessionDir,
7838
+ getWorkerConversationByWorkerId: deps.getWorkerConversationByWorkerId,
7839
+ getWorkerTranscriptId: deps.getWorkerTranscriptId,
7840
+ readTranscriptWindow: (workerId, opts) => {
7841
+ const reader = deps.readTranscriptWindow;
7842
+ if (!reader) throw new Error("readTranscriptWindow dep not wired");
7843
+ return reader(workerId, opts);
7844
+ },
7845
+ listWorkers: () => deps.getActiveWorkers?.() ?? [],
7846
+ listTasks: () => deps.listTasks()
7847
+ }),
7848
+ get_worker_status: getWorkerStatusTool({
7849
+ listWorkers: () => deps.getActiveWorkers?.() ?? [],
7850
+ listTasks: () => deps.listTasks(),
7851
+ readTranscript: deps.readTranscript,
7852
+ getLiveStatus: deps.getWorkerLiveStatus
6509
7853
  }),
6510
7854
  update_task: updateTaskTool({ updateTask: deps.updateTask }),
6511
7855
  query_signals: querySignalsTool({ querySignals: deps.queryMessages }),
@@ -6545,13 +7889,13 @@ function createWorkerTools(deps) {
6545
7889
  }
6546
7890
 
6547
7891
  // src/services/tools/ask-controller.ts
6548
- import { z as z27 } from "zod";
7892
+ import { z as z29 } from "zod";
6549
7893
  function askControllerTool(deps) {
6550
7894
  return defineTool({
6551
7895
  description: "Ask the controller a question and WAIT for the answer. Use ONLY when you are blocked: execution cannot continue as instructed (contradiction, missing resource, unexpected state), or continuing would risk significant wasted work. Do NOT use it for decisions you can make yourself \u2014 decide those autonomously and report via update_progress / done.",
6552
- parameters: z27.object({
6553
- question: z27.string().max(500).describe("The specific question you need answered to proceed. Be concise and self-contained."),
6554
- context: z27.string().max(1e3).optional().describe("What you have tried and where you are stuck, so the controller can answer precisely.")
7896
+ parameters: z29.object({
7897
+ question: z29.string().max(500).describe("The specific question you need answered to proceed. Be concise and self-contained."),
7898
+ context: z29.string().max(1e3).optional().describe("What you have tried and where you are stuck, so the controller can answer precisely.")
6555
7899
  }),
6556
7900
  execute: safeExecute("ask_controller", async ({ question, context }) => {
6557
7901
  return deps.ask(question, context);
@@ -6593,16 +7937,16 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
6593
7937
  },
6594
7938
  ...deps.turnLineage ?? {}
6595
7939
  });
6596
- const result = await new Promise((resolve11) => {
7940
+ const result = await new Promise((resolve12) => {
6597
7941
  const timer = setTimeout(() => {
6598
7942
  pending.delete(requestId);
6599
- resolve11({ approved: false, reason: "timeout" });
7943
+ resolve12({ approved: false, reason: "timeout" });
6600
7944
  }, timeoutMs);
6601
7945
  pending.set(requestId, {
6602
7946
  resolve: (ok, reason) => {
6603
7947
  clearTimeout(timer);
6604
7948
  pending.delete(requestId);
6605
- resolve11({ approved: ok, reason });
7949
+ resolve12({ approved: ok, reason });
6606
7950
  },
6607
7951
  timer
6608
7952
  });
@@ -6639,14 +7983,14 @@ function wrapWithPermissionMiddleware(tools, config, deps) {
6639
7983
  }
6640
7984
 
6641
7985
  // src/services/tools/permission-rules.ts
6642
- import { resolve as resolve7 } from "path";
7986
+ import { resolve as resolve8 } from "path";
6643
7987
  import { homedir } from "os";
6644
7988
  var FILE_TOOL_NAMES = ["read_file", "write_file", "edit_file", "search_files", "search_content"];
6645
7989
  var DANGEROUS_BASH_PATTERN = /(?:^|\||;|&&)\s*(?:rm\s|sudo\b|chmod\b|chown\b|mkfs\b|dd\s|shutdown\b|reboot\b|kill\s+-9)/;
6646
7990
  var URL_RE = /[a-zA-Z][a-zA-Z0-9+.-]*:\/\/\S+/g;
6647
7991
  function expandHome(p) {
6648
7992
  if (p === "~") return homedir();
6649
- if (p.startsWith("~/")) return resolve7(homedir(), p.slice(2));
7993
+ if (p.startsWith("~/")) return resolve8(homedir(), p.slice(2));
6650
7994
  return p;
6651
7995
  }
6652
7996
  function extractPathCandidates(command) {
@@ -6661,7 +8005,7 @@ function extractPathCandidates(command) {
6661
8005
  function needsFilePermission(workspace, args) {
6662
8006
  const rawPath = args.path;
6663
8007
  if (typeof rawPath !== "string" || !rawPath) return false;
6664
- return !isInsideWorkspace(resolve7(workspace, expandHome(rawPath)), workspace);
8008
+ return !isInsideWorkspace(resolve8(workspace, expandHome(rawPath)), workspace);
6665
8009
  }
6666
8010
  function needsBashPermission(workspace, command) {
6667
8011
  if (DANGEROUS_BASH_PATTERN.test(command)) return true;
@@ -6711,7 +8055,7 @@ function formatApiCallError(err) {
6711
8055
  }
6712
8056
 
6713
8057
  // src/services/task-execute-engine.ts
6714
- import { resolve as resolve8 } from "path";
8058
+ import { resolve as resolve9 } from "path";
6715
8059
  var RuntimeState = class extends BaseWayangState {
6716
8060
  constructor(initialData) {
6717
8061
  super(initialData, []);
@@ -6988,6 +8332,24 @@ var TaskExecuteEngine = class {
6988
8332
  }
6989
8333
  });
6990
8334
  }
8335
+ /**
8336
+ * Live run flags of a worker instance — "has the result arrived?" and "has
8337
+ * the stream closed?" (issue #16). Returns undefined for workers that do not
8338
+ * report them or are no longer tracked.
8339
+ */
8340
+ getWorkerLiveStatus(workerId) {
8341
+ return this.workers.get(workerId)?.getLiveStatus?.();
8342
+ }
8343
+ /**
8344
+ * Per-worker activity derived from the in-memory conversation: when it last
8345
+ * produced an entry, and which tool it called last. Fed into heartbeat
8346
+ * signals so the controller can spot a stall without calling a tool.
8347
+ */
8348
+ getWorkerActivity(workerId) {
8349
+ const entries = this.workers.get(workerId)?.getConversation() ?? [];
8350
+ const summary = summarizeTranscript(entries);
8351
+ return { lastActivityAt: summary.lastActivityAt, currentTool: summary.currentTool };
8352
+ }
6991
8353
  getWorkerConversation(taskId) {
6992
8354
  for (const [workerId, tId] of this.workerTaskMap) {
6993
8355
  if (tId === taskId) {
@@ -6997,6 +8359,25 @@ var TaskExecuteEngine = class {
6997
8359
  }
6998
8360
  return [];
6999
8361
  }
8362
+ /**
8363
+ * Live in-memory conversation of a worker looked up by worker id (not task
8364
+ * id). Undefined once the worker is no longer tracked — the on-disk
8365
+ * transcript (via the task's workerTranscriptId) takes over then.
8366
+ */
8367
+ getWorkerConversationByWorkerId(workerId) {
8368
+ const worker = this.workers.get(workerId);
8369
+ return worker === void 0 ? void 0 : worker.getConversation();
8370
+ }
8371
+ /**
8372
+ * The worker instance's OWN id (agent-…/cw-…) — the directory name under
8373
+ * `<sessionDir>/workers/` where its conversation.jsonl lives. Distinct from
8374
+ * the engine worker id (w-…) used in tasks/signals. Only resolvable while
8375
+ * the engine tracks the worker; afterwards the task's persisted
8376
+ * `workerTranscriptId` is the source of truth.
8377
+ */
8378
+ getWorkerTranscriptId(workerId) {
8379
+ return this.workers.get(workerId)?.id;
8380
+ }
7000
8381
  getRunningCount() {
7001
8382
  return this.taskState.get("tasks.running").length;
7002
8383
  }
@@ -7153,6 +8534,20 @@ var TaskExecuteEngine = class {
7153
8534
  this.taskState.append("tasks.running", updated);
7154
8535
  this.logger.info({ taskId: id, workerId: workerSessionId }, "Task moved to running");
7155
8536
  }
8537
+ /**
8538
+ * Record the worker instance's own id (the on-disk transcript directory
8539
+ * name) on the running task. Called right after the worker instance is
8540
+ * created, before its run starts, so the link is on disk even if the
8541
+ * process crashes mid-run.
8542
+ */
8543
+ stampWorkerTranscriptId(taskId, transcriptId) {
8544
+ const running = [...this.taskState.get("tasks.running")];
8545
+ const idx = running.findIndex((t) => t.id === taskId);
8546
+ if (idx === -1) return;
8547
+ if (running[idx].workerTranscriptId === transcriptId) return;
8548
+ running[idx] = { ...running[idx], workerTranscriptId: transcriptId };
8549
+ this.taskState.set("tasks.running", running);
8550
+ }
7156
8551
  completeTask(id, result) {
7157
8552
  this.moveToHistory(id, "completed", { result });
7158
8553
  this.logger.info({ taskId: id }, "Task completed");
@@ -7290,6 +8685,7 @@ var TaskExecuteEngine = class {
7290
8685
  const worker = this.createWorker(workerType);
7291
8686
  this.workers.set(workerId, worker);
7292
8687
  this.workerTaskMap.set(workerId, task.id);
8688
+ this.stampWorkerTranscriptId(task.id, worker.id);
7293
8689
  const executionTurnId = this.registerWorkerTurn(workerId, task);
7294
8690
  const onProgress = this.buildProgressEmitter(workerId, task, workerType);
7295
8691
  const tools = this.wireWorker(worker, task, workerId, workerType);
@@ -7320,7 +8716,9 @@ var TaskExecuteEngine = class {
7320
8716
  assignedFromWorkerId: workerId,
7321
8717
  status: "running",
7322
8718
  startedAt: Date.now(),
7323
- workerSessionId: workerId
8719
+ workerSessionId: workerId,
8720
+ // Same instance keeps writing the same conversation.jsonl.
8721
+ workerTranscriptId: worker.id
7324
8722
  };
7325
8723
  this.taskState.append("tasks.running", taskWithOrigin);
7326
8724
  this.workerTaskMap.set(workerId, task.id);
@@ -7348,10 +8746,22 @@ var TaskExecuteEngine = class {
7348
8746
  buildProgressEmitter(workerId, task, workerType) {
7349
8747
  const meta = getWorkerMeta(workerType, this.workerConfigs);
7350
8748
  return (msg) => {
8749
+ const activity = this.getWorkerActivity(workerId);
7351
8750
  this.enqueueWorkerSignal(workerId, {
7352
8751
  source: "worker",
7353
8752
  type: "progress",
7354
- payload: { workerId, taskId: task.id, taskTitle: task.title, workerType, emoji: meta.emoji, message: msg }
8753
+ payload: {
8754
+ workerId,
8755
+ taskId: task.id,
8756
+ taskTitle: task.title,
8757
+ workerType,
8758
+ emoji: meta.emoji,
8759
+ message: msg,
8760
+ // Activity rides along (issue #16) so the controller sees liveness
8761
+ // from the signal itself instead of polling.
8762
+ lastActivityAt: activity.lastActivityAt,
8763
+ currentTool: activity.currentTool
8764
+ }
7355
8765
  });
7356
8766
  };
7357
8767
  }
@@ -7401,10 +8811,20 @@ var TaskExecuteEngine = class {
7401
8811
  tavilyApiKey: this.ctx.config.tavilyApiKey,
7402
8812
  registry: this.skills,
7403
8813
  reportProgress: (msg, _percent) => {
8814
+ const activity = this.getWorkerActivity(workerId);
7404
8815
  this.enqueueWorkerSignal(workerId, {
7405
8816
  source: "worker",
7406
8817
  type: "progress",
7407
- payload: { workerId, taskId: task.id, taskTitle: task.title, workerType, emoji: workerMeta.emoji, message: msg }
8818
+ payload: {
8819
+ workerId,
8820
+ taskId: task.id,
8821
+ taskTitle: task.title,
8822
+ workerType,
8823
+ emoji: workerMeta.emoji,
8824
+ message: msg,
8825
+ lastActivityAt: activity.lastActivityAt,
8826
+ currentTool: activity.currentTool
8827
+ }
7408
8828
  });
7409
8829
  },
7410
8830
  onComplete: (summary) => {
@@ -7436,7 +8856,7 @@ var TaskExecuteEngine = class {
7436
8856
  if (toolName === "bash") return `Execute shell command: ${args.command}`;
7437
8857
  if (FILE_TOOL_NAMES.includes(toolName)) {
7438
8858
  const rawPath = typeof args.path === "string" ? args.path : "";
7439
- const resolved = rawPath ? resolve8(workspace, rawPath) : workspace;
8859
+ const resolved = rawPath ? resolve9(workspace, rawPath) : workspace;
7440
8860
  return `${toolName}: ${rawPath || "(workspace root)"} (resolved: ${resolved})`;
7441
8861
  }
7442
8862
  return `${toolName}: ${JSON.stringify(args)}`;
@@ -7672,13 +9092,21 @@ var ControllerLoop = class {
7672
9092
  payload: {
7673
9093
  reason: "Workers are running but no signals for a while. Review status and decide if action is needed.",
7674
9094
  idleSinceMs: now - lastWakeAt,
7675
- workers: activeWorkers.map((w) => ({
7676
- workerId: w.workerId,
7677
- taskId: w.taskId,
7678
- taskTitle: w.taskTitle,
7679
- workerType: w.workerType,
7680
- runningForMs: now - w.startedAt
7681
- })),
9095
+ workers: activeWorkers.map((w) => {
9096
+ const activity = this.heartbeatProvider.getWorkerActivity?.(w.workerId);
9097
+ return {
9098
+ workerId: w.workerId,
9099
+ taskId: w.taskId,
9100
+ taskTitle: w.taskTitle,
9101
+ workerType: w.workerType,
9102
+ runningForMs: now - w.startedAt,
9103
+ ...activity ? {
9104
+ lastActivityAt: activity.lastActivityAt,
9105
+ lastActivityAgoMs: activity.lastActivityAt === null ? null : Math.max(0, now - activity.lastActivityAt),
9106
+ currentTool: activity.currentTool
9107
+ } : {}
9108
+ };
9109
+ }),
7682
9110
  pendingTaskCount: this.heartbeatProvider.getPendingCount()
7683
9111
  }
7684
9112
  });
@@ -7695,11 +9123,11 @@ var ControllerLoop = class {
7695
9123
  import * as path5 from "path";
7696
9124
 
7697
9125
  // src/services/session/session-state.ts
7698
- import { join as join8 } from "path";
9126
+ import { join as join10 } from "path";
7699
9127
  var SessionState = class extends BaseWayangState {
7700
9128
  jsonFile;
7701
9129
  constructor(sessionDir) {
7702
- const jsonFile = new JSONFileHelper(join8(sessionDir, "meta.json"));
9130
+ const jsonFile = new JSONFileHelper(join10(sessionDir, "meta.json"));
7703
9131
  super(
7704
9132
  {
7705
9133
  meta: {
@@ -7805,7 +9233,7 @@ var SessionManager = class _SessionManager {
7805
9233
  };
7806
9234
 
7807
9235
  // src/services/agents/controller-state.ts
7808
- import { join as join10 } from "path";
9236
+ import { join as join12 } from "path";
7809
9237
 
7810
9238
  // src/services/conversation/conversation-index.ts
7811
9239
  function emptyUsage() {
@@ -8026,8 +9454,8 @@ var ControllerAgentState = class extends BaseWayangState {
8026
9454
  /** In-memory resolve function for the current inquiry — never persisted. */
8027
9455
  inquiryResolver = null;
8028
9456
  constructor(ctx) {
8029
- const jsonFile = new JSONFileHelper(join10(ctx.sessionDir, "runtime-state.json"));
8030
- const jsonlFile = new JSONLFileHelper(join10(ctx.sessionDir, "conversation.jsonl"));
9457
+ const jsonFile = new JSONFileHelper(join12(ctx.sessionDir, "runtime-state.json"));
9458
+ const jsonlFile = new JSONLFileHelper(join12(ctx.sessionDir, "conversation.jsonl"));
8031
9459
  const initialData = {
8032
9460
  runtimeState: {
8033
9461
  session: { id: "", startedAt: 0 },
@@ -8102,8 +9530,8 @@ var ControllerAgentState = class extends BaseWayangState {
8102
9530
  }
8103
9531
  /** Set a pending inquiry and return a Promise that resolves when the user answers. */
8104
9532
  askInquiry(question) {
8105
- return new Promise((resolve11) => {
8106
- this.inquiryResolver = resolve11;
9533
+ return new Promise((resolve12) => {
9534
+ this.inquiryResolver = resolve12;
8107
9535
  this.set("runtimeState.pendingInquiry", question);
8108
9536
  });
8109
9537
  }
@@ -8263,6 +9691,19 @@ var ControllerAgent = class _ControllerAgent extends BaseAgent {
8263
9691
  listTasks: (status) => engine.list(status),
8264
9692
  getTask: (taskId) => engine.get(taskId),
8265
9693
  getWorkerConversation: (taskId) => engine.getWorkerConversation(taskId),
9694
+ // On-disk transcript access (issue #16): survives worker disposal, so a
9695
+ // finished/cancelled worker's scene stays readable by the controller.
9696
+ readTranscript: (workerId) => readWorkerTranscript(ctx.sessionDir, workerId),
9697
+ // Layered transcript access (issue #16 review): while the engine tracks
9698
+ // the worker, its live in-memory conversation is the source; the on-disk
9699
+ // jsonl lives under the worker instance's OWN id (agent-/cw-…), which
9700
+ // the engine exposes and tasks persist as workerTranscriptId.
9701
+ getWorkerConversationByWorkerId: (workerId) => engine.getWorkerConversationByWorkerId(workerId),
9702
+ getWorkerTranscriptId: (workerId) => engine.getWorkerTranscriptId(workerId),
9703
+ readTranscriptWindow: (workerId, opts2) => readWorkerTranscriptWindow(ctx.sessionDir, workerId, opts2),
9704
+ sessionDir: ctx.sessionDir,
9705
+ getActiveWorkers: () => engine.getActiveWorkers(),
9706
+ getWorkerLiveStatus: (workerId) => engine.getWorkerLiveStatus(workerId),
8266
9707
  cancelTask: (taskId) => engine.cancel(taskId),
8267
9708
  abortWorker: (taskId) => engine.abortByTaskId(taskId),
8268
9709
  updateTask: (taskId, updates) => engine.updatePending(taskId, updates),
@@ -8562,6 +10003,14 @@ function isoMs(ms) {
8562
10003
  if (typeof ms !== "number" || !Number.isFinite(ms)) return void 0;
8563
10004
  return new Date(ms).toISOString();
8564
10005
  }
10006
+ function signalActivityTail(currentTool, lastActivityAt) {
10007
+ const parts = [];
10008
+ if (currentTool) parts.push(`tool=${currentTool}`);
10009
+ if (typeof lastActivityAt === "number" && Number.isFinite(lastActivityAt)) {
10010
+ parts.push(`lastEntry=${new Date(lastActivityAt).toISOString().replace(/\.\d{3}Z$/, "Z")}`);
10011
+ }
10012
+ return parts.length > 0 ? `[activity: ${parts.join(", ")}]` : null;
10013
+ }
8565
10014
  var signalConverters = {
8566
10015
  input: (sig, ts, sessionId) => ({
8567
10016
  type: "user" /* User */,
@@ -8603,6 +10052,7 @@ var signalConverters = {
8603
10052
  },
8604
10053
  progress: (sig, ts, sessionId) => {
8605
10054
  const p = sig.payload;
10055
+ const activity = signalActivityTail(p.currentTool, p.lastActivityAt);
8606
10056
  return {
8607
10057
  ...signalBase(ts, sessionId),
8608
10058
  subtype: "worker_progress" /* WorkerProgress */,
@@ -8611,12 +10061,17 @@ var signalConverters = {
8611
10061
  emoji: p.emoji,
8612
10062
  taskId: p.taskId,
8613
10063
  taskTitle: p.taskTitle,
8614
- content: p.message
10064
+ content: activity ? `${p.message}
10065
+ ${activity}` : p.message
8615
10066
  };
8616
10067
  },
8617
10068
  heartbeat: (sig, ts, sessionId) => {
8618
10069
  const p = sig.payload;
8619
- const workerSummary = p.workers.map((w) => `${w.taskTitle} (${w.workerType}, running ${Math.round(w.runningForMs / 1e3)}s)`).join(", ");
10070
+ const workerSummary = p.workers.map((w) => {
10071
+ const tool2 = w.currentTool ? `, tool ${w.currentTool}` : "";
10072
+ const idle = w.lastActivityAgoMs !== null && w.lastActivityAgoMs !== void 0 ? `, last activity ${Math.round(w.lastActivityAgoMs / 1e3)}s ago` : "";
10073
+ return `${w.taskTitle} [${w.workerId}] (${w.workerType}, running ${Math.round(w.runningForMs / 1e3)}s${tool2}${idle})`;
10074
+ }).join(", ");
8620
10075
  return {
8621
10076
  ...signalBase(ts, sessionId),
8622
10077
  subtype: "heartbeat" /* Heartbeat */,
@@ -8779,8 +10234,8 @@ function signalToEntry(sig, ts, sessionId) {
8779
10234
  }
8780
10235
 
8781
10236
  // src/services/skills/registry.ts
8782
- import { readFileSync as readFileSync8, readdirSync as readdirSync2, statSync } from "fs";
8783
- import { join as join11 } from "path";
10237
+ import { readFileSync as readFileSync9, readdirSync as readdirSync2, statSync as statSync3 } from "fs";
10238
+ import { join as join13 } from "path";
8784
10239
  import { parse as parseYaml } from "yaml";
8785
10240
  var SKILL_FILE = "SKILL.md";
8786
10241
  function parseFrontmatter(raw) {
@@ -8815,7 +10270,7 @@ function listResources(skillDir) {
8815
10270
  const walk = (relDir) => {
8816
10271
  let entries;
8817
10272
  try {
8818
- entries = readdirSync2(join11(skillDir, relDir), { withFileTypes: true });
10273
+ entries = readdirSync2(join13(skillDir, relDir), { withFileTypes: true });
8819
10274
  } catch {
8820
10275
  return;
8821
10276
  }
@@ -8862,10 +10317,10 @@ var SkillRegistry = class {
8862
10317
  if (existing) return existing;
8863
10318
  const desc = this.descriptors.get(name);
8864
10319
  if (!desc) return void 0;
8865
- const filePath = join11(desc.dir, SKILL_FILE);
10320
+ const filePath = join13(desc.dir, SKILL_FILE);
8866
10321
  let raw;
8867
10322
  try {
8868
- raw = readFileSync8(filePath, "utf-8");
10323
+ raw = readFileSync9(filePath, "utf-8");
8869
10324
  } catch (err) {
8870
10325
  this.logger?.warn({ skill: name, error: err instanceof Error ? err.message : String(err) }, "Failed to read skill file");
8871
10326
  return void 0;
@@ -8893,8 +10348,8 @@ var SkillRegistry = class {
8893
10348
  for (const entry of entries) {
8894
10349
  if (!entry.isDirectory()) continue;
8895
10350
  if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
8896
- const skillDir = join11(dir, entry.name);
8897
- const skillFile = join11(skillDir, SKILL_FILE);
10351
+ const skillDir = join13(dir, entry.name);
10352
+ const skillFile = join13(skillDir, SKILL_FILE);
8898
10353
  if (!existsAsFile(skillFile)) continue;
8899
10354
  const { frontmatter } = safeReadFrontmatter(skillFile);
8900
10355
  const description = asString(frontmatter.description);
@@ -8909,35 +10364,35 @@ var SkillRegistry = class {
8909
10364
  };
8910
10365
  function existsAsFile(path7) {
8911
10366
  try {
8912
- return statSync(path7).isFile();
10367
+ return statSync3(path7).isFile();
8913
10368
  } catch {
8914
10369
  return false;
8915
10370
  }
8916
10371
  }
8917
10372
  function safeReadFrontmatter(path7) {
8918
10373
  try {
8919
- return parseFrontmatter(readFileSync8(path7, "utf-8"));
10374
+ return parseFrontmatter(readFileSync9(path7, "utf-8"));
8920
10375
  } catch {
8921
10376
  return { frontmatter: {}, body: "" };
8922
10377
  }
8923
10378
  }
8924
10379
 
8925
10380
  // src/services/skills/skill-dir.ts
8926
- import { existsSync as existsSync6 } from "fs";
10381
+ import { existsSync as existsSync8 } from "fs";
8927
10382
  import { homedir as homedir2 } from "os";
8928
- import { join as join12, resolve as resolve9 } from "path";
10383
+ import { join as join14, resolve as resolve10 } from "path";
8929
10384
  function resolveSkillDirs(workspaceDir2, configDirs) {
8930
10385
  const candidates = [
8931
- join12(homedir2(), ".wayang", "skills"),
8932
- join12(workspaceDir2, ".wayang", "skills"),
8933
- ...(configDirs ?? []).map((d) => resolve9(d))
10386
+ join14(homedir2(), ".wayang", "skills"),
10387
+ join14(workspaceDir2, ".wayang", "skills"),
10388
+ ...(configDirs ?? []).map((d) => resolve10(d))
8934
10389
  ];
8935
10390
  const seen = /* @__PURE__ */ new Set();
8936
10391
  const dirs = [];
8937
10392
  for (const dir of candidates) {
8938
10393
  if (seen.has(dir)) continue;
8939
10394
  seen.add(dir);
8940
- if (existsSync6(dir)) dirs.push(dir);
10395
+ if (existsSync8(dir)) dirs.push(dir);
8941
10396
  }
8942
10397
  return dirs;
8943
10398
  }
@@ -8996,7 +10451,8 @@ var Supervisor = class {
8996
10451
  {
8997
10452
  getRunningCount: () => this.engine.getRunningCount(),
8998
10453
  getActiveWorkers: () => this.engine.getActiveWorkers(),
8999
- getPendingCount: () => this.engine.list("pending").length
10454
+ getPendingCount: () => this.engine.list("pending").length,
10455
+ getWorkerActivity: (workerId) => this.engine.getWorkerActivity(workerId)
9000
10456
  }
9001
10457
  );
9002
10458
  }