@sema-agent/core 1.450.0 → 1.452.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 (57) hide show
  1. package/dist/agents/observer.js +3 -0
  2. package/dist/agents/subagent.js +51 -12
  3. package/dist/core/ask-question.js +5 -5
  4. package/dist/core/background-agent-store.d.ts +2 -0
  5. package/dist/core/background-agent-store.js +5 -1
  6. package/dist/core/exec-output-tail.d.ts +18 -1
  7. package/dist/core/exec-output-tail.js +38 -5
  8. package/dist/core/lsp-session.d.ts +1 -0
  9. package/dist/core/lsp-session.js +24 -6
  10. package/dist/core/lsp.d.ts +10 -0
  11. package/dist/core/lsp.js +63 -6
  12. package/dist/core/mailbox-store.d.ts +3 -2
  13. package/dist/core/mailbox-store.js +19 -4
  14. package/dist/core/memory.js +1 -1
  15. package/dist/core/runner/prepare-task.js +11 -1
  16. package/dist/core/runner/runtask.js +1 -1
  17. package/dist/core/session-reconcile.js +5 -2
  18. package/dist/core/task-notification.d.ts +1 -0
  19. package/dist/core/task-registry.d.ts +15 -9
  20. package/dist/core/task-registry.js +290 -53
  21. package/dist/core/tool-result-store.js +2 -2
  22. package/dist/core/tools.d.ts +5 -0
  23. package/dist/core/tools.js +3 -0
  24. package/dist/core/types.d.ts +2 -0
  25. package/dist/core/workflow-journal-store.d.ts +2 -0
  26. package/dist/core/workflow-journal-store.js +14 -0
  27. package/dist/engine/execution-env/node-execution-env.d.ts +1 -0
  28. package/dist/engine/execution-env/node-execution-env.js +130 -20
  29. package/dist/engine/lsp/node-lsp-manager.d.ts +3 -1
  30. package/dist/engine/lsp/node-lsp-manager.js +22 -5
  31. package/dist/engine/lsp/stdio-lsp-transport.d.ts +1 -1
  32. package/dist/engine/lsp/stdio-lsp-transport.js +17 -6
  33. package/dist/index.d.ts +2 -2
  34. package/dist/index.js +2 -2
  35. package/dist/orchestration/run-workflow-tool.d.ts +2 -0
  36. package/dist/orchestration/run-workflow-tool.js +35 -6
  37. package/dist/orchestration/workflow.d.ts +9 -0
  38. package/dist/orchestration/workflow.js +80 -5
  39. package/dist/stores/cc/mailbox-store.js +6 -1
  40. package/dist/stores/file/background-agent-store.js +3 -2
  41. package/dist/stores/file/mailbox-store.d.ts +1 -1
  42. package/dist/stores/file/mailbox-store.js +9 -7
  43. package/dist/tools/fs/encoding.d.ts +5 -0
  44. package/dist/tools/fs/encoding.js +6 -0
  45. package/dist/tools/fs/index.js +184 -120
  46. package/dist/tools/fs/notebook.d.ts +43 -0
  47. package/dist/tools/fs/notebook.js +141 -0
  48. package/dist/tools/fs/repo-map.js +2 -2
  49. package/dist/tools/fs/search.js +141 -12
  50. package/dist/tools/gitea-issue.js +4 -2
  51. package/dist/tools/monitor.js +12 -8
  52. package/dist/tools/scheduler-tools.js +16 -16
  53. package/dist/tools/task-list.js +34 -12
  54. package/dist/tools/web.d.ts +2 -0
  55. package/dist/tools/web.js +105 -19
  56. package/dist/tools/worktree.js +14 -14
  57. package/package.json +1 -1
