@co0ontty/wand 1.39.1 → 1.40.1
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/cli.js +0 -0
- package/dist/git-quick-commit.js +27 -0
- package/dist/process-manager.d.ts +1 -0
- package/dist/process-manager.js +134 -11
- package/dist/resume-policy.d.ts +1 -0
- package/dist/resume-policy.js +7 -1
- package/dist/server-session-routes.js +30 -10
- package/dist/types.d.ts +4 -0
- package/dist/web-ui/content/scripts.js +293 -203
- package/dist/web-ui/content/styles.css +335 -341
- package/package.json +1 -1
- package/dist/acp-protocol.d.ts +0 -67
- package/dist/acp-protocol.js +0 -291
- package/dist/claude-stream-adapter.d.ts +0 -35
- package/dist/claude-stream-adapter.js +0 -153
- package/dist/claude-structured-runner.d.ts +0 -27
- package/dist/claude-structured-runner.js +0 -106
- package/dist/message-parser.d.ts +0 -2
- package/dist/message-parser.js +0 -329
- package/dist/message-queue.d.ts +0 -57
- package/dist/message-queue.js +0 -127
- package/dist/session-lifecycle.d.ts +0 -81
- package/dist/session-lifecycle.js +0 -176
package/dist/cli.js
CHANGED
|
File without changes
|
package/dist/git-quick-commit.js
CHANGED
|
@@ -64,6 +64,20 @@ function resolvePushRemote(cwd) {
|
|
|
64
64
|
}
|
|
65
65
|
return "origin";
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Derive a default next-version tag from the latest existing tag by bumping the
|
|
69
|
+
* patch component (preserving an optional `v` prefix). Returns a sane starting
|
|
70
|
+
* version when there's no tag yet, or `undefined` if the latest tag isn't semver-ish.
|
|
71
|
+
*/
|
|
72
|
+
function computeSuggestedTag(latestTag) {
|
|
73
|
+
if (!latestTag)
|
|
74
|
+
return "v0.1.0";
|
|
75
|
+
const m = latestTag.match(/^(v?)(\d+)\.(\d+)\.(\d+)$/);
|
|
76
|
+
if (!m)
|
|
77
|
+
return undefined;
|
|
78
|
+
const [, prefix, major, minor, patch] = m;
|
|
79
|
+
return `${prefix}${major}.${minor}.${Number.parseInt(patch, 10) + 1}`;
|
|
80
|
+
}
|
|
67
81
|
function unquotePath(raw) {
|
|
68
82
|
if (raw.startsWith("\"") && raw.endsWith("\"")) {
|
|
69
83
|
return raw.slice(1, -1).replace(/\\"/g, "\"").replace(/\\\\/g, "\\");
|
|
@@ -217,6 +231,17 @@ export function getGitStatus(cwd) {
|
|
|
217
231
|
// `ls-remote` is a synchronous network call that can block the event loop
|
|
218
232
|
// for seconds. The "unpushed tag" UI chip is best-effort and a separate
|
|
219
233
|
// async endpoint should compute it on demand if reintroduced.
|
|
234
|
+
// Latest tag + a locally-derived next-version suggestion (both git-local, fast).
|
|
235
|
+
let latestTag;
|
|
236
|
+
if (!initialCommit) {
|
|
237
|
+
try {
|
|
238
|
+
latestTag = runGit(["describe", "--tags", "--abbrev=0"], cwd) || undefined;
|
|
239
|
+
}
|
|
240
|
+
catch {
|
|
241
|
+
latestTag = undefined;
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
const suggestedTag = computeSuggestedTag(latestTag);
|
|
220
245
|
return {
|
|
221
246
|
isGit: true,
|
|
222
247
|
branch,
|
|
@@ -229,6 +254,8 @@ export function getGitStatus(cwd) {
|
|
|
229
254
|
ahead,
|
|
230
255
|
behind,
|
|
231
256
|
lastCommit,
|
|
257
|
+
latestTag,
|
|
258
|
+
suggestedTag,
|
|
232
259
|
};
|
|
233
260
|
}
|
|
234
261
|
export class QuickCommitError extends Error {
|
|
@@ -81,6 +81,7 @@ export declare class ProcessManager extends EventEmitter {
|
|
|
81
81
|
listCodexHistorySessions(): CodexHistorySession[];
|
|
82
82
|
hasCodexSessionFile(threadId: string): boolean;
|
|
83
83
|
deleteCodexHistoryFiles(threadIds: string[]): number;
|
|
84
|
+
private captureCodexSessionId;
|
|
84
85
|
get(id: string): SessionSnapshot | null;
|
|
85
86
|
getPtyTranscript(id: string): string | null;
|
|
86
87
|
/**
|
package/dist/process-manager.js
CHANGED
|
@@ -12,7 +12,7 @@ import { appendWindow, hasExplicitConfirmSyntax, hasPermissionActionContext, nor
|
|
|
12
12
|
import { buildChildEnv } from "./env-utils.js";
|
|
13
13
|
import { buildLanguageDirective } from "./language-prompt.js";
|
|
14
14
|
import { prepareSessionWorktree } from "./git-worktree.js";
|
|
15
|
-
import { getResumeCommandSessionId } from "./resume-policy.js";
|
|
15
|
+
import { getCodexResumeCommandSessionId, getResumeCommandSessionId } from "./resume-policy.js";
|
|
16
16
|
import { applyThinkingEffortToPrompt, normalizeThinkingEffort } from "./structured-session-manager.js";
|
|
17
17
|
function resolveProviderFromCommand(command) {
|
|
18
18
|
return /^codex\b/.test(command.trim()) ? "codex" : "claude";
|
|
@@ -421,6 +421,40 @@ function listAllCodexHistorySessions() {
|
|
|
421
421
|
}
|
|
422
422
|
return Array.from(byThread.values()).sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
423
423
|
}
|
|
424
|
+
function listCodexSessionMtimes() {
|
|
425
|
+
return new Map(listAllCodexHistorySessions().map((session) => [session.claudeSessionId, session.mtimeMs]));
|
|
426
|
+
}
|
|
427
|
+
function isSameResolvedPath(left, right) {
|
|
428
|
+
if (!left || !right)
|
|
429
|
+
return false;
|
|
430
|
+
return path.resolve(left) === path.resolve(right);
|
|
431
|
+
}
|
|
432
|
+
function selectCodexSessionForRecord(record) {
|
|
433
|
+
const knownMtimes = record.knownCodexSessionMtimes ?? new Map();
|
|
434
|
+
const candidates = listAllCodexHistorySessions()
|
|
435
|
+
.filter((session) => session.hasConversation)
|
|
436
|
+
.filter((session) => isSameResolvedPath(session.cwd, record.cwd))
|
|
437
|
+
.filter((session) => {
|
|
438
|
+
const previousMtime = knownMtimes.get(session.claudeSessionId);
|
|
439
|
+
return previousMtime === undefined || session.mtimeMs > previousMtime;
|
|
440
|
+
})
|
|
441
|
+
.filter((session) => hasRecentProjectActivity(session, record.startedAt))
|
|
442
|
+
.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
443
|
+
if (candidates.length === 0) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const fresh = candidates.filter((session) => !knownMtimes.has(session.claudeSessionId));
|
|
447
|
+
if (fresh.length === 1) {
|
|
448
|
+
return fresh[0];
|
|
449
|
+
}
|
|
450
|
+
if (fresh.length > 1) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
return candidates.length === 1 ? candidates[0] : null;
|
|
454
|
+
}
|
|
455
|
+
function getLatestCodexSessionId(record) {
|
|
456
|
+
return selectCodexSessionForRecord(record)?.claudeSessionId ?? null;
|
|
457
|
+
}
|
|
424
458
|
/** Delete every rollout file belonging to the given codex thread ids. */
|
|
425
459
|
function deleteCodexRolloutFiles(threadIds) {
|
|
426
460
|
if (threadIds.size === 0)
|
|
@@ -548,7 +582,12 @@ export class ProcessManager extends EventEmitter {
|
|
|
548
582
|
}
|
|
549
583
|
const provider = snapshot.provider ?? resolveProviderFromCommand(snapshot.command);
|
|
550
584
|
const isClaudeCmd = provider === "claude";
|
|
551
|
-
const
|
|
585
|
+
const isCodexCmd = provider === "codex";
|
|
586
|
+
const resumeCommandSessionId = isClaudeCmd
|
|
587
|
+
? getResumeCommandSessionId(snapshot.command)
|
|
588
|
+
: isCodexCmd
|
|
589
|
+
? getCodexResumeCommandSessionId(snapshot.command)
|
|
590
|
+
: null;
|
|
552
591
|
// Sessions restored from storage have ptyProcess: null — the old server's PTY
|
|
553
592
|
// belongs to a dead process. Mark running sessions as exited so the UI
|
|
554
593
|
// reflects reality and users can start fresh sessions.
|
|
@@ -591,6 +630,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
591
630
|
knownClaudeTaskIds: undefined,
|
|
592
631
|
claudeTaskDiscoveryTimer: null,
|
|
593
632
|
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(updated.cwd) : undefined,
|
|
633
|
+
knownCodexSessionMtimes: isCodexCmd ? listCodexSessionMtimes() : undefined,
|
|
634
|
+
codexSessionDiscoveryTimer: null,
|
|
594
635
|
claudeSessionId: resumeCommandSessionId ?? updated.claudeSessionId,
|
|
595
636
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
596
637
|
ptyCols: snapshot.ptyCols ?? 120,
|
|
@@ -627,6 +668,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
627
668
|
knownClaudeTaskIds: undefined,
|
|
628
669
|
claudeTaskDiscoveryTimer: null,
|
|
629
670
|
knownClaudeProjectMtimes: isClaudeCmd ? listClaudeProjectSessionMtimes(snapshot.cwd) : undefined,
|
|
671
|
+
knownCodexSessionMtimes: isCodexCmd ? listCodexSessionMtimes() : undefined,
|
|
672
|
+
codexSessionDiscoveryTimer: null,
|
|
630
673
|
claudeSessionId: resumeCommandSessionId ?? snapshot.claudeSessionId,
|
|
631
674
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
632
675
|
ptyCols: snapshot.ptyCols ?? 120,
|
|
@@ -686,7 +729,9 @@ export class ProcessManager extends EventEmitter {
|
|
|
686
729
|
const record = this.sessions.get(id);
|
|
687
730
|
if (record) {
|
|
688
731
|
this.logger.deleteSession(id);
|
|
689
|
-
|
|
732
|
+
if (record.provider === "claude") {
|
|
733
|
+
this.deleteClaudeCache(record);
|
|
734
|
+
}
|
|
690
735
|
}
|
|
691
736
|
this.sessions.delete(id);
|
|
692
737
|
this.lastPersistedMessageState.delete(id);
|
|
@@ -694,6 +739,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
694
739
|
}
|
|
695
740
|
if (toRemove.length > 0) {
|
|
696
741
|
this.claudeHistoryCache = null;
|
|
742
|
+
this.codexHistoryCache = null;
|
|
697
743
|
}
|
|
698
744
|
}
|
|
699
745
|
start(command, cwd, mode, initialInput, opts) {
|
|
@@ -731,9 +777,16 @@ export class ProcessManager extends EventEmitter {
|
|
|
731
777
|
const resumeCommandSessionId = isClaudeProvider
|
|
732
778
|
? getResumeCommandSessionId(processedCommand) ?? getResumeCommandSessionId(command)
|
|
733
779
|
: null;
|
|
780
|
+
const isCodexProvider = provider === "codex";
|
|
781
|
+
const codexResumeCommandSessionId = isCodexProvider
|
|
782
|
+
? getCodexResumeCommandSessionId(processedCommand) ?? getCodexResumeCommandSessionId(command)
|
|
783
|
+
: null;
|
|
734
784
|
const knownClaudeTaskIds = isClaudeProvider ? new Set(listRecentClaudeProjectSessionIds(resolvedCwd, new Date().toISOString())) : null;
|
|
735
785
|
const knownClaudeProjectMtimes = isClaudeProvider ? listClaudeProjectSessionMtimes(resolvedCwd) : null;
|
|
736
|
-
const
|
|
786
|
+
const knownCodexSessionMtimes = isCodexProvider && !codexResumeCommandSessionId ? listCodexSessionMtimes() : null;
|
|
787
|
+
const initialClaudeSessionId = isClaudeProvider
|
|
788
|
+
? resumeCommandSessionId ?? null
|
|
789
|
+
: codexResumeCommandSessionId ?? null;
|
|
737
790
|
const startedAt = new Date().toISOString();
|
|
738
791
|
const record = {
|
|
739
792
|
id,
|
|
@@ -778,6 +831,8 @@ export class ProcessManager extends EventEmitter {
|
|
|
778
831
|
knownClaudeTaskIds: knownClaudeTaskIds ?? undefined,
|
|
779
832
|
claudeTaskDiscoveryTimer: null,
|
|
780
833
|
knownClaudeProjectMtimes: knownClaudeProjectMtimes ?? undefined,
|
|
834
|
+
knownCodexSessionMtimes: knownCodexSessionMtimes ?? undefined,
|
|
835
|
+
codexSessionDiscoveryTimer: null,
|
|
781
836
|
approvalStats: { tool: 0, command: 0, file: 0, total: 0 },
|
|
782
837
|
selectedModel: selectedModel ?? null,
|
|
783
838
|
thinkingEffort: normalizeThinkingEffort(opts?.thinkingEffort),
|
|
@@ -800,9 +855,14 @@ export class ProcessManager extends EventEmitter {
|
|
|
800
855
|
});
|
|
801
856
|
}
|
|
802
857
|
this.sessions.set(id, record);
|
|
803
|
-
this.persist(record);
|
|
858
|
+
this.persist(record, { forceFullSave: true });
|
|
804
859
|
if (initialClaudeSessionId) {
|
|
805
|
-
|
|
860
|
+
if (provider === "codex") {
|
|
861
|
+
this.codexHistoryCache = null;
|
|
862
|
+
}
|
|
863
|
+
else {
|
|
864
|
+
this.claudeHistoryCache = null;
|
|
865
|
+
}
|
|
806
866
|
}
|
|
807
867
|
this.cleanupOldSessions();
|
|
808
868
|
const shellArgs = this.buildShellArgs(processedCommand);
|
|
@@ -842,6 +902,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
842
902
|
clearTimeout(current.claudeTaskDiscoveryTimer);
|
|
843
903
|
current.claudeTaskDiscoveryTimer = null;
|
|
844
904
|
}
|
|
905
|
+
if (current.codexSessionDiscoveryTimer) {
|
|
906
|
+
clearTimeout(current.codexSessionDiscoveryTimer);
|
|
907
|
+
current.codexSessionDiscoveryTimer = null;
|
|
908
|
+
}
|
|
845
909
|
if (current.initialInputTimer) {
|
|
846
910
|
clearTimeout(current.initialInputTimer);
|
|
847
911
|
current.initialInputTimer = null;
|
|
@@ -852,6 +916,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
852
916
|
}
|
|
853
917
|
current.pendingEscalation = null;
|
|
854
918
|
current.ptyPermissionBlocked = false;
|
|
919
|
+
this.captureCodexSessionId(current);
|
|
855
920
|
current.status = current.stopRequested ? "stopped" : exitCode === 0 ? "exited" : "failed";
|
|
856
921
|
current.exitCode = current.stopRequested ? null : exitCode;
|
|
857
922
|
current.endedAt = new Date().toISOString();
|
|
@@ -972,6 +1037,25 @@ export class ProcessManager extends EventEmitter {
|
|
|
972
1037
|
};
|
|
973
1038
|
record.claudeTaskDiscoveryTimer = setTimeout(tryDiscoverClaudeTaskId, 500);
|
|
974
1039
|
}
|
|
1040
|
+
if (record.knownCodexSessionMtimes) {
|
|
1041
|
+
const tryDiscoverCodexSessionId = () => {
|
|
1042
|
+
const current = this.sessions.get(id);
|
|
1043
|
+
if (!current || current.status !== "running" || current.claudeSessionId || !current.knownCodexSessionMtimes) {
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
if (getCodexResumeCommandSessionId(current.command)) {
|
|
1047
|
+
current.codexSessionDiscoveryTimer = null;
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
if (this.captureCodexSessionId(current)) {
|
|
1051
|
+
current.codexSessionDiscoveryTimer = null;
|
|
1052
|
+
this.persist(current);
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
current.codexSessionDiscoveryTimer = setTimeout(tryDiscoverCodexSessionId, 1000);
|
|
1056
|
+
};
|
|
1057
|
+
record.codexSessionDiscoveryTimer = setTimeout(tryDiscoverCodexSessionId, 500);
|
|
1058
|
+
}
|
|
975
1059
|
return this.snapshot(record);
|
|
976
1060
|
}
|
|
977
1061
|
list() {
|
|
@@ -999,7 +1083,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
999
1083
|
// Cross-reference with wand-managed sessions
|
|
1000
1084
|
const managedClaudeIds = new Set();
|
|
1001
1085
|
for (const record of this.sessions.values()) {
|
|
1002
|
-
if (record.claudeSessionId) {
|
|
1086
|
+
if (record.provider === "claude" && record.claudeSessionId) {
|
|
1003
1087
|
managedClaudeIds.add(record.claudeSessionId);
|
|
1004
1088
|
}
|
|
1005
1089
|
}
|
|
@@ -1077,6 +1161,24 @@ export class ProcessManager extends EventEmitter {
|
|
|
1077
1161
|
}
|
|
1078
1162
|
return deleted;
|
|
1079
1163
|
}
|
|
1164
|
+
captureCodexSessionId(record) {
|
|
1165
|
+
if (record.provider !== "codex" || record.claudeSessionId || !record.knownCodexSessionMtimes) {
|
|
1166
|
+
return false;
|
|
1167
|
+
}
|
|
1168
|
+
const discoveredThreadId = getLatestCodexSessionId({
|
|
1169
|
+
cwd: record.cwd,
|
|
1170
|
+
startedAt: record.startedAt,
|
|
1171
|
+
knownCodexSessionMtimes: record.knownCodexSessionMtimes,
|
|
1172
|
+
});
|
|
1173
|
+
if (!discoveredThreadId) {
|
|
1174
|
+
return false;
|
|
1175
|
+
}
|
|
1176
|
+
record.claudeSessionId = discoveredThreadId;
|
|
1177
|
+
record.knownCodexSessionMtimes.set(discoveredThreadId, Date.now());
|
|
1178
|
+
this.codexHistoryCache = null;
|
|
1179
|
+
process.stderr.write(`[wand] Captured Codex thread ID: ${discoveredThreadId}\n`);
|
|
1180
|
+
return true;
|
|
1181
|
+
}
|
|
1080
1182
|
get(id) {
|
|
1081
1183
|
const record = this.sessions.get(id);
|
|
1082
1184
|
if (!record) {
|
|
@@ -1224,6 +1326,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1224
1326
|
clearTimeout(record.claudeTaskDiscoveryTimer);
|
|
1225
1327
|
record.claudeTaskDiscoveryTimer = null;
|
|
1226
1328
|
}
|
|
1329
|
+
if (record.codexSessionDiscoveryTimer) {
|
|
1330
|
+
clearTimeout(record.codexSessionDiscoveryTimer);
|
|
1331
|
+
record.codexSessionDiscoveryTimer = null;
|
|
1332
|
+
}
|
|
1227
1333
|
if (record.initialInputTimer) {
|
|
1228
1334
|
clearTimeout(record.initialInputTimer);
|
|
1229
1335
|
record.initialInputTimer = null;
|
|
@@ -1250,6 +1356,7 @@ export class ProcessManager extends EventEmitter {
|
|
|
1250
1356
|
record.ptyBridge = null;
|
|
1251
1357
|
}
|
|
1252
1358
|
// Update lifecycle
|
|
1359
|
+
this.captureCodexSessionId(record);
|
|
1253
1360
|
this.persist(record);
|
|
1254
1361
|
return this.snapshot(record);
|
|
1255
1362
|
}
|
|
@@ -1262,6 +1369,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1262
1369
|
clearTimeout(record.claudeTaskDiscoveryTimer);
|
|
1263
1370
|
record.claudeTaskDiscoveryTimer = null;
|
|
1264
1371
|
}
|
|
1372
|
+
if (record.codexSessionDiscoveryTimer) {
|
|
1373
|
+
clearTimeout(record.codexSessionDiscoveryTimer);
|
|
1374
|
+
record.codexSessionDiscoveryTimer = null;
|
|
1375
|
+
}
|
|
1265
1376
|
if (record.initialInputTimer) {
|
|
1266
1377
|
clearTimeout(record.initialInputTimer);
|
|
1267
1378
|
record.initialInputTimer = null;
|
|
@@ -1298,6 +1409,10 @@ export class ProcessManager extends EventEmitter {
|
|
|
1298
1409
|
clearTimeout(record.claudeTaskDiscoveryTimer);
|
|
1299
1410
|
record.claudeTaskDiscoveryTimer = null;
|
|
1300
1411
|
}
|
|
1412
|
+
if (record.codexSessionDiscoveryTimer) {
|
|
1413
|
+
clearTimeout(record.codexSessionDiscoveryTimer);
|
|
1414
|
+
record.codexSessionDiscoveryTimer = null;
|
|
1415
|
+
}
|
|
1301
1416
|
if (record.initialInputTimer) {
|
|
1302
1417
|
clearTimeout(record.initialInputTimer);
|
|
1303
1418
|
record.initialInputTimer = null;
|
|
@@ -1335,11 +1450,18 @@ export class ProcessManager extends EventEmitter {
|
|
|
1335
1450
|
// so a storage failure doesn't leave orphan records in the database.
|
|
1336
1451
|
this.storage.deleteSession(id);
|
|
1337
1452
|
this.logger.deleteSession(id);
|
|
1338
|
-
|
|
1453
|
+
if (record.provider === "claude") {
|
|
1454
|
+
this.deleteClaudeCache(record);
|
|
1455
|
+
}
|
|
1339
1456
|
this.sessions.delete(id);
|
|
1340
1457
|
this.lastPersistedMessageState.delete(id);
|
|
1341
1458
|
if (record.claudeSessionId) {
|
|
1342
|
-
|
|
1459
|
+
if (record.provider === "codex") {
|
|
1460
|
+
this.codexHistoryCache = null;
|
|
1461
|
+
}
|
|
1462
|
+
else {
|
|
1463
|
+
this.claudeHistoryCache = null;
|
|
1464
|
+
}
|
|
1343
1465
|
}
|
|
1344
1466
|
}
|
|
1345
1467
|
deleteClaudeCache(record) {
|
|
@@ -1488,14 +1610,15 @@ export class ProcessManager extends EventEmitter {
|
|
|
1488
1610
|
this.persist(record);
|
|
1489
1611
|
return this.snapshot(record);
|
|
1490
1612
|
}
|
|
1491
|
-
persist(record) {
|
|
1613
|
+
persist(record, options) {
|
|
1492
1614
|
// Update messages from bridge before persisting
|
|
1493
1615
|
const messages = record.ptyBridge?.getMessages() ?? record.messages;
|
|
1494
1616
|
if (messages !== record.messages) {
|
|
1495
1617
|
record.messages = messages;
|
|
1496
1618
|
}
|
|
1497
1619
|
const snapshot = this.snapshot(record);
|
|
1498
|
-
const shouldSaveMessages =
|
|
1620
|
+
const shouldSaveMessages = options?.forceFullSave === true
|
|
1621
|
+
|| shouldPersistMessages(this.lastPersistedMessageState.get(record.id), messages);
|
|
1499
1622
|
// Persist full messages as soon as the structured conversation changes so
|
|
1500
1623
|
// service restarts cannot roll the session back to an older tail message.
|
|
1501
1624
|
if (shouldSaveMessages) {
|
package/dist/resume-policy.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
1
|
import { ConversationTurn } from "./types.js";
|
|
2
2
|
export declare function hasRealConversationMessages(messages: ConversationTurn[] | undefined): boolean;
|
|
3
3
|
export declare function getResumeCommandSessionId(command: string): string | null;
|
|
4
|
+
export declare function getCodexResumeCommandSessionId(command: string): string | null;
|
package/dist/resume-policy.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
const REAL_CONVERSATION_MIN_MESSAGES = 2;
|
|
2
|
-
const
|
|
2
|
+
const UUID_PATTERN = "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}";
|
|
3
|
+
const RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)--resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
|
|
4
|
+
const CODEX_RESUME_COMMAND_ID_PATTERN = new RegExp(`(?:^|\\s)resume\\s+(${UUID_PATTERN})(?:\\s|$)`, "i");
|
|
3
5
|
export function hasRealConversationMessages(messages) {
|
|
4
6
|
if (!messages || messages.length < REAL_CONVERSATION_MIN_MESSAGES) {
|
|
5
7
|
return false;
|
|
@@ -14,3 +16,7 @@ export function getResumeCommandSessionId(command) {
|
|
|
14
16
|
const match = RESUME_COMMAND_ID_PATTERN.exec(command);
|
|
15
17
|
return match?.[1] ?? null;
|
|
16
18
|
}
|
|
19
|
+
export function getCodexResumeCommandSessionId(command) {
|
|
20
|
+
const match = CODEX_RESUME_COMMAND_ID_PATTERN.exec(command);
|
|
21
|
+
return match?.[1] ?? null;
|
|
22
|
+
}
|
|
@@ -130,6 +130,13 @@ function getWorktreeMergePayload(error, fallback) {
|
|
|
130
130
|
function getLatestSessionSnapshot(processes, structured, storage, id) {
|
|
131
131
|
return getSessionById(processes, structured, id) ?? storage.getSession(id);
|
|
132
132
|
}
|
|
133
|
+
function buildCodexResumeCommand(command, threadId) {
|
|
134
|
+
const trimmed = command.trim();
|
|
135
|
+
const withoutExistingResume = trimmed
|
|
136
|
+
.replace(/\s+resume\s+[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}(?:\s|$)/i, " ")
|
|
137
|
+
.trim();
|
|
138
|
+
return `${withoutExistingResume || "codex"} resume ${threadId}`;
|
|
139
|
+
}
|
|
133
140
|
function canMergeSession(snapshot) {
|
|
134
141
|
return Boolean(snapshot.worktreeEnabled && snapshot.worktree?.branch && snapshot.worktree?.path);
|
|
135
142
|
}
|
|
@@ -593,28 +600,41 @@ export function registerSessionRoutes(app, processes, structured, storage, defau
|
|
|
593
600
|
return;
|
|
594
601
|
}
|
|
595
602
|
if ((existingSession.sessionKind ?? "pty") !== "pty") {
|
|
596
|
-
res.status(400).json({ error: "结构化会话不支持
|
|
603
|
+
res.status(400).json({ error: "结构化会话不支持 PTY resume。" });
|
|
597
604
|
return;
|
|
598
605
|
}
|
|
599
|
-
|
|
600
|
-
|
|
606
|
+
const command = existingSession.command.trim();
|
|
607
|
+
const provider = existingSession.provider ?? (/^codex\b/.test(command) ? "codex" : "claude");
|
|
608
|
+
if (provider !== "claude" && provider !== "codex") {
|
|
609
|
+
res.status(400).json({ error: "只有 Claude 或 Codex provider 支持恢复功能。" });
|
|
601
610
|
return;
|
|
602
611
|
}
|
|
603
|
-
const
|
|
604
|
-
if (!
|
|
605
|
-
res.status(400).json({ error: "此会话没有 Claude 会话 ID,无法恢复。" });
|
|
612
|
+
const resumeSessionId = existingSession.claudeSessionId;
|
|
613
|
+
if (!resumeSessionId) {
|
|
614
|
+
res.status(400).json({ error: provider === "codex" ? "此会话没有 Codex thread ID,无法恢复。" : "此会话没有 Claude 会话 ID,无法恢复。" });
|
|
606
615
|
return;
|
|
607
616
|
}
|
|
608
|
-
|
|
609
|
-
if (!/^claude\b/.test(command)) {
|
|
617
|
+
if (provider === "claude" && !/^claude\b/.test(command)) {
|
|
610
618
|
res.status(400).json({ error: "只有 Claude 命令支持恢复功能。" });
|
|
611
619
|
return;
|
|
612
620
|
}
|
|
621
|
+
if (provider === "codex") {
|
|
622
|
+
if (!/^codex\b/.test(command)) {
|
|
623
|
+
res.status(400).json({ error: "只有 Codex 命令支持恢复功能。" });
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
if (!processes.hasCodexSessionFile(resumeSessionId)) {
|
|
627
|
+
res.status(400).json({ error: "对应的 Codex 历史会话不存在,无法恢复。" });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
613
631
|
const newMode = body.mode ? normalizeMode(body.mode, defaultMode) : normalizeMode(existingSession.mode, defaultMode);
|
|
614
|
-
const resumeCommand =
|
|
632
|
+
const resumeCommand = provider === "codex"
|
|
633
|
+
? buildCodexResumeCommand(command, resumeSessionId)
|
|
634
|
+
: `${command} --resume ${resumeSessionId}`;
|
|
615
635
|
const reqCols = typeof body.cols === "number" && Number.isFinite(body.cols) ? body.cols : undefined;
|
|
616
636
|
const reqRows = typeof body.rows === "number" && Number.isFinite(body.rows) ? body.rows : undefined;
|
|
617
|
-
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId, cols: reqCols, rows: reqRows });
|
|
637
|
+
const newSnapshot = processes.start(resumeCommand, existingSession.cwd, newMode, undefined, { reuseId: sessionId, cols: reqCols, rows: reqRows, provider });
|
|
618
638
|
res.status(201).json(newSnapshot);
|
|
619
639
|
}
|
|
620
640
|
catch (error) {
|
package/dist/types.d.ts
CHANGED
|
@@ -196,6 +196,10 @@ export interface GitStatusResult {
|
|
|
196
196
|
shortHash: string;
|
|
197
197
|
subject: string;
|
|
198
198
|
};
|
|
199
|
+
/** Most recent tag reachable from HEAD (`git describe --tags --abbrev=0`), if any. */
|
|
200
|
+
latestTag?: string;
|
|
201
|
+
/** Locally-derived next-version suggestion (patch bump of `latestTag`, or a sane default). */
|
|
202
|
+
suggestedTag?: string;
|
|
199
203
|
error?: string;
|
|
200
204
|
}
|
|
201
205
|
export interface QuickCommitResult {
|