teapot-coding-agent 0.22.7 → 0.22.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.
- package/dist/master.js +175 -46
- package/dist/server/api.js +28 -21
- package/package.json +1 -1
- package/public/assets/index-DtMSCXN5.js +30 -0
- package/public/assets/index-DtMSCXN5.js.map +1 -0
- package/public/assets/{shiki-DELzRiTA.js → shiki-wiF0hU8g.js} +2 -2
- package/public/assets/{shiki-DELzRiTA.js.map → shiki-wiF0hU8g.js.map} +1 -1
- package/public/index.html +1 -1
- package/public/assets/index-BXBPj05l.js +0 -30
- package/public/assets/index-BXBPj05l.js.map +0 -1
package/dist/master.js
CHANGED
|
@@ -380,6 +380,9 @@ export class Master {
|
|
|
380
380
|
}
|
|
381
381
|
const sessionDir = this.resolveSessionDir(ac.id, opts.fresh === true);
|
|
382
382
|
await mkdirSync(sessionDir, { recursive: true });
|
|
383
|
+
// every session dir this master hands out carries an ownership manifest —
|
|
384
|
+
// legacy dirs get backfilled on their first restart under the new code
|
|
385
|
+
this.writeSessionManifest(sessionDir, ac.id);
|
|
383
386
|
const agent = new Agent({
|
|
384
387
|
id: ac.id,
|
|
385
388
|
workspace: path.resolve(ac.workspace),
|
|
@@ -639,12 +642,19 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
639
642
|
}
|
|
640
643
|
}
|
|
641
644
|
async messageChild(parentId, childId, text) {
|
|
642
|
-
|
|
645
|
+
// childId is normally the live agent id — but an internal session id
|
|
646
|
+
// (sessions/<id>, the stable timeline handle) also resolves, so callers
|
|
647
|
+
// holding only a session reference can still reach the owning agent.
|
|
648
|
+
let targetId = childId;
|
|
649
|
+
const owned = this.sessionById(childId);
|
|
650
|
+
if (owned)
|
|
651
|
+
targetId = owned.agentId;
|
|
652
|
+
const cfg = this.config.agents.find((a) => a.id === targetId);
|
|
643
653
|
if (!cfg || cfg.parent !== parentId)
|
|
644
654
|
throw new Error(`not your sub-agent: ${childId}`);
|
|
645
|
-
const child = this.agents.get(
|
|
655
|
+
const child = this.agents.get(targetId);
|
|
646
656
|
if (!child)
|
|
647
|
-
throw new Error(`sub-agent not running: ${
|
|
657
|
+
throw new Error(`sub-agent not running: ${targetId}`);
|
|
648
658
|
child.enqueuePrompt(`[harness] Message from @${parentId}:\n\n${text}`, "harness");
|
|
649
659
|
if (child.status !== "running")
|
|
650
660
|
child.start(`message from ${parentId}`);
|
|
@@ -700,51 +710,92 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
700
710
|
sessionsRoot() {
|
|
701
711
|
return path.join(this.config.dataDir, "sessions");
|
|
702
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* Session-dir ownership manifest (sessions/<dir>/session.json). Ownership
|
|
715
|
+
* used to be GUESSED from the directory name's prefix, which is ambiguous:
|
|
716
|
+
* agent "teapot" matches "teapot-3-<hash>", and sibling ids where one is a
|
|
717
|
+
* prefix of the other ("prismrv-root-sub-review" vs
|
|
718
|
+
* "…-review-kernel") excluded each other's own dirs — every restart then
|
|
719
|
+
* generated a fresh empty session and history read as zero. The owner is
|
|
720
|
+
* now recorded explicitly at creation time; legacy dirs without a manifest
|
|
721
|
+
* fall back to longest-prefix inference and get one backfilled on first
|
|
722
|
+
* touch.
|
|
723
|
+
*/
|
|
724
|
+
readSessionOwner(dirName) {
|
|
725
|
+
try {
|
|
726
|
+
const raw = JSON.parse(readFileSync(path.join(this.sessionsRoot(), dirName, "session.json"), "utf8"));
|
|
727
|
+
return typeof raw.agentId === "string" ? raw.agentId : null;
|
|
728
|
+
}
|
|
729
|
+
catch {
|
|
730
|
+
return null; // legacy dir or unreadable manifest → fall back to names
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
/**
|
|
734
|
+
* Owner of a session dir — manifest first; legacy dirs are inferred from
|
|
735
|
+
* the LONGEST configured/live id whose "<id>" or "<id>-<rest>" shape the
|
|
736
|
+
* name matches. Returns {owner, hadManifest} so callers can backfill.
|
|
737
|
+
*/
|
|
738
|
+
inferSessionOwner(dirName, candidateIds) {
|
|
739
|
+
const fromManifest = this.readSessionOwner(dirName);
|
|
740
|
+
if (fromManifest)
|
|
741
|
+
return { owner: fromManifest, hadManifest: true };
|
|
742
|
+
let best = null;
|
|
743
|
+
for (const id of candidateIds) {
|
|
744
|
+
if (!id)
|
|
745
|
+
continue;
|
|
746
|
+
if ((id === dirName || dirName.startsWith(`${id}-`)) && (!best || id.length > best.len))
|
|
747
|
+
best = { id, len: id.length };
|
|
748
|
+
}
|
|
749
|
+
return { owner: best?.id ?? null, hadManifest: false };
|
|
750
|
+
}
|
|
751
|
+
writeSessionManifest(dirPath, agentId) {
|
|
752
|
+
try {
|
|
753
|
+
const file = path.join(dirPath, "session.json");
|
|
754
|
+
if (existsSync(file))
|
|
755
|
+
return; // never clobber an existing manifest
|
|
756
|
+
writeFileSync(file, `${JSON.stringify({ v: 1, agentId }, null, 2)}\n`);
|
|
757
|
+
}
|
|
758
|
+
catch {
|
|
759
|
+
/* best-effort metadata */
|
|
760
|
+
}
|
|
761
|
+
}
|
|
703
762
|
findLatestSessionDir(agentId) {
|
|
704
|
-
// Directory names are
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
const
|
|
710
|
-
for (const a of this.config.agents)
|
|
711
|
-
if (a.id
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
if (liveId !== agentId)
|
|
716
|
-
otherIds.add(liveId);
|
|
717
|
-
}
|
|
718
|
-
const belongsToOther = (dir) => {
|
|
719
|
-
for (const other of otherIds) {
|
|
720
|
-
if (other === dir || dir.startsWith(`${other}-`))
|
|
721
|
-
return true;
|
|
722
|
-
}
|
|
723
|
-
return false;
|
|
724
|
-
};
|
|
763
|
+
// Directory names alone are ambiguous: agent "teapot" prefix-matches
|
|
764
|
+
// "teapot-3-<hash>", and sibling ids where one is a prefix of the other
|
|
765
|
+
// ("prismrv-root-sub-review" vs "…-review-kernel") used to exclude each
|
|
766
|
+
// other's own dirs. Ownership comes from the manifest; legacy dirs are
|
|
767
|
+
// inferred by longest matching id.
|
|
768
|
+
const candidateIds = new Set([agentId]); // the agent itself owns its bare-name dir
|
|
769
|
+
for (const a of this.config.agents)
|
|
770
|
+
if (a.id)
|
|
771
|
+
candidateIds.add(a.id);
|
|
772
|
+
for (const [liveId] of this.agents)
|
|
773
|
+
candidateIds.add(liveId);
|
|
725
774
|
let dirs = [];
|
|
726
775
|
try {
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
.
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
776
|
+
const withHistory = [];
|
|
777
|
+
const empty = [];
|
|
778
|
+
for (const d of readdirSync(this.sessionsRoot())) {
|
|
779
|
+
const { owner } = this.inferSessionOwner(d, candidateIds);
|
|
780
|
+
if (owner !== agentId)
|
|
781
|
+
continue;
|
|
782
|
+
const dirPath = path.join(this.sessionsRoot(), d);
|
|
783
|
+
const m = this.sessionMtime(dirPath);
|
|
784
|
+
// an EMPTY chat.jsonl means the session was never really used — but it
|
|
785
|
+
// must not SHADOW a real history dir: an incarnation's state events
|
|
786
|
+
// keep refreshing the empty log's mtime, and newest-wins would then
|
|
787
|
+
// pick the empty one forever ("timeline of zero" on every restart).
|
|
788
|
+
let size = 0;
|
|
735
789
|
try {
|
|
736
|
-
|
|
737
|
-
}
|
|
738
|
-
catch {
|
|
739
|
-
return false;
|
|
790
|
+
size = statSync(path.join(dirPath, "chat.jsonl")).size;
|
|
740
791
|
}
|
|
741
|
-
|
|
742
|
-
.
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
792
|
+
catch { /* no log yet */ }
|
|
793
|
+
(size > 0 ? withHistory : empty).push({ d, m });
|
|
794
|
+
}
|
|
795
|
+
const pick = (list) => list.sort((a, b) => b.m - a.m)[0]?.d;
|
|
796
|
+
// prefer a dir that actually carries history; fall back to the newest
|
|
797
|
+
// empty one ONLY when no history exists under this id at all
|
|
798
|
+
dirs = [pick(withHistory) ?? pick(empty)].filter((x) => !!x);
|
|
748
799
|
}
|
|
749
800
|
catch {
|
|
750
801
|
return null;
|
|
@@ -763,10 +814,15 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
763
814
|
const root = this.sessionsRoot();
|
|
764
815
|
mkdirSync(root, { recursive: true });
|
|
765
816
|
// exact-restart match first: <agentId> itself (or a previously generated
|
|
766
|
-
// <agentId>-<suffix> dir when the bare name was taken)
|
|
767
|
-
//
|
|
817
|
+
// <agentId>-<suffix> dir when the bare name was taken) — but only when it
|
|
818
|
+
// actually carries history. An empty log here must not shadow the real
|
|
819
|
+
// one found by findLatestSessionDir below.
|
|
768
820
|
const selfDir = path.join(root, agentId);
|
|
769
|
-
|
|
821
|
+
const selfOwner = this.readSessionOwner(agentId);
|
|
822
|
+
const selfUsable = selfOwner === null || selfOwner === agentId;
|
|
823
|
+
if (!fresh &&
|
|
824
|
+
selfUsable &&
|
|
825
|
+
statSync(path.join(selfDir, "chat.jsonl"), { throwIfNoEntry: false })?.size) {
|
|
770
826
|
return selfDir; // restart → continue where we left off
|
|
771
827
|
}
|
|
772
828
|
// one-time migration from the ≤0.5.0 flat layout (<dataDir>/<id>.jsonl)
|
|
@@ -791,8 +847,81 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
791
847
|
do {
|
|
792
848
|
sid = `${agentId}-${randomUUID().slice(0, 8)}`;
|
|
793
849
|
} while (existsSync(path.join(root, sid)));
|
|
850
|
+
this.writeSessionManifest(path.join(root, sid), agentId);
|
|
794
851
|
return path.join(root, sid);
|
|
795
852
|
}
|
|
853
|
+
/**
|
|
854
|
+
* All session dirs on disk with their inferred/manifested owner, newest
|
|
855
|
+
* log first. One readdir powers every "list sessions" need — the UI calls
|
|
856
|
+
* the aggregated endpoint instead of fanning out one request per agent.
|
|
857
|
+
*/
|
|
858
|
+
allSessions() {
|
|
859
|
+
const candidateIds = new Set(this.config.agents.map((a) => a.id).filter(Boolean));
|
|
860
|
+
for (const [liveId] of this.agents)
|
|
861
|
+
candidateIds.add(liveId);
|
|
862
|
+
// dir names themselves are candidate owners too: "s" owns legacy "s",
|
|
863
|
+
// "teapot-3-3c0048d7" is best matched by the longest configured id —
|
|
864
|
+
// inference needs the id set only, so nothing extra to add here.
|
|
865
|
+
let names = [];
|
|
866
|
+
try {
|
|
867
|
+
names = readdirSync(this.sessionsRoot());
|
|
868
|
+
}
|
|
869
|
+
catch {
|
|
870
|
+
return [];
|
|
871
|
+
}
|
|
872
|
+
const out = [];
|
|
873
|
+
for (const d of names) {
|
|
874
|
+
const dirPath = path.join(this.sessionsRoot(), d);
|
|
875
|
+
const { owner, hadManifest } = this.inferSessionOwner(d, candidateIds);
|
|
876
|
+
if (!owner)
|
|
877
|
+
continue; // orphan dir (unknown owner) — not addressable
|
|
878
|
+
// backfill legacy dirs so the inference result becomes durable
|
|
879
|
+
if (!hadManifest)
|
|
880
|
+
this.writeSessionManifest(dirPath, owner);
|
|
881
|
+
let m = 0;
|
|
882
|
+
let size = 0;
|
|
883
|
+
try {
|
|
884
|
+
const st = statSync(path.join(dirPath, "chat.jsonl"));
|
|
885
|
+
m = st.mtimeMs;
|
|
886
|
+
size = st.size;
|
|
887
|
+
}
|
|
888
|
+
catch { /* no log yet — still listable */ }
|
|
889
|
+
out.push({ id: d, agentId: owner, mtimeMs: m, sizeBytes: size });
|
|
890
|
+
}
|
|
891
|
+
return out.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
892
|
+
}
|
|
893
|
+
/**
|
|
894
|
+
* Internal session ids (= directory names) owned by an agent, newest log
|
|
895
|
+
* first. These are the stable handles the web UI routes by — agent ids can
|
|
896
|
+
* be recycled across incarnations, internal session ids cannot.
|
|
897
|
+
*/
|
|
898
|
+
sessionsOf(agentId) {
|
|
899
|
+
return this.allSessions()
|
|
900
|
+
.filter((s) => s.agentId === agentId)
|
|
901
|
+
.map(({ id, mtimeMs, sizeBytes }) => ({ id, mtimeMs, sizeBytes }));
|
|
902
|
+
}
|
|
903
|
+
/** resolve an internal session id to (ownerAgentId, absolute dir) — null when it doesn't exist */
|
|
904
|
+
sessionById(sessionId) {
|
|
905
|
+
// internal ids are directory names: refuse anything path-like up front
|
|
906
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.startsWith("."))
|
|
907
|
+
return null;
|
|
908
|
+
const root = this.sessionsRoot();
|
|
909
|
+
const candidateIds = new Set(this.config.agents.map((a) => a.id).filter(Boolean));
|
|
910
|
+
for (const [liveId] of this.agents)
|
|
911
|
+
candidateIds.add(liveId);
|
|
912
|
+
const { owner } = this.inferSessionOwner(sessionId, candidateIds);
|
|
913
|
+
if (!owner)
|
|
914
|
+
return null;
|
|
915
|
+
try {
|
|
916
|
+
const st = statSync(path.join(root, sessionId));
|
|
917
|
+
if (!st.isDirectory())
|
|
918
|
+
return null;
|
|
919
|
+
}
|
|
920
|
+
catch {
|
|
921
|
+
return null;
|
|
922
|
+
}
|
|
923
|
+
return { agentId: owner, dir: path.join(root, sessionId) };
|
|
924
|
+
}
|
|
796
925
|
async removeAgent(id) {
|
|
797
926
|
const agent = this.agents.get(id);
|
|
798
927
|
if (!agent)
|
package/dist/server/api.js
CHANGED
|
@@ -871,23 +871,6 @@ export function buildApp(master) {
|
|
|
871
871
|
return c.json({ ok: true });
|
|
872
872
|
});
|
|
873
873
|
// edit a previously-sent prompt: forks there, optionally summarizes the tail
|
|
874
|
-
app.post("/api/agents/:id/edit-prompt", async (c) => {
|
|
875
|
-
const a = master.agents.get(c.req.param("id"));
|
|
876
|
-
if (!a)
|
|
877
|
-
return c.json({ error: "not found" }, 404);
|
|
878
|
-
const body = await c.req
|
|
879
|
-
.json()
|
|
880
|
-
.catch(() => null);
|
|
881
|
-
if (!body?.eventId || !body.text?.trim())
|
|
882
|
-
return c.json({ error: "eventId and text required" }, 400);
|
|
883
|
-
try {
|
|
884
|
-
const r = await a.editPromptAt(body.eventId, body.text, body.tail === "summarize" ? "summarize" : "discard");
|
|
885
|
-
return c.json({ ok: true, ...r });
|
|
886
|
-
}
|
|
887
|
-
catch (err) {
|
|
888
|
-
return c.json({ error: err.message }, 409);
|
|
889
|
-
}
|
|
890
|
-
});
|
|
891
874
|
app.post("/api/agents/:id/fork", async (c) => {
|
|
892
875
|
const a = master.agents.get(c.req.param("id"));
|
|
893
876
|
if (!a)
|
|
@@ -923,13 +906,21 @@ export function buildApp(master) {
|
|
|
923
906
|
if (!a)
|
|
924
907
|
return c.json({ error: "not found" }, 404);
|
|
925
908
|
const limit = Math.min(Number(c.req.query("limit") ?? 200), 5000);
|
|
926
|
-
let
|
|
909
|
+
let filePath = a.log.filePath;
|
|
910
|
+
// ?session=<internalId> reads ANY session owned by this agent (past
|
|
911
|
+
// incarnations too) — the internal id, not the agent id, is the stable
|
|
912
|
+
// timeline handle. Ownership is enforced via the manifest.
|
|
913
|
+
const sessionId = c.req.query("session");
|
|
914
|
+
if (sessionId) {
|
|
915
|
+
const rec = master.sessionById(sessionId);
|
|
916
|
+
if (!rec || rec.agentId !== c.req.param("id"))
|
|
917
|
+
return c.json({ error: "not found" }, 404);
|
|
918
|
+
filePath = path.join(rec.dir, "chat.jsonl");
|
|
919
|
+
}
|
|
920
|
+
let events = await readEventsCached(filePath);
|
|
927
921
|
const branch = c.req.query("branch");
|
|
928
|
-
const session = c.req.query("session");
|
|
929
922
|
if (branch)
|
|
930
923
|
events = events.filter((e) => e.branch === branch);
|
|
931
|
-
if (session)
|
|
932
|
-
events = events.filter((e) => e.session === session);
|
|
933
924
|
// cursor pagination for older pages: everything strictly BEFORE this id
|
|
934
925
|
const before = c.req.query("before");
|
|
935
926
|
if (before) {
|
|
@@ -939,6 +930,22 @@ export function buildApp(master) {
|
|
|
939
930
|
}
|
|
940
931
|
return c.json({ events: events.slice(-limit), total: events.length });
|
|
941
932
|
});
|
|
933
|
+
// ONE readdir-backed snapshot of every session on disk, owner included.
|
|
934
|
+
// The UI fetches this once per agent-set change instead of fanning out one
|
|
935
|
+
// request per agent — /api/agents/:id/sessions stays for compatibility.
|
|
936
|
+
app.get("/api/sessions", (c) => c.json({ sessions: master.allSessions() }));
|
|
937
|
+
// list the internal session ids (= stable timeline handles) an agent owns,
|
|
938
|
+
// newest first — the web UI routes /session/<internalId> by these
|
|
939
|
+
app.get("/api/agents/:id/sessions", (c) => {
|
|
940
|
+
const id = c.req.param("id");
|
|
941
|
+
// resolvable when live, configured, OR still owning sessions on disk
|
|
942
|
+
// (a stopped/lazy agent must keep its timeline reachable)
|
|
943
|
+
const known = master.agents.has(id) || master.config.agents.some((a) => a.id === id);
|
|
944
|
+
const owned = master.sessionsOf(id);
|
|
945
|
+
if (!known && owned.length === 0)
|
|
946
|
+
return c.json({ error: "not found" }, 404);
|
|
947
|
+
return c.json({ sessions: owned });
|
|
948
|
+
});
|
|
942
949
|
app.get("/api/agents/:id/branches", async (c) => {
|
|
943
950
|
const a = master.agents.get(c.req.param("id"));
|
|
944
951
|
if (!a)
|