@@ -0,0 +1,43 @@
1
+ export declare function isNotebookPath(p: string): boolean;
2
+ export declare const NOTEBOOK_CELL_OUTPUT_POINTER_CHARS = 10000;
3
+ export declare const NOTEBOOK_OUTPUT_TEXT_CLIP = 10000;
4
+ export declare const NOTEBOOK_IMAGE_BASE64_BUDGET = 262144;
5
+ export interface NotebookOutputImage {
6
+ image_data: string;
7
+ media_type: "image/png" | "image/jpeg";
8
+ }
9
+ export interface NotebookOutput {
10
+ output_type: string;
11
+ text?: string;
12
+ image?: NotebookOutputImage;
13
+ }
14
+ export interface NotebookCell {
15
+ cell_id: string;
16
+ cellType: string;
17
+ source: string;
18
+ language?: string;
19
+ execution_count?: number;
20
+ outputs?: NotebookOutput[];
21
+ }
22
+ export type ParsedNotebook = {
23
+ ok: true;
24
+ cells: NotebookCell[];
25
+ } | {
26
+ ok: false;
27
+ reason: "json" | "shape";
28
+ };
29
+ export declare function parseNotebookCells(text: string, notebookPath: string): ParsedNotebook;
30
+ export interface NotebookRender {
31
+ blocks: Array<{
32
+ type: "text";
33
+ text: string;
34
+ } | {
35
+ type: "image";
36
+ data: string;
37
+ mimeType: string;
38
+ }>;
39
+ textChars: number;
40
+ imageChars: number;
41
+ }
42
+ export declare function renderNotebookCells(cells: readonly NotebookCell[], imageBase64Budget: number): NotebookRender;
43
+ export declare function stripNotebookImageData(cell: NotebookCell): NotebookCell;
@@ -0,0 +1,141 @@
1
+ import { imageMagicMatches } from "./safety.js";
2
+ export function isNotebookPath(p) {
3
+ return p.toLowerCase().endsWith(".ipynb");
4
+ }
5
+ export const NOTEBOOK_CELL_OUTPUT_POINTER_CHARS = 10_000;
6
+ export const NOTEBOOK_OUTPUT_TEXT_CLIP = 10_000;
7
+ export const NOTEBOOK_IMAGE_BASE64_BUDGET = 262_144;
8
+ function joinedText(v) {
9
+ if (v === undefined || v === null)
10
+ return "";
11
+ return Array.isArray(v) ? v.join("") : String(v);
12
+ }
13
+ function clipOutputText(s) {
14
+ if (s.length <= NOTEBOOK_OUTPUT_TEXT_CLIP)
15
+ return s;
16
+ const droppedLines = s.slice(NOTEBOOK_OUTPUT_TEXT_CLIP).split("\n").length;
17
+ return `${s.slice(0, NOTEBOOK_OUTPUT_TEXT_CLIP)}\n\n… [${droppedLines} lines truncated] …`;
18
+ }
19
+ function imageFromData(data) {
20
+ if (data === undefined)
21
+ return undefined;
22
+ for (const mime of ["image/png", "image/jpeg"]) {
23
+ const v = data[mime];
24
+ if (typeof v !== "string")
25
+ continue;
26
+ const stripped = v.replace(/\s/g, "");
27
+ if (!imageMagicMatches(Buffer.from(stripped, "base64"), mime))
28
+ return undefined;
29
+ return { image_data: stripped, media_type: mime };
30
+ }
31
+ return undefined;
32
+ }
33
+ function mapOutput(raw) {
34
+ if (raw === null || typeof raw !== "object")
35
+ return undefined;
36
+ const o = raw;
37
+ switch (o.output_type) {
38
+ case "stream":
39
+ return { output_type: "stream", text: clipOutputText(joinedText(o.text)) };
40
+ case "execute_result":
41
+ case "display_data": {
42
+ const data = o.data !== null && typeof o.data === "object" ? o.data : undefined;
43
+ const image = imageFromData(data);
44
+ return { output_type: o.output_type, text: clipOutputText(joinedText(data?.["text/plain"])), ...(image ? { image } : {}) };
45
+ }
46
+ case "error": {
47
+ const tb = Array.isArray(o.traceback) ? o.traceback.join("\n") : "";
48
+ return { output_type: "error", text: clipOutputText(`${joinedText(o.ename)}: ${joinedText(o.evalue)}\n${tb}`) };
49
+ }
50
+ default:
51
+ return undefined;
52
+ }
53
+ }
54
+ export function parseNotebookCells(text, notebookPath) {
55
+ let nb;
56
+ try {
57
+ nb = JSON.parse(text);
58
+ }
59
+ catch {
60
+ return { ok: false, reason: "json" };
61
+ }
62
+ const top = nb;
63
+ const rawCells = top?.cells;
64
+ if (!Array.isArray(rawCells) || rawCells.some((c) => c === null || typeof c !== "object")) {
65
+ return { ok: false, reason: "shape" };
66
+ }
67
+ const language = typeof top?.metadata?.language_info?.name === "string" ? top.metadata.language_info.name : "python";
68
+ const cells = rawCells.map((raw, i) => {
69
+ const c = raw;
70
+ const cellType = typeof c.cell_type === "string" ? c.cell_type : "code";
71
+ const cell = {
72
+ cell_id: typeof c.id === "string" ? c.id : `cell-${i}`,
73
+ cellType,
74
+ source: joinedText(c.source),
75
+ };
76
+ if (cellType === "code") {
77
+ cell.language = language;
78
+ if (typeof c.execution_count === "number" && c.execution_count)
79
+ cell.execution_count = c.execution_count;
80
+ if (Array.isArray(c.outputs) && c.outputs.length > 0) {
81
+ const mapped = c.outputs.map(mapOutput).filter((o) => o !== undefined);
82
+ let sum = 0;
83
+ let over = false;
84
+ for (const o of mapped) {
85
+ sum += (o.text?.length ?? 0) + (o.image?.image_data.length ?? 0);
86
+ if (sum > NOTEBOOK_CELL_OUTPUT_POINTER_CHARS) {
87
+ over = true;
88
+ break;
89
+ }
90
+ }
91
+ cell.outputs = over
92
+ ? [{ output_type: "stream", text: `Outputs are too large to include. Use Bash with: cat "${notebookPath}" | jq '.cells[${i}].outputs'` }]
93
+ : mapped;
94
+ }
95
+ }
96
+ return cell;
97
+ });
98
+ return { ok: true, cells };
99
+ }
100
+ export function renderNotebookCells(cells, imageBase64Budget) {
101
+ const blocks = [];
102
+ let imageChars = 0;
103
+ const pushText = (t) => {
104
+ const last = blocks[blocks.length - 1];
105
+ if (last !== undefined && last.type === "text")
106
+ last.text += `\n${t}`;
107
+ else
108
+ blocks.push({ type: "text", text: t });
109
+ };
110
+ for (const cell of cells) {
111
+ const tags = [];
112
+ if (cell.cellType !== "code")
113
+ tags.push(`<cell_type>${cell.cellType}</cell_type>`);
114
+ if (cell.language !== undefined && cell.language !== "python" && cell.cellType === "code")
115
+ tags.push(`<language>${cell.language}</language>`);
116
+ pushText(`<cell id="${cell.cell_id}">${tags.join("")}${cell.source}</cell id="${cell.cell_id}">`);
117
+ for (const o of cell.outputs ?? []) {
118
+ if (o.text !== undefined && o.text !== "")
119
+ pushText(`\n${o.text}`);
120
+ if (o.image !== undefined) {
121
+ if (imageChars + o.image.image_data.length > imageBase64Budget) {
122
+ pushText("[image output omitted: notebook image budget exceeded]");
123
+ }
124
+ else {
125
+ imageChars += o.image.image_data.length;
126
+ blocks.push({ type: "image", data: o.image.image_data, mimeType: o.image.media_type });
127
+ }
128
+ }
129
+ }
130
+ }
131
+ const textChars = blocks.reduce((n, b) => n + (b.type === "text" ? b.text.length : 0), 0);
132
+ return { blocks, textChars, imageChars };
133
+ }
134
+ export function stripNotebookImageData(cell) {
135
+ if (cell.outputs === undefined)
136
+ return cell;
137
+ return {
138
+ ...cell,
139
+ outputs: cell.outputs.map((o) => o.image === undefined ? o : { ...o, image: { ...o.image, image_data: `[${o.image.image_data.length} base64 chars omitted]` } }),
140
+ };
141
+ }
@@ -1,5 +1,5 @@
1
1
  import { Type } from "typebox";
