@gajae-code/coding-agent 0.5.2 → 0.5.4

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 (99) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/dist/types/async/job-manager.d.ts +6 -0
  3. package/dist/types/config/model-profiles.d.ts +10 -0
  4. package/dist/types/dap/client.d.ts +2 -1
  5. package/dist/types/edit/read-file.d.ts +6 -0
  6. package/dist/types/eval/js/context-manager.d.ts +3 -0
  7. package/dist/types/eval/js/executor.d.ts +1 -0
  8. package/dist/types/exec/bash-executor.d.ts +2 -0
  9. package/dist/types/gjc-runtime/tmux-sessions.d.ts +7 -1
  10. package/dist/types/lsp/types.d.ts +2 -0
  11. package/dist/types/modes/bridge/bridge-mode.d.ts +1 -0
  12. package/dist/types/modes/components/model-selector.d.ts +2 -0
  13. package/dist/types/modes/components/oauth-selector.d.ts +1 -0
  14. package/dist/types/modes/components/runtime-mcp-add-wizard.d.ts +1 -0
  15. package/dist/types/modes/components/tool-execution.d.ts +1 -0
  16. package/dist/types/modes/interactive-mode.d.ts +1 -0
  17. package/dist/types/modes/types.d.ts +1 -0
  18. package/dist/types/runtime/process-lifecycle.d.ts +108 -0
  19. package/dist/types/runtime-mcp/transports/stdio.d.ts +1 -0
  20. package/dist/types/runtime-mcp/types.d.ts +2 -0
  21. package/dist/types/session/agent-session.d.ts +29 -1
  22. package/dist/types/session/artifacts.d.ts +4 -1
  23. package/dist/types/session/streaming-output.d.ts +12 -0
  24. package/dist/types/slash-commands/helpers/fast-status-report.d.ts +76 -0
  25. package/dist/types/tools/bash.d.ts +1 -0
  26. package/dist/types/tools/browser/tab-supervisor.d.ts +9 -0
  27. package/dist/types/tools/sqlite-reader.d.ts +2 -1
  28. package/dist/types/web/search/providers/codex.d.ts +4 -4
  29. package/package.json +7 -7
  30. package/src/async/job-manager.ts +181 -43
  31. package/src/config/file-lock.ts +9 -1
  32. package/src/config/model-profile-activation.ts +71 -3
  33. package/src/config/model-profiles.ts +39 -14
  34. package/src/dap/client.ts +105 -64
  35. package/src/dap/session.ts +44 -7
  36. package/src/defaults/gjc/skills/deep-interview/SKILL.md +11 -2
  37. package/src/defaults/gjc/skills/ralplan/SKILL.md +2 -2
  38. package/src/defaults/gjc/skills/ultragoal/SKILL.md +2 -2
  39. package/src/edit/read-file.ts +19 -1
  40. package/src/eval/js/context-manager.ts +228 -65
  41. package/src/eval/js/executor.ts +2 -0
  42. package/src/eval/js/index.ts +1 -0
  43. package/src/eval/js/worker-core.ts +10 -6
  44. package/src/eval/py/executor.ts +68 -19
  45. package/src/eval/py/kernel.ts +46 -22
  46. package/src/eval/py/runner.py +68 -14
  47. package/src/exec/bash-executor.ts +49 -13
  48. package/src/gjc-runtime/deep-interview-runtime.ts +14 -13
  49. package/src/gjc-runtime/ralplan-runtime.ts +10 -0
  50. package/src/gjc-runtime/state-runtime.ts +73 -0
  51. package/src/gjc-runtime/tmux-gc.ts +86 -37
  52. package/src/gjc-runtime/tmux-sessions.ts +44 -6
  53. package/src/gjc-runtime/ultragoal-runtime.ts +8 -4
  54. package/src/internal-urls/artifact-protocol.ts +10 -1
  55. package/src/internal-urls/docs-index.generated.ts +2 -2
  56. package/src/lsp/client.ts +64 -26
  57. package/src/lsp/index.ts +2 -1
  58. package/src/lsp/lspmux.ts +33 -9
  59. package/src/lsp/types.ts +2 -0
  60. package/src/modes/bridge/bridge-mode.ts +21 -0
  61. package/src/modes/components/assistant-message.ts +10 -2
  62. package/src/modes/components/bash-execution.ts +5 -1
  63. package/src/modes/components/eval-execution.ts +5 -1
  64. package/src/modes/components/model-selector.ts +34 -2
  65. package/src/modes/components/oauth-selector.ts +5 -0
  66. package/src/modes/components/runtime-mcp-add-wizard.ts +58 -7
  67. package/src/modes/components/skill-message.ts +24 -16
  68. package/src/modes/components/tool-execution.ts +6 -0
  69. package/src/modes/controllers/extension-ui-controller.ts +33 -6
  70. package/src/modes/controllers/input-controller.ts +19 -0
  71. package/src/modes/controllers/selector-controller.ts +6 -1
  72. package/src/modes/interactive-mode.ts +13 -0
  73. package/src/modes/types.ts +1 -0
  74. package/src/modes/utils/ui-helpers.ts +5 -2
  75. package/src/prompts/agents/executor.md +1 -1
  76. package/src/runtime/process-lifecycle.ts +400 -0
  77. package/src/runtime-mcp/manager.ts +164 -50
  78. package/src/runtime-mcp/transports/http.ts +12 -11
  79. package/src/runtime-mcp/transports/stdio.ts +64 -38
  80. package/src/runtime-mcp/types.ts +3 -0
  81. package/src/sdk.ts +27 -0
  82. package/src/session/agent-session.ts +271 -25
  83. package/src/session/artifacts.ts +17 -2
  84. package/src/session/blob-store.ts +36 -2
  85. package/src/session/session-manager.ts +29 -13
  86. package/src/session/streaming-output.ts +95 -3
  87. package/src/setup/model-onboarding-guidance.ts +10 -3
  88. package/src/skill-state/active-state.ts +79 -7
  89. package/src/slash-commands/builtin-registry.ts +30 -3
  90. package/src/slash-commands/helpers/fast-status-report.ts +111 -0
  91. package/src/tools/archive-reader.ts +10 -1
  92. package/src/tools/bash.ts +11 -4
  93. package/src/tools/browser/registry.ts +17 -1
  94. package/src/tools/browser/tab-supervisor.ts +22 -0
  95. package/src/tools/browser.ts +38 -4
  96. package/src/tools/cron.ts +2 -6
  97. package/src/tools/read.ts +11 -12
  98. package/src/tools/sqlite-reader.ts +19 -5
  99. package/src/web/search/providers/codex.ts +6 -5
