Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion plugins/codex/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "jieli",
"version": "0.1.25",
"version": "0.1.26",
"description": "Sync Codex sessions to Jieli threads and attach Jieli thread trailers to Codex-created commits.",
"author": {
"name": "Jieli"
Expand Down
18 changes: 14 additions & 4 deletions plugins/codex/scripts/jieli_node.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const TRANSCRIPT_FLUSH_TIMEOUT_MS = 1500;
const ATTACHMENT_CACHE_FILE = "codex-attachments.json";
const SESSION_MAPPING_FILE = "codex-sessions.json";
const HANDOFF_CONTEXT_FILE = "codex-handoff-context.json";
const QUOTA_NOTICE_FILE = "quota-notices.json";
const TOOL_OUTPUT_MAX_CHARS = 20000;
const SETTINGS_FILE_NAME = "settings.json";
const TRAILER_KEY = "Jieli-Thread";
Expand Down Expand Up @@ -253,10 +254,18 @@ function buildMissingConfigHookResponse(trigger, missing) {
};
}

function buildQuotaExceededHookResponse(trigger, error) {
if (String(trigger || "").toLowerCase() !== "sessionstart") return {};
function buildQuotaExceededHookResponse(trigger, error, sessionId = "") {
if (String(trigger || "").toLowerCase() !== "stop") return {};
const message = formatError(error);
if (!/\b409\b/.test(message) || !/quota/i.test(message)) return {};
const cleanSessionId = String(sessionId || "").trim();
if (!cleanSessionId) return {};
const path = join(homeDir(), ".jieli", QUOTA_NOTICE_FILE);
const notices = readJson(path, {});
const key = `${PROVIDER}|${cleanSessionId}|message-quota-exceeded`;
if (notices[key]) return {};
notices[key] = new Date().toISOString();
writeJsonAtomic(path, notices);
return {
continue: true,
systemMessage:
Expand Down Expand Up @@ -389,8 +398,9 @@ function loadHookStdin() {

async function syncMain(args) {
const opts = parseArgs(args, { boolean: new Set(["jieli-hook"]) });
let hookData = {};
try {
const hookData = loadHookStdin();
hookData = loadHookStdin();
hookData.trigger = opts.trigger || "";
writeHandoffContext(hookData);
captureBaselineFromHook(hookData, opts.trigger || "");
Expand Down Expand Up @@ -424,7 +434,7 @@ async function syncMain(args) {
releaseSyncLock(lock);
}
} catch (error) {
const response = buildQuotaExceededHookResponse(opts.trigger || "", error);
const response = buildQuotaExceededHookResponse(opts.trigger || "", error, hookData.session_id || "");
if (Object.keys(response).length) console.log(JSON.stringify(response));
logHookError(`sync ${opts.trigger || ""}: ${formatError(error)}`);
}
Expand Down
38 changes: 38 additions & 0 deletions plugins/codex/tests/runtime-node.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -665,6 +665,44 @@ test("configuration, upload, lock, session mapping, and missing transcript behav
assert.throws(() => statSync(join(transcriptHome, ".jieli", "hooks.log")));
});

test("sync CLI reports message quota exceeded only once per session on stop", async () => {
const home = makeTempDir();
const transcript = join(home, "sessions", "quota-session.jsonl");
mkdirSync(dirname(transcript), { recursive: true });
writeJsonl(transcript, [
{ type: "session_meta", payload: { id: "codex-quota", cwd: "/Users/alice/work/jieli", git: { branch: "main" } } },
{ type: "response_item", payload: { type: "message", role: "user", content: [{ type: "input_text", text: "sync me" }] } },
{ type: "response_item", payload: { type: "message", role: "assistant", content: [{ type: "output_text", text: "ok" }] } },
]);
const { server } = createMockJieliServer({ uploadStatus: 409, uploadResponse: { success: false, message: "message quota exceeded", data: null } });
const baseUrl = await listen(server);
const env = { HOME: home, PATH: process.env.PATH, JIELI_API_KEY: "secret", JIELI_BASE_URL: baseUrl };
try {
const sessionStart = await runNode([join(pluginRoot, "scripts", "sync.mjs"), "--trigger", "sessionstart", "--jieli-hook"], {
input: JSON.stringify({ transcript_path: transcript, session_id: "codex-quota", cwd: "/Users/alice/work/jieli" }),
env,
});
assert.equal(sessionStart.status, 0);
assert.equal(sessionStart.stdout, "");

const firstStop = await runNode([join(pluginRoot, "scripts", "sync.mjs"), "--trigger", "stop", "--jieli-hook"], {
input: JSON.stringify({ transcript_path: transcript, session_id: "codex-quota", cwd: "/Users/alice/work/jieli" }),
env,
});
assert.equal(firstStop.status, 0);
assert.match(JSON.parse(firstStop.stdout).systemMessage, /message quota exceeded/);

const secondStop = await runNode([join(pluginRoot, "scripts", "sync.mjs"), "--trigger", "stop", "--jieli-hook"], {
input: JSON.stringify({ transcript_path: transcript, session_id: "codex-quota", cwd: "/Users/alice/work/jieli" }),
env,
});
assert.equal(secondStop.status, 0);
assert.equal(secondStop.stdout, "");
} finally {
await close(server);
}
});

test("read-thread and find-threads helpers validate ids, shape requests, truncate output, and format markdown", async () => {
assert.throws(() => runtime.validateThreadId("https://jieli.example.test/threads/T-1"), /provider thread id/);
assert.throws(() => runtime.validateThreadId("T-1.md"), /without .md/);
Expand Down
Loading