2
- import { defineTool } from "../../core/tools.js";
2
+ import { defineTool, errorResult } from "../../core/tools.js";
3
3
  import { resolveKey, violationText } from "./safety.js";
4
4
  import { buildIgnore, walk, walkIsPartial } from "./search.js";
5
5
  const DEFAULT_MAX_CHARS = 16_000;
@@ -101,7 +101,7 @@ export function makeRepoMapTool(env, rootCanonical, additionalRoots) {
101
101
  if (a.path !== undefined) {
102
102
  const r = await resolveKey(env, rootCanonical, a.path, signal, rootCanonical, additionalRoots);
103
103
  if (!r.ok)
104
- return violationText("RepoMap", r.violation);
104
+ return errorResult(violationText("RepoMap", r.violation));
105
105
  start = r.key;
106
106
  }
107
107
  const maxChars = Math.max(1, Math.floor(a.max_chars ?? DEFAULT_MAX_CHARS));
@@ -76,7 +76,7 @@ export function splitAbsoluteGlobPattern(pattern) {
76
76
  export function shellQuote(s) {
77
77
  return `'${s.replace(/'/g, `'\\''`)}'`;
78
78
  }
79
- function globTokenToRegExp(token, anchored, braces = false) {
79
+ function globTokenToRegExp(token, anchored, glob = false) {
80
80
  const charToRe = (ch) => ch === "*" ? "[^/]*" : ch === "?" ? "[^/]" : ch.replace(/[.+^${}()|[\]\\]/g, "\\$&");
81
81
  let re = "";
82
82
  for (let i = 0; i < token.length; i++) {
@@ -93,7 +93,7 @@ function globTokenToRegExp(token, anchored, braces = false) {
93
93
  }
94
94
  else if (c === "?")
95
95
  re += "[^/]";
96
- else if (braces && c === "{") {
96
+ else if (glob && c === "{") {
97
97
  const close = token.indexOf("}", i);
98
98
  if (close > i + 1) {
99
99
  const alts = token.slice(i + 1, close).split(",").map((alt) => [...alt].map(charToRe).join(""));
@@ -103,11 +103,52 @@ function globTokenToRegExp(token, anchored, braces = false) {
103
103
  else
104
104
  re += "\\{";
105
105
  }
106
+ else if (glob && c === "[") {
107
+ let j = i + 1;
108
+ let neg = false;
109
+ if (token[j] === "!" || token[j] === "^") {
110
+ neg = true;
111
+ j++;
112
+ }
113
+ const bodyStart = j;
114
+ if (token[j] === "]")
115
+ j++;
116
+ const close = token.indexOf("]", j);
117
+ let cls;
118
+ if (close !== -1) {
119
+ const body = token.slice(bodyStart, close).replace(/[\\[\]]/g, "\\$&").replace(/\//g, "");
120
+ cls = `[${neg ? "^/" : ""}${body}]`;
121
+ try {
122
+ new RegExp(cls);
123
+ }
124
+ catch {
125
+ cls = undefined;
126
+ }
127
+ }
128
+ if (cls !== undefined) {
129
+ re += cls;
130
+ i = close;
131
+ }
132
+ else
133
+ re += "\\[";
134
+ }
106
135
  else
107
136
  re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
108
137
  }
109
138
  return new RegExp(`${anchored ? "^" : "(^|/)"}${re}$`);
110
139
  }
140
+ function normalizeGlobToken(token) {
141
+ let t = token;
142
+ while (t.startsWith("./"))
143
+ t = t.slice(2);
144
+ return t.replace(/^\/+/, "");
145
+ }
146
+ function globIsAnchored(token) {
147
+ let t = token;
148
+ while (t.startsWith("./"))
149
+ t = t.slice(2);
150
+ return t.includes("/");
151
+ }
111
152
  export function gitignoreMatcher(content) {
112
153
  const rules = content
113
154
  .split(/\r?\n/)
@@ -736,9 +777,9 @@ export async function jsGrep(env, root, p, signal, guards) {
736
777
  return p.path || r === abs ? r : `./${r}`;
737
778
  };
738
779
  if (p.glob) {
739
- const globRes = splitGlobParam(p.glob).map((g) => globTokenToRegExp(g, false, true));
780
+ const globRes = splitGlobParam(p.glob).map((g) => globTokenToRegExp(normalizeGlobToken(g), globIsAnchored(g), true));
740
781
  if (globRes.length > 0)
741
- files = files.filter((f) => globRes.some((re) => re.test(rel(f))));
782
+ files = files.filter((f) => globRes.some((re) => re.test(rel(f).replace(/\\/g, "/"))));
742
783
  }
743
784
  let typeNote = "";
744
785
  if (p.type) {
@@ -863,8 +904,8 @@ export async function jsGrep(env, root, p, signal, guards) {
863
904
  totalContent += fileCount;
864
905
  if (fileCount > 0) {
865
906
  totalFiles++;
866
- if (mode === "files_with_matches" && fileMatches.length < collectCap)
867
- fileMatches.push(relOut(f));
907
+ if (mode === "files_with_matches")
908
+ fileMatches.push(f);
868
909
  else if (mode === "count" && counts.length < collectCap)
869
910
  counts.push(`${relOut(f)}:${fileCount}`);
870
911
  }
@@ -888,7 +929,14 @@ export async function jsGrep(env, root, p, signal, guards) {
888
929
  const paged = (arr) => (off > 0 ? arr.slice(off, off + cap) : arr);
889
930
  const offNote = off > 0 ? `\n[offset ${off}]` : "";
890
931
  if (mode === "files_with_matches") {
891
- const body = paged(fileMatches);
932
+ const haveMtimes = fileMatches.length > 0 && fileMatches.every((f) => walked.mtimes.has(f));
933
+ const sortedFileMatches = haveMtimes
934
+ ? fileMatches
935
+ .map((f) => ({ r: relOut(f), m: walked.mtimes.get(f) }))
936
+ .sort((a, b) => b.m - a.m || (a.r < b.r ? -1 : a.r > b.r ? 1 : 0))
937
+ .map((x) => x.r)
938
+ : fileMatches.map(relOut).sort();
939
+ const body = sortedFileMatches.slice(off, off + cap);
892
940
  return body.length === 0
893
941
  ? NO_MATCHES + offNote + typeNote + caveat
894
942
  : body.join("\n") + (totalFiles > off + cap ? `\n…[capped at ${cap} of ${totalFiles}]` : "") + offNote + typeNote + caveat;
@@ -921,6 +969,25 @@ export function detectRipgrep(env) {
921
969
  }
922
970
  return cached;
923
971
  }
972
+ async function sortRgFilesByMtime(env, root, stdout, signal) {
973
+ const paths = stdout.split("\n").filter((l) => l.length > 0);
974
+ if (paths.length === 0)
975
+ return stdout;
976
+ const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
977
+ const toAbs = (p) => {
978
+ const stripped = p.startsWith("./") ? p.slice(2) : p;
979
+ return stripped.startsWith("/") || /^[A-Za-z]:[\\/]/.test(stripped) ? stripped : `${rootPrefix}${stripped}`;
980
+ };
981
+ const infos = await Promise.all(paths.map((p) => env.fileInfo(toAbs(p), signal)));
982
+ const withMtime = paths.map((p, i) => {
983
+ const r = infos[i];
984
+ return { p, m: r.ok && typeof r.value.mtimeMs === "number" ? r.value.mtimeMs : undefined };
985
+ });
986
+ const sorted = withMtime.every((x) => x.m !== undefined)
987
+ ? [...withMtime].sort((a, b) => b.m - a.m || (a.p < b.p ? -1 : a.p > b.p ? 1 : 0))
988
+ : [...withMtime].sort((a, b) => (a.p < b.p ? -1 : a.p > b.p ? 1 : 0));
989
+ return sorted.map((x) => x.p).join("\n");
990
+ }
924
991
  function formatRgStdout(stdout, p, caveat = "") {
925
992
  const cap = p.head_limit === 0 ? Infinity : Math.max(1, Math.floor(p.head_limit ?? GREP_DEFAULT_CAP));
926
993
  const off = Math.max(0, Math.floor(p.offset ?? 0));
@@ -1003,7 +1070,8 @@ export async function rgGrepDetailed(env, root, p, signal) {
1003
1070
  }
1004
1071
  return jsGrepFallback(env, root, p, signal, `exited with code ${exitCode} and produced no output`);
1005
1072
  }
1006
- return { text: formatRgStdout(stdout, p) };
1073
+ const orderedStdout = mode === "files_with_matches" ? await sortRgFilesByMtime(env, root, stdout, signal) : stdout;
1074
+ return { text: formatRgStdout(orderedStdout, p) };
1007
1075
  }
1008
1076
  export async function runGrepDetailed(env, root, p, signal) {
1009
1077
  if (await detectRipgrep(env))
@@ -1018,16 +1086,74 @@ export async function runGlob(env, root, pattern, opts = {}, signal) {
1018
1086
  }
1019
1087
  export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1020
1088
  const t0 = Date.now();
1021
- const ignore = await buildIgnore(env, root, signal);
1089
+ const baseIgnore = await buildIgnore(env, root, signal);
1022
1090
  const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
1023
1091
  const start = opts.path ? (opts.path.startsWith("/") || /^[A-Za-z]:[\\/]/.test(opts.path) ? opts.path : `${rootPrefix}${opts.path}`) : root;
1092
+ const pat = normalizeGlobToken(pattern);
1093
+ const anchored = globIsAnchored(pattern);
1094
+ const re = globTokenToRegExp(pat, anchored, true);
1095
+ const startPrefix = start.replace(/[\\/]+$/, "") + (start.includes("\\") ? "\\" : "/");
1096
+ const startRelPrefix = startPrefix.startsWith(rootPrefix) ? startPrefix.slice(rootPrefix.length) : startPrefix;
1097
+ const toStartRel = (p) => {
1098
+ const s = p.startsWith(startPrefix)
1099
+ ? p.slice(startPrefix.length)
1100
+ : startRelPrefix.length > 0 && p.startsWith(startRelPrefix)
1101
+ ? p.slice(startRelPrefix.length)
1102
+ : p;
1103
+ return s.replace(/\\/g, "/");
1104
+ };
1105
+ const patternSegs = pat.split("/");
1106
+ const groupSpansSlash = /\{[^}]*\/[^}]*\}|\[[^\]]*\/[^\]]*\]/.test(pat);
1107
+ const prunable = anchored && !groupSpansSlash;
1108
+ const firstStarStar = patternSegs.findIndex((s) => s.includes("**"));
1109
+ const starStarIdx = firstStarStar === -1 ? Infinity : firstStarStar;
1110
+ const segRes = prunable ? patternSegs.map((s) => globTokenToRegExp(s, true, true)) : [];
1111
+ const isLiteralSeg = (s) => s.length > 0 && !/[*?[{]/.test(s);
1112
+ const literalAt = prunable ? patternSegs.map((s) => (isLiteralSeg(s) ? s : undefined)) : [];
1113
+ const literalAfterStarStar = new Set(prunable ? patternSegs.filter((s, i) => i > starStarIdx && isLiteralSeg(s)) : []);
1114
+ const mayContainMatch = (dirRel) => {
1115
+ if (!prunable)
1116
+ return true;
1117
+ const d = dirRel.split("/");
1118
+ if (d.length >= patternSegs.length && starStarIdx === Infinity)
1119
+ return false;
1120
+ const lim = Math.min(d.length, starStarIdx);
1121
+ for (let i = 0; i < lim; i++)
1122
+ if (!segRes[i].test(d[i]))
1123
+ return false;
1124
+ return true;
1125
+ };
1126
+ const explicitlyNamed = (sr) => {
1127
+ if (!prunable)
1128
+ return false;
1129
+ const segs = sr.split("/");
1130
+ const k = segs.length - 1;
1131
+ return k < starStarIdx ? literalAt[k] === segs[k] : literalAfterStarStar.has(segs[k]);
1132
+ };
1133
+ let ignoredDirs = 0;
1134
+ const rescued = [];
1135
+ const ignore = (relPath, isDir) => {
1136
+ const sr = toStartRel(relPath);
1137
+ if (isDir && !mayContainMatch(sr))
1138
+ return true;
1139
+ if (explicitlyNamed(sr)) {
1140
+ if (isDir && baseIgnore(relPath, true))
1141
+ rescued.push(sr + "/");
1142
+ return false;
1143
+ }
1144
+ if (rescued.length > 0 && rescued.some((r) => sr.startsWith(r)))
1145
+ return false;
1146
+ const ig = baseIgnore(relPath, isDir);
1147
+ if (ig && isDir)
1148
+ ignoredDirs++;
1149
+ return ig;
1150
+ };
1024
1151
  const walked = await walk(env, root, start, ignore, signal);
1025
1152
  const caveat = walkCaveat(walked, false);
1026
1153
  const files = [...walked.files, ...walked.nameOnly];
1027
- const re = globTokenToRegExp(pattern, false, true);
1028
1154
  const rel = (abs) => (abs.startsWith(rootPrefix) ? abs.slice(rootPrefix.length) : abs);
1029
1155
  const cap = Math.max(1, Math.floor(opts.max ?? 500));
1030
- const matchedAbs = files.filter((f) => re.test(rel(f)) || re.test(basename(f)));
1156
+ const matchedAbs = files.filter((f) => re.test(toStartRel(f)) || (!anchored && re.test(basename(f))));
1031
1157
  const haveMtimes = matchedAbs.length > 0 && matchedAbs.every((f) => walked.mtimes.has(f));
1032
1158
  const matchedAll = haveMtimes
1033
1159
  ? matchedAbs
@@ -1044,6 +1170,9 @@ export async function runGlobDetailed(env, root, pattern, opts = {}, signal) {
1044
1170
  : countIsComplete
1045
1171
  ? `\n(Showing ${matched.length} of ${totalMatches} matching files; ${totalMatches - matched.length} more are not listed. Narrow the pattern or path to see the rest.)`
1046
1172
  : `\n(Showing the first ${matched.length} files; there are more than ${totalMatches} matches. Narrow the pattern or path to see the rest.)`;
1047
- const text = matched.length === 0 ? "No files matched." + caveat : matched.join("\n") + capNote + caveat;
1173
+ const ignoreNote = matched.length === 0 && ignoredDirs > 0
1174
+ ? `\n[note: ${ignoredDirs} ignored director${ignoredDirs === 1 ? "y was" : "ies were"} not searched (dependency/build/VCS trees and .gitignore) — name a directory in the pattern (e.g. "dist/**") or pass \`path\` to include it]`
1175
+ : "";
1176
+ const text = matched.length === 0 ? "No files matched." + ignoreNote + caveat : matched.join("\n") + capNote + caveat;
1048
1177
  return { text, filenames: matched, numFiles: matched.length, truncated, durationMs: Date.now() - t0, totalMatches, countIsComplete };
1049
1178
  }
@@ -23,7 +23,7 @@ export function createGiteaIssueTool(opts) {
23
23
  const a = args;
24
24
  const title = String(a.title ?? "").trim();
25
25
  if (!title) {
26
- return { content: "Issue not created: a non-empty title is required.", details: { error: "empty title" } };
26
+ return { content: "Issue not created: a non-empty title is required.", details: { error: "empty title" }, isError: true };
27
27
  }
28
28
  const modelLabels = opts.allowModelLabels && Array.isArray(a.labels) ? a.labels : [];
29
29
  const labels = [...new Set([...(opts.defaultLabels ?? []), ...modelLabels])];
@@ -43,13 +43,14 @@ export function createGiteaIssueTool(opts) {
43
43
  }
44
44
  catch (e) {
45
45
  const msg = e instanceof Error ? e.message : String(e);
46
- return { content: `Issue not created: request failed (${msg}).`, details: { error: msg } };
46
+ return { content: `Issue not created: request failed (${msg}).`, details: { error: msg }, isError: true };
47
47
  }
48
48
  if (!res.ok) {
49
49
  const detail = await res.text().catch(() => "");
50
50
  return {
51
51
  content: `Issue not created: Gitea returned HTTP ${res.status}. ${detail.slice(0, 300)}`,
52
52
  details: { status: res.status },
53
+ isError: true,
53
54
  };
54
55
  }
55
56
  const PARSE_FAILED = Symbol("json-parse-failed");
@@ -60,6 +61,7 @@ export function createGiteaIssueTool(opts) {
60
61
  content: `Issue request returned HTTP ${res.status} but the response body was not a valid issue ` +
61
62
  `(no number) — the issue may NOT have been created; verify before retrying.`,
62
63
  details: { status: res.status, parsed: parsed === PARSE_FAILED ? "unparseable" : "no-number" },
64
+ isError: true,
63
65
  };
64
66
  }
65
67
  const issue = parsed;
@@ -1,5 +1,5 @@
1
1
  import { Type } from "typebox";
2
- import { defineTool } from "../core/tools.js";
2
+ import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { hasBackgroundShell } from "../core/background-shell.js";
4
4
  import { MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, } from "../core/task-registry.js";
5
5
  const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its stdout as a stream of events.\n" +
@@ -11,8 +11,9 @@ const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its
11
11
  "- stderr does NOT trigger notifications, but it is captured — read it any time with TaskOutput(task_id), " +
12
12
  "which also serves the full re-readable stdout spool.\n" +
13
13
  "- A monitor that produces too many events is stopped automatically (the stop notification says why).\n" +
14
- "- Set persistent: true for a session-resident watch with no timeout; stop it with TaskStop(task_id) or it " +
15
- "ends when the session is released.\n" +
14
+ "- Set persistent: true for a session-resident watch with no watch timeout of its own; it still ends when " +
15
+ "the execution environment's background time budget expires, when you stop it with TaskStop(task_id), or " +
16
+ "when the session is released.\n" +
16
17
  "- After starting a monitor you do not need to poll — end your turn if nothing else is pending; events will " +
17
18
  "reach you as notifications.\n" +
18
19
  "- Use this for logs, dev servers, build watchers, or any wait-for-a-condition loop (e.g. `tail -f app.log`, " +
@@ -44,14 +45,14 @@ export function createMonitorTool(env, opts) {
44
45
  description: `How long the watch may run before it is killed, in milliseconds (default ${MONITOR_DEFAULT_TIMEOUT_MS}, max ${MONITOR_MAX_TIMEOUT_MS}). Ignored when persistent is true.`,
45
46
  })),
46
47
  persistent: Type.Optional(Type.Boolean({
47
- description: "Set to true for a session-resident monitor with no timeout (default false). It keeps watching until you stop it with TaskStop or the session ends.",
48
+ description: "Set to true for a session-resident monitor with no watch timeout of its own (default false). It keeps watching until the execution environment's background time budget expires, until you stop it with TaskStop, or until the session ends.",
48
49
  })),
49
50
  }),
