taskchef 7.18.0 → 7.19.0
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/.codex-plugin/plugin.json +1 -1
- package/README.md +6 -0
- package/docs/spec.md +11 -0
- package/package.json +1 -1
- package/src/codex-app.js +87 -9
- package/src/dashboard/actions.js +38 -0
- package/src/dashboard/app.js +24 -1
- package/src/dashboard/index.html +1 -0
- package/src/dashboard/state.js +7 -0
- package/src/dashboard/styles.css +3 -2
- package/src/dashboard.js +78 -0
- package/src/version.js +1 -1
package/README.md
CHANGED
|
@@ -263,6 +263,12 @@ The loopback dashboard watches `tasks.jsonl`, groups current states, and opens
|
|
|
263
263
|
linked Codex tasks. List snapshots and SSE events carry only the latest
|
|
264
264
|
request/result pair; opening task details fetches the full newest-first activity
|
|
265
265
|
timeline, including clearly labeled interrupted turns.
|
|
266
|
+
Task details also offer **Archive chat** for every linked task whose current
|
|
267
|
+
TaskChef state is not `working`. After confirmation, TaskChef invokes only the
|
|
268
|
+
Codex CLI at the canonical ChatGPT or Codex desktop app location under
|
|
269
|
+
`/Applications`, to archive the exact thread UUID. The Codex chat leaves active chat lists while the TaskChef record
|
|
270
|
+
and its activity timeline remain unchanged. If the bundled CLI is unavailable,
|
|
271
|
+
the dashboard does not fall back to another `codex` executable from `PATH`.
|
|
266
272
|
The header shows the running TaskChef package version reported by the same
|
|
267
273
|
bounded health identity used for compatible-listener checks.
|
|
268
274
|
Task and result times are relative through 29 days (with minute detail for the
|
package/docs/spec.md
CHANGED
|
@@ -427,6 +427,17 @@ startup safely. Direct thread navigation
|
|
|
427
427
|
MUST require a canonical Codex UUIDv7. Otherwise it MAY open the revalidated
|
|
428
428
|
configured project. Project paths from task history MUST be matched against
|
|
429
429
|
current configuration before use.
|
|
430
|
+
The task-detail dashboard MAY offer Codex chat archival only when the stored
|
|
431
|
+
thread ID is a canonical Codex UUID and the current TaskChef state is not
|
|
432
|
+
`working`. It MUST revalidate both conditions for the POST request, require the
|
|
433
|
+
exact loopback origin, and obtain explicit user confirmation in the browser.
|
|
434
|
+
It MUST invoke the `archive` subcommand by directly executing only a Codex CLI
|
|
435
|
+
inside the canonical ChatGPT or Codex desktop application bundle under macOS
|
|
436
|
+
`/Applications`, with the thread UUID as a separate argument. It MUST NOT use a shell, a generic `PATH` fallback, a
|
|
437
|
+
private desktop endpoint, or direct session-file manipulation. Successful
|
|
438
|
+
archival MUST NOT modify dispatcher files or remove the TaskChef task record.
|
|
439
|
+
The UI MUST disclose that spawned descendant chats may also be archived and
|
|
440
|
+
that TaskChef history remains available.
|
|
430
441
|
Snapshot and SSE list payloads MUST omit full `turns` and derived `results`
|
|
431
442
|
history and include `latestTurn`. The bounded per-task detail endpoint MAY
|
|
432
443
|
return the full validated task so the dialog can render the paired timeline newest first.
|
package/package.json
CHANGED
package/src/codex-app.js
CHANGED
|
@@ -1,14 +1,21 @@
|
|
|
1
1
|
import { execFile as execFileCallback } from "node:child_process";
|
|
2
|
-
import { access } from "node:fs/promises";
|
|
2
|
+
import { access, realpath } from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
|
|
6
|
+
import { normalizeCodexThreadId } from "./delegation.js";
|
|
7
|
+
|
|
6
8
|
const execFile = promisify(execFileCallback);
|
|
7
9
|
const CODEX_COMMAND_TIMEOUT_MS = 10_000;
|
|
8
|
-
const
|
|
10
|
+
const CODEX_ARCHIVE_TIMEOUT_MS = 4_000;
|
|
11
|
+
const CODEX_COMMAND_MAX_BUFFER_BYTES = 64 * 1024;
|
|
9
12
|
|
|
10
|
-
function runCodex(run, filePath, args) {
|
|
11
|
-
return run(filePath, args, {
|
|
13
|
+
function runCodex(run, filePath, args, timeout = CODEX_COMMAND_TIMEOUT_MS) {
|
|
14
|
+
return run(filePath, args, {
|
|
15
|
+
timeout,
|
|
16
|
+
killSignal: "SIGKILL",
|
|
17
|
+
maxBuffer: CODEX_COMMAND_MAX_BUFFER_BYTES,
|
|
18
|
+
});
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
async function executable(filePath) {
|
|
@@ -20,15 +27,29 @@ async function executable(filePath) {
|
|
|
20
27
|
}
|
|
21
28
|
}
|
|
22
29
|
|
|
23
|
-
function pathCandidates(env) {
|
|
30
|
+
function pathCandidates(env, platform = process.platform) {
|
|
24
31
|
return (env.PATH ?? "")
|
|
25
32
|
.split(path.delimiter)
|
|
26
33
|
.filter(Boolean)
|
|
27
|
-
.map((directory) => path.join(directory,
|
|
34
|
+
.map((directory) => path.join(directory, platform === "win32" ? "codex.exe" : "codex"));
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
function isDesktopBundleCandidate(filePath) {
|
|
31
|
-
|
|
38
|
+
if (path.basename(filePath) !== "codex") return false;
|
|
39
|
+
const resources = path.dirname(filePath);
|
|
40
|
+
const contents = path.dirname(resources);
|
|
41
|
+
const application = path.dirname(contents);
|
|
42
|
+
return path.basename(resources) === "Resources"
|
|
43
|
+
&& path.basename(contents) === "Contents"
|
|
44
|
+
&& new Set(["ChatGPT.app", "Codex.app"]).has(path.basename(application));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function defaultDesktopBundleCandidates(_env, platform) {
|
|
48
|
+
if (platform !== "darwin") return [];
|
|
49
|
+
return [
|
|
50
|
+
"/Applications/ChatGPT.app/Contents/Resources/codex",
|
|
51
|
+
"/Applications/Codex.app/Contents/Resources/codex",
|
|
52
|
+
];
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
async function supportsAppCommand(filePath, run) {
|
|
@@ -40,6 +61,37 @@ async function supportsAppCommand(filePath, run) {
|
|
|
40
61
|
}
|
|
41
62
|
}
|
|
42
63
|
|
|
64
|
+
async function supportsArchiveCommand(filePath, run) {
|
|
65
|
+
try {
|
|
66
|
+
const { stdout, stderr } = await runCodex(run, filePath, ["archive", "--help"]);
|
|
67
|
+
return /(?:^|\n)Usage:\s+codex\s+archive(?:\s|$)/.test(`${stdout}\n${stderr}`);
|
|
68
|
+
} catch {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function discoverBundledCodexCli({
|
|
74
|
+
candidates = null,
|
|
75
|
+
env = process.env,
|
|
76
|
+
platform = process.platform,
|
|
77
|
+
run = execFile,
|
|
78
|
+
trustedBundlePaths = null,
|
|
79
|
+
} = {}) {
|
|
80
|
+
const trustedPaths = trustedBundlePaths ?? defaultDesktopBundleCandidates(env, platform);
|
|
81
|
+
const trustedCandidates = new Set(trustedPaths.map((candidate) => path.resolve(candidate)));
|
|
82
|
+
const bundledCandidates = candidates ?? trustedPaths;
|
|
83
|
+
for (const filePath of new Set(bundledCandidates.map((candidate) => path.resolve(candidate)))) {
|
|
84
|
+
const executableCandidate = await executable(filePath);
|
|
85
|
+
const candidate = executableCandidate === null ? null : await realpath(executableCandidate);
|
|
86
|
+
if (platform === "darwin" && candidate && trustedCandidates.has(candidate)
|
|
87
|
+
&& isDesktopBundleCandidate(candidate)
|
|
88
|
+
&& await supportsArchiveCommand(candidate, run)) {
|
|
89
|
+
return { path: candidate, source: "desktop-bundle" };
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
throw new Error("Chat archiving requires the Codex CLI bundled with the ChatGPT or Codex desktop app");
|
|
93
|
+
}
|
|
94
|
+
|
|
43
95
|
export async function discoverCodexCli({
|
|
44
96
|
explicit = null,
|
|
45
97
|
env = process.env,
|
|
@@ -56,7 +108,7 @@ export async function discoverCodexCli({
|
|
|
56
108
|
}
|
|
57
109
|
|
|
58
110
|
const candidates = pathCandidates(env);
|
|
59
|
-
for (const pathCandidate of candidates.filter(isDesktopBundleCandidate)) {
|
|
111
|
+
for (const pathCandidate of candidates.filter((candidate) => isDesktopBundleCandidate(candidate))) {
|
|
60
112
|
const candidate = await executable(pathCandidate);
|
|
61
113
|
if (candidate && await supportsAppCommand(candidate, run)) {
|
|
62
114
|
return { path: candidate, source: "desktop-path" };
|
|
@@ -89,7 +141,12 @@ export async function openWorkspaceInCodex(workspace, options = {}) {
|
|
|
89
141
|
}
|
|
90
142
|
|
|
91
143
|
export function isCodexThreadDeepLinkId(threadId) {
|
|
92
|
-
|
|
144
|
+
try {
|
|
145
|
+
normalizeCodexThreadId(threadId);
|
|
146
|
+
return true;
|
|
147
|
+
} catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
93
150
|
}
|
|
94
151
|
|
|
95
152
|
export async function openThreadInCodex(threadId, options = {}) {
|
|
@@ -108,3 +165,24 @@ export async function openThreadInCodex(threadId, options = {}) {
|
|
|
108
165
|
}
|
|
109
166
|
return { status: "requested", mechanism: "codex-deep-link", threadId, url };
|
|
110
167
|
}
|
|
168
|
+
|
|
169
|
+
export async function archiveThreadInCodex(threadId, options = {}) {
|
|
170
|
+
if (!isCodexThreadDeepLinkId(threadId)) {
|
|
171
|
+
throw new Error("Codex thread ID is not supported by chat archiving");
|
|
172
|
+
}
|
|
173
|
+
const run = options.run ?? execFile;
|
|
174
|
+
const cli = options.cli ?? await discoverBundledCodexCli({ run });
|
|
175
|
+
await runCodex(
|
|
176
|
+
run,
|
|
177
|
+
cli.path,
|
|
178
|
+
["archive", threadId],
|
|
179
|
+
options.timeoutMs ?? CODEX_ARCHIVE_TIMEOUT_MS,
|
|
180
|
+
);
|
|
181
|
+
return {
|
|
182
|
+
status: "archived",
|
|
183
|
+
mechanism: "codex-archive-cli",
|
|
184
|
+
codexCli: cli.path,
|
|
185
|
+
codexCliSource: cli.source,
|
|
186
|
+
threadId,
|
|
187
|
+
};
|
|
188
|
+
}
|
package/src/dashboard/actions.js
CHANGED
|
@@ -15,3 +15,41 @@ export async function openTaskFromControl(event, taskId, {
|
|
|
15
15
|
control.disabled = false;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
export async function archiveTaskFromControl(event, task, {
|
|
20
|
+
confirmAction = globalThis.confirm,
|
|
21
|
+
fetchAction = globalThis.fetch,
|
|
22
|
+
onArchived = () => {},
|
|
23
|
+
showMessage,
|
|
24
|
+
} = {}) {
|
|
25
|
+
event.stopPropagation();
|
|
26
|
+
const accepted = confirmAction(
|
|
27
|
+
`Archive “${task.title}” in Codex?\n\n`
|
|
28
|
+
+ "The chat will leave active Codex lists, and spawned descendant chats may also be archived. "
|
|
29
|
+
+ "TaskChef history will remain available.",
|
|
30
|
+
);
|
|
31
|
+
if (!accepted) return false;
|
|
32
|
+
const control = event.currentTarget;
|
|
33
|
+
let archived = false;
|
|
34
|
+
control.disabled = true;
|
|
35
|
+
try {
|
|
36
|
+
const response = await fetchAction(
|
|
37
|
+
`/api/tasks/${encodeURIComponent(task.id)}/archive-codex`,
|
|
38
|
+
{ method: "POST" },
|
|
39
|
+
);
|
|
40
|
+
const result = await response.json();
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
showMessage(result.message ?? "Codex could not archive this chat.");
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
onArchived(task.threadId);
|
|
46
|
+
archived = true;
|
|
47
|
+
showMessage(result.message ?? "Archived the Codex chat. TaskChef history remains available.");
|
|
48
|
+
return true;
|
|
49
|
+
} catch {
|
|
50
|
+
showMessage("Codex chat archiving is temporarily unavailable. Try again.");
|
|
51
|
+
return false;
|
|
52
|
+
} finally {
|
|
53
|
+
if (!archived) control.disabled = false;
|
|
54
|
+
}
|
|
55
|
+
}
|
package/src/dashboard/app.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
+
canArchiveTask,
|
|
2
3
|
clearNotifications,
|
|
3
4
|
dismissNotification,
|
|
4
5
|
filterTasks,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
taskStatusLabel,
|
|
16
17
|
turnPresentation,
|
|
17
18
|
} from "./state.js";
|
|
18
|
-
import { openTaskFromControl } from "./actions.js";
|
|
19
|
+
import { archiveTaskFromControl, openTaskFromControl } from "./actions.js";
|
|
19
20
|
import {
|
|
20
21
|
githubReferenceAccessibleLabel,
|
|
21
22
|
githubReferenceDisplayLabels,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
import { formatRelativeTime, RelativeTimeController, parsedTimestamp } from "./time.js";
|
|
26
27
|
|
|
27
28
|
const state = {
|
|
29
|
+
archivedThreadIds: new Set(),
|
|
28
30
|
tasks: [],
|
|
29
31
|
signatures: new Map(),
|
|
30
32
|
notifications: [],
|
|
@@ -39,6 +41,7 @@ let copyTaskIdTimer = null;
|
|
|
39
41
|
const relativeTimes = new RelativeTimeController();
|
|
40
42
|
|
|
41
43
|
const elements = {
|
|
44
|
+
archiveTask: document.querySelector("#archive-codex"),
|
|
42
45
|
clearNotifications: document.querySelector("#clear-notifications"),
|
|
43
46
|
closeDialog: document.querySelector("#close-dialog"),
|
|
44
47
|
connectionDot: document.querySelector("#connection-dot"),
|
|
@@ -446,6 +449,15 @@ function renderDialog(task) {
|
|
|
446
449
|
...detailRow("Updated by", task.updatedBy),
|
|
447
450
|
);
|
|
448
451
|
configureOpenTaskControl(elements.openProject, `Open ${task.title} in Codex`);
|
|
452
|
+
const canArchive = canArchiveTask(task);
|
|
453
|
+
const archived = state.archivedThreadIds.has(task.threadId);
|
|
454
|
+
elements.archiveTask.hidden = !canArchive;
|
|
455
|
+
elements.archiveTask.disabled = archived;
|
|
456
|
+
elements.archiveTask.textContent = archived ? "Archived" : "Archive chat";
|
|
457
|
+
elements.archiveTask.setAttribute(
|
|
458
|
+
"aria-label",
|
|
459
|
+
archived ? `${task.title} is archived in Codex` : `Archive ${task.title} in Codex`,
|
|
460
|
+
);
|
|
449
461
|
}
|
|
450
462
|
|
|
451
463
|
async function openDialog(task) {
|
|
@@ -652,3 +664,14 @@ elements.openProject.addEventListener("click", async (event) => {
|
|
|
652
664
|
if (!state.selectedTask) return;
|
|
653
665
|
await openTaskFromControl(event, state.selectedTask.id, { showMessage });
|
|
654
666
|
});
|
|
667
|
+
elements.archiveTask.addEventListener("click", async (event) => {
|
|
668
|
+
const task = state.selectedTask;
|
|
669
|
+
if (!task || !canArchiveTask(task)) return;
|
|
670
|
+
await archiveTaskFromControl(event, task, {
|
|
671
|
+
onArchived: (threadId) => {
|
|
672
|
+
state.archivedThreadIds.add(threadId);
|
|
673
|
+
if (state.selectedTask?.id === task.id) renderDialog(state.selectedTask);
|
|
674
|
+
},
|
|
675
|
+
showMessage,
|
|
676
|
+
});
|
|
677
|
+
});
|
package/src/dashboard/index.html
CHANGED
|
@@ -113,6 +113,7 @@
|
|
|
113
113
|
<span>Open task</span>
|
|
114
114
|
</button>
|
|
115
115
|
<button id="copy-task-id" class="secondary-button" type="button" aria-label="Copy Task ID">Copy Task ID</button>
|
|
116
|
+
<button id="archive-codex" class="danger-button" type="button" aria-label="Archive this chat in Codex" hidden>Archive chat</button>
|
|
116
117
|
</div>
|
|
117
118
|
<nav id="dialog-related-links" class="github-links" aria-label="Related GitHub links" hidden></nav>
|
|
118
119
|
<section>
|
package/src/dashboard/state.js
CHANGED
|
@@ -13,6 +13,7 @@ export const STATUS_FILTERS = [
|
|
|
13
13
|
{ value: "completed", label: "Completed" },
|
|
14
14
|
{ value: "failed", label: "Failed" },
|
|
15
15
|
];
|
|
16
|
+
const CODEX_THREAD_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
16
17
|
|
|
17
18
|
const NOTIFICATION_TITLES = new Map([
|
|
18
19
|
["created", "Task created"],
|
|
@@ -38,6 +39,12 @@ export function taskStatusLabel(task) {
|
|
|
38
39
|
return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
|
|
39
40
|
}
|
|
40
41
|
|
|
42
|
+
export function canArchiveTask(task) {
|
|
43
|
+
return task.status !== "working"
|
|
44
|
+
&& typeof task.threadId === "string"
|
|
45
|
+
&& CODEX_THREAD_ID_PATTERN.test(task.threadId);
|
|
46
|
+
}
|
|
47
|
+
|
|
41
48
|
export function latestTurnPresentation(task) {
|
|
42
49
|
const turn = task.latestTurn ?? null;
|
|
43
50
|
const result = turn?.result ?? null;
|
package/src/dashboard/styles.css
CHANGED
|
@@ -149,10 +149,11 @@ dialog::backdrop { background: rgb(18 23 21 / 50%); backdrop-filter: blur(2px);
|
|
|
149
149
|
.dialog-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 20px; }
|
|
150
150
|
.dialog-header h2 { margin-bottom: 12px; font-size: 1.6rem; }
|
|
151
151
|
.dialog-actions { display: flex; gap: 8px; margin-bottom: 26px; }
|
|
152
|
-
.primary-button, .secondary-button { padding: 8px 12px; border-radius: 7px; font-weight: 700; cursor: pointer; }
|
|
152
|
+
.primary-button, .secondary-button, .danger-button { padding: 8px 12px; border-radius: 7px; font-weight: 700; cursor: pointer; }
|
|
153
153
|
.primary-button { border: 1px solid var(--accent); background: var(--accent); color: white; }
|
|
154
154
|
.secondary-button { border: 1px solid var(--border); background: var(--surface); }
|
|
155
|
-
.
|
|
155
|
+
.danger-button { border: 1px solid var(--danger); background: var(--surface); color: var(--danger); }
|
|
156
|
+
.primary-button:disabled, .secondary-button:disabled, .danger-button:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
156
157
|
dialog section + section { margin-top: 24px; }
|
|
157
158
|
.preserve-lines { white-space: pre-wrap; }
|
|
158
159
|
.result-history { display: grid; gap: 10px; }
|
package/src/dashboard.js
CHANGED
|
@@ -6,15 +6,19 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
+
archiveThreadInCodex,
|
|
10
|
+
discoverBundledCodexCli,
|
|
9
11
|
isCodexThreadDeepLinkId,
|
|
10
12
|
openThreadInCodex,
|
|
11
13
|
openWorkspaceInCodex,
|
|
12
14
|
} from "./codex-app.js";
|
|
13
15
|
import {
|
|
16
|
+
acquireWorkspaceLock,
|
|
14
17
|
canonicalDirectory,
|
|
15
18
|
canonicalGitRoot,
|
|
16
19
|
parseTaskLogContent,
|
|
17
20
|
readConfig,
|
|
21
|
+
readTask,
|
|
18
22
|
} from "./workspace.js";
|
|
19
23
|
import { DASHBOARD_SERVER_VERSION, TASKCHEF_VERSION } from "./version.js";
|
|
20
24
|
import { taskGitHubProjection } from "./dashboard/github-links.js";
|
|
@@ -439,6 +443,8 @@ function publicMonitorError() {
|
|
|
439
443
|
}
|
|
440
444
|
|
|
441
445
|
export async function createDashboardServer({
|
|
446
|
+
archiveThread = archiveThreadInCodex,
|
|
447
|
+
discoverArchiveCli = discoverBundledCodexCli,
|
|
442
448
|
workspace,
|
|
443
449
|
host = "127.0.0.1",
|
|
444
450
|
maxEventClients = DEFAULT_MAX_EVENT_CLIENTS,
|
|
@@ -472,6 +478,7 @@ export async function createDashboardServer({
|
|
|
472
478
|
throw new Error("dashboard identity exceeds the health response limit");
|
|
473
479
|
}
|
|
474
480
|
const clients = new Set();
|
|
481
|
+
const archiveRequests = new Set();
|
|
475
482
|
let allowedAuthority;
|
|
476
483
|
let allowedOrigin;
|
|
477
484
|
|
|
@@ -617,6 +624,77 @@ export async function createDashboardServer({
|
|
|
617
624
|
return;
|
|
618
625
|
}
|
|
619
626
|
|
|
627
|
+
const archiveMatch = url.pathname.match(/^\/api\/tasks\/([a-zA-Z0-9._-]+)\/archive-codex$/);
|
|
628
|
+
if (archiveMatch && method === "POST") {
|
|
629
|
+
if (request.headers.origin !== allowedOrigin) {
|
|
630
|
+
sendJson(response, 403, { message: "Dashboard origin validation failed." });
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
const taskId = archiveMatch[1];
|
|
634
|
+
const visibleTask = monitor.tasks.find((candidate) => candidate.id === taskId);
|
|
635
|
+
if (!visibleTask) {
|
|
636
|
+
sendJson(response, 404, { message: "Task not found." });
|
|
637
|
+
return;
|
|
638
|
+
}
|
|
639
|
+
if (!isCodexThreadDeepLinkId(visibleTask.threadId)) {
|
|
640
|
+
sendJson(response, 409, { message: "This task does not have an archivable Codex chat." });
|
|
641
|
+
return;
|
|
642
|
+
}
|
|
643
|
+
if (visibleTask.status === "working") {
|
|
644
|
+
sendJson(response, 409, { message: "Working tasks cannot be archived from the dashboard." });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
if (archiveRequests.has(taskId)) {
|
|
648
|
+
sendJson(response, 409, { message: "This Codex chat is already being archived." });
|
|
649
|
+
return;
|
|
650
|
+
}
|
|
651
|
+
archiveRequests.add(taskId);
|
|
652
|
+
let releaseWorkspaceLock = null;
|
|
653
|
+
try {
|
|
654
|
+
const cli = await discoverArchiveCli();
|
|
655
|
+
releaseWorkspaceLock = await acquireWorkspaceLock(monitor.workspace);
|
|
656
|
+
let task;
|
|
657
|
+
try {
|
|
658
|
+
task = await readTask(monitor.workspace, taskId);
|
|
659
|
+
} catch (error) {
|
|
660
|
+
if (/^task not found:/.test(error?.message ?? "")) {
|
|
661
|
+
sendJson(response, 404, { message: "Task not found." });
|
|
662
|
+
return;
|
|
663
|
+
}
|
|
664
|
+
throw error;
|
|
665
|
+
}
|
|
666
|
+
if (!isCodexThreadDeepLinkId(task.threadId)) {
|
|
667
|
+
sendJson(response, 409, { message: "This task does not have an archivable Codex chat." });
|
|
668
|
+
return;
|
|
669
|
+
}
|
|
670
|
+
if (task.status === "working") {
|
|
671
|
+
sendJson(response, 409, { message: "Working tasks cannot be archived from the dashboard." });
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
await archiveThread(task.threadId, { cli, timeoutMs: 4_000 });
|
|
675
|
+
sendJson(response, 200, {
|
|
676
|
+
message: "Archived the Codex chat. TaskChef history remains available.",
|
|
677
|
+
status: "archived",
|
|
678
|
+
});
|
|
679
|
+
} catch (error) {
|
|
680
|
+
if (error?.killed || error?.code === "ETIMEDOUT") {
|
|
681
|
+
sendJson(response, 504, { message: "Codex chat archiving timed out. Try again." });
|
|
682
|
+
} else if (/requires the Codex CLI bundled/i.test(error?.message ?? "")) {
|
|
683
|
+
sendJson(response, 503, {
|
|
684
|
+
message: "Chat archiving requires the Codex CLI bundled with the ChatGPT or Codex desktop app.",
|
|
685
|
+
});
|
|
686
|
+
} else {
|
|
687
|
+
sendJson(response, 409, {
|
|
688
|
+
message: "Codex could not archive this chat. It may be active, already archived, or stored on another host or profile.",
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
} finally {
|
|
692
|
+
await releaseWorkspaceLock?.();
|
|
693
|
+
archiveRequests.delete(taskId);
|
|
694
|
+
}
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
620
698
|
if (method === "POST") {
|
|
621
699
|
sendJson(response, 404, { message: "Not found." });
|
|
622
700
|
return;
|
package/src/version.js
CHANGED