teapot-coding-agent 0.37.92 → 0.37.93

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(() => { });
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.93",
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",