50
51
  effect: "write",
51
52
  execute: async (args, ctx) => {
52
53
  const { command, description, timeout_ms, persistent } = args;
53
54
  if (!hasBackgroundShell(env)) {
54
- return "Error (Monitor): this environment does not support background processes.";
55
+ return errorResult("Error (Monitor): this environment does not support background processes.");
55
56
  }
56
57
  const isPersistent = persistent === true;
57
58
  const timeoutMs = Math.min(MONITOR_MAX_TIMEOUT_MS, Math.max(1, Math.floor(timeout_ms ?? MONITOR_DEFAULT_TIMEOUT_MS)));
@@ -60,7 +61,7 @@ export function createMonitorTool(env, opts) {
60
61
  ...(isPersistent ? {} : { timeout: Math.ceil(timeoutMs / 1000) + 5 }),
61
62
  });
62
63
  if (!spawn.ok)
63
- return `Error (Monitor): ${spawn.error.message}`;
64
+ return errorResult(`Error (Monitor): ${spawn.error.message}`);
64
65
  const sessionScoped = isPersistent && opts.sessionId !== undefined;
65
66
  const owner = sessionScoped ? opts.sessionId : (ctx.taskId ?? opts.owner);
66
67
  const scope = ctx.principal ?? opts.scope;
@@ -85,10 +86,13 @@ export function createMonitorTool(env, opts) {
85
86
  }
