teapot-coding-agent 0.22.7 → 0.22.8
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 +107 -14
- package/dist/server/api.js +23 -4
- package/package.json +1 -1
- package/public/assets/{index-BXBPj05l.js → index-CRguS0Sf.js} +13 -13
- package/public/assets/index-CRguS0Sf.js.map +1 -0
- package/public/assets/{shiki-DELzRiTA.js → shiki-BMAxEzAD.js} +2 -2
- package/public/assets/{shiki-DELzRiTA.js.map → shiki-BMAxEzAD.js.map} +1 -1
- package/public/index.html +1 -1
- 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,12 +710,43 @@ 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; prefix matching only remains
|
|
721
|
+
* as a fallback for legacy dirs without a manifest.
|
|
722
|
+
*/
|
|
723
|
+
readSessionOwner(dirName) {
|
|
724
|
+
try {
|
|
725
|
+
const raw = JSON.parse(readFileSync(path.join(this.sessionsRoot(), dirName, "session.json"), "utf8"));
|
|
726
|
+
return typeof raw.agentId === "string" ? raw.agentId : null;
|
|
727
|
+
}
|
|
728
|
+
catch {
|
|
729
|
+
return null; // legacy dir or unreadable manifest → fall back to names
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
writeSessionManifest(dirPath, agentId) {
|
|
733
|
+
try {
|
|
734
|
+
const file = path.join(dirPath, "session.json");
|
|
735
|
+
if (existsSync(file))
|
|
736
|
+
return; // never clobber an existing manifest
|
|
737
|
+
writeFileSync(file, `${JSON.stringify({ v: 1, agentId }, null, 2)}\n`);
|
|
738
|
+
}
|
|
739
|
+
catch {
|
|
740
|
+
/* best-effort metadata */
|
|
741
|
+
}
|
|
742
|
+
}
|
|
703
743
|
findLatestSessionDir(agentId) {
|
|
704
|
-
// Directory names are
|
|
705
|
-
//
|
|
706
|
-
//
|
|
707
|
-
//
|
|
708
|
-
//
|
|
744
|
+
// Directory names alone are ambiguous: agent "teapot" prefix-matches
|
|
745
|
+
// "teapot-3-<hash>", and sibling ids where one is a prefix of the other
|
|
746
|
+
// ("prismrv-root-sub-review" vs "…-review-kernel") used to exclude each
|
|
747
|
+
// other's own dirs — every restart then generated a fresh empty session
|
|
748
|
+
// and history read as zero. The manifest written at creation time
|
|
749
|
+
// decides; only legacy dirs without one fall back to name matching.
|
|
709
750
|
const otherIds = new Set();
|
|
710
751
|
for (const a of this.config.agents) {
|
|
711
752
|
if (a.id && a.id !== agentId)
|
|
@@ -715,18 +756,28 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
715
756
|
if (liveId !== agentId)
|
|
716
757
|
otherIds.add(liveId);
|
|
717
758
|
}
|
|
759
|
+
// legacy fallback: ownership goes to the LONGEST id matching the name
|
|
718
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;
|
|
719
764
|
for (const other of otherIds) {
|
|
720
|
-
|
|
721
|
-
|
|
765
|
+
const len = matchedLen(other);
|
|
766
|
+
if (len > bestOther)
|
|
767
|
+
bestOther = len;
|
|
722
768
|
}
|
|
723
|
-
return
|
|
769
|
+
return bestOther > own; // equal-length ids cannot both match one dir
|
|
724
770
|
};
|
|
725
771
|
let dirs = [];
|
|
726
772
|
try {
|
|
727
773
|
dirs = readdirSync(this.sessionsRoot())
|
|
728
|
-
.filter((d) =>
|
|
729
|
-
|
|
774
|
+
.filter((d) => {
|
|
775
|
+
const owner = this.readSessionOwner(d);
|
|
776
|
+
if (owner)
|
|
777
|
+
return owner === agentId; // manifest decides
|
|
778
|
+
// legacy dir without a manifest: name-prefix + longest-match rule
|
|
779
|
+
return (d === agentId || d.startsWith(`${agentId}-`)) && !belongsToOther(d);
|
|
780
|
+
})
|
|
730
781
|
.filter((d) => {
|
|
731
782
|
// an EMPTY chat.jsonl means the session was never used (created via
|
|
732
783
|
// web, then abandoned). Reusing it would resurrect another agent's
|
|
@@ -766,7 +817,11 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
766
817
|
// <agentId>-<suffix> dir when the bare name was taken). Prefix matching
|
|
767
818
|
// alone could hand agent "proj-2" agent "proj"'s directory.
|
|
768
819
|
const selfDir = path.join(root, agentId);
|
|
769
|
-
|
|
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
|
+
const selfOwner = this.readSessionOwner(agentId);
|
|
823
|
+
const selfUsable = selfOwner === null || selfOwner === agentId;
|
|
824
|
+
if (!fresh && selfUsable && statSync(path.join(selfDir, "chat.jsonl"), { throwIfNoEntry: false })?.size) {
|
|
770
825
|
return selfDir; // restart → continue where we left off
|
|
771
826
|
}
|
|
772
827
|
// one-time migration from the ≤0.5.0 flat layout (<dataDir>/<id>.jsonl)
|
|
@@ -791,8 +846,46 @@ When this task is done (or truly blocked), call finish() with a summary for @${p
|
|
|
791
846
|
do {
|
|
792
847
|
sid = `${agentId}-${randomUUID().slice(0, 8)}`;
|
|
793
848
|
} while (existsSync(path.join(root, sid)));
|
|
849
|
+
this.writeSessionManifest(path.join(root, sid), agentId);
|
|
794
850
|
return path.join(root, sid);
|
|
795
851
|
}
|
|
852
|
+
/**
|
|
853
|
+
* Internal session ids (= directory names) owned by an agent, newest log
|
|
854
|
+
* first. These are the stable handles the web UI routes by — agent ids can
|
|
855
|
+
* be recycled across incarnations, internal session ids cannot.
|
|
856
|
+
*/
|
|
857
|
+
sessionsOf(agentId) {
|
|
858
|
+
let names = [];
|
|
859
|
+
try {
|
|
860
|
+
names = readdirSync(this.sessionsRoot());
|
|
861
|
+
}
|
|
862
|
+
catch {
|
|
863
|
+
return [];
|
|
864
|
+
}
|
|
865
|
+
return names
|
|
866
|
+
.filter((d) => this.readSessionOwner(d) === agentId)
|
|
867
|
+
.map((d) => ({ id: d, mtimeMs: this.sessionMtime(path.join(this.sessionsRoot(), d)) }))
|
|
868
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
869
|
+
}
|
|
870
|
+
/** resolve an internal session id to (ownerAgentId, absolute dir) — null when it doesn't exist */
|
|
871
|
+
sessionById(sessionId) {
|
|
872
|
+
// internal ids are directory names: refuse anything path-like up front
|
|
873
|
+
if (!sessionId || sessionId.includes("/") || sessionId.includes("\\") || sessionId.startsWith("."))
|
|
874
|
+
return null;
|
|
875
|
+
const root = this.sessionsRoot();
|
|
876
|
+
const owner = this.readSessionOwner(sessionId);
|
|
877
|
+
if (!owner)
|
|
878
|
+
return null;
|
|
879
|
+
try {
|
|
880
|
+
const st = statSync(path.join(root, sessionId));
|
|
881
|
+
if (!st.isDirectory())
|
|
882
|
+
return null;
|
|
883
|
+
}
|
|
884
|
+
catch {
|
|
885
|
+
return null;
|
|
886
|
+
}
|
|
887
|
+
return { agentId: owner, dir: path.join(root, sessionId) };
|
|
888
|
+
}
|
|
796
889
|
async removeAgent(id) {
|
|
797
890
|
const agent = this.agents.get(id);
|
|
798
891
|
if (!agent)
|
package/dist/server/api.js
CHANGED
|
@@ -923,13 +923,21 @@ export function buildApp(master) {
|
|
|
923
923
|
if (!a)
|
|
924
924
|
return c.json({ error: "not found" }, 404);
|
|
925
925
|
const limit = Math.min(Number(c.req.query("limit") ?? 200), 5000);
|
|
926
|
-
let
|
|
926
|
+
let filePath = a.log.filePath;
|
|
927
|
+
// ?session=<internalId> reads ANY session owned by this agent (past
|
|
928
|
+
// incarnations too) — the internal id, not the agent id, is the stable
|
|
929
|
+
// timeline handle. Ownership is enforced via the manifest.
|
|
930
|
+
const sessionId = c.req.query("session");
|
|
931
|
+
if (sessionId) {
|
|
932
|
+
const rec = master.sessionById(sessionId);
|
|
933
|
+
if (!rec || rec.agentId !== c.req.param("id"))
|
|
934
|
+
return c.json({ error: "not found" }, 404);
|
|
935
|
+
filePath = path.join(rec.dir, "chat.jsonl");
|
|
936
|
+
}
|
|
937
|
+
let events = await readEventsCached(filePath);
|
|
927
938
|
const branch = c.req.query("branch");
|
|
928
|
-
const session = c.req.query("session");
|
|
929
939
|
if (branch)
|
|
930
940
|
events = events.filter((e) => e.branch === branch);
|
|
931
|
-
if (session)
|
|
932
|
-
events = events.filter((e) => e.session === session);
|
|
933
941
|
// cursor pagination for older pages: everything strictly BEFORE this id
|
|
934
942
|
const before = c.req.query("before");
|
|
935
943
|
if (before) {
|
|
@@ -939,6 +947,17 @@ export function buildApp(master) {
|
|
|
939
947
|
}
|
|
940
948
|
return c.json({ events: events.slice(-limit), total: events.length });
|
|
941
949
|
});
|
|
950
|
+
// list the internal session ids (= stable timeline handles) an agent owns,
|
|
951
|
+
// newest first — the web UI routes /session/<internalId> by these
|
|
952
|
+
app.get("/api/agents/:id/sessions", (c) => {
|
|
953
|
+
const id = c.req.param("id");
|
|
954
|
+
// resolvable when live, configured, OR still owning sessions on disk
|
|
955
|
+
// (a stopped/lazy agent must keep its timeline reachable)
|
|
956
|
+
const known = master.agents.has(id) || master.config.agents.some((a) => a.id === id);
|
|
957
|
+
if (!known && master.sessionsOf(id).length === 0)
|
|
958
|
+
return c.json({ error: "not found" }, 404);
|
|
959
|
+
return c.json({ sessions: master.sessionsOf(id) });
|
|
960
|
+
});
|
|
942
961
|
app.get("/api/agents/:id/branches", async (c) => {
|
|
943
962
|
const a = master.agents.get(c.req.param("id"));
|
|
944
963
|
if (!a)
|