teapot-coding-agent 0.17.7 → 0.17.9

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.
@@ -100,6 +100,8 @@ export class Agent {
100
100
  runChain = Promise.resolve();
101
101
  /** whether contextTokenBudget came from config (manual) or was derived */
102
102
  manualCompactBudget = false;
103
+ /** workspace dir absent at boot — session runs "as ghost" until created */
104
+ workspaceMissing = false;
103
105
  lastProgressAt = Date.now();
104
106
  /** the provider's own prompt_tokens from the last completed turn */
105
107
  lastUsage;
@@ -192,6 +194,18 @@ export class Agent {
192
194
  get workspace() {
193
195
  return this.opts.workspace;
194
196
  }
197
+ /** true while the workspace directory does not exist on disk yet */
198
+ get isGhost() {
199
+ return this.workspaceMissing;
200
+ }
201
+ /** Create the workspace on demand — before any tool touches the fs. */
202
+ async ensureWorkspace() {
203
+ if (!this.workspaceMissing)
204
+ return;
205
+ await fs.mkdir(this.opts.workspace, { recursive: true });
206
+ this.workspaceMissing = false;
207
+ bus.emit("update", { kind: "agent-update", agentId: this.opts.id });
208
+ }
195
209
  /** true when the operator pinned contextTokenBudget in the config */
196
210
  get compactBudgetIsManual() {
197
211
  return this.manualCompactBudget;
@@ -232,7 +246,15 @@ export class Agent {
232
246
  }
233
247
  async init() {
234
248
  await this.log.load();
235
- await fs.mkdir(this.workspace, { recursive: true });
249
+ // NOTE: the workspace is NOT created here. Boot-time mkdir resurrected
250
+ // deleted project directories on every restart, which operators hated.
251
+ // It is created lazily in ensureWorkspace() when the agent first needs
252
+ // to touch the filesystem. Until then a missing workspace marks the
253
+ // session as "ghost" (visible-but-flagged in the UI).
254
+ this.workspaceMissing = !(await fs
255
+ .stat(this.workspace)
256
+ .then(() => true)
257
+ .catch(() => false));
236
258
  // goal lives next to the session log (dataDir), NOT in the workspace —
237
259
  // migrate a legacy workspace GOAL.md once, then never touch the workspace
238
260
  await this.migrateGoalFromWorkspace();
@@ -402,7 +424,9 @@ export class Agent {
402
424
  this.stats.compactions++;
403
425
  }
404
426
  }
405
- // last usage seeds the context gauge right after reload
427
+ // newest usage event seeds the context gauge right after reload —
428
+ // inputTokens from the LAST turn is the live context size, and cached
429
+ // must ride along or the "⚡ N% cached" pill disappears after a restart
406
430
  for (let i = own.length - 1; i >= 0; i--) {
407
431
  if (own[i].type === "usage") {
408
432
  const u = own[i].data;
@@ -629,6 +653,7 @@ export class Agent {
629
653
  status: this.status,
630
654
  statusReason: this.statusReason,
631
655
  workspace: this.workspace,
656
+ workspaceMissing: this.workspaceMissing,
632
657
  session: this.currentSession,
633
658
  branch: this.currentBranch,
634
659
  goal: {
@@ -1199,6 +1224,8 @@ export class Agent {
1199
1224
  argsRaw: call.function.arguments, // byte-exact for cache-safe restore
1200
1225
  });
1201
1226
  const t0 = Date.now();
1227
+ // first filesystem touch creates a missing workspace (never at boot)
1228
+ await this.ensureWorkspace();
1202
1229
  const result = await executeTool(call.function.name, call.function.arguments, this.toolCtx);
1203
1230
  this.stats.toolCalls++;
1204
1231
  await this.log.append("tool_result", this.currentSession, this.currentBranch, {
package/dist/master.js CHANGED
@@ -357,8 +357,14 @@ export class Master {
357
357
  // endpoint, cached) unless the config already pins one explicitly
358
358
  let inferredWindow;
359
359
  if (!ac.contextWindowTokens && !this.config.contextWindowTokens) {
360
- const list = await fetchModelList(llm.baseUrl, llm.apiKey).catch(() => []);
361
- inferredWindow = contextLengthFor(list, llm.model);
360
+ // don't block the event loop on provider metadata: fire the lookup but
361
+ // keep boot moving. The agent's gauge fills in when it resolves.
362
+ const listPromise = fetchModelList(llm.baseUrl, llm.apiKey);
363
+ inferredWindow = await Promise.race([
364
+ listPromise.then((l) => contextLengthFor(l, llm.model)),
365
+ new Promise((r) => setTimeout(() => r(undefined), 1_500)),
366
+ ]);
367
+ void listPromise.catch(() => []);
362
368
  }
363
369
  const sessionDir = this.resolveSessionDir(ac.id, opts.fresh === true);
364
370
  await mkdirSync(sessionDir, { recursive: true });
@@ -19,7 +19,9 @@ export async function fetchModelList(baseUrl, apiKey) {
19
19
  ...(apiKey ? { authorization: `Bearer ${apiKey}` } : {}),
20
20
  ...providerHeaders(baseUrl), // OpenRouter app attribution
21
21
  },
22
- signal: AbortSignal.timeout(15_000),
22
+ // boot-time calls happen per agent; a slow/unroutable endpoint used to
23
+ // stall startup for up to 15s PER AGENT before this was tightened
24
+ signal: AbortSignal.timeout(3_000),
23
25
  });
24
26
  if (!res.ok)
25
27
  throw new Error(`HTTP ${res.status}`);
@@ -387,6 +387,8 @@ export function buildApp(master) {
387
387
  for (const d of dirents) {
388
388
  if (d.name.startsWith(".") && !(showHidden && d.name !== "." && d.name !== ".."))
389
389
  continue; // hidden files stay out unless the caller opted in
390
+ // .git behaves like an ignored entry (dimmed/hidden with the rest)
391
+ const forceIgnored = !d.isDirectory() ? false : d.name === ".git";
390
392
  const isDir = d.isDirectory();
391
393
  let size;
392
394
  if (!isDir) {
@@ -397,11 +399,12 @@ export function buildApp(master) {
397
399
  /* vanished mid-scan — fine */
398
400
  }
399
401
  }
402
+ const gitIg = ignoredNames.has(d.name);
400
403
  entries.push({
401
404
  name: d.name,
402
405
  dir: isDir,
403
406
  ...(size !== undefined ? { size } : {}),
404
- ...(ignoredNames.has(d.name) ? { ignored: true } : {}),
407
+ ...(gitIg || forceIgnored ? { ignored: true } : {}),
405
408
  });
406
409
  }
407
410
  entries.sort((x, y) => (x.dir !== y.dir ? (x.dir ? -1 : 1) : x.name.localeCompare(y.name)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.17.7",
3
+ "version": "0.17.9",
4
4
  "description": "A lightweight, always-on multi-agent harness for AI coding agents",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-or-later",