86
87
  catch (e) {
87
88
  await env.killBackground(spawn.value.shellId).catch(() => undefined);
88
- return `Error (Monitor): the watch process was started but could not be registered (${e instanceof Error ? e.message : String(e)}); it has been terminated.`;
89
+ return errorResult(`Error (Monitor): the watch process was started but could not be registered (${e instanceof Error ? e.message : String(e)}); it has been terminated.`);
89
90
  }
91
+ const bgTimeout = env.backgroundCapabilities.defaultBgTimeoutSec;
90
92
  const lifetime = isPersistent
91
- ? "It is persistent: no timeout — it runs until you stop it or the session ends."
93
+ ? typeof bgTimeout === "number"
94
+ ? `It is persistent: no watch timeout of its own, but the execution environment kills background processes after ${bgTimeout}s; it runs until then, until you stop it, or until the session ends.`
95
+ : "It is persistent: no watch timeout of its own, but the execution environment's background time budget still applies; it runs until then, until you stop it, or until the session ends."
92
96
  : `It will be killed after ${timeoutMs}ms if still running.`;
93
97
  return onNotify !== undefined
94
98
  ? `Monitoring in background; task_id=${taskId}. Each stdout line becomes a notification event (lines within ` +
@@ -1,5 +1,5 @@
1
1
  import { Type } from "typebox";
