Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2a31537b80 | |||
| 0cd853e330 |
@@ -7316,7 +7316,7 @@ exports[`screen snapshots > Settings — about 1`] = `
|
||||
<span
|
||||
class="text-status-warning/90"
|
||||
>
|
||||
UI build v2.8.73 (rebuild client to match)
|
||||
UI build v2.8.74 (rebuild client to match)
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "2.8.73",
|
||||
"version": "2.8.74",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "agenthub",
|
||||
"version": "2.8.73",
|
||||
"version": "2.8.74",
|
||||
"hasInstallScript": true,
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "agenthub",
|
||||
"version": "2.8.73",
|
||||
"version": "2.8.74",
|
||||
"bin": {
|
||||
"agenthub": "bin/agenthub.js"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @file Regression: transcript lookups in claude-home.js find a file by
|
||||
* `stat`, never `fs.existsSync`. existsSync is access(2), which the kernel runs
|
||||
* with the capabilities cleared, so under S3 it reported a member's live
|
||||
* transcript (behind their 0700 `~/.claude/projects`) as missing and the chat
|
||||
* served the stale import-time snapshot — a whole turn shown in the terminal
|
||||
* never reached the chat. Here `fs.existsSync` is stubbed to answer "no" for
|
||||
* everything under the temp Claude home, as access(2) does for the unit.
|
||||
* @author Nguyễn Ngọc Trí Vĩ <vinnt@smartgift.vn>
|
||||
*/
|
||||
|
||||
const os = require("node:os");
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { describe, it, before, after } = require("node:test");
|
||||
const assert = require("node:assert/strict");
|
||||
|
||||
const ROOT = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-access-blind-"));
|
||||
const SAVED_HOME = process.env.CLAUDE_HOME;
|
||||
process.env.CLAUDE_HOME = ROOT;
|
||||
|
||||
const ch = require("../lib/claude-home");
|
||||
|
||||
const SID = "11111111-2222-3333-4444-555555555555";
|
||||
const CWD = "/work/demo-repo";
|
||||
const PROJECT = path.join(ROOT, "projects", ch.encodeCwd(CWD));
|
||||
const MAIN = path.join(PROJECT, `${SID}.jsonl`);
|
||||
const SUB = path.join(PROJECT, SID, "subagents", "agent-abc.jsonl");
|
||||
const NESTED = path.join(PROJECT, SID, "subagents", "workflows", "wf_1", "agent-xyz.jsonl");
|
||||
|
||||
const realExists = fs.existsSync;
|
||||
|
||||
describe("claude-home transcript lookups when access(2) is blind", () => {
|
||||
before(() => {
|
||||
for (const f of [MAIN, SUB, NESTED]) {
|
||||
fs.mkdirSync(path.dirname(f), { recursive: true });
|
||||
fs.writeFileSync(f, "{}\n");
|
||||
}
|
||||
fs.existsSync = (p) => (String(p).startsWith(ROOT) ? false : realExists(p));
|
||||
});
|
||||
after(() => {
|
||||
fs.existsSync = realExists;
|
||||
if (SAVED_HOME === undefined) delete process.env.CLAUDE_HOME;
|
||||
else process.env.CLAUDE_HOME = SAVED_HOME;
|
||||
fs.rmSync(ROOT, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("getTranscriptPath finds the live main transcript", () => {
|
||||
assert.equal(ch.getTranscriptPath(SID, CWD), MAIN);
|
||||
});
|
||||
|
||||
it("findTranscriptPath finds it by scanning", () => {
|
||||
assert.equal(ch.findTranscriptPath(SID, CWD), MAIN);
|
||||
});
|
||||
|
||||
it("subagent transcripts (flat and nested Workflow) are found", () => {
|
||||
assert.equal(ch.findSubagentTranscriptPath(SID, "abc", null, CWD), SUB);
|
||||
assert.equal(ch.findSubagentTranscriptPath(SID, "xyz", "wf_1", CWD), NESTED);
|
||||
assert.equal(ch.findSubagentTranscriptPath(SID, "xyz", null, CWD), NESTED);
|
||||
});
|
||||
|
||||
it("a missing transcript is still reported missing", () => {
|
||||
assert.equal(ch.getTranscriptPath("no-such-session", CWD), null);
|
||||
assert.equal(ch.exists(path.join(ROOT, "nope")), false);
|
||||
});
|
||||
});
|
||||
+28
-11
@@ -11,6 +11,22 @@ const path = require("path");
|
||||
const os = require("os");
|
||||
const fs = require("fs");
|
||||
|
||||
/**
|
||||
* Whether `p` exists, by `stat`. Not `fs.existsSync`: that is access(2), which
|
||||
* checks the REAL uid with capabilities cleared, so under S3 it says "no" for a
|
||||
* member's transcript behind their 0700 `~/.claude/projects` — and the chat
|
||||
* fell back to a stale snapshot while the unit itself reads the live file
|
||||
* through its CAP_DAC_READ_SEARCH.
|
||||
*/
|
||||
function exists(p) {
|
||||
try {
|
||||
fs.statSync(p);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getClaudeHome() {
|
||||
return process.env.CLAUDE_HOME || path.join(os.homedir(), ".claude");
|
||||
}
|
||||
@@ -143,7 +159,7 @@ function getTranscriptPath(sessionId, cwd) {
|
||||
if (!cwd) return null;
|
||||
const encoded = encodeCwd(cwd);
|
||||
const candidate = path.join(projectsDirFor(cwd), encoded, `${sessionId}.jsonl`);
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
if (exists(candidate)) return candidate;
|
||||
// Fallback: scan the owner's projects/ subdirectories
|
||||
return findTranscriptPath(sessionId, cwd);
|
||||
}
|
||||
@@ -170,14 +186,14 @@ function getTranscriptPath(sessionId, cwd) {
|
||||
function resolveAgentTranscriptInDir(subagentsDir, agentId, runId = null) {
|
||||
if (!subagentsDir) return null;
|
||||
const flat = path.join(subagentsDir, `agent-${agentId}.jsonl`);
|
||||
if (fs.existsSync(flat)) return flat;
|
||||
if (exists(flat)) return flat;
|
||||
|
||||
const workflowsDir = path.join(subagentsDir, "workflows");
|
||||
if (!fs.existsSync(workflowsDir)) return null;
|
||||
if (!exists(workflowsDir)) return null;
|
||||
|
||||
if (runId) {
|
||||
const nested = path.join(workflowsDir, runId, `agent-${agentId}.jsonl`);
|
||||
return fs.existsSync(nested) ? nested : null;
|
||||
return exists(nested) ? nested : null;
|
||||
}
|
||||
|
||||
// Unknown run: accept only an unambiguous single match across all runs.
|
||||
@@ -186,7 +202,7 @@ function resolveAgentTranscriptInDir(subagentsDir, agentId, runId = null) {
|
||||
for (const d of fs.readdirSync(workflowsDir, { withFileTypes: true })) {
|
||||
if (!d.isDirectory()) continue;
|
||||
const cand = path.join(workflowsDir, d.name, `agent-${agentId}.jsonl`);
|
||||
if (fs.existsSync(cand)) matches.push(cand);
|
||||
if (exists(cand)) matches.push(cand);
|
||||
if (matches.length > 1) break;
|
||||
}
|
||||
return matches.length === 1 ? matches[0] : null;
|
||||
@@ -218,13 +234,13 @@ function getSubagentTranscriptPath(sessionId, cwd, agentId, runId = null) {
|
||||
*/
|
||||
function findTranscriptPath(sessionId, cwd = null) {
|
||||
for (const projectsDir of sessionProjectsDirs(cwd)) {
|
||||
if (!fs.existsSync(projectsDir)) continue;
|
||||
if (!exists(projectsDir)) continue;
|
||||
try {
|
||||
const dirs = fs.readdirSync(projectsDir, { withFileTypes: true });
|
||||
for (const d of dirs) {
|
||||
if (!d.isDirectory()) continue;
|
||||
const candidate = path.join(projectsDir, d.name, `${sessionId}.jsonl`);
|
||||
if (fs.existsSync(candidate)) return candidate;
|
||||
if (exists(candidate)) return candidate;
|
||||
}
|
||||
} catch {
|
||||
// Permission or IO error, ignore
|
||||
@@ -242,7 +258,7 @@ function findTranscriptPath(sessionId, cwd = null) {
|
||||
*/
|
||||
function getSnapshotTranscriptPath(sessionId) {
|
||||
const candidate = path.join(getTranscriptSnapshotDir(), `${sessionId}.jsonl`);
|
||||
return fs.existsSync(candidate) ? candidate : null;
|
||||
return exists(candidate) ? candidate : null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -255,7 +271,7 @@ function getSnapshotTranscriptPath(sessionId) {
|
||||
*/
|
||||
function getSnapshotSubagentTranscriptPath(sessionId, agentId, runId = null) {
|
||||
const subDir = path.join(getTranscriptSnapshotDir(), sessionId, "subagents");
|
||||
if (!fs.existsSync(subDir)) return null;
|
||||
if (!exists(subDir)) return null;
|
||||
const hit = resolveAgentTranscriptInDir(subDir, agentId, runId);
|
||||
if (hit) return hit;
|
||||
if (agentId.startsWith("acompact-")) {
|
||||
@@ -287,13 +303,13 @@ function findSubagentTranscriptPath(sessionId, agentId, runId = null, cwd = null
|
||||
}
|
||||
|
||||
function findSubagentIn(projectsDir, sessionId, agentId, runId) {
|
||||
if (!fs.existsSync(projectsDir)) return null;
|
||||
if (!exists(projectsDir)) return null;
|
||||
try {
|
||||
const dirs = fs.readdirSync(projectsDir, { withFileTypes: true });
|
||||
for (const d of dirs) {
|
||||
if (!d.isDirectory()) continue;
|
||||
const subagentsDir = path.join(projectsDir, d.name, sessionId, "subagents");
|
||||
if (!fs.existsSync(subagentsDir)) continue;
|
||||
if (!exists(subagentsDir)) continue;
|
||||
|
||||
// Exact match (flat or nested Workflow-tool layout)
|
||||
const hit = resolveAgentTranscriptInDir(subagentsDir, agentId, runId);
|
||||
@@ -358,4 +374,5 @@ module.exports = {
|
||||
allProjectsDirs,
|
||||
sessionProjectsDirs,
|
||||
setClaudeHome,
|
||||
exists,
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ const {
|
||||
getSnapshotSubagentTranscriptPath,
|
||||
findTranscriptPath,
|
||||
findSubagentTranscriptPath,
|
||||
exists,
|
||||
} = require("../lib/claude-home");
|
||||
const {
|
||||
classifyTranscriptSender,
|
||||
@@ -908,7 +909,7 @@ router.get("/:id/transcripts", async (req, res) => {
|
||||
getTranscriptPath(req.params.id, session.cwd) ||
|
||||
findTranscriptPath(req.params.id, session.cwd) ||
|
||||
getSnapshotTranscriptPath(req.params.id);
|
||||
if (mainPath && fs.existsSync(mainPath)) {
|
||||
if (mainPath && exists(mainPath)) {
|
||||
// Main agent database ID format: <sessionId>-main
|
||||
const mainDbAgent = dbAgents.find((a) => a.type === "main");
|
||||
result.push({
|
||||
@@ -928,18 +929,18 @@ router.get("/:id/transcripts", async (req, res) => {
|
||||
if (encoded) {
|
||||
// A member's session (S3) wrote under their own home.
|
||||
const directDir = path.join(projectsDirFor(session.cwd), encoded, req.params.id, "subagents");
|
||||
if (fs.existsSync(directDir)) subagentDirs.push(directDir);
|
||||
if (exists(directDir)) subagentDirs.push(directDir);
|
||||
}
|
||||
|
||||
// Fallback: scan the owner's project directory when the direct path doesn't exist
|
||||
if (subagentDirs.length === 0) {
|
||||
for (const projectsDir of sessionProjectsDirs(session.cwd)) {
|
||||
if (fs.existsSync(projectsDir)) {
|
||||
if (exists(projectsDir)) {
|
||||
try {
|
||||
for (const d of fs.readdirSync(projectsDir, { withFileTypes: true })) {
|
||||
if (!d.isDirectory()) continue;
|
||||
const candidate = path.join(projectsDir, d.name, req.params.id, "subagents");
|
||||
if (fs.existsSync(candidate)) subagentDirs.push(candidate);
|
||||
if (exists(candidate)) subagentDirs.push(candidate);
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
@@ -958,7 +959,7 @@ router.get("/:id/transcripts", async (req, res) => {
|
||||
// Try reading meta.json for agent type info
|
||||
let meta = null;
|
||||
const metaPath = path.join(dir, file.replace(".jsonl", ".meta.json"));
|
||||
if (fs.existsSync(metaPath)) {
|
||||
if (exists(metaPath)) {
|
||||
try {
|
||||
meta = JSON.parse(
|
||||
require("../lib/run-as").readFileSafe(metaPath, "utf8", { max: 1 << 20 })
|
||||
|
||||
Reference in New Issue
Block a user