teapot-coding-agent 0.22.8 → 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 +100 -64
- package/dist/server/api.js +7 -19
- 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-BMAxEzAD.js → shiki-wiF0hU8g.js} +2 -2
- package/public/assets/{shiki-BMAxEzAD.js.map → shiki-wiF0hU8g.js.map} +1 -1
- package/public/index.html +1 -1
- package/public/assets/index-CRguS0Sf.js +0 -30
- package/public/assets/index-CRguS0Sf.js.map +0 -1
package/dist/master.js
CHANGED
|
@@ -717,8 +717,9 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
717
717
|
* prefix of the other ("prismrv-root-sub-review" vs
|
|
718
718
|
* "…-review-kernel") excluded each other's own dirs — every restart then
|
|
719
719
|
* generated a fresh empty session and history read as zero. The owner is
|
|
720
|
-
* now recorded explicitly at creation time;
|
|
721
|
-
*
|
|
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.
|
|
722
723
|
*/
|
|
723
724
|
readSessionOwner(dirName) {
|
|
724
725
|
try {
|
|
@@ -729,6 +730,24 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
729
730
|
return null; // legacy dir or unreadable manifest → fall back to names
|
|
730
731
|
}
|
|
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
|
+
}
|
|
732
751
|
writeSessionManifest(dirPath, agentId) {
|
|
733
752
|
try {
|
|
734
753
|
const file = path.join(dirPath, "session.json");
|
|
@@ -744,58 +763,39 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
744
763
|
// Directory names alone are ambiguous: agent "teapot" prefix-matches
|
|
745
764
|
// "teapot-3-<hash>", and sibling ids where one is a prefix of the other
|
|
746
765
|
// ("prismrv-root-sub-review" vs "…-review-kernel") used to exclude each
|
|
747
|
-
// other's own dirs
|
|
748
|
-
//
|
|
749
|
-
|
|
750
|
-
const
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
for (const [liveId] of this.agents) {
|
|
756
|
-
if (liveId !== agentId)
|
|
757
|
-
otherIds.add(liveId);
|
|
758
|
-
}
|
|
759
|
-
// legacy fallback: ownership goes to the LONGEST id matching the name
|
|
760
|
-
const belongsToOther = (dir) => {
|
|
761
|
-
const matchedLen = (id) => id === dir || dir.startsWith(`${id}-`) ? id.length : 0;
|
|
762
|
-
const own = matchedLen(agentId); // always > 0: caller pre-filtered by id
|
|
763
|
-
let bestOther = 0;
|
|
764
|
-
for (const other of otherIds) {
|
|
765
|
-
const len = matchedLen(other);
|
|
766
|
-
if (len > bestOther)
|
|
767
|
-
bestOther = len;
|
|
768
|
-
}
|
|
769
|
-
return bestOther > own; // equal-length ids cannot both match one dir
|
|
770
|
-
};
|
|
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);
|
|
771
774
|
let dirs = [];
|
|
772
775
|
try {
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
.
|
|
782
|
-
//
|
|
783
|
-
//
|
|
784
|
-
//
|
|
785
|
-
|
|
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;
|
|
786
789
|
try {
|
|
787
|
-
|
|
790
|
+
size = statSync(path.join(dirPath, "chat.jsonl")).size;
|
|
788
791
|
}
|
|
789
|
-
catch {
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
const mb = this.sessionMtime(path.join(this.sessionsRoot(), b));
|
|
797
|
-
return mb - ma;
|
|
798
|
-
});
|
|
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);
|
|
799
799
|
}
|
|
800
800
|
catch {
|
|
801
801
|
return null;
|
|
@@ -814,14 +814,15 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
814
814
|
const root = this.sessionsRoot();
|
|
815
815
|
mkdirSync(root, { recursive: true });
|
|
816
816
|
// exact-restart match first: <agentId> itself (or a previously generated
|
|
817
|
-
// <agentId>-<suffix> dir when the bare name was taken)
|
|
818
|
-
//
|
|
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.
|
|
819
820
|
const selfDir = path.join(root, agentId);
|
|
820
|
-
// the manifest on the bare-name dir wins: another agent may own it now
|
|
821
|
-
// (ids are recycled across incarnations). No manifest → legacy behavior.
|
|
822
821
|
const selfOwner = this.readSessionOwner(agentId);
|
|
823
822
|
const selfUsable = selfOwner === null || selfOwner === agentId;
|
|
824
|
-
if (!fresh &&
|
|
823
|
+
if (!fresh &&
|
|
824
|
+
selfUsable &&
|
|
825
|
+
statSync(path.join(selfDir, "chat.jsonl"), { throwIfNoEntry: false })?.size) {
|
|
825
826
|
return selfDir; // restart → continue where we left off
|
|
826
827
|
}
|
|
827
828
|
// one-time migration from the ≤0.5.0 flat layout (<dataDir>/<id>.jsonl)
|
|
@@ -850,11 +851,17 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
850
851
|
return path.join(root, sid);
|
|
851
852
|
}
|
|
852
853
|
/**
|
|
853
|
-
*
|
|
854
|
-
* first.
|
|
855
|
-
*
|
|
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.
|
|
856
857
|
*/
|
|
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.
|
|
858
865
|
let names = [];
|
|
859
866
|
try {
|
|
860
867
|
names = readdirSync(this.sessionsRoot());
|
|
@@ -862,10 +869,36 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
862
869
|
catch {
|
|
863
870
|
return [];
|
|
864
871
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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 }));
|
|
869
902
|
}
|
|
870
903
|
/** resolve an internal session id to (ownerAgentId, absolute dir) — null when it doesn't exist */
|
|
871
904
|
sessionById(sessionId) {
|
|
@@ -873,7 +906,10 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
873
906
|
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.startsWith("."))
|
|
874
907
|
return null;
|
|
875
908
|
const root = this.sessionsRoot();
|
|
876
|
-
const
|
|
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);
|
|
877
913
|
if (!owner)
|
|
878
914
|
return null;
|
|
879
915
|
try {
|
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)
|
|
@@ -947,6 +930,10 @@ export function buildApp(master) {
|
|
|
947
930
|
}
|
|
948
931
|
return c.json({ events: events.slice(-limit), total: events.length });
|
|
949
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() }));
|
|
950
937
|
// list the internal session ids (= stable timeline handles) an agent owns,
|
|
951
938
|
// newest first — the web UI routes /session/<internalId> by these
|
|
952
939
|
app.get("/api/agents/:id/sessions", (c) => {
|
|
@@ -954,9 +941,10 @@ export function buildApp(master) {
|
|
|
954
941
|
// resolvable when live, configured, OR still owning sessions on disk
|
|
955
942
|
// (a stopped/lazy agent must keep its timeline reachable)
|
|
956
943
|
const known = master.agents.has(id) || master.config.agents.some((a) => a.id === id);
|
|
957
|
-
|
|
944
|
+
const owned = master.sessionsOf(id);
|
|
945
|
+
if (!known && owned.length === 0)
|
|
958
946
|
return c.json({ error: "not found" }, 404);
|
|
959
|
-
return c.json({ sessions:
|
|
947
|
+
return c.json({ sessions: owned });
|
|
960
948
|
});
|
|
961
949
|
app.get("/api/agents/:id/branches", async (c) => {
|
|
962
950
|
const a = master.agents.get(c.req.param("id"));
|