2
- import { defineTool } from "../core/tools.js";
2
+ import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { hasScheduler, isValidCronExpr, } from "../core/scheduler.js";
4
4
  export const SCHEDULE_WAKEUP_TOOL_NAME = "ScheduleWakeup";
5
5
  export const AUTONOMOUS_LOOP_SENTINEL = "<<autonomous-loop>>";
@@ -210,33 +210,33 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
210
210
  execute: async (args) => {
211
211
  const a = args;
212
212
  if (a.schedule !== undefined && a.cron !== undefined) {
213
- return "Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.";
213
+ return errorResult("Error (CronCreate): pass exactly one of `schedule` (object form) or `cron` (string form), not both.");
214
214
  }
215
215
  if (a.schedule === undefined && a.cron === undefined) {
216
- return "Error (CronCreate): a schedule is required — pass `schedule` (kinds cron/at/delay) or the shorthand `cron` string.";
216
+ return errorResult("Error (CronCreate): a schedule is required — pass `schedule` (kinds cron/at/delay) or the shorthand `cron` string.");
217
217
  }
218
218
  if (a.prompt === undefined) {
219
- return "Error (CronCreate): `prompt` is required — the task runs unattended, so it needs self-contained instructions.";
219
+ return errorResult("Error (CronCreate): `prompt` is required — the task runs unattended, so it needs self-contained instructions.");
220
220
  }
221
221
  const when = a.schedule ?? { kind: "cron", expr: a.cron };
222
222
  if (when.kind === "cron") {
223
223
  if (!isValidCronExpr(when.expr)) {
224
- return `Error (CronCreate): invalid cron expression "${when.expr}" — only 5/6 space-separated cron fields (digits and * / , -) are allowed.`;
224
+ return errorResult(`Error (CronCreate): invalid cron expression "${when.expr}" — only 5/6 space-separated cron fields (digits and * / , -) are allowed.`);
225
225
  }
226
226
  const deepErr = cronScheduleError(when.expr);
227
227
  if (deepErr)
228
- return `Error (CronCreate): cron expression "${when.expr}" — ${deepErr}`;
228
+ return errorResult(`Error (CronCreate): cron expression "${when.expr}" — ${deepErr}`);
229
229
  }
230
230
  else if (a.recurring !== undefined) {
231
- return `Error (CronCreate): \`recurring\` only applies to cron schedules — "${when.kind}" fires once by nature; omit \`recurring\`.`;
231
+ return errorResult(`Error (CronCreate): \`recurring\` only applies to cron schedules — "${when.kind}" fires once by nature; omit \`recurring\`.`);
232
232
  }
233
233
  const durable = a.durable ?? a.schedule !== undefined;
234
234
  if (!durable) {
235
235
  if (sched.schedulerCapabilities.supportsSessionLifetime !== true) {
236
- return "Error (CronCreate): this scheduler backend does not support session-scoped jobs (they could not be cleaned up when the session ends). Pass `durable: true` to schedule a persistent job instead.";
236
+ return errorResult("Error (CronCreate): this scheduler backend does not support session-scoped jobs (they could not be cleaned up when the session ends). Pass `durable: true` to schedule a persistent job instead.");
237
237
  }
238
238
  if (ctx.sessionId === undefined) {
239
- return "Error (CronCreate): session-scoped jobs need a session to bind to, and this run has none. Pass `durable: true` to schedule a persistent job instead.";
239
+ return errorResult("Error (CronCreate): session-scoped jobs need a session to bind to, and this run has none. Pass `durable: true` to schedule a persistent job instead.");
240
240
  }
241
241
  }
242
242
  const intent = {
@@ -248,7 +248,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
248
248
  };
249
249
  const r = await sched.schedule(intent, schedCtx);
250
250
  if (!r.ok)
251
- return `Error (CronCreate): ${r.error.message}`;
251
+ return errorResult(`Error (CronCreate): ${r.error.message}`);
252
252
  const humanSchedule = when.kind === "cron"
253
253
  ? cronToHuman(when.expr)
254
254
  : when.kind === "delay"
@@ -278,7 +278,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
278
278
  if (!r.ok) {
279
279
  if (r.error.code === "not_found")
280
280
  return `CronDelete: no scheduled task "${id}" (already gone or not yours).`;
281
- return `Error (CronDelete): ${r.error.message}`;
281
+ return errorResult(`Error (CronDelete): ${r.error.message}`);
282
282
  }
283
283
  return { content: `Cancelled scheduled task ${id}.`, details: { type: "cron-delete", id } };
284
284
  },