@@ -8,7 +8,7 @@ import * as fs from "node:fs";
8
8
 
9
9
  import { worktree } from "../utils/git";
10
10
  import type { GcCollectResult, GcContext, GcPruneOutcome, GcRecord, GcStoreAdapter } from "./gc-runtime";
11
- import { GJC_TMUX_PROFILE_VALUE } from "./tmux-common";
11
+ import { GJC_TMUX_PROFILE_VALUE, GJC_TMUX_SESSION_PREFIX } from "./tmux-common";
12
12
  import {
13
13
  type GjcTmuxSessionStatus,
14
14
  type GjcTmuxSessionsForGc,
@@ -19,6 +19,7 @@ import {
19
19
 
20
20
  const STORE = "tmux_sessions" as const;
21
21
  const TOCTOU_SKIP = "tmux_revalidation_failed_or_became_live";
22
+ const ORPHAN_MAX_AGE_MS = 24 * 60 * 60 * 1000;
22
23
 
23
24
  function pathExists(path: string): boolean {
24
25
  try {
@@ -65,39 +66,62 @@ async function hasLiveWorktreeForBranch(project: string, branch: string): Promis
65
66
  return entries.some(entry => branchMatches(entry.branch, branch));
66
67
  }
67
68
 
69
+ function isSessionLive(session: Pick<GjcTmuxSessionStatus, "attached" | "panePids">): boolean {
70
+ return session.attached || session.panePids.length > 0;
71
+ }
72
+
73
+ function liveRecord(session: GjcTmuxSessionStatus, reason: string): GcRecord {
74
+ return {
75
+ store: STORE,
76
+ id: session.name,
77
+ path: session.project,
78
+ root: session.project,
79
+ pid_status: "alive",
80
+ status: "live",
81
+ stale: false,
82
+ removable: false,
83
+ action: "none",
84
+ reason,
85
+ detail: detail(session.project, session.branch),
86
+ };
87
+ }
88
+
89
+ function staleRecord(session: GjcTmuxSessionStatus, reason: string): GcRecord {
90
+ return {
91
+ store: STORE,
92
+ id: session.name,
93
+ path: session.project,
94
+ root: session.project,
95
+ pid_status: "none",
96
+ status: "stale",
97
+ stale: true,
98
+ removable: true,
99
+ action: "none",
100
+ reason,
101
+ detail: `${detail(session.project, session.branch) ?? ""} createdAt=${session.createdAt}`.trim(),
102
+ };
103
+ }
104
+
105
+ function isOldEnoughForOrphanGc(session: GjcTmuxSessionStatus): boolean {
106
+ const createdAt = Date.parse(session.createdAt);
107
+ return Number.isFinite(createdAt) && Date.now() - createdAt >= ORPHAN_MAX_AGE_MS;
108
+ }
109
+
110
+ function isGjcOwnedOrphan(session: GjcTmuxSessionStatus): boolean {
111
+ return session.name.startsWith(GJC_TMUX_SESSION_PREFIX) || session.name === "gajae_code";
112
+ }
113
+
68
114
  async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcRecord> {
69
115
  const { name, project, branch } = session;
70
- if (!project || !branch) return unclassifiedRecord(name, "missing_project_or_branch_tag", project, branch);
71
- if (!pathExists(project)) {
72
- return {
73
- store: STORE,
74
- id: name,
75
- path: project,
76
- root: project,
77
- pid_status: "none",
78
- status: "stale",
79
- stale: true,
80
- removable: true,
81
- action: "none",
82
- reason: "project_missing",
83
- detail: detail(project, branch),
84
- };
85
- }
86
- if (!(await hasLiveWorktreeForBranch(project, branch))) {
87
- return {
88
- store: STORE,
89
- id: name,
90
- path: project,
91
- root: project,
92
- pid_status: "none",
93
- status: "stale",
94
- stale: true,
95
- removable: true,
96
- action: "none",
97
- reason: "branch_no_worktree",
98
- detail: detail(project, branch),
99
- };
116
+ if (isSessionLive(session)) return liveRecord(session, "tmux_session_attached_or_has_live_panes");
117
+ if (!project || !branch) {
118
+ if (isGjcOwnedOrphan(session) && isOldEnoughForOrphanGc(session)) {
119
+ return staleRecord(session, "metadata_less_gjc_owned_idle_orphan");
120
+ }
121
+ return unclassifiedRecord(name, "missing_project_or_branch_tag", project, branch);
100
122
  }
123
+ if (!pathExists(project)) return staleRecord(session, "project_missing");
124
+ if (!(await hasLiveWorktreeForBranch(project, branch))) return staleRecord(session, "branch_no_worktree");
101
125
  return {
102
126
  store: STORE,
103
127
  id: name,
@@ -113,9 +137,34 @@ async function classifyTaggedSession(session: GjcTmuxSessionStatus): Promise<GcR
113
137
  };
114
138
  }
115
139
 
116
- async function revalidateRemovable(name: string, env: NodeJS.ProcessEnv): Promise<boolean> {
117
- const tags = readTmuxSessionTagsForGc(name, env);
118
- if (tags.profile !== GJC_TMUX_PROFILE_VALUE || !tags.project || !tags.branch) return false;
140
+ function classifyUntaggedSession(session: GjcTmuxSessionStatus): GcRecord {
141
+ return unclassifiedRecord(session.name, "untagged_tmux_session");
142
+ }
143
+
144
+ async function revalidateRemovable(record: GcRecord, env: NodeJS.ProcessEnv): Promise<boolean> {
145
+ const tags = readTmuxSessionTagsForGc(record.id, env);
146
+ if (
147
+ tags.createdAt &&
148
+ record.detail?.includes("createdAt=") &&
149
+ !record.detail.includes(`createdAt=${tags.createdAt}`)
150
+ ) {
151
+ return false;
152
+ }
153
+ if (tags.attached || (tags.panePids?.length ?? 0) > 0) return false;
154
+ if (tags.profile !== GJC_TMUX_PROFILE_VALUE) return false;
155
+ if (!tags.project || !tags.branch)
156
+ return (
157
+ record.reason === "metadata_less_gjc_owned_idle_orphan" &&
158
+ isGjcOwnedOrphan({
159
+ name: record.id,
160
+ attached: false,
161
+ windows: 0,
162
+ panes: 0,
163
+ panePids: [],
164
+ bindings: "",
165
+ createdAt: tags.createdAt ?? "",
166
+ })
167
+ );
119
168
  if (!pathExists(tags.project)) return true;
120
169
  return !(await hasLiveWorktreeForBranch(tags.project, tags.branch));
121
170
  }
@@ -154,8 +203,8 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
154
203
  }
155
204
  }
156
205
 
157
- for (const name of sessions.untagged) {
158
- records.push(unclassifiedRecord(name, "untagged_tmux_session"));
206
+ for (const session of sessions.untagged) {
207
+ records.push(classifyUntaggedSession(session));
159
208
  }
160
209
 
161
210
  return { records, errors };
@@ -165,7 +214,7 @@ export const tmuxSessionsGcAdapter: GcStoreAdapter = {
165
214
  return { removed: false, skipped: "not_removable_tmux_session" };
166
215
  }
167
216
  try {
168
- if (!(await revalidateRemovable(record.id, ctx.env))) {
217
+ if (!(await revalidateRemovable(record, ctx.env))) {
169
218
  return { removed: false, skipped: TOCTOU_SKIP };
170
219
  }
171
220
  removeGjcTmuxSession(record.id, ctx.env);
@@ -22,17 +22,23 @@ export interface GjcTmuxSessionStatus {
22
22
  branch?: string;
23
23
  branchSlug?: string;
24
24
  project?: string;
25
+ panePids: number[];
26
+ profile?: string;
25
27
  }
26
28
 
27
29
  export interface GjcTmuxSessionTagsForGc {
28
30
  profile?: string;
29
31
  project?: string;
30
32
  branch?: string;
33
+ branchSlug?: string;
34
+ createdAt?: string;
35
+ attached?: boolean;
36
+ panePids?: number[];
31
37
  }
32
38
 
33
39
  export interface GjcTmuxSessionsForGc {
34
40
  tagged: GjcTmuxSessionStatus[];
35
- untagged: string[];
41
+ untagged: GjcTmuxSessionStatus[];
36
42
  }
37
43
 
38
44
  function runTmux(args: string[], env: NodeJS.ProcessEnv = process.env): string {
@@ -68,21 +74,27 @@ function parseSessionLine(line: string): GjcTmuxSessionStatus | null {
68
74
  profile = "",
69
75
  bindings = "",
70
76
  panes = "0",
77
+ panePids = "",
71
78
  branch = "",
72
79
  branchSlug = "",
73
80
  project = "",
74
81
  ] = line.split("\t");
75
- if (!name || profile !== GJC_TMUX_PROFILE_VALUE) return null;
82
+ if (!name) return null;
76
83
  return {
77
84
  name,
78
85
  attached: parseBooleanFlag(attached),
79
86
  windows: parseNumber(windows),
80
87
  panes: parseNumber(panes),
88
+ panePids: panePids
89
+ .split(",")
90
+ .map(pid => parseNumber(pid))
91
+ .filter(pid => pid > 0),
81
92
  bindings,
82
93
  createdAt: normalizeTmuxCreatedAt(created),
83
94
  branch: branch || undefined,
84
95
  branchSlug: branchSlug || undefined,
85
96
  project: project || undefined,
97
+ profile: profile || undefined,
86
98
  };
87
99
  }
88
100
 
@@ -103,7 +115,7 @@ function runListSessions(format: string, env: NodeJS.ProcessEnv = process.env):
103
115
 
104
116
  function listSessionLines(env: NodeJS.ProcessEnv = process.env): string[] {
105
117
  return runListSessions(
106
- `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}`,
118
+ `#{session_name}\t#{session_windows}\t#{session_attached}\t#{session_created}\t#{${GJC_TMUX_PROFILE_OPTION}}\t#{session_key_table}\t#{session_panes}\t#{pane_pid}\t#{${GJC_TMUX_BRANCH_OPTION}}\t#{${GJC_TMUX_BRANCH_SLUG_OPTION}}\t#{${GJC_TMUX_PROJECT_OPTION}}`,
107
119
  env,
108
120
  );
109
121
  }
@@ -115,17 +127,35 @@ function listRawTmuxSessionNames(env: NodeJS.ProcessEnv = process.env): string[]
115
127
  export function listGjcTmuxSessions(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus[] {
116
128
  return listSessionLines(env)
117
129
  .map(parseSessionLine)
118
- .filter((session): session is GjcTmuxSessionStatus => session != null)
130
+ .filter((session): session is GjcTmuxSessionStatus => session?.profile === GJC_TMUX_PROFILE_VALUE)
119
131
  .sort((a, b) => a.name.localeCompare(b.name));
120
132
  }
121
133
 
122
134
  /** @internal */
123
135
  export function listTmuxSessionsForGc(env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionsForGc {
124
- const tagged = listGjcTmuxSessions(env);
136
+ const sessions = listSessionLines(env)
137
+ .map(parseSessionLine)
138
+ .filter((session): session is GjcTmuxSessionStatus => session != null);
139
+ const tagged = sessions
140
+ .filter(session => session.profile === GJC_TMUX_PROFILE_VALUE)
141
+ .sort((a, b) => a.name.localeCompare(b.name));
125
142
  const taggedNames = new Set(tagged.map(session => session.name));
143
+ const byName = new Map(sessions.map(session => [session.name, session]));
126
144
  const untagged = listRawTmuxSessionNames(env)
127
145
  .filter(name => !taggedNames.has(name))
128
- .sort((a, b) => a.localeCompare(b));
146
+ .map(
147
+ name =>
148
+ byName.get(name) ?? {
149
+ name,
150
+ attached: false,
151
+ windows: 0,
152
+ panes: 0,
153
+ panePids: [],
154
+ bindings: "",
155
+ createdAt: "",
156
+ },
157
+ )
158
+ .sort((a, b) => a.name.localeCompare(b.name));
129
159
  return { tagged, untagged };
130
160
  }
131
161
 
@@ -192,15 +222,23 @@ export function readTmuxSessionTagsForGc(
192
222
  sessionName: string,
193
223
  env: NodeJS.ProcessEnv = process.env,
194
224
  ): GjcTmuxSessionTagsForGc {
225
+ const session = listGjcTmuxSessions(env).find(candidate => candidate.name === sessionName);
195
226
  return {
196
227
  profile: readExactOptionForGc(sessionName, GJC_TMUX_PROFILE_OPTION, env),
197
228
  project: readExactOptionForGc(sessionName, GJC_TMUX_PROJECT_OPTION, env),
198
229
  branch: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_OPTION, env),
230
+ branchSlug: readExactOptionForGc(sessionName, GJC_TMUX_BRANCH_SLUG_OPTION, env),
231
+ createdAt: session?.createdAt,
232
+ attached: session?.attached,
233
+ panePids: session?.panePids,
199
234
  };
200
235
  }
201
236
 
202
237
  export function removeGjcTmuxSession(sessionName: string, env: NodeJS.ProcessEnv = process.env): GjcTmuxSessionStatus {
203
238
  const session = statusGjcTmuxSession(sessionName, env);
239
+ if (session.attached || session.panePids.length > 0) {
240
+ throw new Error(`gjc_tmux_session_live:${sessionName}`);
241
+ }
204
242
  if (readProfileForExactTarget(session.name, env) !== GJC_TMUX_PROFILE_VALUE) {
205
243
  throw new Error(`gjc_tmux_session_not_managed:${sessionName}`);
206
244
  }
@@ -1247,7 +1247,7 @@ const CLI_REPLAY_MAX_OUTPUT_BYTES = 1024 * 1024;
1247
1247
  const CLI_REPLAY_DEFAULT_TIMEOUT_MS = 10_000;
1248
1248
  const CLI_REPLAY_MIN_TIMEOUT_MS = 1_000;
1249
1249
  const CLI_REPLAY_MAX_TIMEOUT_MS = 30_000;
1250
- const CLI_REPLAY_EXEMPT_REASON_CODES = new Set([
1250
+ const CLI_REPLAY_EXEMPT_REASON_CODES = [
1251
1251
  "unsafe_side_effect",
1252
1252
  "requires_credentials",
1253
1253
  "requires_network",
@@ -1255,8 +1255,10 @@ const CLI_REPLAY_EXEMPT_REASON_CODES = new Set([
1255
1255
  "destructive",
1256
1256
  "interactive_only",
1257
1257
  "platform_unavailable",
1258
- ]);
1258
+ ] as const;
1259
+ const CLI_REPLAY_EXEMPT_REASON_CODE_SET = new Set<string>(CLI_REPLAY_EXEMPT_REASON_CODES);
1259
1260
  const CLI_REPLAY_ENV_BASE: Record<string, string> = { CI: "1", NO_COLOR: "1", GJC_ULTRAGOAL_REPLAY: "1" };
1261
+ const CLI_REPLAY_EXEMPT_REASON_CODE_LIST = CLI_REPLAY_EXEMPT_REASON_CODES.join(", ");
1260
1262
  const CLI_REPLAY_SAFE_ENV_NAMES = new Set(["LANG", "LC_ALL", "LC_CTYPE", "TZ"]);
1261
1263
  const CLI_REPLAY_DANGEROUS_ENV_NAME_PATTERN =
1262
1264
  /^(?:NODE_OPTIONS|GIT_EXTERNAL_DIFF|GIT_SSH|GIT_SSH_COMMAND|GIT_PAGER|PATH|LD_PRELOAD|LD_LIBRARY_PATH)$|^(?:GIT_CONFIG|DYLD_|BUN_|NPM_CONFIG_)|(?:^|_)OPTIONS$|PRELOAD$/;
@@ -1568,8 +1570,10 @@ async function validateReplayExemptFallback(
1568
1570
  const exempt = qualityGateObject(record.replayExempt);
1569
1571
  if (!exempt) return false;
1570
1572
  const reasonCode = requiredStringField(exempt, "reasonCode", `${fieldName}.replayExempt`);
1571
- if (!CLI_REPLAY_EXEMPT_REASON_CODES.has(reasonCode))
1572
- throw new Error(`qualityGate ${fieldName}.replayExempt.reasonCode is not recognized`);
1573
+ if (!CLI_REPLAY_EXEMPT_REASON_CODE_SET.has(reasonCode))
1574
+ throw new Error(
1575
+ `qualityGate ${fieldName}.replayExempt.reasonCode must be one of: ${CLI_REPLAY_EXEMPT_REASON_CODE_LIST}`,
1576
+ );
1573
1577
  const reason = requiredStringField(exempt, "reason", `${fieldName}.replayExempt`);
1574
1578
  if (!isSubstantiveEvidence(reason) || reason.length < 30)
1575
1579
  throw new Error(`qualityGate ${fieldName}.replayExempt.reason must be audited and substantive`);
@@ -63,7 +63,16 @@ export class ArtifactProtocolHandler implements ProtocolHandler {
63
63
  throw new Error(`artifact://${id} not found`);
64
64
  }
65
65
 
66
- const content = await Bun.file(foundPath).text();
66
+ // F20: cap the materialized artifact so reading a huge spilled artifact cannot
67
+ // buffer GBs into memory (the range selector is applied downstream, so without a
68
+ // cap a `artifact://id:range` over a multi-GB artifact still reads it whole).
69
+ const MAX_ARTIFACT_READ_BYTES = 16 * 1024 * 1024;
70
+ const file = Bun.file(foundPath);
71
+ const fullSize = file.size;
72
+ const content =
73
+ fullSize > MAX_ARTIFACT_READ_BYTES
74
+ ? `${await file.slice(0, MAX_ARTIFACT_READ_BYTES).text()}\n\n[Artifact truncated: first ${MAX_ARTIFACT_READ_BYTES} of ${fullSize} bytes shown; use a narrower range or a specialized tool for the full content.]`
75
+ : await file.text();
67
76
  return {
68
77
  url: url.href,
69
78
  content,