teapot-coding-agent 0.37.92 → 0.37.94

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.
@@ -7,7 +7,6 @@ import { auditPrompt, parseAuditVerdict } from "./goal-audit.js";
7
7
  import { createCatalog } from "./tool-catalog.js";
8
8
  import { resetToolCatalog, executeTool, toolSpecs, currentSkills, safeJoin, isContextOverflow, isRequestNodeLimit, hasRunningBgShells, hasRunningBgShellsFor, killBgShells } from "./tools.js";
9
9
  import { effortChoosable } from "./llm.js";
10
- import { sessionTitleFrom } from "../session-title.js";
11
10
  import { mcp } from "./mcp-registry.js";
12
11
  import { flattenToolName } from "./mcp-registry.js";
13
12
  import { metaToolSpecs } from "./meta-tools.js";
@@ -298,10 +297,6 @@ export class Agent {
298
297
  skillsStamp = "";
299
298
  lastCompactSummary;
300
299
  compactRules = [];
301
- setSessionTitle(title) {
302
- this.sessionTitle = title?.trim() || undefined;
303
- }
304
- sessionTitle;
305
300
  overflowRecoveries = 0;
306
301
  consecutiveCompactFailures = 0;
307
302
  memoryDigestText() {
@@ -747,9 +742,6 @@ export class Agent {
747
742
  const last = lineage[lineage.length - 1];
748
743
  const msgs = await rebuildMessagesFrom(lineage, this.imagesDir);
749
744
  for (const e of own) {
750
- if (this.sessionTitle === undefined && e.type === "prompt" && e.data?.source === "user") {
751
- this.sessionTitle = sessionTitleFrom(e.data?.text) ?? undefined;
752
- }
753
745
  if (e.type === "state") {
754
746
  const d = e.data;
755
747
  if (d.detail === "llm turn start")
@@ -1059,6 +1051,16 @@ export class Agent {
1059
1051
  });
1060
1052
  }
1061
1053
  snapshot() {
1054
+ /**
1055
+ * The session's name comes from ONE place — `config.sessionTitles`, via the resolver Master hands
1056
+ * down. The agent used to keep its own copy, seeded at creation and never updated when the map
1057
+ * learned the real title, so the panel showed the internal id while `config.json` had the name
1058
+ * ("config.jsonにチャットタイトルが生成されているのに表示が内部IDになってる").
1059
+ *
1060
+ * A copy that has to be PUSHED is a copy that goes stale; deriving it means there is nothing to
1061
+ * push. The session id is the fallback so the panel is never blank.
1062
+ */
1063
+ const title = this.opts.sessionTitleOf?.(this.currentSession) || this.currentSession;
1062
1064
  return {
1063
1065
  id: this.opts.id,
1064
1066
  status: this.status === "idle" && this.isLive() ? "waiting" : this.status,
@@ -1067,7 +1069,7 @@ export class Agent {
1067
1069
  workspace: this.workspace,
1068
1070
  workspaceMissing: this.isGhost,
1069
1071
  session: this.currentSession,
1070
- ...(this.sessionTitle ? { sessionTitle: this.sessionTitle } : {}),
1072
+ ...(title ? { sessionTitle: title } : {}),
1071
1073
  branch: this.currentBranch,
1072
1074
  goal: {
1073
1075
  status: this.goal.status,
@@ -1155,7 +1157,7 @@ export class Agent {
1155
1157
  })
1156
1158
  .then(() => {
1157
1159
  if (source === "user")
1158
- this.opts.onFirstUserPrompt?.(this.currentSession);
1160
+ this.opts.onFirstUserPrompt?.(this.currentSession, text);
1159
1161
  })
1160
1162
  .then(() => bus.emit("update", { kind: "agent-update", agentId: this.opts.id }))
1161
1163
  .catch(() => { });
@@ -1792,7 +1794,7 @@ export class Agent {
1792
1794
  reasoning: partial.reasoning,
1793
1795
  interrupted: true,
1794
1796
  });
1795
- this.messages.push({ role: "assistant", content: partial.text ?? "" });
1797
+ this.messages.push(assistantTurn({ role: "assistant", content: partial.text ?? "" }, partial.reasoning));
1796
1798
  }
1797
1799
  else if (this.stopRequested) {
1798
1800
  await this.log.append("system_note", this.currentSession, this.currentBranch, {
@@ -1859,16 +1861,8 @@ export class Agent {
1859
1861
  };
1860
1862
  await this.log.append("usage", this.currentSession, this.currentBranch, res.usage);
1861
1863
  }
1862
- const m = res.message;
1863
- /**
1864
- * Carry the thinking on the message itself, not only in the log.
1865
- *
1866
- * `res.reasoning` was written to `chat.jsonl` and then dropped: `this.messages.push(m)` stored a
1867
- * message with no reasoning, so the NEXT request sent back an assistant turn with its thinking
1868
- * missing. Providers that require it (Beam, #187) answered 400; the rest simply lost the thread.
1869
- */
1870
- if (res.reasoning && !m.reasoning)
1871
- m.reasoning = res.reasoning;
1864
+ // `assistantTurn` carries the thinking, which is what `reasoning_content` is sent from (#187)
1865
+ const m = assistantTurn(res.message, res.reasoning);
1872
1866
  const isPlaceholder = m.content === "(tool call)" || m.content === "(no content)";
1873
1867
  await this.log.append("message", this.currentSession, this.currentBranch, {
1874
1868
  role: "assistant",
@@ -2191,7 +2185,7 @@ export class Agent {
2191
2185
  await step({ step: "reply", text: text.slice(0, 400) });
2192
2186
  return text;
2193
2187
  }
2194
- messages.push({ role: "assistant", content: text, tool_calls: calls });
2188
+ messages.push(assistantTurn({ role: "assistant", content: text, tool_calls: calls }, res.reasoning));
2195
2189
  for (const call of calls) {
2196
2190
  await step({ step: "tool", name: call.function?.name, args: String(call.function?.arguments ?? "").slice(0, 200) });
2197
2191
  let out;
@@ -2224,6 +2218,22 @@ export class Agent {
2224
2218
  async handleFinish(argsJson) {
2225
2219
  const args = safeParse(argsJson);
2226
2220
  if (args.goalComplete === true && this.goal.status !== "done") {
2221
+ /**
2222
+ * A PAUSED goal is not the agent's to complete.
2223
+ *
2224
+ * `paused` means the operator took the goal off the agent's hands, so a `finish(goalComplete)`
2225
+ * must not run the audit — reported as "goal pausedでもgoal auditが動く". It also must not flip the
2226
+ * status to done: that would silently undo the pause. The final message is still appended, because
2227
+ * the work itself is real and the operator should see the summary.
2228
+ */
2229
+ if (this.goal.status === "paused") {
2230
+ await this.log.append("system_note", this.currentSession, this.currentBranch, {
2231
+ event: "finish-ignored-while-paused",
2232
+ detail: "the goal is paused, so a completion claim neither runs the audit nor marks it done",
2233
+ });
2234
+ await this.appendFinalMessage(args);
2235
+ return;
2236
+ }
2227
2237
  if (this.goal.verify?.trim()) {
2228
2238
  await this.log.append("goal", this.currentSession, this.currentBranch, {
2229
2239
  event: "audit-started",
@@ -2973,6 +2983,23 @@ function safeParse(json) {
2973
2983
  return {};
2974
2984
  }
2975
2985
  }
2986
+ /**
2987
+ * The conversation entry for a reply, with its thinking attached.
2988
+ *
2989
+ * THREE places used to build this by hand — the main loop, the AUDITOR's loop, and the interrupted-partial
2990
+ * path — and each independently forgot `reasoning`, which is what `reasoning_content` is derived from
2991
+ * (#187). MEASURED consequences: every audit attempt on `mizuki-rakutenai-3-205c7912` died with
2992
+ * `messages[1].reasoning_content` (index 1 IS the auditor's synthetic assistant turn) and the run ended
2993
+ * at `audit-unavailable-cap`, while the main loop had already been fixed.
2994
+ *
2995
+ * One builder means a fourth caller cannot be forgotten.
2996
+ */
2997
+ function assistantTurn(m, reasoning) {
2998
+ const out = { ...m };
2999
+ if (reasoning && !out.reasoning)
3000
+ out.reasoning = reasoning;
3001
+ return out;
3002
+ }
2976
3003
  function lineageOf(events) {
2977
3004
  if (events.length === 0)
2978
3005
  return [];
package/dist/master.js CHANGED
@@ -648,9 +648,11 @@ export class Master {
648
648
  const agent = new Agent({
649
649
  id: ac.id,
650
650
  workspace: resolveWorkspace(ac.workspace, path.dirname(this.configPath)),
651
- onFirstUserPrompt: (sessionId) => {
652
- void this.ensureSessionTitle(ac.id, sessionId);
651
+ onFirstUserPrompt: (sessionId, firstText) => {
652
+ void this.ensureSessionTitle(ac.id, sessionId, firstText);
653
653
  },
654
+ // ONE owner for a session's name: this map. The agent derives it on every snapshot.
655
+ sessionTitleOf: (sessionId) => this.readSessionTitle(sessionId) ?? undefined,
654
656
  llm,
655
657
  sessionDir,
656
658
  // boot must touch ONLY config.json and skills; this agent's directory is opened when it is used
@@ -740,27 +742,13 @@ export class Master {
740
742
  }
741
743
  await agent.init();
742
744
  /**
743
- * Seed the title from the SESSION, so a restart shows the name the operator gave it.
745
+ * No title is seeded, and nothing is derived here.
744
746
  *
745
- * `snapshot().sessionTitle` comes from the agent's own field, and that was only ever set by
746
- * `renameSession` — so after a restart the UI fell back to the auto-generated name even though the
747
- * title was on disk. The title comes from `config.sessionTitles`, which is the one copy.
747
+ * The agent derives its name from `config.sessionTitles` on every `snapshot()` (see
748
+ * `sessionTitleOf`), so there is no copy to seed. Deriving a missing one here would read
749
+ * `chat.jsonl`, and boot must not open session directories — "起動中に読んでいいのはconfig.jsonと
750
+ * skillsだけ / セッションディレクトリはセッションがロードされるまで開かない".
748
751
  */
749
- const seedTitle = this.readSessionTitle(path.basename(sessionDir));
750
- if (seedTitle)
751
- agent.setSessionTitle(seedTitle);
752
- else {
753
- /**
754
- * NOT derived here. Deriving a missing title reads `chat.jsonl`, and boot must not open session
755
- * directories at all — "起動中に読んでいいのはconfig.jsonとskillsだけ / セッションディレクトリは
756
- * セッションがロードされるまで開かない".
757
- *
758
- * The session ID is a perfectly good fallback for the panel, and the real derivation happens
759
- * when that session is actually loaded (`ensureSessionTitle`, via the agent's first prompt).
760
- */
761
- const id = path.basename(sessionDir);
762
- agent.setSessionTitle(this.readSessionTitle(id) ?? id);
763
- }
764
752
  this.agents.set(ac.id, agent);
765
753
  if (opts.persist) {
766
754
  this.config.agents.push(ac);
@@ -1105,6 +1093,17 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
1105
1093
  }
1106
1094
  this.config.sessionTitles ??= {};
1107
1095
  this.config.sessionTitles[sessionId] = title;
1096
+ /**
1097
+ * …and the LIVE agent has to be told, or the panel keeps showing the id.
1098
+ *
1099
+ * `addAgent` seeds an agent's title from this map when it is created, falling back to the session id.
1100
+ * A NEW session gets its entry written later (on the first prompt, from `chat.jsonl`), and nothing
1101
+ * pushed it back — so the agent still held the id and the panel displayed it while `config.json` had
1102
+ * the real name. Reported as "config.jsonにチャットタイトルが生成されているのに表示が内部IDになってる".
1103
+ *
1104
+ * Pushing here rather than in each caller means every writer (the derivation, a rename) is covered by
1105
+ * one rule: the map changed, so the agent that is showing it changes too.
1106
+ */
1108
1107
  this.saveConfig();
1109
1108
  }
1110
1109
  /**
@@ -1114,14 +1113,23 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
1114
1113
  * per session on every call (MEASURED earlier: 0.87s for one archive toggle). Writing it down once,
1115
1114
  * where the session lives, keeps that scan off the hot path and keeps the answer in ONE place.
1116
1115
  */
1117
- async ensureSessionTitle(agentId, sessionId) {
1116
+ async ensureSessionTitle(agentId, sessionId, firstText) {
1118
1117
  const a = this.config.agents.find((x) => x.id === agentId);
1119
1118
  if (!a || a.session !== sessionId)
1120
1119
  return;
1120
+ // the map is the master: once it has a name, nothing may replace it
1121
1121
  if (this.readSessionTitle(sessionId))
1122
1122
  return;
1123
- const dir = path.join(this.sessionsRoot(), sessionId);
1124
- this.writeSessionTitle(sessionId, this.sessionTitleOf(dir) ?? sessionId);
1123
+ /**
1124
+ * A session is named from its FIRST USER MESSAGE — that is the whole point of this hook, and it must
1125
+ * keep working for a session created at runtime.
1126
+ *
1127
+ * The text arrives from the agent, which already has it, so this needs no read of `chat.jsonl`. The
1128
+ * log is only a fallback for a caller that could not supply it.
1129
+ */
1130
+ const fromText = sessionTitleFrom(firstText) ?? undefined;
1131
+ const derived = fromText ?? this.sessionTitleOf(path.join(this.sessionsRoot(), sessionId)) ?? sessionId;
1132
+ this.writeSessionTitle(sessionId, derived);
1125
1133
  }
1126
1134
  sessionTitleOf(dirPath) {
1127
1135
  let fd;
@@ -1355,7 +1363,7 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
1355
1363
  */
1356
1364
  this.writeSessionTitle(sessionId, clean);
1357
1365
  delete a.sessionTitle;
1358
- this.agents.get(agentId)?.setSessionTitle(clean);
1366
+ // no field to set: the panel derives the name from the map, so the bus nudge is the whole update
1359
1367
  bus.emit("update", { kind: "agent-update", agentId });
1360
1368
  return { title: clean };
1361
1369
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teapot-coding-agent",
3
- "version": "0.37.92",
3
+ "version": "0.37.94",
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",