@@ -292,7 +292,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
292
292
  execute: async () => {
293
293
  const r = await sched.list(schedCtx);
294
294
  if (!r.ok)
295
- return `Error (CronList): ${r.error.message}`;
295
+ return errorResult(`Error (CronList): ${r.error.message}`);
296
296
  if (r.value.length === 0) {
297
297
  return { content: "No scheduled tasks.", details: { type: "cron-list", jobs: [] } };
298
298
  }
@@ -367,7 +367,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
367
367
  if (a.stop === true) {
368
368
  const pending = await listPendingWakeups();
369
369
  if ("err" in pending)
370
- return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${pending.err}`;
370
+ return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${pending.err}`);
371
371
  const cancelledWakeups = await cancelWakeups(pending.ids);
372
372
  return {
373
373
  content: `Loop stopped. Cancelled ${cancelledWakeups} pending wakeup(s); recurring cron tasks are unaffected.`,
@@ -375,10 +375,10 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
375
375
  };
376
376
  }
377
377
  if (a.delaySeconds === undefined || a.reason === undefined || a.prompt === undefined) {
378
- return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): delaySeconds, reason and prompt are required unless \`stop\` is true.`;
378
+ return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): delaySeconds, reason and prompt are required unless \`stop\` is true.`);
379
379
  }
380
380
  if (sched.schedulerCapabilities.supportsSessionWakeup === false) {
381
- return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): this environment has no resident scheduler that can honor a session wakeup — the wakeup would never fire. Wait in the foreground instead, or start the work in a self-detaching form.`;
381
+ return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): this environment has no resident scheduler that can honor a session wakeup — the wakeup would never fire. Wait in the foreground instead, or start the work in a self-detaching form.`);
382
382
  }
383
383
  const clampedDelaySeconds = Math.min(3600, Math.max(60, Math.round(a.delaySeconds)));
384
384
  const wasClamped = clampedDelaySeconds !== a.delaySeconds;
@@ -387,7 +387,7 @@ Only use minute 0 or 30 when the user names that exact time and clearly means it
387
387
  await cancelWakeups(pending.ids);
388
388
  const r = await sched.schedule({ prompt: a.prompt, when: { kind: "delay", delaySec: clampedDelaySeconds }, label: WAKEUP_LABEL, mode: "session-wakeup" }, schedCtx);
389
389
  if (!r.ok)
390
- return `Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${r.error.message}`;
390
+ return errorResult(`Error (${SCHEDULE_WAKEUP_TOOL_NAME}): ${r.error.message}`);
391
391
  const scheduledFor = Date.now() + clampedDelaySeconds * 1000;
392
392
  const hhmmss = new Date(scheduledFor).toTimeString().slice(0, 8);
393
393
  const clampNote = wasClamped ? ` (clamped to ${clampedDelaySeconds}s from your requested value)` : "";