@remixhq/claude-plugin 0.1.21 → 0.1.22
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/.claude-plugin/plugin.json +1 -1
- package/dist/hook-post-collab.cjs +4 -4
- package/dist/hook-post-collab.cjs.map +1 -1
- package/dist/hook-stop-collab.cjs +1857 -847
- package/dist/hook-stop-collab.cjs.map +1 -1
- package/dist/hook-user-prompt.cjs +28058 -220
- package/dist/hook-user-prompt.cjs.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/mcp-server.cjs +28 -10
- package/dist/mcp-server.cjs.map +1 -1
- package/package.json +4 -4
- package/skills/init-or-remix/SKILL.md +5 -3
- package/skills/safe-collab-workflow/SKILL.md +15 -8
- package/skills/submit-change-step/SKILL.md +13 -12
|
@@ -37,8 +37,8 @@ var require_windows = __commonJS({
|
|
|
37
37
|
"use strict";
|
|
38
38
|
module2.exports = isexe;
|
|
39
39
|
isexe.sync = sync;
|
|
40
|
-
var
|
|
41
|
-
function checkPathExt(
|
|
40
|
+
var fs13 = require("fs");
|
|
41
|
+
function checkPathExt(path16, options) {
|
|
42
42
|
var pathext = options.pathExt !== void 0 ? options.pathExt : process.env.PATHEXT;
|
|
43
43
|
if (!pathext) {
|
|
44
44
|
return true;
|
|
@@ -49,25 +49,25 @@ var require_windows = __commonJS({
|
|
|
49
49
|
}
|
|
50
50
|
for (var i2 = 0; i2 < pathext.length; i2++) {
|
|
51
51
|
var p = pathext[i2].toLowerCase();
|
|
52
|
-
if (p &&
|
|
52
|
+
if (p && path16.substr(-p.length).toLowerCase() === p) {
|
|
53
53
|
return true;
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
return false;
|
|
57
57
|
}
|
|
58
|
-
function checkStat(stat,
|
|
58
|
+
function checkStat(stat, path16, options) {
|
|
59
59
|
if (!stat.isSymbolicLink() && !stat.isFile()) {
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
|
-
return checkPathExt(
|
|
62
|
+
return checkPathExt(path16, options);
|
|
63
63
|
}
|
|
64
|
-
function isexe(
|
|
65
|
-
|
|
66
|
-
cb(er, er ? false : checkStat(stat,
|
|
64
|
+
function isexe(path16, options, cb) {
|
|
65
|
+
fs13.stat(path16, function(er, stat) {
|
|
66
|
+
cb(er, er ? false : checkStat(stat, path16, options));
|
|
67
67
|
});
|
|
68
68
|
}
|
|
69
|
-
function sync(
|
|
70
|
-
return checkStat(
|
|
69
|
+
function sync(path16, options) {
|
|
70
|
+
return checkStat(fs13.statSync(path16), path16, options);
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
});
|
|
@@ -78,14 +78,14 @@ var require_mode = __commonJS({
|
|
|
78
78
|
"use strict";
|
|
79
79
|
module2.exports = isexe;
|
|
80
80
|
isexe.sync = sync;
|
|
81
|
-
var
|
|
82
|
-
function isexe(
|
|
83
|
-
|
|
81
|
+
var fs13 = require("fs");
|
|
82
|
+
function isexe(path16, options, cb) {
|
|
83
|
+
fs13.stat(path16, function(er, stat) {
|
|
84
84
|
cb(er, er ? false : checkStat(stat, options));
|
|
85
85
|
});
|
|
86
86
|
}
|
|
87
|
-
function sync(
|
|
88
|
-
return checkStat(
|
|
87
|
+
function sync(path16, options) {
|
|
88
|
+
return checkStat(fs13.statSync(path16), options);
|
|
89
89
|
}
|
|
90
90
|
function checkStat(stat, options) {
|
|
91
91
|
return stat.isFile() && checkMode(stat, options);
|
|
@@ -110,7 +110,7 @@ var require_mode = __commonJS({
|
|
|
110
110
|
var require_isexe = __commonJS({
|
|
111
111
|
"node_modules/isexe/index.js"(exports2, module2) {
|
|
112
112
|
"use strict";
|
|
113
|
-
var
|
|
113
|
+
var fs13 = require("fs");
|
|
114
114
|
var core;
|
|
115
115
|
if (process.platform === "win32" || global.TESTING_WINDOWS) {
|
|
116
116
|
core = require_windows();
|
|
@@ -119,7 +119,7 @@ var require_isexe = __commonJS({
|
|
|
119
119
|
}
|
|
120
120
|
module2.exports = isexe;
|
|
121
121
|
isexe.sync = sync;
|
|
122
|
-
function isexe(
|
|
122
|
+
function isexe(path16, options, cb) {
|
|
123
123
|
if (typeof options === "function") {
|
|
124
124
|
cb = options;
|
|
125
125
|
options = {};
|
|
@@ -129,7 +129,7 @@ var require_isexe = __commonJS({
|
|
|
129
129
|
throw new TypeError("callback not provided");
|
|
130
130
|
}
|
|
131
131
|
return new Promise(function(resolve, reject) {
|
|
132
|
-
isexe(
|
|
132
|
+
isexe(path16, options || {}, function(er, is) {
|
|
133
133
|
if (er) {
|
|
134
134
|
reject(er);
|
|
135
135
|
} else {
|
|
@@ -138,7 +138,7 @@ var require_isexe = __commonJS({
|
|
|
138
138
|
});
|
|
139
139
|
});
|
|
140
140
|
}
|
|
141
|
-
core(
|
|
141
|
+
core(path16, options || {}, function(er, is) {
|
|
142
142
|
if (er) {
|
|
143
143
|
if (er.code === "EACCES" || options && options.ignoreErrors) {
|
|
144
144
|
er = null;
|
|
@@ -148,9 +148,9 @@ var require_isexe = __commonJS({
|
|
|
148
148
|
cb(er, is);
|
|
149
149
|
});
|
|
150
150
|
}
|
|
151
|
-
function sync(
|
|
151
|
+
function sync(path16, options) {
|
|
152
152
|
try {
|
|
153
|
-
return core.sync(
|
|
153
|
+
return core.sync(path16, options || {});
|
|
154
154
|
} catch (er) {
|
|
155
155
|
if (options && options.ignoreErrors || er.code === "EACCES") {
|
|
156
156
|
return false;
|
|
@@ -167,7 +167,7 @@ var require_which = __commonJS({
|
|
|
167
167
|
"node_modules/which/which.js"(exports2, module2) {
|
|
168
168
|
"use strict";
|
|
169
169
|
var isWindows = process.platform === "win32" || process.env.OSTYPE === "cygwin" || process.env.OSTYPE === "msys";
|
|
170
|
-
var
|
|
170
|
+
var path16 = require("path");
|
|
171
171
|
var COLON = isWindows ? ";" : ":";
|
|
172
172
|
var isexe = require_isexe();
|
|
173
173
|
var getNotFoundError = (cmd) => Object.assign(new Error(`not found: ${cmd}`), { code: "ENOENT" });
|
|
@@ -205,7 +205,7 @@ var require_which = __commonJS({
|
|
|
205
205
|
return opt.all && found.length ? resolve(found) : reject(getNotFoundError(cmd));
|
|
206
206
|
const ppRaw = pathEnv[i2];
|
|
207
207
|
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
208
|
-
const pCmd =
|
|
208
|
+
const pCmd = path16.join(pathPart, cmd);
|
|
209
209
|
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
210
210
|
resolve(subStep(p, i2, 0));
|
|
211
211
|
});
|
|
@@ -232,7 +232,7 @@ var require_which = __commonJS({
|
|
|
232
232
|
for (let i2 = 0; i2 < pathEnv.length; i2++) {
|
|
233
233
|
const ppRaw = pathEnv[i2];
|
|
234
234
|
const pathPart = /^".*"$/.test(ppRaw) ? ppRaw.slice(1, -1) : ppRaw;
|
|
235
|
-
const pCmd =
|
|
235
|
+
const pCmd = path16.join(pathPart, cmd);
|
|
236
236
|
const p = !pathPart && /^\.[\\\/]/.test(cmd) ? cmd.slice(0, 2) + pCmd : pCmd;
|
|
237
237
|
for (let j = 0; j < pathExt.length; j++) {
|
|
238
238
|
const cur = p + pathExt[j];
|
|
@@ -280,7 +280,7 @@ var require_path_key = __commonJS({
|
|
|
280
280
|
var require_resolveCommand = __commonJS({
|
|
281
281
|
"node_modules/cross-spawn/lib/util/resolveCommand.js"(exports2, module2) {
|
|
282
282
|
"use strict";
|
|
283
|
-
var
|
|
283
|
+
var path16 = require("path");
|
|
284
284
|
var which = require_which();
|
|
285
285
|
var getPathKey = require_path_key();
|
|
286
286
|
function resolveCommandAttempt(parsed, withoutPathExt) {
|
|
@@ -298,7 +298,7 @@ var require_resolveCommand = __commonJS({
|
|
|
298
298
|
try {
|
|
299
299
|
resolved = which.sync(parsed.command, {
|
|
300
300
|
path: env[getPathKey({ env })],
|
|
301
|
-
pathExt: withoutPathExt ?
|
|
301
|
+
pathExt: withoutPathExt ? path16.delimiter : void 0
|
|
302
302
|
});
|
|
303
303
|
} catch (e) {
|
|
304
304
|
} finally {
|
|
@@ -307,7 +307,7 @@ var require_resolveCommand = __commonJS({
|
|
|
307
307
|
}
|
|
308
308
|
}
|
|
309
309
|
if (resolved) {
|
|
310
|
-
resolved =
|
|
310
|
+
resolved = path16.resolve(hasCustomCwd ? parsed.options.cwd : "", resolved);
|
|
311
311
|
}
|
|
312
312
|
return resolved;
|
|
313
313
|
}
|
|
@@ -361,8 +361,8 @@ var require_shebang_command = __commonJS({
|
|
|
361
361
|
if (!match) {
|
|
362
362
|
return null;
|
|
363
363
|
}
|
|
364
|
-
const [
|
|
365
|
-
const binary =
|
|
364
|
+
const [path16, argument] = match[0].replace(/#! ?/, "").split(" ");
|
|
365
|
+
const binary = path16.split("/").pop();
|
|
366
366
|
if (binary === "env") {
|
|
367
367
|
return argument;
|
|
368
368
|
}
|
|
@@ -375,16 +375,16 @@ var require_shebang_command = __commonJS({
|
|
|
375
375
|
var require_readShebang = __commonJS({
|
|
376
376
|
"node_modules/cross-spawn/lib/util/readShebang.js"(exports2, module2) {
|
|
377
377
|
"use strict";
|
|
378
|
-
var
|
|
378
|
+
var fs13 = require("fs");
|
|
379
379
|
var shebangCommand = require_shebang_command();
|
|
380
380
|
function readShebang(command) {
|
|
381
381
|
const size = 150;
|
|
382
382
|
const buffer = Buffer.alloc(size);
|
|
383
383
|
let fd;
|
|
384
384
|
try {
|
|
385
|
-
fd =
|
|
386
|
-
|
|
387
|
-
|
|
385
|
+
fd = fs13.openSync(command, "r");
|
|
386
|
+
fs13.readSync(fd, buffer, 0, size, 0);
|
|
387
|
+
fs13.closeSync(fd);
|
|
388
388
|
} catch (e) {
|
|
389
389
|
}
|
|
390
390
|
return shebangCommand(buffer.toString());
|
|
@@ -397,7 +397,7 @@ var require_readShebang = __commonJS({
|
|
|
397
397
|
var require_parse = __commonJS({
|
|
398
398
|
"node_modules/cross-spawn/lib/parse.js"(exports2, module2) {
|
|
399
399
|
"use strict";
|
|
400
|
-
var
|
|
400
|
+
var path16 = require("path");
|
|
401
401
|
var resolveCommand = require_resolveCommand();
|
|
402
402
|
var escape = require_escape();
|
|
403
403
|
var readShebang = require_readShebang();
|
|
@@ -422,7 +422,7 @@ var require_parse = __commonJS({
|
|
|
422
422
|
const needsShell = !isExecutableRegExp.test(commandFile);
|
|
423
423
|
if (parsed.options.forceShell || needsShell) {
|
|
424
424
|
const needsDoubleEscapeMetaChars = isCmdShimRegExp.test(commandFile);
|
|
425
|
-
parsed.command =
|
|
425
|
+
parsed.command = path16.normalize(parsed.command);
|
|
426
426
|
parsed.command = escape.command(parsed.command);
|
|
427
427
|
parsed.args = parsed.args.map((arg) => escape.argument(arg, needsDoubleEscapeMetaChars));
|
|
428
428
|
const shellCommand = [parsed.command].concat(parsed.args).join(" ");
|
|
@@ -512,7 +512,7 @@ var require_cross_spawn = __commonJS({
|
|
|
512
512
|
var cp = require("child_process");
|
|
513
513
|
var parse = require_parse();
|
|
514
514
|
var enoent = require_enoent();
|
|
515
|
-
function
|
|
515
|
+
function spawn5(command, args, options) {
|
|
516
516
|
const parsed = parse(command, args, options);
|
|
517
517
|
const spawned = cp.spawn(parsed.command, parsed.args, parsed.options);
|
|
518
518
|
enoent.hookChildProcess(spawned, parsed);
|
|
@@ -524,8 +524,8 @@ var require_cross_spawn = __commonJS({
|
|
|
524
524
|
result.error = result.error || enoent.verifyENOENTSync(result.status, parsed);
|
|
525
525
|
return result;
|
|
526
526
|
}
|
|
527
|
-
module2.exports =
|
|
528
|
-
module2.exports.spawn =
|
|
527
|
+
module2.exports = spawn5;
|
|
528
|
+
module2.exports.spawn = spawn5;
|
|
529
529
|
module2.exports.sync = spawnSync3;
|
|
530
530
|
module2.exports._parse = parse;
|
|
531
531
|
module2.exports._enoent = enoent;
|
|
@@ -538,9 +538,7 @@ __export(hook_stop_collab_exports, {
|
|
|
538
538
|
runHookStopCollab: () => runHookStopCollab
|
|
539
539
|
});
|
|
540
540
|
module.exports = __toCommonJS(hook_stop_collab_exports);
|
|
541
|
-
var
|
|
542
|
-
var import_node_fs7 = require("fs");
|
|
543
|
-
var import_node_path10 = __toESM(require("path"), 1);
|
|
541
|
+
var import_node_child_process9 = require("child_process");
|
|
544
542
|
|
|
545
543
|
// node_modules/@remixhq/core/dist/chunk-YZ34ICNN.js
|
|
546
544
|
var RemixError = class extends Error {
|
|
@@ -4937,13 +4935,13 @@ var logOutputSync = ({ serializedResult, fdNumber, state, verboseInfo, encoding,
|
|
|
4937
4935
|
}
|
|
4938
4936
|
};
|
|
4939
4937
|
var writeToFiles = (serializedResult, stdioItems, outputFiles) => {
|
|
4940
|
-
for (const { path:
|
|
4941
|
-
const pathString = typeof
|
|
4938
|
+
for (const { path: path16, append } of stdioItems.filter(({ type }) => FILE_TYPES.has(type))) {
|
|
4939
|
+
const pathString = typeof path16 === "string" ? path16 : path16.toString();
|
|
4942
4940
|
if (append || outputFiles.has(pathString)) {
|
|
4943
|
-
(0, import_node_fs4.appendFileSync)(
|
|
4941
|
+
(0, import_node_fs4.appendFileSync)(path16, serializedResult);
|
|
4944
4942
|
} else {
|
|
4945
4943
|
outputFiles.add(pathString);
|
|
4946
|
-
(0, import_node_fs4.writeFileSync)(
|
|
4944
|
+
(0, import_node_fs4.writeFileSync)(path16, serializedResult);
|
|
4947
4945
|
}
|
|
4948
4946
|
}
|
|
4949
4947
|
};
|
|
@@ -10031,202 +10029,1059 @@ var FINALIZE_PREFLIGHT_FAILURE_CODES = [
|
|
|
10031
10029
|
"re_anchor_required"
|
|
10032
10030
|
];
|
|
10033
10031
|
var CODE_SET = new Set(FINALIZE_PREFLIGHT_FAILURE_CODES);
|
|
10032
|
+
function isFinalizePreflightFailureCode(value) {
|
|
10033
|
+
return typeof value === "string" && CODE_SET.has(value);
|
|
10034
|
+
}
|
|
10034
10035
|
|
|
10035
|
-
//
|
|
10036
|
-
|
|
10037
|
-
|
|
10038
|
-
|
|
10036
|
+
// src/auto-fix-dispatcher.ts
|
|
10037
|
+
var import_node_child_process6 = require("child_process");
|
|
10038
|
+
var import_node_fs6 = require("fs");
|
|
10039
|
+
var import_node_path9 = __toESM(require("path"), 1);
|
|
10040
|
+
|
|
10041
|
+
// src/finalize-failure-marker.ts
|
|
10042
|
+
var import_promises18 = __toESM(require("fs/promises"), 1);
|
|
10043
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
10044
|
+
var FINALIZE_FAILURE_MARKER_REL = import_node_path6.default.join(".remix", ".last-finalize-failure.json");
|
|
10045
|
+
function markerPath(repoRoot) {
|
|
10046
|
+
return import_node_path6.default.join(repoRoot, FINALIZE_FAILURE_MARKER_REL);
|
|
10047
|
+
}
|
|
10048
|
+
async function writeFinalizeFailureMarker(marker) {
|
|
10049
|
+
const filePath = markerPath(marker.repoRoot);
|
|
10050
|
+
await import_promises18.default.mkdir(import_node_path6.default.dirname(filePath), { recursive: true });
|
|
10051
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
10052
|
+
await import_promises18.default.writeFile(tmpPath, JSON.stringify(marker, null, 2), "utf8");
|
|
10053
|
+
await import_promises18.default.rename(tmpPath, filePath);
|
|
10054
|
+
}
|
|
10055
|
+
async function clearFinalizeFailureMarker(repoRoot) {
|
|
10056
|
+
await import_promises18.default.rm(markerPath(repoRoot), { force: true }).catch(() => void 0);
|
|
10057
|
+
}
|
|
10058
|
+
function buildFreshFailureMarker(params) {
|
|
10059
|
+
return {
|
|
10060
|
+
schemaVersion: 1,
|
|
10061
|
+
failedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10062
|
+
repoRoot: params.repoRoot,
|
|
10063
|
+
preflightCode: params.preflightCode,
|
|
10064
|
+
message: params.message,
|
|
10065
|
+
hint: params.hint,
|
|
10066
|
+
recommendedCommand: params.recommendedCommand,
|
|
10067
|
+
autoFix: {
|
|
10068
|
+
status: "not_attempted",
|
|
10069
|
+
command: null,
|
|
10070
|
+
pid: null,
|
|
10071
|
+
logPath: null,
|
|
10072
|
+
attemptedAt: null,
|
|
10073
|
+
failureMessage: null
|
|
10074
|
+
}
|
|
10075
|
+
};
|
|
10076
|
+
}
|
|
10077
|
+
|
|
10078
|
+
// src/hook-diagnostics.ts
|
|
10079
|
+
var import_node_crypto2 = require("crypto");
|
|
10080
|
+
var import_promises20 = __toESM(require("fs/promises"), 1);
|
|
10081
|
+
var import_node_os5 = __toESM(require("os"), 1);
|
|
10082
|
+
var import_node_path8 = __toESM(require("path"), 1);
|
|
10083
|
+
|
|
10084
|
+
// src/hook-state.ts
|
|
10085
|
+
var import_promises19 = __toESM(require("fs/promises"), 1);
|
|
10086
|
+
var import_node_os4 = __toESM(require("os"), 1);
|
|
10087
|
+
var import_node_path7 = __toESM(require("path"), 1);
|
|
10088
|
+
var import_node_crypto = require("crypto");
|
|
10089
|
+
function stateRoot() {
|
|
10090
|
+
const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
|
|
10091
|
+
return configured || import_node_path7.default.join(import_node_os4.default.tmpdir(), "remix-claude-plugin-hooks");
|
|
10092
|
+
}
|
|
10093
|
+
function statePath(sessionId) {
|
|
10094
|
+
return import_node_path7.default.join(stateRoot(), `${sessionId}.json`);
|
|
10095
|
+
}
|
|
10096
|
+
function stateLockPath(sessionId) {
|
|
10097
|
+
return import_node_path7.default.join(stateRoot(), `${sessionId}.lock`);
|
|
10098
|
+
}
|
|
10099
|
+
function stateLockMetaPath(sessionId) {
|
|
10100
|
+
return import_node_path7.default.join(stateLockPath(sessionId), "owner.json");
|
|
10101
|
+
}
|
|
10102
|
+
async function writeJsonAtomic2(filePath, value) {
|
|
10103
|
+
await import_promises19.default.mkdir(import_node_path7.default.dirname(filePath), { recursive: true });
|
|
10104
|
+
const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
10105
|
+
await import_promises19.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
10106
|
+
await import_promises19.default.rename(tmpPath, filePath);
|
|
10107
|
+
}
|
|
10108
|
+
var STATE_LOCK_WAIT_MS = 2e3;
|
|
10109
|
+
var STATE_LOCK_POLL_MS = 25;
|
|
10110
|
+
var STATE_LOCK_STALE_MS = 3e4;
|
|
10111
|
+
var STATE_LOCK_HEARTBEAT_MS = 5e3;
|
|
10112
|
+
async function sleep2(ms) {
|
|
10113
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
10114
|
+
}
|
|
10115
|
+
async function readStateLockMetadata(sessionId) {
|
|
10116
|
+
const raw = await import_promises19.default.readFile(stateLockMetaPath(sessionId), "utf8").catch(() => null);
|
|
10117
|
+
if (!raw) return null;
|
|
10039
10118
|
try {
|
|
10040
|
-
|
|
10119
|
+
const parsed = JSON.parse(raw);
|
|
10120
|
+
if (typeof parsed.ownerId !== "string" || typeof parsed.pid !== "number" || typeof parsed.createdAt !== "string" || typeof parsed.heartbeatAt !== "string") {
|
|
10121
|
+
return null;
|
|
10122
|
+
}
|
|
10123
|
+
return {
|
|
10124
|
+
ownerId: parsed.ownerId,
|
|
10125
|
+
pid: parsed.pid,
|
|
10126
|
+
createdAt: parsed.createdAt,
|
|
10127
|
+
heartbeatAt: parsed.heartbeatAt
|
|
10128
|
+
};
|
|
10041
10129
|
} catch {
|
|
10042
10130
|
return null;
|
|
10043
10131
|
}
|
|
10044
10132
|
}
|
|
10045
|
-
function
|
|
10046
|
-
|
|
10047
|
-
|
|
10048
|
-
|
|
10049
|
-
|
|
10050
|
-
|
|
10051
|
-
|
|
10052
|
-
|
|
10053
|
-
|
|
10054
|
-
|
|
10133
|
+
async function writeStateLockMetadata(sessionId, metadata) {
|
|
10134
|
+
await writeJsonAtomic2(stateLockMetaPath(sessionId), metadata);
|
|
10135
|
+
}
|
|
10136
|
+
async function tryRemoveStaleStateLock(sessionId) {
|
|
10137
|
+
const lockPath = stateLockPath(sessionId);
|
|
10138
|
+
const metadata = await readStateLockMetadata(sessionId);
|
|
10139
|
+
const staleByHeartbeat = metadata && Date.now() - new Date(metadata.heartbeatAt).getTime() > STATE_LOCK_STALE_MS;
|
|
10140
|
+
if (staleByHeartbeat) {
|
|
10141
|
+
await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
10142
|
+
return true;
|
|
10143
|
+
}
|
|
10144
|
+
if (!metadata) {
|
|
10145
|
+
const lockStat = await import_promises19.default.stat(lockPath).catch(() => null);
|
|
10146
|
+
if (lockStat && Date.now() - lockStat.mtimeMs > STATE_LOCK_STALE_MS) {
|
|
10147
|
+
await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
10148
|
+
return true;
|
|
10055
10149
|
}
|
|
10056
|
-
|
|
10057
|
-
|
|
10058
|
-
|
|
10059
|
-
|
|
10060
|
-
|
|
10061
|
-
|
|
10062
|
-
|
|
10063
|
-
|
|
10064
|
-
|
|
10065
|
-
|
|
10150
|
+
}
|
|
10151
|
+
return false;
|
|
10152
|
+
}
|
|
10153
|
+
async function acquireStateLock(sessionId) {
|
|
10154
|
+
const lockPath = stateLockPath(sessionId);
|
|
10155
|
+
const deadline = Date.now() + STATE_LOCK_WAIT_MS;
|
|
10156
|
+
await import_promises19.default.mkdir(stateRoot(), { recursive: true });
|
|
10157
|
+
while (true) {
|
|
10158
|
+
try {
|
|
10159
|
+
await import_promises19.default.mkdir(lockPath);
|
|
10160
|
+
const ownerId = (0, import_node_crypto.randomUUID)();
|
|
10161
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10162
|
+
const metadata = {
|
|
10163
|
+
ownerId,
|
|
10164
|
+
pid: process.pid,
|
|
10165
|
+
createdAt,
|
|
10166
|
+
heartbeatAt: createdAt
|
|
10167
|
+
};
|
|
10168
|
+
await writeStateLockMetadata(sessionId, metadata);
|
|
10169
|
+
let released = false;
|
|
10170
|
+
const heartbeat = setInterval(() => {
|
|
10171
|
+
if (released) return;
|
|
10172
|
+
void writeStateLockMetadata(sessionId, {
|
|
10173
|
+
...metadata,
|
|
10174
|
+
heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10175
|
+
}).catch(() => void 0);
|
|
10176
|
+
}, STATE_LOCK_HEARTBEAT_MS);
|
|
10177
|
+
heartbeat.unref?.();
|
|
10178
|
+
return async () => {
|
|
10179
|
+
if (released) return;
|
|
10180
|
+
released = true;
|
|
10181
|
+
clearInterval(heartbeat);
|
|
10182
|
+
const currentMetadata = await readStateLockMetadata(sessionId);
|
|
10183
|
+
if (currentMetadata?.ownerId === ownerId) {
|
|
10184
|
+
await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
10185
|
+
}
|
|
10186
|
+
};
|
|
10187
|
+
} catch (error) {
|
|
10188
|
+
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
10189
|
+
if (code !== "EEXIST") {
|
|
10190
|
+
throw error;
|
|
10066
10191
|
}
|
|
10067
|
-
|
|
10068
|
-
|
|
10069
|
-
|
|
10070
|
-
|
|
10071
|
-
|
|
10072
|
-
|
|
10073
|
-
|
|
10074
|
-
const body = await readJsonSafe(res);
|
|
10075
|
-
const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
|
|
10076
|
-
throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
|
|
10192
|
+
if (await tryRemoveStaleStateLock(sessionId)) {
|
|
10193
|
+
continue;
|
|
10194
|
+
}
|
|
10195
|
+
if (Date.now() >= deadline) {
|
|
10196
|
+
throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
|
|
10197
|
+
}
|
|
10198
|
+
await sleep2(STATE_LOCK_POLL_MS);
|
|
10077
10199
|
}
|
|
10078
|
-
const json = await readJsonSafe(res);
|
|
10079
|
-
return json ?? null;
|
|
10080
10200
|
}
|
|
10081
|
-
|
|
10082
|
-
|
|
10083
|
-
|
|
10084
|
-
|
|
10085
|
-
|
|
10086
|
-
|
|
10201
|
+
}
|
|
10202
|
+
async function withStateLock(sessionId, fn) {
|
|
10203
|
+
const release = await acquireStateLock(sessionId);
|
|
10204
|
+
try {
|
|
10205
|
+
return await fn();
|
|
10206
|
+
} finally {
|
|
10207
|
+
await release();
|
|
10208
|
+
}
|
|
10209
|
+
}
|
|
10210
|
+
function normalizeIntent(value) {
|
|
10211
|
+
return value === "memory_first" || value === "collab_state" || value === "git_facts" ? value : "neutral";
|
|
10212
|
+
}
|
|
10213
|
+
function normalizeString(value) {
|
|
10214
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
10215
|
+
}
|
|
10216
|
+
function normalizeStringArray(value) {
|
|
10217
|
+
if (!Array.isArray(value)) return [];
|
|
10218
|
+
return Array.from(
|
|
10219
|
+
new Set(
|
|
10220
|
+
value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
|
|
10221
|
+
)
|
|
10222
|
+
);
|
|
10223
|
+
}
|
|
10224
|
+
function normalizeManualRecordingScope(value) {
|
|
10225
|
+
if (value === "full_turn") {
|
|
10226
|
+
return "full_turn";
|
|
10227
|
+
}
|
|
10228
|
+
return null;
|
|
10229
|
+
}
|
|
10230
|
+
function normalizeTouchedRepo(value, repoRoot) {
|
|
10231
|
+
if (!value || typeof value !== "object") return null;
|
|
10232
|
+
const parsed = value;
|
|
10233
|
+
const normalizedRepoRoot = normalizeString(parsed.repoRoot) ?? repoRoot.trim();
|
|
10234
|
+
if (!normalizedRepoRoot) return null;
|
|
10235
|
+
return {
|
|
10236
|
+
repoRoot: normalizedRepoRoot,
|
|
10237
|
+
projectId: normalizeString(parsed.projectId),
|
|
10238
|
+
currentAppId: normalizeString(parsed.currentAppId),
|
|
10239
|
+
upstreamAppId: normalizeString(parsed.upstreamAppId),
|
|
10240
|
+
firstTouchedAt: normalizeString(parsed.firstTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10241
|
+
lastTouchedAt: normalizeString(parsed.lastTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
10242
|
+
lastObservedWriteAt: normalizeString(parsed.lastObservedWriteAt),
|
|
10243
|
+
touchedBy: normalizeStringArray(parsed.touchedBy),
|
|
10244
|
+
hasObservedWrite: Boolean(parsed.hasObservedWrite),
|
|
10245
|
+
manuallyRecorded: Boolean(parsed.manuallyRecorded),
|
|
10246
|
+
manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),
|
|
10247
|
+
manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),
|
|
10248
|
+
manualRecordingScope: normalizeManualRecordingScope(parsed.manualRecordingScope),
|
|
10249
|
+
manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),
|
|
10250
|
+
stopAttempted: Boolean(parsed.stopAttempted),
|
|
10251
|
+
stopRecorded: Boolean(parsed.stopRecorded),
|
|
10252
|
+
stopRecordedAt: normalizeString(parsed.stopRecordedAt),
|
|
10253
|
+
stopRecordedMode: parsed.stopRecordedMode === "changed_turn" || parsed.stopRecordedMode === "no_diff_turn" ? parsed.stopRecordedMode : null,
|
|
10254
|
+
recordingFailureMessage: normalizeString(parsed.recordingFailureMessage),
|
|
10255
|
+
recordingFailureHint: normalizeString(parsed.recordingFailureHint),
|
|
10256
|
+
recordingFailedAt: normalizeString(parsed.recordingFailedAt)
|
|
10257
|
+
};
|
|
10258
|
+
}
|
|
10259
|
+
function normalizeTouchedRepos(value) {
|
|
10260
|
+
if (!value || typeof value !== "object") return {};
|
|
10261
|
+
const entries = Object.entries(value).map(([repoRoot, repo]) => normalizeTouchedRepo(repo, repoRoot)).filter((repo) => repo !== null).sort((a2, b) => a2.repoRoot.localeCompare(b.repoRoot));
|
|
10262
|
+
return Object.fromEntries(entries.map((repo) => [repo.repoRoot, repo]));
|
|
10263
|
+
}
|
|
10264
|
+
function createTouchedRepo(params) {
|
|
10265
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
10266
|
+
const touchedBy = params.touchedBy?.trim() ? [params.touchedBy.trim()] : [];
|
|
10267
|
+
return {
|
|
10268
|
+
repoRoot: params.repoRoot,
|
|
10269
|
+
projectId: normalizeString(params.projectId),
|
|
10270
|
+
currentAppId: normalizeString(params.currentAppId),
|
|
10271
|
+
upstreamAppId: normalizeString(params.upstreamAppId),
|
|
10272
|
+
firstTouchedAt: now,
|
|
10273
|
+
lastTouchedAt: now,
|
|
10274
|
+
lastObservedWriteAt: params.hasObservedWrite ? now : null,
|
|
10275
|
+
touchedBy,
|
|
10276
|
+
hasObservedWrite: Boolean(params.hasObservedWrite),
|
|
10277
|
+
manuallyRecorded: false,
|
|
10278
|
+
manuallyRecordedAt: null,
|
|
10279
|
+
manuallyRecordedByTool: null,
|
|
10280
|
+
manualRecordingScope: null,
|
|
10281
|
+
manualRemoteChangeRecordedAt: null,
|
|
10282
|
+
stopAttempted: false,
|
|
10283
|
+
stopRecorded: false,
|
|
10284
|
+
stopRecordedAt: null,
|
|
10285
|
+
stopRecordedMode: null,
|
|
10286
|
+
recordingFailureMessage: null,
|
|
10287
|
+
recordingFailureHint: null,
|
|
10288
|
+
recordingFailedAt: null
|
|
10289
|
+
};
|
|
10290
|
+
}
|
|
10291
|
+
async function updatePendingTurnState(sessionId, updater) {
|
|
10292
|
+
return withStateLock(sessionId, async () => {
|
|
10293
|
+
const existing = await loadPendingTurnState(sessionId);
|
|
10294
|
+
if (!existing) return null;
|
|
10295
|
+
const result = updater(existing);
|
|
10296
|
+
if (result === false) return existing;
|
|
10297
|
+
await savePendingTurnState(existing);
|
|
10298
|
+
return existing;
|
|
10299
|
+
});
|
|
10300
|
+
}
|
|
10301
|
+
async function loadPendingTurnState(sessionId) {
|
|
10302
|
+
const raw = await import_promises19.default.readFile(statePath(sessionId), "utf8").catch(() => null);
|
|
10303
|
+
if (!raw) return null;
|
|
10304
|
+
try {
|
|
10305
|
+
const parsed = JSON.parse(raw);
|
|
10306
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
10307
|
+
if (typeof parsed.sessionId !== "string" || typeof parsed.turnId !== "string" || typeof parsed.prompt !== "string") {
|
|
10308
|
+
return null;
|
|
10087
10309
|
}
|
|
10088
|
-
|
|
10089
|
-
|
|
10090
|
-
|
|
10091
|
-
|
|
10092
|
-
|
|
10093
|
-
|
|
10094
|
-
|
|
10095
|
-
|
|
10096
|
-
|
|
10097
|
-
|
|
10310
|
+
return {
|
|
10311
|
+
sessionId: parsed.sessionId,
|
|
10312
|
+
turnId: parsed.turnId,
|
|
10313
|
+
prompt: parsed.prompt,
|
|
10314
|
+
initialCwd: normalizeString(parsed.initialCwd),
|
|
10315
|
+
intent: normalizeIntent(parsed.intent),
|
|
10316
|
+
submittedAt: typeof parsed.submittedAt === "string" ? parsed.submittedAt : (/* @__PURE__ */ new Date()).toISOString(),
|
|
10317
|
+
consultedMemory: Boolean(parsed.consultedMemory),
|
|
10318
|
+
touchedRepos: normalizeTouchedRepos(parsed.touchedRepos),
|
|
10319
|
+
turnFailureMessage: normalizeString(parsed.turnFailureMessage),
|
|
10320
|
+
turnFailureHint: normalizeString(parsed.turnFailureHint),
|
|
10321
|
+
turnFailedAt: normalizeString(parsed.turnFailedAt)
|
|
10322
|
+
};
|
|
10323
|
+
} catch {
|
|
10324
|
+
return null;
|
|
10325
|
+
}
|
|
10326
|
+
}
|
|
10327
|
+
async function savePendingTurnState(state) {
|
|
10328
|
+
await writeJsonAtomic2(statePath(state.sessionId), state);
|
|
10329
|
+
}
|
|
10330
|
+
async function upsertTouchedRepo(sessionId, params) {
|
|
10331
|
+
const normalizedRepoRoot = params.repoRoot.trim();
|
|
10332
|
+
if (!normalizedRepoRoot) return null;
|
|
10333
|
+
const state = await updatePendingTurnState(sessionId, (existing) => {
|
|
10334
|
+
const current = existing.touchedRepos[normalizedRepoRoot] ?? createTouchedRepo({
|
|
10335
|
+
repoRoot: normalizedRepoRoot,
|
|
10336
|
+
projectId: params.projectId,
|
|
10337
|
+
currentAppId: params.currentAppId,
|
|
10338
|
+
upstreamAppId: params.upstreamAppId,
|
|
10339
|
+
touchedBy: params.touchedBy,
|
|
10340
|
+
hasObservedWrite: params.hasObservedWrite
|
|
10098
10341
|
});
|
|
10099
|
-
|
|
10100
|
-
|
|
10101
|
-
|
|
10102
|
-
|
|
10342
|
+
current.projectId = normalizeString(params.projectId) ?? current.projectId;
|
|
10343
|
+
current.currentAppId = normalizeString(params.currentAppId) ?? current.currentAppId;
|
|
10344
|
+
current.upstreamAppId = normalizeString(params.upstreamAppId) ?? current.upstreamAppId;
|
|
10345
|
+
current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10346
|
+
if (params.touchedBy?.trim() && !current.touchedBy.includes(params.touchedBy.trim())) {
|
|
10347
|
+
current.touchedBy = [...current.touchedBy, params.touchedBy.trim()].sort((a2, b) => a2.localeCompare(b));
|
|
10103
10348
|
}
|
|
10104
|
-
if (
|
|
10105
|
-
|
|
10106
|
-
|
|
10107
|
-
throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
|
|
10349
|
+
if (params.hasObservedWrite) {
|
|
10350
|
+
current.hasObservedWrite = true;
|
|
10351
|
+
current.lastObservedWriteAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10108
10352
|
}
|
|
10109
|
-
|
|
10110
|
-
|
|
10111
|
-
|
|
10112
|
-
|
|
10113
|
-
|
|
10114
|
-
|
|
10115
|
-
|
|
10353
|
+
existing.touchedRepos[normalizedRepoRoot] = current;
|
|
10354
|
+
});
|
|
10355
|
+
return state?.touchedRepos[normalizedRepoRoot] ?? null;
|
|
10356
|
+
}
|
|
10357
|
+
async function markTouchedRepoStopAttempted(sessionId, repoRoot) {
|
|
10358
|
+
await updatePendingTurnState(sessionId, (existing) => {
|
|
10359
|
+
const current = existing.touchedRepos[repoRoot];
|
|
10360
|
+
if (!current) return false;
|
|
10361
|
+
current.stopAttempted = true;
|
|
10362
|
+
current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10363
|
+
});
|
|
10364
|
+
}
|
|
10365
|
+
async function markTouchedRepoStopRecorded(sessionId, repoRoot, params) {
|
|
10366
|
+
await updatePendingTurnState(sessionId, (existing) => {
|
|
10367
|
+
const current = existing.touchedRepos[repoRoot];
|
|
10368
|
+
if (!current) return false;
|
|
10369
|
+
current.stopAttempted = true;
|
|
10370
|
+
current.stopRecorded = true;
|
|
10371
|
+
current.stopRecordedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10372
|
+
current.stopRecordedMode = params.mode;
|
|
10373
|
+
current.recordingFailureMessage = null;
|
|
10374
|
+
current.recordingFailureHint = null;
|
|
10375
|
+
current.recordingFailedAt = null;
|
|
10376
|
+
current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10377
|
+
});
|
|
10378
|
+
}
|
|
10379
|
+
async function markTouchedRepoRecordingFailure(sessionId, repoRoot, params) {
|
|
10380
|
+
await updatePendingTurnState(sessionId, (existing) => {
|
|
10381
|
+
const current = existing.touchedRepos[repoRoot];
|
|
10382
|
+
if (!current) return false;
|
|
10383
|
+
current.stopAttempted = true;
|
|
10384
|
+
current.recordingFailureMessage = params.message.trim();
|
|
10385
|
+
current.recordingFailureHint = params.hint?.trim() || null;
|
|
10386
|
+
current.recordingFailedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10387
|
+
current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10388
|
+
});
|
|
10389
|
+
}
|
|
10390
|
+
function lastFinalizedPath(sessionId) {
|
|
10391
|
+
return import_node_path7.default.join(stateRoot(), `${sessionId}.last-finalized.json`);
|
|
10392
|
+
}
|
|
10393
|
+
async function markLastFinalizedTurn(sessionId, turnId, prompt) {
|
|
10394
|
+
const record = {
|
|
10395
|
+
sessionId,
|
|
10396
|
+
turnId,
|
|
10397
|
+
prompt,
|
|
10398
|
+
finalizedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
10399
|
+
};
|
|
10400
|
+
await writeJsonAtomic2(lastFinalizedPath(sessionId), record);
|
|
10401
|
+
}
|
|
10402
|
+
async function loadLastFinalizedTurn(sessionId) {
|
|
10403
|
+
const raw = await import_promises19.default.readFile(lastFinalizedPath(sessionId), "utf8").catch(() => null);
|
|
10404
|
+
if (!raw) return null;
|
|
10405
|
+
try {
|
|
10406
|
+
const parsed = JSON.parse(raw);
|
|
10407
|
+
if (typeof parsed.sessionId === "string" && typeof parsed.turnId === "string" && typeof parsed.prompt === "string" && typeof parsed.finalizedAt === "string") {
|
|
10408
|
+
return {
|
|
10409
|
+
sessionId: parsed.sessionId,
|
|
10410
|
+
turnId: parsed.turnId,
|
|
10411
|
+
prompt: parsed.prompt,
|
|
10412
|
+
finalizedAt: parsed.finalizedAt
|
|
10413
|
+
};
|
|
10414
|
+
}
|
|
10415
|
+
return null;
|
|
10416
|
+
} catch {
|
|
10417
|
+
return null;
|
|
10116
10418
|
}
|
|
10117
|
-
|
|
10118
|
-
|
|
10119
|
-
|
|
10120
|
-
|
|
10121
|
-
|
|
10122
|
-
|
|
10123
|
-
|
|
10124
|
-
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
|
|
10128
|
-
|
|
10129
|
-
|
|
10130
|
-
|
|
10131
|
-
|
|
10132
|
-
|
|
10133
|
-
|
|
10134
|
-
|
|
10135
|
-
|
|
10136
|
-
|
|
10137
|
-
|
|
10138
|
-
|
|
10139
|
-
|
|
10140
|
-
|
|
10141
|
-
|
|
10142
|
-
|
|
10143
|
-
|
|
10144
|
-
|
|
10145
|
-
|
|
10146
|
-
|
|
10147
|
-
|
|
10148
|
-
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
|
|
10155
|
-
|
|
10156
|
-
|
|
10157
|
-
|
|
10158
|
-
|
|
10159
|
-
|
|
10160
|
-
|
|
10161
|
-
|
|
10162
|
-
|
|
10163
|
-
|
|
10164
|
-
|
|
10165
|
-
|
|
10166
|
-
|
|
10167
|
-
|
|
10168
|
-
|
|
10169
|
-
|
|
10170
|
-
|
|
10171
|
-
|
|
10172
|
-
|
|
10173
|
-
|
|
10174
|
-
|
|
10175
|
-
|
|
10176
|
-
|
|
10177
|
-
|
|
10178
|
-
|
|
10179
|
-
|
|
10180
|
-
|
|
10181
|
-
|
|
10182
|
-
|
|
10183
|
-
|
|
10184
|
-
|
|
10185
|
-
|
|
10186
|
-
|
|
10187
|
-
|
|
10188
|
-
|
|
10189
|
-
|
|
10190
|
-
|
|
10191
|
-
|
|
10192
|
-
|
|
10193
|
-
|
|
10194
|
-
|
|
10195
|
-
|
|
10196
|
-
|
|
10197
|
-
|
|
10198
|
-
|
|
10199
|
-
|
|
10200
|
-
|
|
10201
|
-
|
|
10202
|
-
|
|
10203
|
-
|
|
10204
|
-
|
|
10205
|
-
|
|
10206
|
-
|
|
10207
|
-
|
|
10208
|
-
|
|
10209
|
-
|
|
10210
|
-
|
|
10211
|
-
|
|
10212
|
-
|
|
10213
|
-
|
|
10214
|
-
|
|
10215
|
-
|
|
10216
|
-
|
|
10217
|
-
|
|
10218
|
-
|
|
10219
|
-
|
|
10220
|
-
|
|
10221
|
-
|
|
10222
|
-
|
|
10223
|
-
|
|
10224
|
-
|
|
10225
|
-
|
|
10226
|
-
|
|
10227
|
-
|
|
10228
|
-
|
|
10229
|
-
|
|
10419
|
+
}
|
|
10420
|
+
async function markPendingTurnFailure(sessionId, params) {
|
|
10421
|
+
await updatePendingTurnState(sessionId, (existing) => {
|
|
10422
|
+
existing.turnFailureMessage = params.message.trim();
|
|
10423
|
+
existing.turnFailureHint = params.hint?.trim() || null;
|
|
10424
|
+
existing.turnFailedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10425
|
+
});
|
|
10426
|
+
}
|
|
10427
|
+
async function listTouchedRepos(sessionId) {
|
|
10428
|
+
const existing = await loadPendingTurnState(sessionId);
|
|
10429
|
+
if (!existing) return [];
|
|
10430
|
+
return Object.values(existing.touchedRepos).sort((a2, b) => a2.repoRoot.localeCompare(b.repoRoot));
|
|
10431
|
+
}
|
|
10432
|
+
async function clearPendingTurnState(sessionId) {
|
|
10433
|
+
await withStateLock(sessionId, async () => {
|
|
10434
|
+
await import_promises19.default.rm(statePath(sessionId), { force: true }).catch(() => void 0);
|
|
10435
|
+
});
|
|
10436
|
+
}
|
|
10437
|
+
|
|
10438
|
+
// package.json
|
|
10439
|
+
var package_default = {
|
|
10440
|
+
name: "@remixhq/claude-plugin",
|
|
10441
|
+
version: "0.1.22",
|
|
10442
|
+
description: "Claude Code plugin for Remix collaboration workflows",
|
|
10443
|
+
homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
|
|
10444
|
+
license: "MIT",
|
|
10445
|
+
repository: {
|
|
10446
|
+
type: "git",
|
|
10447
|
+
url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
|
|
10448
|
+
},
|
|
10449
|
+
type: "module",
|
|
10450
|
+
engines: {
|
|
10451
|
+
node: ">=20"
|
|
10452
|
+
},
|
|
10453
|
+
publishConfig: {
|
|
10454
|
+
access: "public"
|
|
10455
|
+
},
|
|
10456
|
+
files: [
|
|
10457
|
+
"dist",
|
|
10458
|
+
".claude-plugin/plugin.json",
|
|
10459
|
+
".mcp.json",
|
|
10460
|
+
"skills",
|
|
10461
|
+
"hooks",
|
|
10462
|
+
"agents"
|
|
10463
|
+
],
|
|
10464
|
+
exports: {
|
|
10465
|
+
".": {
|
|
10466
|
+
types: "./dist/index.d.ts",
|
|
10467
|
+
import: "./dist/index.js"
|
|
10468
|
+
}
|
|
10469
|
+
},
|
|
10470
|
+
scripts: {
|
|
10471
|
+
build: "tsup",
|
|
10472
|
+
postbuild: `node -e "const fs=require('node:fs'); for (const p of ['dist/mcp-server.cjs','dist/hook-pre-git.cjs','dist/hook-user-prompt.cjs','dist/hook-post-collab.cjs','dist/hook-stop-collab.cjs']) fs.chmodSync(p, 0o755);"`,
|
|
10473
|
+
dev: "tsx src/mcp-server.ts",
|
|
10474
|
+
typecheck: "tsc -p tsconfig.json --noEmit",
|
|
10475
|
+
test: "node --import tsx --test 'src/**/*.test.ts'",
|
|
10476
|
+
prepack: "npm run build"
|
|
10477
|
+
},
|
|
10478
|
+
dependencies: {
|
|
10479
|
+
"@remixhq/core": "^0.1.17",
|
|
10480
|
+
"@remixhq/mcp": "^0.1.17"
|
|
10481
|
+
},
|
|
10482
|
+
devDependencies: {
|
|
10483
|
+
"@types/node": "^25.4.0",
|
|
10484
|
+
tsup: "^8.5.1",
|
|
10485
|
+
tsx: "^4.21.0",
|
|
10486
|
+
typescript: "^5.9.3"
|
|
10487
|
+
}
|
|
10488
|
+
};
|
|
10489
|
+
|
|
10490
|
+
// src/metadata.ts
|
|
10491
|
+
var pluginMetadata = {
|
|
10492
|
+
name: package_default.name,
|
|
10493
|
+
version: package_default.version,
|
|
10494
|
+
description: package_default.description,
|
|
10495
|
+
pluginId: "remix",
|
|
10496
|
+
agentName: "remix-collab"
|
|
10497
|
+
};
|
|
10498
|
+
|
|
10499
|
+
// src/hook-diagnostics.ts
|
|
10500
|
+
var MAX_LOG_BYTES = 512 * 1024;
|
|
10501
|
+
function resolveClaudeRoot() {
|
|
10502
|
+
const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
10503
|
+
return configured || import_node_path8.default.join(import_node_os5.default.homedir(), ".claude");
|
|
10504
|
+
}
|
|
10505
|
+
function resolvePluginDataDirName() {
|
|
10506
|
+
return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
|
|
10507
|
+
}
|
|
10508
|
+
function getHookDiagnosticsDirPath() {
|
|
10509
|
+
const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
|
|
10510
|
+
return configured || import_node_path8.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
|
|
10511
|
+
}
|
|
10512
|
+
function getHookDiagnosticsLogPath() {
|
|
10513
|
+
return import_node_path8.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
|
|
10514
|
+
}
|
|
10515
|
+
function toFieldValue(value) {
|
|
10516
|
+
if (value === null) return null;
|
|
10517
|
+
if (typeof value === "string") return value;
|
|
10518
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
10519
|
+
if (typeof value === "boolean") return value;
|
|
10520
|
+
return void 0;
|
|
10521
|
+
}
|
|
10522
|
+
function normalizeFields(fields) {
|
|
10523
|
+
if (!fields) return {};
|
|
10524
|
+
const normalizedEntries = Object.entries(fields).map(([key, value]) => {
|
|
10525
|
+
const normalized = toFieldValue(value);
|
|
10526
|
+
return normalized === void 0 ? null : [key, normalized];
|
|
10527
|
+
}).filter((entry) => entry !== null);
|
|
10528
|
+
return Object.fromEntries(normalizedEntries);
|
|
10529
|
+
}
|
|
10530
|
+
async function rotateLogIfNeeded(logPath) {
|
|
10531
|
+
const stat = await import_promises20.default.stat(logPath).catch(() => null);
|
|
10532
|
+
if (!stat || stat.size < MAX_LOG_BYTES) {
|
|
10533
|
+
return;
|
|
10534
|
+
}
|
|
10535
|
+
const rotatedPath = `${logPath}.1`;
|
|
10536
|
+
await import_promises20.default.rm(rotatedPath, { force: true }).catch(() => void 0);
|
|
10537
|
+
await import_promises20.default.rename(logPath, rotatedPath).catch(() => void 0);
|
|
10538
|
+
}
|
|
10539
|
+
function summarizeText(value) {
|
|
10540
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
10541
|
+
return {
|
|
10542
|
+
present: false,
|
|
10543
|
+
length: 0,
|
|
10544
|
+
sha256Prefix: null
|
|
10545
|
+
};
|
|
10546
|
+
}
|
|
10547
|
+
const trimmed = value.trim();
|
|
10548
|
+
return {
|
|
10549
|
+
present: true,
|
|
10550
|
+
length: trimmed.length,
|
|
10551
|
+
sha256Prefix: (0, import_node_crypto2.createHash)("sha256").update(trimmed).digest("hex").slice(0, 12)
|
|
10552
|
+
};
|
|
10553
|
+
}
|
|
10554
|
+
async function appendHookDiagnosticsEvent(params) {
|
|
10555
|
+
try {
|
|
10556
|
+
const logPath = getHookDiagnosticsLogPath();
|
|
10557
|
+
await import_promises20.default.mkdir(import_node_path8.default.dirname(logPath), { recursive: true });
|
|
10558
|
+
await rotateLogIfNeeded(logPath);
|
|
10559
|
+
const event = {
|
|
10560
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10561
|
+
hook: params.hook,
|
|
10562
|
+
pluginVersion: pluginMetadata.version,
|
|
10563
|
+
pid: process.pid,
|
|
10564
|
+
sessionId: params.sessionId?.trim() || null,
|
|
10565
|
+
turnId: params.turnId?.trim() || null,
|
|
10566
|
+
stage: params.stage.trim(),
|
|
10567
|
+
result: params.result,
|
|
10568
|
+
reason: params.reason?.trim() || null,
|
|
10569
|
+
toolName: params.toolName?.trim() || null,
|
|
10570
|
+
repoRoot: params.repoRoot?.trim() || null,
|
|
10571
|
+
message: params.message?.trim() || null,
|
|
10572
|
+
fields: normalizeFields(params.fields)
|
|
10573
|
+
};
|
|
10574
|
+
await import_promises20.default.appendFile(logPath, `${JSON.stringify(event)}
|
|
10575
|
+
`, "utf8");
|
|
10576
|
+
} catch {
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
|
|
10580
|
+
// src/auto-fix-dispatcher.ts
|
|
10581
|
+
var AUTO_FIX_COMMAND = {
|
|
10582
|
+
// Already auto-spawned by hook-user-prompt's branch-init path, but we
|
|
10583
|
+
// include it here too so a finalize-time failure (e.g. binding got
|
|
10584
|
+
// deleted between init and the next finalize) also self-heals.
|
|
10585
|
+
branch_binding_missing: ["collab", "init"],
|
|
10586
|
+
// External base diverged. Re-anchor declares the new local state as
|
|
10587
|
+
// truth. Risky if the user hard-reset to the wrong commit, but the
|
|
10588
|
+
// worst case is "Remix forgets the previous anchor" — recorded turns
|
|
10589
|
+
// are preserved on the server, so this is recoverable.
|
|
10590
|
+
re_anchor_required: ["collab", "re-anchor"],
|
|
10591
|
+
// Server moved ahead. `collab sync` is fast-forward-safe by default;
|
|
10592
|
+
// it refuses non-FF on its own, so we don't need to gate here.
|
|
10593
|
+
pull_required: ["collab", "sync"]
|
|
10594
|
+
};
|
|
10595
|
+
function isAutoFixableFinalizeFailureCode(code) {
|
|
10596
|
+
return Boolean(code && AUTO_FIX_COMMAND[code]);
|
|
10597
|
+
}
|
|
10598
|
+
var RECOMMENDED_USER_COMMAND = {
|
|
10599
|
+
not_bound: "remix collab init",
|
|
10600
|
+
branch_binding_missing: "remix collab init",
|
|
10601
|
+
family_ambiguous: "remix collab status",
|
|
10602
|
+
metadata_conflict: "remix collab status",
|
|
10603
|
+
branch_mismatch: "remix collab status",
|
|
10604
|
+
missing_head: "remix collab status",
|
|
10605
|
+
remote_error: "remix collab status",
|
|
10606
|
+
pull_required: "remix collab sync",
|
|
10607
|
+
re_anchor_required: "remix collab re-anchor"
|
|
10608
|
+
};
|
|
10609
|
+
var SPAWN_LOCK_REL = (cmdSlug) => import_node_path9.default.join(".remix", `.${cmdSlug}-spawning`);
|
|
10610
|
+
var SPAWN_LOG_REL = (cmdSlug) => import_node_path9.default.join(".remix", `${cmdSlug}.log`);
|
|
10611
|
+
var SPAWN_THROTTLE_MS = 5 * 60 * 1e3;
|
|
10612
|
+
function commandSlug(args) {
|
|
10613
|
+
return args.join("-").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
10614
|
+
}
|
|
10615
|
+
function spawnFixDetached(repoRoot, args) {
|
|
10616
|
+
const slug = commandSlug(args);
|
|
10617
|
+
const command = `remix ${args.join(" ")}`;
|
|
10618
|
+
const remixDir = import_node_path9.default.join(repoRoot, ".remix");
|
|
10619
|
+
const lockPath = import_node_path9.default.join(repoRoot, SPAWN_LOCK_REL(slug));
|
|
10620
|
+
const logPath = import_node_path9.default.join(repoRoot, SPAWN_LOG_REL(slug));
|
|
10621
|
+
try {
|
|
10622
|
+
if ((0, import_node_fs6.existsSync)(lockPath)) {
|
|
10623
|
+
const ageMs = Date.now() - (0, import_node_fs6.statSync)(lockPath).mtimeMs;
|
|
10624
|
+
if (ageMs < SPAWN_THROTTLE_MS) {
|
|
10625
|
+
return { kind: "spawn_throttled", command, reason: "spawn_lock_held" };
|
|
10626
|
+
}
|
|
10627
|
+
}
|
|
10628
|
+
} catch {
|
|
10629
|
+
}
|
|
10630
|
+
try {
|
|
10631
|
+
(0, import_node_fs6.mkdirSync)(remixDir, { recursive: true });
|
|
10632
|
+
} catch {
|
|
10633
|
+
}
|
|
10634
|
+
let out;
|
|
10635
|
+
let err;
|
|
10636
|
+
try {
|
|
10637
|
+
out = (0, import_node_fs6.openSync)(logPath, "a");
|
|
10638
|
+
err = (0, import_node_fs6.openSync)(logPath, "a");
|
|
10639
|
+
} catch (logErr) {
|
|
10640
|
+
return {
|
|
10641
|
+
kind: "spawn_failed",
|
|
10642
|
+
command,
|
|
10643
|
+
reason: "log_open_failed",
|
|
10644
|
+
message: logErr instanceof Error ? logErr.message : String(logErr)
|
|
10645
|
+
};
|
|
10646
|
+
}
|
|
10647
|
+
try {
|
|
10648
|
+
const child = (0, import_node_child_process6.spawn)("remix", [...args], {
|
|
10649
|
+
cwd: repoRoot,
|
|
10650
|
+
detached: true,
|
|
10651
|
+
stdio: ["ignore", out, err],
|
|
10652
|
+
env: { ...process.env, REMIX_AUTO_FIX_SPAWN: "1" }
|
|
10653
|
+
});
|
|
10654
|
+
child.unref();
|
|
10655
|
+
try {
|
|
10656
|
+
(0, import_node_fs6.writeFileSync)(lockPath, String(child.pid ?? ""), "utf8");
|
|
10657
|
+
(0, import_node_fs6.utimesSync)(lockPath, /* @__PURE__ */ new Date(), /* @__PURE__ */ new Date());
|
|
10658
|
+
} catch {
|
|
10659
|
+
}
|
|
10660
|
+
return { kind: "spawned", command, pid: child.pid, logPath };
|
|
10661
|
+
} catch (spawnErr) {
|
|
10662
|
+
return {
|
|
10663
|
+
kind: "spawn_failed",
|
|
10664
|
+
command,
|
|
10665
|
+
reason: "spawn_failed",
|
|
10666
|
+
message: spawnErr instanceof Error ? spawnErr.message : String(spawnErr)
|
|
10667
|
+
};
|
|
10668
|
+
}
|
|
10669
|
+
}
|
|
10670
|
+
async function dispatchFinalizeFailure(input) {
|
|
10671
|
+
const recommendedCommand = input.preflightCode ? RECOMMENDED_USER_COMMAND[input.preflightCode] ?? null : null;
|
|
10672
|
+
const marker = buildFreshFailureMarker({
|
|
10673
|
+
repoRoot: input.repoRoot,
|
|
10674
|
+
preflightCode: input.preflightCode,
|
|
10675
|
+
message: input.message,
|
|
10676
|
+
hint: input.hint,
|
|
10677
|
+
recommendedCommand
|
|
10678
|
+
});
|
|
10679
|
+
let outcome;
|
|
10680
|
+
const autoFixArgs = input.preflightCode ? AUTO_FIX_COMMAND[input.preflightCode] : void 0;
|
|
10681
|
+
if (!autoFixArgs) {
|
|
10682
|
+
outcome = {
|
|
10683
|
+
kind: "warn_only",
|
|
10684
|
+
reason: input.preflightCode ? "no_auto_fix_for_code" : "unknown_code"
|
|
10685
|
+
};
|
|
10686
|
+
} else {
|
|
10687
|
+
outcome = spawnFixDetached(input.repoRoot, autoFixArgs);
|
|
10688
|
+
marker.autoFix = mergeOutcomeIntoMarker(marker.autoFix, outcome);
|
|
10689
|
+
}
|
|
10690
|
+
try {
|
|
10691
|
+
await writeFinalizeFailureMarker(marker);
|
|
10692
|
+
} catch (writeErr) {
|
|
10693
|
+
await appendHookDiagnosticsEvent({
|
|
10694
|
+
hook: input.hook,
|
|
10695
|
+
sessionId: input.sessionId,
|
|
10696
|
+
turnId: input.turnId ?? void 0,
|
|
10697
|
+
stage: "finalize_failure_marker_write_failed",
|
|
10698
|
+
result: "error",
|
|
10699
|
+
reason: "exception",
|
|
10700
|
+
repoRoot: input.repoRoot,
|
|
10701
|
+
message: writeErr instanceof Error ? writeErr.message : String(writeErr)
|
|
10702
|
+
});
|
|
10703
|
+
}
|
|
10704
|
+
await appendHookDiagnosticsEvent({
|
|
10705
|
+
hook: input.hook,
|
|
10706
|
+
sessionId: input.sessionId,
|
|
10707
|
+
turnId: input.turnId ?? void 0,
|
|
10708
|
+
stage: "auto_fix_dispatched",
|
|
10709
|
+
result: outcome.kind === "spawned" ? "success" : outcome.kind === "warn_only" ? "info" : "error",
|
|
10710
|
+
reason: outcome.kind,
|
|
10711
|
+
repoRoot: input.repoRoot,
|
|
10712
|
+
fields: {
|
|
10713
|
+
preflightCode: input.preflightCode,
|
|
10714
|
+
command: "command" in outcome ? outcome.command : null,
|
|
10715
|
+
pid: outcome.kind === "spawned" ? outcome.pid ?? null : null,
|
|
10716
|
+
logPath: outcome.kind === "spawned" ? outcome.logPath : null,
|
|
10717
|
+
recommendedCommand
|
|
10718
|
+
},
|
|
10719
|
+
message: outcome.kind === "spawn_failed" ? outcome.message : null
|
|
10720
|
+
});
|
|
10721
|
+
return outcome;
|
|
10722
|
+
}
|
|
10723
|
+
function mergeOutcomeIntoMarker(existing, outcome) {
|
|
10724
|
+
if (outcome.kind === "spawned") {
|
|
10725
|
+
return {
|
|
10726
|
+
status: "in_progress",
|
|
10727
|
+
command: outcome.command,
|
|
10728
|
+
pid: outcome.pid ?? null,
|
|
10729
|
+
logPath: outcome.logPath,
|
|
10730
|
+
attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10731
|
+
failureMessage: null
|
|
10732
|
+
};
|
|
10733
|
+
}
|
|
10734
|
+
if (outcome.kind === "spawn_throttled") {
|
|
10735
|
+
return {
|
|
10736
|
+
status: "in_progress",
|
|
10737
|
+
command: outcome.command,
|
|
10738
|
+
pid: existing.pid,
|
|
10739
|
+
logPath: existing.logPath,
|
|
10740
|
+
attemptedAt: existing.attemptedAt,
|
|
10741
|
+
failureMessage: null
|
|
10742
|
+
};
|
|
10743
|
+
}
|
|
10744
|
+
if (outcome.kind === "spawn_failed") {
|
|
10745
|
+
return {
|
|
10746
|
+
status: "spawn_failed",
|
|
10747
|
+
command: outcome.command,
|
|
10748
|
+
pid: null,
|
|
10749
|
+
logPath: null,
|
|
10750
|
+
attemptedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10751
|
+
failureMessage: outcome.message
|
|
10752
|
+
};
|
|
10753
|
+
}
|
|
10754
|
+
return existing;
|
|
10755
|
+
}
|
|
10756
|
+
|
|
10757
|
+
// src/deferred-turn-queue.ts
|
|
10758
|
+
var import_promises21 = __toESM(require("fs/promises"), 1);
|
|
10759
|
+
var import_node_os6 = __toESM(require("os"), 1);
|
|
10760
|
+
var import_node_path10 = __toESM(require("path"), 1);
|
|
10761
|
+
var DEFERRED_TURN_SCHEMA_VERSION = 1;
|
|
10762
|
+
var DEFERRED_TURN_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
10763
|
+
var DEFERRED_TURN_DIR = "deferred-turns";
|
|
10764
|
+
function stateRoot2() {
|
|
10765
|
+
const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_STATE_ROOT?.trim();
|
|
10766
|
+
return configured || import_node_path10.default.join(import_node_os6.default.tmpdir(), "remix-claude-plugin-hooks");
|
|
10767
|
+
}
|
|
10768
|
+
function getDeferredTurnDirPath() {
|
|
10769
|
+
return import_node_path10.default.join(stateRoot2(), DEFERRED_TURN_DIR);
|
|
10770
|
+
}
|
|
10771
|
+
function deferredTurnFileName(sessionId, turnId) {
|
|
10772
|
+
const safe = (s) => s.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
10773
|
+
return `${safe(sessionId)}-${safe(turnId)}.json`;
|
|
10774
|
+
}
|
|
10775
|
+
function getDeferredTurnFilePath(sessionId, turnId) {
|
|
10776
|
+
return import_node_path10.default.join(getDeferredTurnDirPath(), deferredTurnFileName(sessionId, turnId));
|
|
10777
|
+
}
|
|
10778
|
+
async function writeDeferredTurn(record) {
|
|
10779
|
+
if (record.schemaVersion !== DEFERRED_TURN_SCHEMA_VERSION) {
|
|
10780
|
+
throw new Error(`writeDeferredTurn: unsupported schemaVersion ${record.schemaVersion}`);
|
|
10781
|
+
}
|
|
10782
|
+
if (!record.prompt.trim() || !record.assistantResponse.trim()) {
|
|
10783
|
+
throw new Error("writeDeferredTurn: prompt and assistantResponse must be non-empty");
|
|
10784
|
+
}
|
|
10785
|
+
const dir = getDeferredTurnDirPath();
|
|
10786
|
+
await import_promises21.default.mkdir(dir, { recursive: true });
|
|
10787
|
+
const filePath = getDeferredTurnFilePath(record.sessionId, record.turnId);
|
|
10788
|
+
const tmpPath = `${filePath}.tmp-${process.pid}-${Date.now()}`;
|
|
10789
|
+
await import_promises21.default.writeFile(tmpPath, JSON.stringify(record), "utf8");
|
|
10790
|
+
await import_promises21.default.rename(tmpPath, filePath);
|
|
10791
|
+
return filePath;
|
|
10792
|
+
}
|
|
10793
|
+
async function readDeferredTurnFile(filePath) {
|
|
10794
|
+
const raw = await import_promises21.default.readFile(filePath, "utf8").catch(() => null);
|
|
10795
|
+
if (!raw) return null;
|
|
10796
|
+
let parsed;
|
|
10797
|
+
try {
|
|
10798
|
+
parsed = JSON.parse(raw);
|
|
10799
|
+
} catch {
|
|
10800
|
+
return null;
|
|
10801
|
+
}
|
|
10802
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
10803
|
+
const record = parsed;
|
|
10804
|
+
if (record.schemaVersion !== DEFERRED_TURN_SCHEMA_VERSION) return null;
|
|
10805
|
+
if (typeof record.sessionId !== "string" || typeof record.turnId !== "string" || typeof record.repoRoot !== "string" || typeof record.prompt !== "string" || typeof record.assistantResponse !== "string" || typeof record.submittedAt !== "string" || typeof record.deferredAt !== "string" || record.reason !== "current_branch_unbound" && record.reason !== "recovery_in_progress") {
|
|
10806
|
+
return null;
|
|
10807
|
+
}
|
|
10808
|
+
return {
|
|
10809
|
+
schemaVersion: DEFERRED_TURN_SCHEMA_VERSION,
|
|
10810
|
+
sessionId: record.sessionId,
|
|
10811
|
+
turnId: record.turnId,
|
|
10812
|
+
repoRoot: record.repoRoot,
|
|
10813
|
+
prompt: record.prompt,
|
|
10814
|
+
assistantResponse: record.assistantResponse,
|
|
10815
|
+
submittedAt: record.submittedAt,
|
|
10816
|
+
deferredAt: record.deferredAt,
|
|
10817
|
+
reason: record.reason,
|
|
10818
|
+
branchAtDefer: typeof record.branchAtDefer === "string" || record.branchAtDefer === null ? record.branchAtDefer : null
|
|
10819
|
+
};
|
|
10820
|
+
}
|
|
10821
|
+
async function listDeferredTurnsForRepo(repoRoot) {
|
|
10822
|
+
const dir = getDeferredTurnDirPath();
|
|
10823
|
+
const dirEntries = await import_promises21.default.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
10824
|
+
const entries = [];
|
|
10825
|
+
for (const entry of dirEntries) {
|
|
10826
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
10827
|
+
const filePath = import_node_path10.default.join(dir, entry.name);
|
|
10828
|
+
const record = await readDeferredTurnFile(filePath);
|
|
10829
|
+
if (!record) continue;
|
|
10830
|
+
if (record.repoRoot !== repoRoot) continue;
|
|
10831
|
+
entries.push({ filePath, record });
|
|
10832
|
+
}
|
|
10833
|
+
entries.sort((a2, b) => {
|
|
10834
|
+
const aMs = Date.parse(a2.record.submittedAt);
|
|
10835
|
+
const bMs = Date.parse(b.record.submittedAt);
|
|
10836
|
+
if (Number.isFinite(aMs) && Number.isFinite(bMs)) return aMs - bMs;
|
|
10837
|
+
return 0;
|
|
10838
|
+
});
|
|
10839
|
+
return entries;
|
|
10840
|
+
}
|
|
10841
|
+
async function deleteDeferredTurnFile(filePath) {
|
|
10842
|
+
await import_promises21.default.rm(filePath, { force: true }).catch(() => void 0);
|
|
10843
|
+
}
|
|
10844
|
+
async function pruneStaleDeferredTurns(maxAgeMs = DEFERRED_TURN_TTL_MS) {
|
|
10845
|
+
const dir = getDeferredTurnDirPath();
|
|
10846
|
+
const dirEntries = await import_promises21.default.readdir(dir, { withFileTypes: true }).catch(() => []);
|
|
10847
|
+
const pruned = [];
|
|
10848
|
+
const now = Date.now();
|
|
10849
|
+
for (const entry of dirEntries) {
|
|
10850
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) continue;
|
|
10851
|
+
const filePath = import_node_path10.default.join(dir, entry.name);
|
|
10852
|
+
const record = await readDeferredTurnFile(filePath);
|
|
10853
|
+
if (!record) {
|
|
10854
|
+
const stat = await import_promises21.default.stat(filePath).catch(() => null);
|
|
10855
|
+
if (stat && now - stat.mtimeMs > maxAgeMs) {
|
|
10856
|
+
await deleteDeferredTurnFile(filePath);
|
|
10857
|
+
pruned.push(filePath);
|
|
10858
|
+
}
|
|
10859
|
+
continue;
|
|
10860
|
+
}
|
|
10861
|
+
const deferredAtMs = Date.parse(record.deferredAt);
|
|
10862
|
+
if (!Number.isFinite(deferredAtMs)) continue;
|
|
10863
|
+
if (now - deferredAtMs > maxAgeMs) {
|
|
10864
|
+
await deleteDeferredTurnFile(filePath);
|
|
10865
|
+
pruned.push(filePath);
|
|
10866
|
+
}
|
|
10867
|
+
}
|
|
10868
|
+
return pruned;
|
|
10869
|
+
}
|
|
10870
|
+
function buildDeferredTurnRecord(params) {
|
|
10871
|
+
return {
|
|
10872
|
+
schemaVersion: DEFERRED_TURN_SCHEMA_VERSION,
|
|
10873
|
+
sessionId: params.sessionId,
|
|
10874
|
+
turnId: params.turnId,
|
|
10875
|
+
repoRoot: params.repoRoot,
|
|
10876
|
+
prompt: params.prompt,
|
|
10877
|
+
assistantResponse: params.assistantResponse,
|
|
10878
|
+
submittedAt: params.submittedAt,
|
|
10879
|
+
deferredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
10880
|
+
reason: params.reason ?? "current_branch_unbound",
|
|
10881
|
+
branchAtDefer: params.branchAtDefer
|
|
10882
|
+
};
|
|
10883
|
+
}
|
|
10884
|
+
|
|
10885
|
+
// src/deferred-turn-drainer.ts
|
|
10886
|
+
var import_promises23 = __toESM(require("fs/promises"), 1);
|
|
10887
|
+
var import_node_path11 = __toESM(require("path"), 1);
|
|
10888
|
+
var import_node_crypto3 = require("crypto");
|
|
10889
|
+
|
|
10890
|
+
// node_modules/@remixhq/core/dist/chunk-US5SM7ZC.js
|
|
10891
|
+
async function readJsonSafe(res) {
|
|
10892
|
+
const ct = res.headers.get("content-type") ?? "";
|
|
10893
|
+
if (!ct.toLowerCase().includes("application/json")) return null;
|
|
10894
|
+
try {
|
|
10895
|
+
return await res.json();
|
|
10896
|
+
} catch {
|
|
10897
|
+
return null;
|
|
10898
|
+
}
|
|
10899
|
+
}
|
|
10900
|
+
function createApiClient(config, opts) {
|
|
10901
|
+
const apiKey = (opts?.apiKey ?? "").trim();
|
|
10902
|
+
const tokenProvider = opts?.tokenProvider;
|
|
10903
|
+
const CLIENT_KEY_HEADER = "x-comerge-api-key";
|
|
10904
|
+
async function request(path16, init) {
|
|
10905
|
+
if (!tokenProvider) {
|
|
10906
|
+
throw new RemixError("API client is missing a token provider.", {
|
|
10907
|
+
exitCode: 1,
|
|
10908
|
+
hint: "Configure auth before creating the Remix API client."
|
|
10909
|
+
});
|
|
10910
|
+
}
|
|
10911
|
+
const auth = await tokenProvider();
|
|
10912
|
+
const url = new URL(path16, config.apiUrl).toString();
|
|
10913
|
+
const doFetch = async (bearer) => fetch(url, {
|
|
10914
|
+
...init,
|
|
10915
|
+
headers: {
|
|
10916
|
+
Accept: "application/json",
|
|
10917
|
+
"Content-Type": "application/json",
|
|
10918
|
+
...init?.headers ?? {},
|
|
10919
|
+
Authorization: `Bearer ${bearer}`,
|
|
10920
|
+
...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
|
|
10921
|
+
}
|
|
10922
|
+
});
|
|
10923
|
+
let res = await doFetch(auth.token);
|
|
10924
|
+
if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
|
|
10925
|
+
const refreshed = await tokenProvider({ forceRefresh: true });
|
|
10926
|
+
res = await doFetch(refreshed.token);
|
|
10927
|
+
}
|
|
10928
|
+
if (!res.ok) {
|
|
10929
|
+
const body = await readJsonSafe(res);
|
|
10930
|
+
const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
|
|
10931
|
+
throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
|
|
10932
|
+
}
|
|
10933
|
+
const json = await readJsonSafe(res);
|
|
10934
|
+
return json ?? null;
|
|
10935
|
+
}
|
|
10936
|
+
async function requestBinary(path16, init) {
|
|
10937
|
+
if (!tokenProvider) {
|
|
10938
|
+
throw new RemixError("API client is missing a token provider.", {
|
|
10939
|
+
exitCode: 1,
|
|
10940
|
+
hint: "Configure auth before creating the Remix API client."
|
|
10941
|
+
});
|
|
10942
|
+
}
|
|
10943
|
+
const auth = await tokenProvider();
|
|
10944
|
+
const url = new URL(path16, config.apiUrl).toString();
|
|
10945
|
+
const doFetch = async (bearer) => fetch(url, {
|
|
10946
|
+
...init,
|
|
10947
|
+
headers: {
|
|
10948
|
+
Accept: "*/*",
|
|
10949
|
+
...init?.headers ?? {},
|
|
10950
|
+
Authorization: `Bearer ${bearer}`,
|
|
10951
|
+
...apiKey ? { [CLIENT_KEY_HEADER]: apiKey } : {}
|
|
10952
|
+
}
|
|
10953
|
+
});
|
|
10954
|
+
let res = await doFetch(auth.token);
|
|
10955
|
+
if (res.status === 401 && !auth.fromEnv && auth.session?.refresh_token) {
|
|
10956
|
+
const refreshed = await tokenProvider({ forceRefresh: true });
|
|
10957
|
+
res = await doFetch(refreshed.token);
|
|
10958
|
+
}
|
|
10959
|
+
if (!res.ok) {
|
|
10960
|
+
const body = await readJsonSafe(res);
|
|
10961
|
+
const msg = (body && typeof body === "object" && body && "message" in body && typeof body.message === "string" ? body.message : null) ?? `Request failed (status ${res.status})`;
|
|
10962
|
+
throw new RemixError(msg, { exitCode: 1, hint: body ? JSON.stringify(body, null, 2) : null });
|
|
10963
|
+
}
|
|
10964
|
+
const contentDisposition = res.headers.get("content-disposition") ?? "";
|
|
10965
|
+
const fileNameMatch = contentDisposition.match(/filename=\"([^\"]+)\"/i);
|
|
10966
|
+
return {
|
|
10967
|
+
data: Buffer.from(await res.arrayBuffer()),
|
|
10968
|
+
fileName: fileNameMatch?.[1] ?? null,
|
|
10969
|
+
contentType: res.headers.get("content-type")
|
|
10970
|
+
};
|
|
10971
|
+
}
|
|
10972
|
+
return {
|
|
10973
|
+
getMe: () => request("/v1/me", { method: "GET" }),
|
|
10974
|
+
listOrganizations: () => request("/v1/organizations", { method: "GET" }),
|
|
10975
|
+
getOrganization: (orgId) => request(`/v1/organizations/${encodeURIComponent(orgId)}`, { method: "GET" }),
|
|
10976
|
+
listProjects: (params) => {
|
|
10977
|
+
const qs = new URLSearchParams();
|
|
10978
|
+
if (params?.organizationId) qs.set("organizationId", params.organizationId);
|
|
10979
|
+
if (params?.clientAppId) qs.set("clientAppId", params.clientAppId);
|
|
10980
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
10981
|
+
return request(`/v1/projects${suffix}`, { method: "GET" });
|
|
10982
|
+
},
|
|
10983
|
+
getProject: (projectId) => request(`/v1/projects/${encodeURIComponent(projectId)}`, { method: "GET" }),
|
|
10984
|
+
resolveProjectBinding: (params) => {
|
|
10985
|
+
const qs = new URLSearchParams();
|
|
10986
|
+
if (params.repoFingerprint) qs.set("repoFingerprint", params.repoFingerprint);
|
|
10987
|
+
if (params.remoteUrl) qs.set("remoteUrl", params.remoteUrl);
|
|
10988
|
+
if (params.branchName) qs.set("branchName", params.branchName);
|
|
10989
|
+
return request(`/v1/projects/bindings/resolve?${qs.toString()}`, { method: "GET" });
|
|
10990
|
+
},
|
|
10991
|
+
resolveProjectLaneBinding: (params) => {
|
|
10992
|
+
const qs = new URLSearchParams();
|
|
10993
|
+
if (params.projectId) qs.set("projectId", params.projectId);
|
|
10994
|
+
if (params.repoFingerprint) qs.set("repoFingerprint", params.repoFingerprint);
|
|
10995
|
+
if (params.remoteUrl) qs.set("remoteUrl", params.remoteUrl);
|
|
10996
|
+
if (params.defaultBranch) qs.set("defaultBranch", params.defaultBranch);
|
|
10997
|
+
qs.set("branchName", params.branchName);
|
|
10998
|
+
return request(`/v1/projects/bindings/resolve-lane?${qs.toString()}`, { method: "GET" });
|
|
10999
|
+
},
|
|
11000
|
+
ensureProjectLaneBinding: (payload) => request("/v1/projects/bindings/ensure-lane", { method: "POST", body: JSON.stringify(payload) }),
|
|
11001
|
+
bootstrapFreshProjectLane: (payload) => request("/v1/projects/bindings/bootstrap-fresh-lane", { method: "POST", body: JSON.stringify(payload) }),
|
|
11002
|
+
autoEnableDeveloper: () => request("/v1/developer/auto-enable", { method: "POST" }),
|
|
11003
|
+
listClientApps: (params) => {
|
|
11004
|
+
const qs = params?.orgId ? `?orgId=${encodeURIComponent(params.orgId)}` : "";
|
|
11005
|
+
return request(`/v1/developer/client-apps${qs}`, { method: "GET" });
|
|
11006
|
+
},
|
|
11007
|
+
createClientApp: (payload) => request("/v1/developer/client-apps", { method: "POST", body: JSON.stringify(payload) }),
|
|
11008
|
+
createClientAppKey: (clientAppId, payload) => request(`/v1/developer/client-apps/${encodeURIComponent(clientAppId)}/keys`, {
|
|
11009
|
+
method: "POST",
|
|
11010
|
+
body: JSON.stringify(payload ?? {})
|
|
11011
|
+
}),
|
|
11012
|
+
listApps: (params) => {
|
|
11013
|
+
const qs = new URLSearchParams();
|
|
11014
|
+
if (params?.projectId) qs.set("projectId", params.projectId);
|
|
11015
|
+
if (params?.organizationId) qs.set("organizationId", params.organizationId);
|
|
11016
|
+
if (params?.ownership) qs.set("ownership", params.ownership);
|
|
11017
|
+
if (params?.accessScope) qs.set("accessScope", params.accessScope);
|
|
11018
|
+
if (params?.createdBy) qs.set("createdBy", params.createdBy);
|
|
11019
|
+
if (params?.forked) qs.set("forked", params.forked);
|
|
11020
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
11021
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
11022
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
11023
|
+
return request(`/v1/apps${suffix}`, { method: "GET" });
|
|
11024
|
+
},
|
|
11025
|
+
getApp: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}`, { method: "GET" }),
|
|
11026
|
+
getAppContext: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/context`, { method: "GET" }),
|
|
11027
|
+
getAppOverview: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/overview`, { method: "GET" }),
|
|
11028
|
+
listAppTimeline: (appId, params) => {
|
|
11029
|
+
const qs = new URLSearchParams();
|
|
11030
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
11031
|
+
if (params?.cursor) qs.set("cursor", params.cursor);
|
|
11032
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
11033
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/timeline${suffix}`, { method: "GET" });
|
|
11034
|
+
},
|
|
11035
|
+
getAppTimelineEvent: (appId, eventId) => request(`/v1/apps/${encodeURIComponent(appId)}/timeline/${encodeURIComponent(eventId)}`, { method: "GET" }),
|
|
11036
|
+
listAppEditQueue: (appId, params) => {
|
|
11037
|
+
const qs = new URLSearchParams();
|
|
11038
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
11039
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
11040
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
11041
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/edit-queue${suffix}`, { method: "GET" });
|
|
11042
|
+
},
|
|
11043
|
+
listAppJobQueue: (appId, params) => {
|
|
11044
|
+
const qs = new URLSearchParams();
|
|
11045
|
+
if (typeof params?.limit === "number") qs.set("limit", String(params.limit));
|
|
11046
|
+
if (typeof params?.offset === "number") qs.set("offset", String(params.offset));
|
|
11047
|
+
for (const kind of params?.kind ?? []) qs.append("kind", kind);
|
|
11048
|
+
for (const status of params?.status ?? []) qs.append("status", status);
|
|
11049
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
11050
|
+
return request(`/v1/apps/${encodeURIComponent(appId)}/job-queue${suffix}`, { method: "GET" });
|
|
11051
|
+
},
|
|
11052
|
+
getMergeRequest: (mrId) => request(`/v1/merge-requests/${encodeURIComponent(mrId)}`, { method: "GET" }),
|
|
11053
|
+
presignImportUpload: (payload) => request("/v1/apps/import/upload/presign", { method: "POST", body: JSON.stringify(payload) }),
|
|
11054
|
+
importFromUpload: (payload) => request("/v1/apps/import/upload", { method: "POST", body: JSON.stringify(payload) }),
|
|
11055
|
+
presignImportUploadFirstParty: (payload) => request("/v1/apps/import/upload/presign/first-party", { method: "POST", body: JSON.stringify(payload) }),
|
|
11056
|
+
importFromUploadFirstParty: (payload) => request("/v1/apps/import/upload/first-party", { method: "POST", body: JSON.stringify(payload) }),
|
|
11057
|
+
importFromGithubFirstParty: (payload) => request("/v1/apps/import/github/first-party", { method: "POST", body: JSON.stringify(payload) }),
|
|
11058
|
+
forkApp: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/fork`, { method: "POST", body: JSON.stringify(payload ?? {}) }),
|
|
11059
|
+
getAppHead: (appId) => request(`/v1/apps/${encodeURIComponent(appId)}/head`, { method: "GET" }),
|
|
11060
|
+
getAppDelta: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/delta`, {
|
|
11061
|
+
method: "POST",
|
|
11062
|
+
body: JSON.stringify(payload)
|
|
11063
|
+
}),
|
|
11064
|
+
downloadAppBundle: (appId) => requestBinary(`/v1/apps/${encodeURIComponent(appId)}/download.bundle`, { method: "GET" }),
|
|
11065
|
+
createChangeStep: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/change-steps`, {
|
|
11066
|
+
method: "POST",
|
|
11067
|
+
body: JSON.stringify(payload)
|
|
11068
|
+
}),
|
|
11069
|
+
createCollabTurn: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns`, {
|
|
11070
|
+
method: "POST",
|
|
11071
|
+
body: JSON.stringify(payload)
|
|
11072
|
+
}),
|
|
11073
|
+
attachCollabTurnUsage: (appId, payload) => request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns/attach-usage`, {
|
|
11074
|
+
method: "POST",
|
|
11075
|
+
body: JSON.stringify(payload)
|
|
11076
|
+
}),
|
|
11077
|
+
listCollabTurns: (appId, params) => {
|
|
11078
|
+
const qs = new URLSearchParams();
|
|
11079
|
+
if (params?.limit !== void 0) qs.set("limit", String(params.limit));
|
|
11080
|
+
if (params?.offset !== void 0) qs.set("offset", String(params.offset));
|
|
11081
|
+
if (params?.changeStepId) qs.set("changeStepId", params.changeStepId);
|
|
11082
|
+
if (params?.threadId) qs.set("threadId", params.threadId);
|
|
11083
|
+
if (params?.collabLaneId) qs.set("collabLaneId", params.collabLaneId);
|
|
11084
|
+
if (params?.createdAfter) qs.set("createdAfter", params.createdAfter);
|
|
10230
11085
|
if (params?.createdBefore) qs.set("createdBefore", params.createdBefore);
|
|
10231
11086
|
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
10232
11087
|
return request(`/v1/apps/${encodeURIComponent(appId)}/collab-turns${suffix}`, { method: "GET" });
|
|
@@ -10936,8 +11791,8 @@ function getErrorMap() {
|
|
|
10936
11791
|
|
|
10937
11792
|
// node_modules/zod/v3/helpers/parseUtil.js
|
|
10938
11793
|
var makeIssue = (params) => {
|
|
10939
|
-
const { data, path:
|
|
10940
|
-
const fullPath = [...
|
|
11794
|
+
const { data, path: path16, errorMaps, issueData } = params;
|
|
11795
|
+
const fullPath = [...path16, ...issueData.path || []];
|
|
10941
11796
|
const fullIssue = {
|
|
10942
11797
|
...issueData,
|
|
10943
11798
|
path: fullPath
|
|
@@ -11053,11 +11908,11 @@ var errorUtil;
|
|
|
11053
11908
|
|
|
11054
11909
|
// node_modules/zod/v3/types.js
|
|
11055
11910
|
var ParseInputLazyPath = class {
|
|
11056
|
-
constructor(parent, value,
|
|
11911
|
+
constructor(parent, value, path16, key) {
|
|
11057
11912
|
this._cachedPath = [];
|
|
11058
11913
|
this.parent = parent;
|
|
11059
11914
|
this.data = value;
|
|
11060
|
-
this._path =
|
|
11915
|
+
this._path = path16;
|
|
11061
11916
|
this._key = key;
|
|
11062
11917
|
}
|
|
11063
11918
|
get path() {
|
|
@@ -14500,7 +15355,7 @@ var coerce = {
|
|
|
14500
15355
|
var NEVER = INVALID;
|
|
14501
15356
|
|
|
14502
15357
|
// node_modules/@remixhq/core/dist/chunk-P6JHXOV4.js
|
|
14503
|
-
var
|
|
15358
|
+
var import_promises22 = __toESM(require("fs/promises"), 1);
|
|
14504
15359
|
var import_os3 = __toESM(require("os"), 1);
|
|
14505
15360
|
var import_path7 = __toESM(require("path"), 1);
|
|
14506
15361
|
|
|
@@ -14908,7 +15763,7 @@ var PostgrestError = class extends Error {
|
|
|
14908
15763
|
};
|
|
14909
15764
|
}
|
|
14910
15765
|
};
|
|
14911
|
-
function
|
|
15766
|
+
function sleep3(ms, signal) {
|
|
14912
15767
|
return new Promise((resolve) => {
|
|
14913
15768
|
if (signal === null || signal === void 0 ? void 0 : signal.aborted) {
|
|
14914
15769
|
resolve();
|
|
@@ -15104,7 +15959,7 @@ var PostgrestBuilder = class {
|
|
|
15104
15959
|
if (_this.retryEnabled && attemptCount < DEFAULT_MAX_RETRIES) {
|
|
15105
15960
|
const delay = getRetryDelay(attemptCount);
|
|
15106
15961
|
attemptCount++;
|
|
15107
|
-
await
|
|
15962
|
+
await sleep3(delay, _this.signal);
|
|
15108
15963
|
continue;
|
|
15109
15964
|
}
|
|
15110
15965
|
throw fetchError;
|
|
@@ -15115,7 +15970,7 @@ var PostgrestBuilder = class {
|
|
|
15115
15970
|
const delay = retryAfterHeader !== null ? Math.max(0, parseInt(retryAfterHeader, 10) || 0) * 1e3 : getRetryDelay(attemptCount);
|
|
15116
15971
|
await res$1.text();
|
|
15117
15972
|
attemptCount++;
|
|
15118
|
-
await
|
|
15973
|
+
await sleep3(delay, _this.signal);
|
|
15119
15974
|
continue;
|
|
15120
15975
|
}
|
|
15121
15976
|
return await _this.processResponse(res$1);
|
|
@@ -23605,8 +24460,8 @@ var IcebergError = class extends Error {
|
|
|
23605
24460
|
return this.status === 419;
|
|
23606
24461
|
}
|
|
23607
24462
|
};
|
|
23608
|
-
function buildUrl(baseUrl,
|
|
23609
|
-
const url = new URL(
|
|
24463
|
+
function buildUrl(baseUrl, path16, query) {
|
|
24464
|
+
const url = new URL(path16, baseUrl);
|
|
23610
24465
|
if (query) {
|
|
23611
24466
|
for (const [key, value] of Object.entries(query)) {
|
|
23612
24467
|
if (value !== void 0) {
|
|
@@ -23636,12 +24491,12 @@ function createFetchClient(options) {
|
|
|
23636
24491
|
return {
|
|
23637
24492
|
async request({
|
|
23638
24493
|
method,
|
|
23639
|
-
path:
|
|
24494
|
+
path: path16,
|
|
23640
24495
|
query,
|
|
23641
24496
|
body,
|
|
23642
24497
|
headers
|
|
23643
24498
|
}) {
|
|
23644
|
-
const url = buildUrl(options.baseUrl,
|
|
24499
|
+
const url = buildUrl(options.baseUrl, path16, query);
|
|
23645
24500
|
const authHeaders = await buildAuthHeaders(options.auth);
|
|
23646
24501
|
const res = await fetchFn(url, {
|
|
23647
24502
|
method,
|
|
@@ -24479,7 +25334,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24479
25334
|
* @param path The relative file path. Should be of the format `folder/subfolder/filename.png`. The bucket must already exist before attempting to upload.
|
|
24480
25335
|
* @param fileBody The body of the file to be stored in the bucket.
|
|
24481
25336
|
*/
|
|
24482
|
-
async uploadOrUpdate(method,
|
|
25337
|
+
async uploadOrUpdate(method, path16, fileBody, fileOptions) {
|
|
24483
25338
|
var _this = this;
|
|
24484
25339
|
return _this.handleOperation(async () => {
|
|
24485
25340
|
let body;
|
|
@@ -24503,7 +25358,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24503
25358
|
if ((typeof ReadableStream !== "undefined" && body instanceof ReadableStream || body && typeof body === "object" && "pipe" in body && typeof body.pipe === "function") && !options.duplex) options.duplex = "half";
|
|
24504
25359
|
}
|
|
24505
25360
|
if (fileOptions === null || fileOptions === void 0 ? void 0 : fileOptions.headers) for (const [key, value] of Object.entries(fileOptions.headers)) headers = setHeader(headers, key, value);
|
|
24506
|
-
const cleanPath = _this._removeEmptyFolders(
|
|
25361
|
+
const cleanPath = _this._removeEmptyFolders(path16);
|
|
24507
25362
|
const _path = _this._getFinalPath(cleanPath);
|
|
24508
25363
|
const data = await (method == "PUT" ? put : post)(_this.fetch, `${_this.url}/object/${_path}`, body, _objectSpread22({ headers }, (options === null || options === void 0 ? void 0 : options.duplex) ? { duplex: options.duplex } : {}));
|
|
24509
25364
|
return {
|
|
@@ -24564,8 +25419,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24564
25419
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24565
25420
|
* - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Upload file using `ArrayBuffer` from base64 file data instead, see example below.
|
|
24566
25421
|
*/
|
|
24567
|
-
async upload(
|
|
24568
|
-
return this.uploadOrUpdate("POST",
|
|
25422
|
+
async upload(path16, fileBody, fileOptions) {
|
|
25423
|
+
return this.uploadOrUpdate("POST", path16, fileBody, fileOptions);
|
|
24569
25424
|
}
|
|
24570
25425
|
/**
|
|
24571
25426
|
* Upload a file with a token generated from `createSignedUploadUrl`.
|
|
@@ -24604,9 +25459,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24604
25459
|
* - `objects` table permissions: none
|
|
24605
25460
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24606
25461
|
*/
|
|
24607
|
-
async uploadToSignedUrl(
|
|
25462
|
+
async uploadToSignedUrl(path16, token, fileBody, fileOptions) {
|
|
24608
25463
|
var _this3 = this;
|
|
24609
|
-
const cleanPath = _this3._removeEmptyFolders(
|
|
25464
|
+
const cleanPath = _this3._removeEmptyFolders(path16);
|
|
24610
25465
|
const _path = _this3._getFinalPath(cleanPath);
|
|
24611
25466
|
const url = new URL(_this3.url + `/object/upload/sign/${_path}`);
|
|
24612
25467
|
url.searchParams.set("token", token);
|
|
@@ -24668,10 +25523,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24668
25523
|
* - `objects` table permissions: `insert`
|
|
24669
25524
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24670
25525
|
*/
|
|
24671
|
-
async createSignedUploadUrl(
|
|
25526
|
+
async createSignedUploadUrl(path16, options) {
|
|
24672
25527
|
var _this4 = this;
|
|
24673
25528
|
return _this4.handleOperation(async () => {
|
|
24674
|
-
let _path = _this4._getFinalPath(
|
|
25529
|
+
let _path = _this4._getFinalPath(path16);
|
|
24675
25530
|
const headers = _objectSpread22({}, _this4.headers);
|
|
24676
25531
|
if (options === null || options === void 0 ? void 0 : options.upsert) headers["x-upsert"] = "true";
|
|
24677
25532
|
const data = await post(_this4.fetch, `${_this4.url}/object/upload/sign/${_path}`, {}, { headers });
|
|
@@ -24680,7 +25535,7 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24680
25535
|
if (!token) throw new StorageError("No token returned by API");
|
|
24681
25536
|
return {
|
|
24682
25537
|
signedUrl: url.toString(),
|
|
24683
|
-
path:
|
|
25538
|
+
path: path16,
|
|
24684
25539
|
token
|
|
24685
25540
|
};
|
|
24686
25541
|
});
|
|
@@ -24736,8 +25591,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24736
25591
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24737
25592
|
* - For React Native, using either `Blob`, `File` or `FormData` does not work as intended. Update file using `ArrayBuffer` from base64 file data instead, see example below.
|
|
24738
25593
|
*/
|
|
24739
|
-
async update(
|
|
24740
|
-
return this.uploadOrUpdate("PUT",
|
|
25594
|
+
async update(path16, fileBody, fileOptions) {
|
|
25595
|
+
return this.uploadOrUpdate("PUT", path16, fileBody, fileOptions);
|
|
24741
25596
|
}
|
|
24742
25597
|
/**
|
|
24743
25598
|
* Moves an existing file to a new path in the same bucket.
|
|
@@ -24885,10 +25740,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
24885
25740
|
* - `objects` table permissions: `select`
|
|
24886
25741
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
24887
25742
|
*/
|
|
24888
|
-
async createSignedUrl(
|
|
25743
|
+
async createSignedUrl(path16, expiresIn, options) {
|
|
24889
25744
|
var _this8 = this;
|
|
24890
25745
|
return _this8.handleOperation(async () => {
|
|
24891
|
-
let _path = _this8._getFinalPath(
|
|
25746
|
+
let _path = _this8._getFinalPath(path16);
|
|
24892
25747
|
const hasTransform = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0;
|
|
24893
25748
|
let data = await post(_this8.fetch, `${_this8.url}/object/sign/${_path}`, _objectSpread22({ expiresIn }, hasTransform ? { transform: options.transform } : {}), { headers: _this8.headers });
|
|
24894
25749
|
const query = new URLSearchParams();
|
|
@@ -25022,13 +25877,13 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
25022
25877
|
* - `objects` table permissions: `select`
|
|
25023
25878
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
25024
25879
|
*/
|
|
25025
|
-
download(
|
|
25880
|
+
download(path16, options, parameters) {
|
|
25026
25881
|
const renderPath = typeof (options === null || options === void 0 ? void 0 : options.transform) === "object" && options.transform !== null && Object.keys(options.transform).length > 0 ? "render/image/authenticated" : "object";
|
|
25027
25882
|
const query = new URLSearchParams();
|
|
25028
25883
|
if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
|
|
25029
25884
|
if ((options === null || options === void 0 ? void 0 : options.cacheNonce) != null) query.set("cacheNonce", String(options.cacheNonce));
|
|
25030
25885
|
const queryString = query.toString();
|
|
25031
|
-
const _path = this._getFinalPath(
|
|
25886
|
+
const _path = this._getFinalPath(path16);
|
|
25032
25887
|
const downloadFn = () => get(this.fetch, `${this.url}/${renderPath}/${_path}${queryString ? `?${queryString}` : ""}`, {
|
|
25033
25888
|
headers: this.headers,
|
|
25034
25889
|
noResolveJson: true
|
|
@@ -25058,9 +25913,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
25058
25913
|
* }
|
|
25059
25914
|
* ```
|
|
25060
25915
|
*/
|
|
25061
|
-
async info(
|
|
25916
|
+
async info(path16) {
|
|
25062
25917
|
var _this10 = this;
|
|
25063
|
-
const _path = _this10._getFinalPath(
|
|
25918
|
+
const _path = _this10._getFinalPath(path16);
|
|
25064
25919
|
return _this10.handleOperation(async () => {
|
|
25065
25920
|
return recursiveToCamel(await get(_this10.fetch, `${_this10.url}/object/info/${_path}`, { headers: _this10.headers }));
|
|
25066
25921
|
});
|
|
@@ -25080,9 +25935,9 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
25080
25935
|
* .exists('folder/avatar1.png')
|
|
25081
25936
|
* ```
|
|
25082
25937
|
*/
|
|
25083
|
-
async exists(
|
|
25938
|
+
async exists(path16) {
|
|
25084
25939
|
var _this11 = this;
|
|
25085
|
-
const _path = _this11._getFinalPath(
|
|
25940
|
+
const _path = _this11._getFinalPath(path16);
|
|
25086
25941
|
try {
|
|
25087
25942
|
await head(_this11.fetch, `${_this11.url}/object/${_path}`, { headers: _this11.headers });
|
|
25088
25943
|
return {
|
|
@@ -25160,8 +26015,8 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
25160
26015
|
* - `objects` table permissions: none
|
|
25161
26016
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
25162
26017
|
*/
|
|
25163
|
-
getPublicUrl(
|
|
25164
|
-
const _path = this._getFinalPath(
|
|
26018
|
+
getPublicUrl(path16, options) {
|
|
26019
|
+
const _path = this._getFinalPath(path16);
|
|
25165
26020
|
const query = new URLSearchParams();
|
|
25166
26021
|
if (options === null || options === void 0 ? void 0 : options.download) query.set("download", options.download === true ? "" : options.download);
|
|
25167
26022
|
if (options === null || options === void 0 ? void 0 : options.transform) this.applyTransformOptsToQuery(query, options.transform);
|
|
@@ -25298,10 +26153,10 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
25298
26153
|
* - `objects` table permissions: `select`
|
|
25299
26154
|
* - Refer to the [Storage guide](/docs/guides/storage/security/access-control) on how access control works
|
|
25300
26155
|
*/
|
|
25301
|
-
async list(
|
|
26156
|
+
async list(path16, options, parameters) {
|
|
25302
26157
|
var _this13 = this;
|
|
25303
26158
|
return _this13.handleOperation(async () => {
|
|
25304
|
-
const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix:
|
|
26159
|
+
const body = _objectSpread22(_objectSpread22(_objectSpread22({}, DEFAULT_SEARCH_OPTIONS), options), {}, { prefix: path16 || "" });
|
|
25305
26160
|
return await post(_this13.fetch, `${_this13.url}/object/list/${_this13.bucketId}`, body, { headers: _this13.headers }, parameters);
|
|
25306
26161
|
});
|
|
25307
26162
|
}
|
|
@@ -25365,11 +26220,11 @@ var StorageFileApi = class extends BaseApiClient {
|
|
|
25365
26220
|
if (typeof Buffer !== "undefined") return Buffer.from(data).toString("base64");
|
|
25366
26221
|
return btoa(data);
|
|
25367
26222
|
}
|
|
25368
|
-
_getFinalPath(
|
|
25369
|
-
return `${this.bucketId}/${
|
|
26223
|
+
_getFinalPath(path16) {
|
|
26224
|
+
return `${this.bucketId}/${path16.replace(/^\/+/, "")}`;
|
|
25370
26225
|
}
|
|
25371
|
-
_removeEmptyFolders(
|
|
25372
|
-
return
|
|
26226
|
+
_removeEmptyFolders(path16) {
|
|
26227
|
+
return path16.replace(/^\/|\/$/g, "").replace(/\/+/g, "/");
|
|
25373
26228
|
}
|
|
25374
26229
|
/** Modifies the `query`, appending values the from `transform` */
|
|
25375
26230
|
applyTransformOptsToQuery(query, transform) {
|
|
@@ -27112,7 +27967,7 @@ function decodeJWT(token) {
|
|
|
27112
27967
|
};
|
|
27113
27968
|
return data;
|
|
27114
27969
|
}
|
|
27115
|
-
async function
|
|
27970
|
+
async function sleep4(time) {
|
|
27116
27971
|
return await new Promise((accept) => {
|
|
27117
27972
|
setTimeout(() => accept(null), time);
|
|
27118
27973
|
});
|
|
@@ -32905,7 +33760,7 @@ var GoTrueClient = class _GoTrueClient {
|
|
|
32905
33760
|
const startedAt = Date.now();
|
|
32906
33761
|
return await retryable(async (attempt) => {
|
|
32907
33762
|
if (attempt > 0) {
|
|
32908
|
-
await
|
|
33763
|
+
await sleep4(200 * Math.pow(2, attempt - 1));
|
|
32909
33764
|
}
|
|
32910
33765
|
this._debug(debugName, "refreshing attempt", attempt);
|
|
32911
33766
|
return await _request(this.fetch, "POST", `${this.url}/token?grant_type=refresh_token`, {
|
|
@@ -34477,24 +35332,24 @@ async function maybeLoadKeytar() {
|
|
|
34477
35332
|
}
|
|
34478
35333
|
async function ensurePathPermissions(filePath) {
|
|
34479
35334
|
const dir = import_path7.default.dirname(filePath);
|
|
34480
|
-
await
|
|
35335
|
+
await import_promises22.default.mkdir(dir, { recursive: true });
|
|
34481
35336
|
try {
|
|
34482
|
-
await
|
|
35337
|
+
await import_promises22.default.chmod(dir, 448);
|
|
34483
35338
|
} catch {
|
|
34484
35339
|
}
|
|
34485
35340
|
try {
|
|
34486
|
-
await
|
|
35341
|
+
await import_promises22.default.chmod(filePath, 384);
|
|
34487
35342
|
} catch {
|
|
34488
35343
|
}
|
|
34489
35344
|
}
|
|
34490
|
-
async function
|
|
34491
|
-
await
|
|
35345
|
+
async function writeJsonAtomic3(filePath, value) {
|
|
35346
|
+
await import_promises22.default.mkdir(import_path7.default.dirname(filePath), { recursive: true });
|
|
34492
35347
|
const tmpPath = `${filePath}.tmp-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
34493
|
-
await
|
|
34494
|
-
await
|
|
35348
|
+
await import_promises22.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
35349
|
+
await import_promises22.default.rename(tmpPath, filePath);
|
|
34495
35350
|
}
|
|
34496
35351
|
async function writeSessionFileFallback(filePath, session) {
|
|
34497
|
-
await
|
|
35352
|
+
await writeJsonAtomic3(filePath, session);
|
|
34498
35353
|
await ensurePathPermissions(filePath);
|
|
34499
35354
|
}
|
|
34500
35355
|
function createLocalSessionStore(params) {
|
|
@@ -34514,7 +35369,7 @@ function createLocalSessionStore(params) {
|
|
|
34514
35369
|
}
|
|
34515
35370
|
}
|
|
34516
35371
|
async function readFile() {
|
|
34517
|
-
const raw = await
|
|
35372
|
+
const raw = await import_promises22.default.readFile(filePath, "utf8").catch(() => null);
|
|
34518
35373
|
if (!raw) return null;
|
|
34519
35374
|
try {
|
|
34520
35375
|
const parsed = storedSessionSchema.safeParse(JSON.parse(raw));
|
|
@@ -34712,514 +35567,376 @@ async function createHookCollabApiClient() {
|
|
|
34712
35567
|
});
|
|
34713
35568
|
}
|
|
34714
35569
|
|
|
34715
|
-
// src/
|
|
34716
|
-
var
|
|
34717
|
-
var
|
|
34718
|
-
var
|
|
34719
|
-
|
|
34720
|
-
|
|
34721
|
-
|
|
34722
|
-
|
|
34723
|
-
|
|
34724
|
-
var
|
|
34725
|
-
var
|
|
34726
|
-
|
|
34727
|
-
|
|
34728
|
-
|
|
34729
|
-
|
|
34730
|
-
|
|
34731
|
-
|
|
34732
|
-
|
|
34733
|
-
|
|
34734
|
-
|
|
34735
|
-
}
|
|
34736
|
-
function stateLockMetaPath(sessionId) {
|
|
34737
|
-
return import_node_path6.default.join(stateLockPath(sessionId), "owner.json");
|
|
35570
|
+
// src/deferred-turn-drainer.ts
|
|
35571
|
+
var collabFinalizeTurn2 = collabFinalizeTurn;
|
|
35572
|
+
var drainPendingFinalizeQueue2 = drainPendingFinalizeQueue;
|
|
35573
|
+
var HOOK_ACTOR = {
|
|
35574
|
+
type: "agent",
|
|
35575
|
+
name: "claude-code",
|
|
35576
|
+
version: pluginMetadata.version,
|
|
35577
|
+
provider: "anthropic"
|
|
35578
|
+
};
|
|
35579
|
+
var DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS = 3e3;
|
|
35580
|
+
var DEFERRED_TURN_DRAIN_MAX_WAIT_MS = 15 * 60 * 1e3;
|
|
35581
|
+
var DEFERRED_TURN_DRAIN_LOCK_HEARTBEAT_MS = 3e4;
|
|
35582
|
+
var DEFERRED_TURN_DRAIN_LOCK_STALE_MS = 9e4;
|
|
35583
|
+
function isPidAlive(pid) {
|
|
35584
|
+
if (!Number.isFinite(pid) || pid <= 0) return false;
|
|
35585
|
+
try {
|
|
35586
|
+
process.kill(pid, 0);
|
|
35587
|
+
return true;
|
|
35588
|
+
} catch {
|
|
35589
|
+
return false;
|
|
35590
|
+
}
|
|
34738
35591
|
}
|
|
34739
|
-
|
|
34740
|
-
|
|
34741
|
-
|
|
34742
|
-
await import_promises19.default.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
34743
|
-
await import_promises19.default.rename(tmpPath, filePath);
|
|
35592
|
+
function repoLockFileName(repoRoot) {
|
|
35593
|
+
const hash = (0, import_node_crypto3.createHash)("sha256").update(repoRoot).digest("hex").slice(0, 16);
|
|
35594
|
+
return `.drainer-${hash}.lock`;
|
|
34744
35595
|
}
|
|
34745
|
-
|
|
34746
|
-
|
|
34747
|
-
var STATE_LOCK_STALE_MS = 3e4;
|
|
34748
|
-
var STATE_LOCK_HEARTBEAT_MS = 5e3;
|
|
34749
|
-
async function sleep4(ms) {
|
|
34750
|
-
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
35596
|
+
function repoLockPath(repoRoot) {
|
|
35597
|
+
return import_node_path11.default.join(getDeferredTurnDirPath(), repoLockFileName(repoRoot));
|
|
34751
35598
|
}
|
|
34752
|
-
async function
|
|
34753
|
-
const raw = await
|
|
35599
|
+
async function readDrainLockMetadata(lockPath) {
|
|
35600
|
+
const raw = await import_promises23.default.readFile(lockPath, "utf8").catch(() => null);
|
|
34754
35601
|
if (!raw) return null;
|
|
34755
35602
|
try {
|
|
34756
35603
|
const parsed = JSON.parse(raw);
|
|
34757
|
-
if (typeof parsed.
|
|
35604
|
+
if (typeof parsed.pid !== "number" || typeof parsed.repoRoot !== "string" || typeof parsed.startedAt !== "string") {
|
|
34758
35605
|
return null;
|
|
34759
35606
|
}
|
|
34760
|
-
return {
|
|
34761
|
-
ownerId: parsed.ownerId,
|
|
34762
|
-
pid: parsed.pid,
|
|
34763
|
-
createdAt: parsed.createdAt,
|
|
34764
|
-
heartbeatAt: parsed.heartbeatAt
|
|
34765
|
-
};
|
|
35607
|
+
return { pid: parsed.pid, repoRoot: parsed.repoRoot, startedAt: parsed.startedAt };
|
|
34766
35608
|
} catch {
|
|
34767
35609
|
return null;
|
|
34768
35610
|
}
|
|
34769
35611
|
}
|
|
34770
|
-
async function
|
|
34771
|
-
|
|
35612
|
+
async function writeDrainLockMetadata(lockPath, metadata) {
|
|
35613
|
+
const tmpPath = `${lockPath}.tmp-${process.pid}-${Date.now()}`;
|
|
35614
|
+
await import_promises23.default.writeFile(tmpPath, JSON.stringify(metadata), "utf8");
|
|
35615
|
+
await import_promises23.default.rename(tmpPath, lockPath);
|
|
34772
35616
|
}
|
|
34773
|
-
async function
|
|
34774
|
-
const lockPath =
|
|
34775
|
-
|
|
34776
|
-
const
|
|
34777
|
-
if (
|
|
34778
|
-
await
|
|
34779
|
-
|
|
34780
|
-
|
|
34781
|
-
|
|
34782
|
-
|
|
34783
|
-
|
|
34784
|
-
await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
34785
|
-
return true;
|
|
34786
|
-
}
|
|
34787
|
-
}
|
|
34788
|
-
return false;
|
|
34789
|
-
}
|
|
34790
|
-
async function acquireStateLock(sessionId) {
|
|
34791
|
-
const lockPath = stateLockPath(sessionId);
|
|
34792
|
-
const deadline = Date.now() + STATE_LOCK_WAIT_MS;
|
|
34793
|
-
await import_promises19.default.mkdir(stateRoot(), { recursive: true });
|
|
34794
|
-
while (true) {
|
|
34795
|
-
try {
|
|
34796
|
-
await import_promises19.default.mkdir(lockPath);
|
|
34797
|
-
const ownerId = (0, import_node_crypto.randomUUID)();
|
|
34798
|
-
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34799
|
-
const metadata = {
|
|
34800
|
-
ownerId,
|
|
34801
|
-
pid: process.pid,
|
|
34802
|
-
createdAt,
|
|
34803
|
-
heartbeatAt: createdAt
|
|
34804
|
-
};
|
|
34805
|
-
await writeStateLockMetadata(sessionId, metadata);
|
|
34806
|
-
let released = false;
|
|
34807
|
-
const heartbeat = setInterval(() => {
|
|
34808
|
-
if (released) return;
|
|
34809
|
-
void writeStateLockMetadata(sessionId, {
|
|
34810
|
-
...metadata,
|
|
34811
|
-
heartbeatAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
34812
|
-
}).catch(() => void 0);
|
|
34813
|
-
}, STATE_LOCK_HEARTBEAT_MS);
|
|
34814
|
-
heartbeat.unref?.();
|
|
34815
|
-
return async () => {
|
|
34816
|
-
if (released) return;
|
|
34817
|
-
released = true;
|
|
34818
|
-
clearInterval(heartbeat);
|
|
34819
|
-
const currentMetadata = await readStateLockMetadata(sessionId);
|
|
34820
|
-
if (currentMetadata?.ownerId === ownerId) {
|
|
34821
|
-
await import_promises19.default.rm(lockPath, { recursive: true, force: true }).catch(() => void 0);
|
|
34822
|
-
}
|
|
34823
|
-
};
|
|
34824
|
-
} catch (error) {
|
|
34825
|
-
const code = error && typeof error === "object" && "code" in error ? error.code : null;
|
|
34826
|
-
if (code !== "EEXIST") {
|
|
34827
|
-
throw error;
|
|
34828
|
-
}
|
|
34829
|
-
if (await tryRemoveStaleStateLock(sessionId)) {
|
|
34830
|
-
continue;
|
|
34831
|
-
}
|
|
34832
|
-
if (Date.now() >= deadline) {
|
|
34833
|
-
throw new Error(`Timed out acquiring hook state lock for session ${sessionId}.`);
|
|
34834
|
-
}
|
|
34835
|
-
await sleep4(STATE_LOCK_POLL_MS);
|
|
35617
|
+
async function tryAcquireDrainLock(repoRoot) {
|
|
35618
|
+
const lockPath = repoLockPath(repoRoot);
|
|
35619
|
+
await import_promises23.default.mkdir(import_node_path11.default.dirname(lockPath), { recursive: true });
|
|
35620
|
+
const existingMeta = await readDrainLockMetadata(lockPath);
|
|
35621
|
+
if (existingMeta) {
|
|
35622
|
+
const lockStat = await import_promises23.default.stat(lockPath).catch(() => null);
|
|
35623
|
+
const ageMs = lockStat ? Date.now() - lockStat.mtimeMs : Number.POSITIVE_INFINITY;
|
|
35624
|
+
const fresh = ageMs <= DEFERRED_TURN_DRAIN_LOCK_STALE_MS;
|
|
35625
|
+
const alive = isPidAlive(existingMeta.pid);
|
|
35626
|
+
if (fresh && alive) {
|
|
35627
|
+
return { acquired: false, lockPath };
|
|
34836
35628
|
}
|
|
34837
35629
|
}
|
|
35630
|
+
await writeDrainLockMetadata(lockPath, {
|
|
35631
|
+
pid: process.pid,
|
|
35632
|
+
repoRoot,
|
|
35633
|
+
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
35634
|
+
});
|
|
35635
|
+
return { acquired: true, lockPath };
|
|
34838
35636
|
}
|
|
34839
|
-
async function
|
|
34840
|
-
const
|
|
34841
|
-
|
|
34842
|
-
|
|
34843
|
-
} finally {
|
|
34844
|
-
await release();
|
|
34845
|
-
}
|
|
35637
|
+
async function releaseDrainLock(lockPath) {
|
|
35638
|
+
const meta = await readDrainLockMetadata(lockPath);
|
|
35639
|
+
if (meta && meta.pid !== process.pid) return;
|
|
35640
|
+
await import_promises23.default.rm(lockPath, { force: true }).catch(() => void 0);
|
|
34846
35641
|
}
|
|
34847
|
-
function
|
|
34848
|
-
|
|
35642
|
+
async function heartbeatDrainLock(lockPath) {
|
|
35643
|
+
const now = /* @__PURE__ */ new Date();
|
|
35644
|
+
await import_promises23.default.utimes(lockPath, now, now).catch(() => void 0);
|
|
34849
35645
|
}
|
|
34850
|
-
function
|
|
34851
|
-
|
|
35646
|
+
async function sleep5(ms) {
|
|
35647
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
34852
35648
|
}
|
|
34853
|
-
function
|
|
34854
|
-
|
|
34855
|
-
return Array.from(
|
|
34856
|
-
new Set(
|
|
34857
|
-
value.filter((entry) => typeof entry === "string" && entry.trim().length > 0).map((entry) => entry.trim())
|
|
34858
|
-
)
|
|
34859
|
-
);
|
|
35649
|
+
function buildIdempotencyKey(turnId, repoRoot) {
|
|
35650
|
+
return `${turnId}:${repoRoot}:finalize_turn`;
|
|
34860
35651
|
}
|
|
34861
|
-
function
|
|
34862
|
-
if (
|
|
34863
|
-
|
|
35652
|
+
async function pushPendingFinalizeQueueToServer(params) {
|
|
35653
|
+
if (typeof drainPendingFinalizeQueue2 !== "function") {
|
|
35654
|
+
await appendHookDiagnosticsEvent({
|
|
35655
|
+
hook: "deferredTurnDrainer",
|
|
35656
|
+
sessionId: params.sessionMarker,
|
|
35657
|
+
stage: "finalize_queue_push_skipped",
|
|
35658
|
+
result: "info",
|
|
35659
|
+
reason: "drain_pending_finalize_queue_unavailable",
|
|
35660
|
+
repoRoot: params.repoRoot
|
|
35661
|
+
});
|
|
35662
|
+
return;
|
|
34864
35663
|
}
|
|
34865
|
-
return null;
|
|
34866
|
-
}
|
|
34867
|
-
function normalizeTouchedRepo(value, repoRoot) {
|
|
34868
|
-
if (!value || typeof value !== "object") return null;
|
|
34869
|
-
const parsed = value;
|
|
34870
|
-
const normalizedRepoRoot = normalizeString(parsed.repoRoot) ?? repoRoot.trim();
|
|
34871
|
-
if (!normalizedRepoRoot) return null;
|
|
34872
|
-
return {
|
|
34873
|
-
repoRoot: normalizedRepoRoot,
|
|
34874
|
-
projectId: normalizeString(parsed.projectId),
|
|
34875
|
-
currentAppId: normalizeString(parsed.currentAppId),
|
|
34876
|
-
upstreamAppId: normalizeString(parsed.upstreamAppId),
|
|
34877
|
-
firstTouchedAt: normalizeString(parsed.firstTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
34878
|
-
lastTouchedAt: normalizeString(parsed.lastTouchedAt) ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
34879
|
-
lastObservedWriteAt: normalizeString(parsed.lastObservedWriteAt),
|
|
34880
|
-
touchedBy: normalizeStringArray(parsed.touchedBy),
|
|
34881
|
-
hasObservedWrite: Boolean(parsed.hasObservedWrite),
|
|
34882
|
-
manuallyRecorded: Boolean(parsed.manuallyRecorded),
|
|
34883
|
-
manuallyRecordedAt: normalizeString(parsed.manuallyRecordedAt),
|
|
34884
|
-
manuallyRecordedByTool: normalizeString(parsed.manuallyRecordedByTool),
|
|
34885
|
-
manualRecordingScope: normalizeManualRecordingScope(parsed.manualRecordingScope),
|
|
34886
|
-
manualRemoteChangeRecordedAt: normalizeString(parsed.manualRemoteChangeRecordedAt),
|
|
34887
|
-
stopAttempted: Boolean(parsed.stopAttempted),
|
|
34888
|
-
stopRecorded: Boolean(parsed.stopRecorded),
|
|
34889
|
-
stopRecordedAt: normalizeString(parsed.stopRecordedAt),
|
|
34890
|
-
stopRecordedMode: parsed.stopRecordedMode === "changed_turn" || parsed.stopRecordedMode === "no_diff_turn" ? parsed.stopRecordedMode : null,
|
|
34891
|
-
recordingFailureMessage: normalizeString(parsed.recordingFailureMessage),
|
|
34892
|
-
recordingFailureHint: normalizeString(parsed.recordingFailureHint),
|
|
34893
|
-
recordingFailedAt: normalizeString(parsed.recordingFailedAt)
|
|
34894
|
-
};
|
|
34895
|
-
}
|
|
34896
|
-
function normalizeTouchedRepos(value) {
|
|
34897
|
-
if (!value || typeof value !== "object") return {};
|
|
34898
|
-
const entries = Object.entries(value).map(([repoRoot, repo]) => normalizeTouchedRepo(repo, repoRoot)).filter((repo) => repo !== null).sort((a2, b) => a2.repoRoot.localeCompare(b.repoRoot));
|
|
34899
|
-
return Object.fromEntries(entries.map((repo) => [repo.repoRoot, repo]));
|
|
34900
|
-
}
|
|
34901
|
-
function createTouchedRepo(params) {
|
|
34902
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34903
|
-
const touchedBy = params.touchedBy?.trim() ? [params.touchedBy.trim()] : [];
|
|
34904
|
-
return {
|
|
34905
|
-
repoRoot: params.repoRoot,
|
|
34906
|
-
projectId: normalizeString(params.projectId),
|
|
34907
|
-
currentAppId: normalizeString(params.currentAppId),
|
|
34908
|
-
upstreamAppId: normalizeString(params.upstreamAppId),
|
|
34909
|
-
firstTouchedAt: now,
|
|
34910
|
-
lastTouchedAt: now,
|
|
34911
|
-
lastObservedWriteAt: params.hasObservedWrite ? now : null,
|
|
34912
|
-
touchedBy,
|
|
34913
|
-
hasObservedWrite: Boolean(params.hasObservedWrite),
|
|
34914
|
-
manuallyRecorded: false,
|
|
34915
|
-
manuallyRecordedAt: null,
|
|
34916
|
-
manuallyRecordedByTool: null,
|
|
34917
|
-
manualRecordingScope: null,
|
|
34918
|
-
manualRemoteChangeRecordedAt: null,
|
|
34919
|
-
stopAttempted: false,
|
|
34920
|
-
stopRecorded: false,
|
|
34921
|
-
stopRecordedAt: null,
|
|
34922
|
-
stopRecordedMode: null,
|
|
34923
|
-
recordingFailureMessage: null,
|
|
34924
|
-
recordingFailureHint: null,
|
|
34925
|
-
recordingFailedAt: null
|
|
34926
|
-
};
|
|
34927
|
-
}
|
|
34928
|
-
async function updatePendingTurnState(sessionId, updater) {
|
|
34929
|
-
return withStateLock(sessionId, async () => {
|
|
34930
|
-
const existing = await loadPendingTurnState(sessionId);
|
|
34931
|
-
if (!existing) return null;
|
|
34932
|
-
const result = updater(existing);
|
|
34933
|
-
if (result === false) return existing;
|
|
34934
|
-
await savePendingTurnState(existing);
|
|
34935
|
-
return existing;
|
|
34936
|
-
});
|
|
34937
|
-
}
|
|
34938
|
-
async function loadPendingTurnState(sessionId) {
|
|
34939
|
-
const raw = await import_promises19.default.readFile(statePath(sessionId), "utf8").catch(() => null);
|
|
34940
|
-
if (!raw) return null;
|
|
34941
35664
|
try {
|
|
34942
|
-
|
|
34943
|
-
|
|
34944
|
-
|
|
34945
|
-
|
|
34946
|
-
|
|
34947
|
-
|
|
34948
|
-
|
|
34949
|
-
|
|
34950
|
-
|
|
34951
|
-
|
|
34952
|
-
|
|
34953
|
-
|
|
34954
|
-
|
|
34955
|
-
|
|
34956
|
-
|
|
34957
|
-
|
|
34958
|
-
|
|
34959
|
-
};
|
|
34960
|
-
} catch {
|
|
34961
|
-
return null;
|
|
35665
|
+
await drainPendingFinalizeQueue2({ api: params.api });
|
|
35666
|
+
await appendHookDiagnosticsEvent({
|
|
35667
|
+
hook: "deferredTurnDrainer",
|
|
35668
|
+
sessionId: params.sessionMarker,
|
|
35669
|
+
stage: "finalize_queue_pushed",
|
|
35670
|
+
result: "success",
|
|
35671
|
+
repoRoot: params.repoRoot
|
|
35672
|
+
});
|
|
35673
|
+
} catch (err) {
|
|
35674
|
+
await appendHookDiagnosticsEvent({
|
|
35675
|
+
hook: "deferredTurnDrainer",
|
|
35676
|
+
sessionId: params.sessionMarker,
|
|
35677
|
+
stage: "finalize_queue_push_failed",
|
|
35678
|
+
result: "error",
|
|
35679
|
+
reason: "exception",
|
|
35680
|
+
repoRoot: params.repoRoot,
|
|
35681
|
+
message: err instanceof Error ? err.message : String(err)
|
|
35682
|
+
});
|
|
34962
35683
|
}
|
|
34963
35684
|
}
|
|
34964
|
-
async function
|
|
34965
|
-
|
|
35685
|
+
async function recordOneDeferredTurn(params) {
|
|
35686
|
+
const { entry, api } = params;
|
|
35687
|
+
const { record, filePath } = entry;
|
|
35688
|
+
try {
|
|
35689
|
+
await collabFinalizeTurn2({
|
|
35690
|
+
api,
|
|
35691
|
+
cwd: record.repoRoot,
|
|
35692
|
+
prompt: record.prompt,
|
|
35693
|
+
assistantResponse: record.assistantResponse,
|
|
35694
|
+
idempotencyKey: buildIdempotencyKey(record.turnId, record.repoRoot),
|
|
35695
|
+
actor: HOOK_ACTOR,
|
|
35696
|
+
turnUsage: null,
|
|
35697
|
+
// The deferred queue can hold a turn for a long time (until the next
|
|
35698
|
+
// `remix collab init` lands a binding), so the server's ingestion
|
|
35699
|
+
// timestamp would otherwise be hours/days off from the real prompt
|
|
35700
|
+
// time. Forward the original submit time so the dashboard timeline
|
|
35701
|
+
// sorts this turn into its true position relative to siblings.
|
|
35702
|
+
promptedAt: record.submittedAt
|
|
35703
|
+
});
|
|
35704
|
+
await deleteDeferredTurnFile(filePath);
|
|
35705
|
+
return { recorded: true };
|
|
35706
|
+
} catch (error) {
|
|
35707
|
+
return { recorded: false, error };
|
|
35708
|
+
}
|
|
34966
35709
|
}
|
|
34967
|
-
async function
|
|
34968
|
-
const
|
|
34969
|
-
|
|
34970
|
-
const
|
|
34971
|
-
|
|
34972
|
-
|
|
34973
|
-
|
|
34974
|
-
|
|
34975
|
-
|
|
34976
|
-
|
|
34977
|
-
|
|
35710
|
+
async function runStandaloneDeferredTurnDrainer(repoRoot) {
|
|
35711
|
+
const startedAt = Date.now();
|
|
35712
|
+
const sessionMarker = `drainer-${process.pid}-${Math.random().toString(36).slice(2, 10)}`;
|
|
35713
|
+
const acquireResult = await tryAcquireDrainLock(repoRoot);
|
|
35714
|
+
if (!acquireResult.acquired) {
|
|
35715
|
+
await appendHookDiagnosticsEvent({
|
|
35716
|
+
hook: "deferredTurnDrainer",
|
|
35717
|
+
sessionId: sessionMarker,
|
|
35718
|
+
stage: "lock_skipped",
|
|
35719
|
+
result: "skip",
|
|
35720
|
+
reason: "another_drainer_active",
|
|
35721
|
+
repoRoot
|
|
34978
35722
|
});
|
|
34979
|
-
|
|
34980
|
-
|
|
34981
|
-
|
|
34982
|
-
|
|
34983
|
-
|
|
34984
|
-
|
|
34985
|
-
|
|
34986
|
-
|
|
34987
|
-
|
|
34988
|
-
|
|
35723
|
+
return;
|
|
35724
|
+
}
|
|
35725
|
+
await appendHookDiagnosticsEvent({
|
|
35726
|
+
hook: "deferredTurnDrainer",
|
|
35727
|
+
sessionId: sessionMarker,
|
|
35728
|
+
stage: "drainer_started",
|
|
35729
|
+
result: "info",
|
|
35730
|
+
repoRoot,
|
|
35731
|
+
fields: {
|
|
35732
|
+
pid: process.pid,
|
|
35733
|
+
maxWaitMs: DEFERRED_TURN_DRAIN_MAX_WAIT_MS,
|
|
35734
|
+
pollIntervalMs: DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS
|
|
34989
35735
|
}
|
|
34990
|
-
existing.touchedRepos[normalizedRepoRoot] = current;
|
|
34991
|
-
});
|
|
34992
|
-
return state?.touchedRepos[normalizedRepoRoot] ?? null;
|
|
34993
|
-
}
|
|
34994
|
-
async function markTouchedRepoStopAttempted(sessionId, repoRoot) {
|
|
34995
|
-
await updatePendingTurnState(sessionId, (existing) => {
|
|
34996
|
-
const current = existing.touchedRepos[repoRoot];
|
|
34997
|
-
if (!current) return false;
|
|
34998
|
-
current.stopAttempted = true;
|
|
34999
|
-
current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
35000
35736
|
});
|
|
35001
|
-
|
|
35002
|
-
|
|
35003
|
-
|
|
35004
|
-
|
|
35005
|
-
|
|
35006
|
-
|
|
35007
|
-
|
|
35008
|
-
|
|
35009
|
-
current.stopRecordedMode = params.mode;
|
|
35010
|
-
current.recordingFailureMessage = null;
|
|
35011
|
-
current.recordingFailureHint = null;
|
|
35012
|
-
current.recordingFailedAt = null;
|
|
35013
|
-
current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
35014
|
-
});
|
|
35015
|
-
}
|
|
35016
|
-
async function markTouchedRepoRecordingFailure(sessionId, repoRoot, params) {
|
|
35017
|
-
await updatePendingTurnState(sessionId, (existing) => {
|
|
35018
|
-
const current = existing.touchedRepos[repoRoot];
|
|
35019
|
-
if (!current) return false;
|
|
35020
|
-
current.stopAttempted = true;
|
|
35021
|
-
current.recordingFailureMessage = params.message.trim();
|
|
35022
|
-
current.recordingFailureHint = params.hint?.trim() || null;
|
|
35023
|
-
current.recordingFailedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
35024
|
-
current.lastTouchedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
35025
|
-
});
|
|
35026
|
-
}
|
|
35027
|
-
function lastFinalizedPath(sessionId) {
|
|
35028
|
-
return import_node_path6.default.join(stateRoot(), `${sessionId}.last-finalized.json`);
|
|
35029
|
-
}
|
|
35030
|
-
async function markLastFinalizedTurn(sessionId, turnId, prompt) {
|
|
35031
|
-
const record = {
|
|
35032
|
-
sessionId,
|
|
35033
|
-
turnId,
|
|
35034
|
-
prompt,
|
|
35035
|
-
finalizedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
35036
|
-
};
|
|
35037
|
-
await writeJsonAtomic3(lastFinalizedPath(sessionId), record);
|
|
35038
|
-
}
|
|
35039
|
-
async function loadLastFinalizedTurn(sessionId) {
|
|
35040
|
-
const raw = await import_promises19.default.readFile(lastFinalizedPath(sessionId), "utf8").catch(() => null);
|
|
35041
|
-
if (!raw) return null;
|
|
35737
|
+
const heartbeat = setInterval(() => {
|
|
35738
|
+
void heartbeatDrainLock(acquireResult.lockPath).catch(() => void 0);
|
|
35739
|
+
}, DEFERRED_TURN_DRAIN_LOCK_HEARTBEAT_MS);
|
|
35740
|
+
heartbeat.unref?.();
|
|
35741
|
+
let api = null;
|
|
35742
|
+
let recordedTotal = 0;
|
|
35743
|
+
let failedTotal = 0;
|
|
35744
|
+
let exitReason = "queue_empty";
|
|
35042
35745
|
try {
|
|
35043
|
-
|
|
35044
|
-
|
|
35045
|
-
|
|
35046
|
-
|
|
35047
|
-
|
|
35048
|
-
|
|
35049
|
-
|
|
35050
|
-
|
|
35746
|
+
while (true) {
|
|
35747
|
+
if (Date.now() - startedAt > DEFERRED_TURN_DRAIN_MAX_WAIT_MS) {
|
|
35748
|
+
exitReason = "timeout";
|
|
35749
|
+
break;
|
|
35750
|
+
}
|
|
35751
|
+
let entries = [];
|
|
35752
|
+
try {
|
|
35753
|
+
entries = await listDeferredTurnsForRepo(repoRoot);
|
|
35754
|
+
} catch (listErr) {
|
|
35755
|
+
await appendHookDiagnosticsEvent({
|
|
35756
|
+
hook: "deferredTurnDrainer",
|
|
35757
|
+
sessionId: sessionMarker,
|
|
35758
|
+
stage: "list_failed",
|
|
35759
|
+
result: "error",
|
|
35760
|
+
reason: "exception",
|
|
35761
|
+
repoRoot,
|
|
35762
|
+
message: listErr instanceof Error ? listErr.message : String(listErr)
|
|
35763
|
+
});
|
|
35764
|
+
await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
|
|
35765
|
+
continue;
|
|
35766
|
+
}
|
|
35767
|
+
if (entries.length === 0) {
|
|
35768
|
+
exitReason = "queue_empty";
|
|
35769
|
+
break;
|
|
35770
|
+
}
|
|
35771
|
+
const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
|
|
35772
|
+
const currentBranch = bindingState?.currentBranch ?? null;
|
|
35773
|
+
const isCurrentBranchBound = bindingState?.binding != null;
|
|
35774
|
+
const attemptable = entries.filter(
|
|
35775
|
+
(e) => isCurrentBranchBound && (!e.record.branchAtDefer || e.record.branchAtDefer === currentBranch)
|
|
35776
|
+
);
|
|
35777
|
+
if (attemptable.length === 0) {
|
|
35778
|
+
await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
|
|
35779
|
+
continue;
|
|
35780
|
+
}
|
|
35781
|
+
if (!api) {
|
|
35782
|
+
try {
|
|
35783
|
+
api = await createHookCollabApiClient();
|
|
35784
|
+
} catch (apiErr) {
|
|
35785
|
+
await appendHookDiagnosticsEvent({
|
|
35786
|
+
hook: "deferredTurnDrainer",
|
|
35787
|
+
sessionId: sessionMarker,
|
|
35788
|
+
stage: "api_client_failed",
|
|
35789
|
+
result: "error",
|
|
35790
|
+
reason: "exception",
|
|
35791
|
+
repoRoot,
|
|
35792
|
+
message: apiErr instanceof Error ? apiErr.message : String(apiErr)
|
|
35793
|
+
});
|
|
35794
|
+
exitReason = "api_init_failed";
|
|
35795
|
+
break;
|
|
35796
|
+
}
|
|
35797
|
+
}
|
|
35798
|
+
let recordedThisPass = 0;
|
|
35799
|
+
let failedThisPass = 0;
|
|
35800
|
+
for (const entry of attemptable) {
|
|
35801
|
+
const result = await recordOneDeferredTurn({ entry, api });
|
|
35802
|
+
if (result.recorded) {
|
|
35803
|
+
recordedThisPass += 1;
|
|
35804
|
+
recordedTotal += 1;
|
|
35805
|
+
await appendHookDiagnosticsEvent({
|
|
35806
|
+
hook: "deferredTurnDrainer",
|
|
35807
|
+
sessionId: sessionMarker,
|
|
35808
|
+
stage: "deferred_turn_recorded",
|
|
35809
|
+
result: "success",
|
|
35810
|
+
repoRoot,
|
|
35811
|
+
fields: {
|
|
35812
|
+
deferredTurnId: entry.record.turnId,
|
|
35813
|
+
deferredSessionId: entry.record.sessionId,
|
|
35814
|
+
deferredAt: entry.record.deferredAt,
|
|
35815
|
+
submittedAt: entry.record.submittedAt,
|
|
35816
|
+
recordingDelayMs: Math.max(0, Date.now() - Date.parse(entry.record.deferredAt)),
|
|
35817
|
+
recoveredBy: "standalone_drainer"
|
|
35818
|
+
}
|
|
35819
|
+
});
|
|
35820
|
+
} else {
|
|
35821
|
+
failedThisPass += 1;
|
|
35822
|
+
failedTotal += 1;
|
|
35823
|
+
await appendHookDiagnosticsEvent({
|
|
35824
|
+
hook: "deferredTurnDrainer",
|
|
35825
|
+
sessionId: sessionMarker,
|
|
35826
|
+
stage: "deferred_turn_record_failed",
|
|
35827
|
+
result: "error",
|
|
35828
|
+
reason: "exception",
|
|
35829
|
+
repoRoot,
|
|
35830
|
+
message: result.error instanceof Error ? result.error.message : String(result.error ?? ""),
|
|
35831
|
+
fields: {
|
|
35832
|
+
deferredTurnId: entry.record.turnId,
|
|
35833
|
+
deferredSessionId: entry.record.sessionId
|
|
35834
|
+
}
|
|
35835
|
+
});
|
|
35836
|
+
}
|
|
35837
|
+
}
|
|
35838
|
+
if (recordedThisPass > 0) {
|
|
35839
|
+
await pushPendingFinalizeQueueToServer({
|
|
35840
|
+
sessionMarker,
|
|
35841
|
+
repoRoot,
|
|
35842
|
+
api
|
|
35843
|
+
});
|
|
35844
|
+
}
|
|
35845
|
+
if (recordedThisPass > 0 && failedThisPass === 0) {
|
|
35846
|
+
const remaining = await listDeferredTurnsForRepo(repoRoot).catch(() => []);
|
|
35847
|
+
if (remaining.length === 0) {
|
|
35848
|
+
exitReason = "queue_empty";
|
|
35849
|
+
break;
|
|
35850
|
+
}
|
|
35851
|
+
}
|
|
35852
|
+
await sleep5(DEFERRED_TURN_DRAIN_POLL_INTERVAL_MS);
|
|
35051
35853
|
}
|
|
35052
|
-
|
|
35053
|
-
|
|
35054
|
-
|
|
35055
|
-
|
|
35056
|
-
|
|
35057
|
-
|
|
35058
|
-
await updatePendingTurnState(sessionId, (existing) => {
|
|
35059
|
-
existing.turnFailureMessage = params.message.trim();
|
|
35060
|
-
existing.turnFailureHint = params.hint?.trim() || null;
|
|
35061
|
-
existing.turnFailedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
35062
|
-
});
|
|
35063
|
-
}
|
|
35064
|
-
async function listTouchedRepos(sessionId) {
|
|
35065
|
-
const existing = await loadPendingTurnState(sessionId);
|
|
35066
|
-
if (!existing) return [];
|
|
35067
|
-
return Object.values(existing.touchedRepos).sort((a2, b) => a2.repoRoot.localeCompare(b.repoRoot));
|
|
35068
|
-
}
|
|
35069
|
-
async function clearPendingTurnState(sessionId) {
|
|
35070
|
-
await withStateLock(sessionId, async () => {
|
|
35071
|
-
await import_promises19.default.rm(statePath(sessionId), { force: true }).catch(() => void 0);
|
|
35072
|
-
});
|
|
35073
|
-
}
|
|
35074
|
-
|
|
35075
|
-
// package.json
|
|
35076
|
-
var package_default = {
|
|
35077
|
-
name: "@remixhq/claude-plugin",
|
|
35078
|
-
version: "0.1.21",
|
|
35079
|
-
description: "Claude Code plugin for Remix collaboration workflows",
|
|
35080
|
-
homepage: "https://github.com/RemixDotOne/remix-claude-plugin",
|
|
35081
|
-
license: "MIT",
|
|
35082
|
-
repository: {
|
|
35083
|
-
type: "git",
|
|
35084
|
-
url: "https://github.com/RemixDotOne/remix-claude-plugin.git"
|
|
35085
|
-
},
|
|
35086
|
-
type: "module",
|
|
35087
|
-
engines: {
|
|
35088
|
-
node: ">=20"
|
|
35089
|
-
},
|
|
35090
|
-
publishConfig: {
|
|
35091
|
-
access: "public"
|
|
35092
|
-
},
|
|
35093
|
-
files: [
|
|
35094
|
-
"dist",
|
|
35095
|
-
".claude-plugin/plugin.json",
|
|
35096
|
-
".mcp.json",
|
|
35097
|
-
"skills",
|
|
35098
|
-
"hooks",
|
|
35099
|
-
"agents"
|
|
35100
|
-
],
|
|
35101
|
-
exports: {
|
|
35102
|
-
".": {
|
|
35103
|
-
types: "./dist/index.d.ts",
|
|
35104
|
-
import: "./dist/index.js"
|
|
35854
|
+
if (recordedTotal > 0 && api) {
|
|
35855
|
+
await pushPendingFinalizeQueueToServer({
|
|
35856
|
+
sessionMarker,
|
|
35857
|
+
repoRoot,
|
|
35858
|
+
api
|
|
35859
|
+
});
|
|
35105
35860
|
}
|
|
35106
|
-
|
|
35107
|
-
|
|
35108
|
-
|
|
35109
|
-
|
|
35110
|
-
|
|
35111
|
-
|
|
35112
|
-
|
|
35113
|
-
|
|
35114
|
-
|
|
35115
|
-
|
|
35116
|
-
|
|
35117
|
-
|
|
35118
|
-
|
|
35119
|
-
|
|
35120
|
-
|
|
35121
|
-
|
|
35122
|
-
|
|
35123
|
-
|
|
35861
|
+
try {
|
|
35862
|
+
const pruned = await pruneStaleDeferredTurns();
|
|
35863
|
+
if (pruned.length > 0) {
|
|
35864
|
+
await appendHookDiagnosticsEvent({
|
|
35865
|
+
hook: "deferredTurnDrainer",
|
|
35866
|
+
sessionId: sessionMarker,
|
|
35867
|
+
stage: "ttl_pruned",
|
|
35868
|
+
result: "info",
|
|
35869
|
+
repoRoot,
|
|
35870
|
+
fields: { prunedCount: pruned.length }
|
|
35871
|
+
});
|
|
35872
|
+
}
|
|
35873
|
+
} catch {
|
|
35874
|
+
}
|
|
35875
|
+
} finally {
|
|
35876
|
+
clearInterval(heartbeat);
|
|
35877
|
+
await releaseDrainLock(acquireResult.lockPath);
|
|
35878
|
+
await appendHookDiagnosticsEvent({
|
|
35879
|
+
hook: "deferredTurnDrainer",
|
|
35880
|
+
sessionId: sessionMarker,
|
|
35881
|
+
stage: "drainer_completed",
|
|
35882
|
+
result: exitReason === "queue_empty" ? "success" : "info",
|
|
35883
|
+
reason: exitReason,
|
|
35884
|
+
repoRoot,
|
|
35885
|
+
fields: {
|
|
35886
|
+
recordedTotal,
|
|
35887
|
+
failedTotal,
|
|
35888
|
+
elapsedMs: Date.now() - startedAt
|
|
35889
|
+
}
|
|
35890
|
+
});
|
|
35124
35891
|
}
|
|
35125
|
-
};
|
|
35126
|
-
|
|
35127
|
-
// src/metadata.ts
|
|
35128
|
-
var pluginMetadata = {
|
|
35129
|
-
name: package_default.name,
|
|
35130
|
-
version: package_default.version,
|
|
35131
|
-
description: package_default.description,
|
|
35132
|
-
pluginId: "remix",
|
|
35133
|
-
agentName: "remix-collab"
|
|
35134
|
-
};
|
|
35135
|
-
|
|
35136
|
-
// src/hook-diagnostics.ts
|
|
35137
|
-
var MAX_LOG_BYTES = 512 * 1024;
|
|
35138
|
-
function resolveClaudeRoot() {
|
|
35139
|
-
const configured = process.env.CLAUDE_CONFIG_DIR?.trim();
|
|
35140
|
-
return configured || import_node_path7.default.join(import_node_os5.default.homedir(), ".claude");
|
|
35141
|
-
}
|
|
35142
|
-
function resolvePluginDataDirName() {
|
|
35143
|
-
return `${pluginMetadata.pluginId}-${pluginMetadata.pluginId}`;
|
|
35144
|
-
}
|
|
35145
|
-
function getHookDiagnosticsDirPath() {
|
|
35146
|
-
const configured = process.env.REMIX_CLAUDE_PLUGIN_HOOK_DIAGNOSTICS_DIR?.trim();
|
|
35147
|
-
return configured || import_node_path7.default.join(resolveClaudeRoot(), "plugins", "data", resolvePluginDataDirName());
|
|
35148
|
-
}
|
|
35149
|
-
function getHookDiagnosticsLogPath() {
|
|
35150
|
-
return import_node_path7.default.join(getHookDiagnosticsDirPath(), "hooks.ndjson");
|
|
35151
|
-
}
|
|
35152
|
-
function toFieldValue(value) {
|
|
35153
|
-
if (value === null) return null;
|
|
35154
|
-
if (typeof value === "string") return value;
|
|
35155
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
35156
|
-
if (typeof value === "boolean") return value;
|
|
35157
|
-
return void 0;
|
|
35158
35892
|
}
|
|
35159
|
-
function
|
|
35160
|
-
|
|
35161
|
-
|
|
35162
|
-
|
|
35163
|
-
|
|
35164
|
-
|
|
35165
|
-
|
|
35166
|
-
|
|
35167
|
-
|
|
35168
|
-
|
|
35169
|
-
|
|
35170
|
-
|
|
35893
|
+
function parseDeferredTurnDrainerArgv(argv) {
|
|
35894
|
+
for (let i2 = 0; i2 < argv.length; i2 += 1) {
|
|
35895
|
+
const arg = argv[i2];
|
|
35896
|
+
if (!arg) continue;
|
|
35897
|
+
if (arg === "--drain-deferred-turns") {
|
|
35898
|
+
const next = argv[i2 + 1];
|
|
35899
|
+
if (next && !next.startsWith("--")) return next;
|
|
35900
|
+
return null;
|
|
35901
|
+
}
|
|
35902
|
+
if (arg.startsWith("--drain-deferred-turns=")) {
|
|
35903
|
+
const value = arg.slice("--drain-deferred-turns=".length);
|
|
35904
|
+
return value || null;
|
|
35905
|
+
}
|
|
35171
35906
|
}
|
|
35172
|
-
|
|
35173
|
-
await import_promises20.default.rm(rotatedPath, { force: true }).catch(() => void 0);
|
|
35174
|
-
await import_promises20.default.rename(logPath, rotatedPath).catch(() => void 0);
|
|
35907
|
+
return null;
|
|
35175
35908
|
}
|
|
35176
|
-
function
|
|
35177
|
-
|
|
35178
|
-
|
|
35179
|
-
|
|
35180
|
-
|
|
35181
|
-
sha256Prefix: null
|
|
35182
|
-
};
|
|
35183
|
-
}
|
|
35184
|
-
const trimmed = value.trim();
|
|
35185
|
-
return {
|
|
35186
|
-
present: true,
|
|
35187
|
-
length: trimmed.length,
|
|
35188
|
-
sha256Prefix: (0, import_node_crypto2.createHash)("sha256").update(trimmed).digest("hex").slice(0, 12)
|
|
35189
|
-
};
|
|
35909
|
+
async function maybeRunDeferredTurnDrainerFromArgv() {
|
|
35910
|
+
const repoRoot = parseDeferredTurnDrainerArgv(process.argv);
|
|
35911
|
+
if (!repoRoot) return false;
|
|
35912
|
+
await runStandaloneDeferredTurnDrainer(repoRoot);
|
|
35913
|
+
return true;
|
|
35190
35914
|
}
|
|
35191
|
-
|
|
35192
|
-
|
|
35193
|
-
|
|
35194
|
-
|
|
35195
|
-
|
|
35196
|
-
|
|
35197
|
-
|
|
35198
|
-
|
|
35199
|
-
|
|
35200
|
-
|
|
35201
|
-
|
|
35202
|
-
|
|
35203
|
-
|
|
35204
|
-
|
|
35205
|
-
|
|
35206
|
-
|
|
35207
|
-
|
|
35208
|
-
message: params.message?.trim() || null,
|
|
35209
|
-
fields: normalizeFields(params.fields)
|
|
35210
|
-
};
|
|
35211
|
-
await import_promises20.default.appendFile(logPath, `${JSON.stringify(event)}
|
|
35212
|
-
`, "utf8");
|
|
35213
|
-
} catch {
|
|
35214
|
-
}
|
|
35915
|
+
|
|
35916
|
+
// src/spawn-helpers.ts
|
|
35917
|
+
var import_node_child_process7 = require("child_process");
|
|
35918
|
+
function spawnDeferredTurnDrainer(repoRoot) {
|
|
35919
|
+
const entrypoint = process.argv[1];
|
|
35920
|
+
if (!entrypoint) return;
|
|
35921
|
+
if (!repoRoot) return;
|
|
35922
|
+
const child = (0, import_node_child_process7.spawn)(
|
|
35923
|
+
process.execPath,
|
|
35924
|
+
[...process.execArgv, entrypoint, "--drain-deferred-turns", repoRoot],
|
|
35925
|
+
{
|
|
35926
|
+
detached: true,
|
|
35927
|
+
stdio: "ignore",
|
|
35928
|
+
env: process.env
|
|
35929
|
+
}
|
|
35930
|
+
);
|
|
35931
|
+
child.unref();
|
|
35215
35932
|
}
|
|
35216
35933
|
|
|
35217
35934
|
// node_modules/@remixhq/core/dist/history.js
|
|
35218
|
-
var
|
|
35935
|
+
var import_promises24 = __toESM(require("fs/promises"), 1);
|
|
35219
35936
|
async function readAndParseTranscript(transcriptPath) {
|
|
35220
35937
|
let raw;
|
|
35221
35938
|
try {
|
|
35222
|
-
raw = await
|
|
35939
|
+
raw = await import_promises24.default.readFile(transcriptPath, "utf8");
|
|
35223
35940
|
} catch (err) {
|
|
35224
35941
|
const code = err && typeof err === "object" && "code" in err ? err.code : null;
|
|
35225
35942
|
if (code === "ENOENT") {
|
|
@@ -35662,10 +36379,10 @@ function harvestClaudeCodeUsage(input) {
|
|
|
35662
36379
|
}
|
|
35663
36380
|
|
|
35664
36381
|
// src/usage/claudeCodeSession.ts
|
|
35665
|
-
var
|
|
35666
|
-
var
|
|
35667
|
-
var
|
|
35668
|
-
var
|
|
36382
|
+
var import_node_child_process8 = require("child_process");
|
|
36383
|
+
var import_node_fs7 = require("fs");
|
|
36384
|
+
var import_node_os7 = require("os");
|
|
36385
|
+
var import_node_path12 = require("path");
|
|
35669
36386
|
var CACHE_SCHEMA_VERSION = 1;
|
|
35670
36387
|
var SUCCESS_TTL_MS = 60 * 60 * 1e3;
|
|
35671
36388
|
var FAILURE_TTL_MS = 5 * 60 * 1e3;
|
|
@@ -35675,7 +36392,7 @@ var spawnerImpl = defaultSpawnClaudeAuthStatus;
|
|
|
35675
36392
|
function defaultSpawnClaudeAuthStatus(timeoutMs) {
|
|
35676
36393
|
let result;
|
|
35677
36394
|
try {
|
|
35678
|
-
result = (0,
|
|
36395
|
+
result = (0, import_node_child_process8.spawnSync)("claude", ["auth", "status", "--json"], {
|
|
35679
36396
|
stdio: ["ignore", "pipe", "pipe"],
|
|
35680
36397
|
timeout: timeoutMs,
|
|
35681
36398
|
env: process.env
|
|
@@ -35694,10 +36411,10 @@ function defaultSpawnClaudeAuthStatus(timeoutMs) {
|
|
|
35694
36411
|
}
|
|
35695
36412
|
function getCollabStateRoot2() {
|
|
35696
36413
|
const configured = process.env.REMIX_COLLAB_STATE_ROOT?.trim();
|
|
35697
|
-
return configured || (0,
|
|
36414
|
+
return configured || (0, import_node_path12.join)((0, import_node_os7.homedir)(), ".remix", "collab-state");
|
|
35698
36415
|
}
|
|
35699
36416
|
function getAuthCachePath() {
|
|
35700
|
-
return (0,
|
|
36417
|
+
return (0, import_node_path12.join)(getCollabStateRoot2(), "claude-auth-cache.json");
|
|
35701
36418
|
}
|
|
35702
36419
|
function getSpawnTimeoutMs() {
|
|
35703
36420
|
const raw = process.env.REMIX_CLAUDE_AUTH_TIMEOUT_MS?.trim();
|
|
@@ -35709,7 +36426,7 @@ function getSpawnTimeoutMs() {
|
|
|
35709
36426
|
function readAuthCache() {
|
|
35710
36427
|
let raw;
|
|
35711
36428
|
try {
|
|
35712
|
-
raw = (0,
|
|
36429
|
+
raw = (0, import_node_fs7.readFileSync)(getAuthCachePath(), "utf8");
|
|
35713
36430
|
} catch {
|
|
35714
36431
|
return null;
|
|
35715
36432
|
}
|
|
@@ -35738,10 +36455,10 @@ function isCacheFresh(record) {
|
|
|
35738
36455
|
function writeAuthCache(record) {
|
|
35739
36456
|
const cachePath = getAuthCachePath();
|
|
35740
36457
|
try {
|
|
35741
|
-
(0,
|
|
36458
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path12.dirname)(cachePath), { recursive: true });
|
|
35742
36459
|
const tmpPath = `${cachePath}.${process.pid}.${Date.now()}.tmp`;
|
|
35743
|
-
(0,
|
|
35744
|
-
(0,
|
|
36460
|
+
(0, import_node_fs7.writeFileSync)(tmpPath, JSON.stringify(record), "utf8");
|
|
36461
|
+
(0, import_node_fs7.renameSync)(tmpPath, cachePath);
|
|
35745
36462
|
} catch {
|
|
35746
36463
|
}
|
|
35747
36464
|
}
|
|
@@ -35795,8 +36512,8 @@ function resolveClaudeCodeSession(hookPayload) {
|
|
|
35795
36512
|
}
|
|
35796
36513
|
|
|
35797
36514
|
// src/hook-utils.ts
|
|
35798
|
-
var
|
|
35799
|
-
var
|
|
36515
|
+
var import_promises25 = __toESM(require("fs/promises"), 1);
|
|
36516
|
+
var import_node_path13 = __toESM(require("path"), 1);
|
|
35800
36517
|
async function readJsonStdin() {
|
|
35801
36518
|
const chunks = [];
|
|
35802
36519
|
for await (const chunk of process.stdin) {
|
|
@@ -35858,16 +36575,16 @@ function extractBoolean(input, keys) {
|
|
|
35858
36575
|
}
|
|
35859
36576
|
async function findBoundRepo(startPath) {
|
|
35860
36577
|
if (!startPath) return null;
|
|
35861
|
-
let current =
|
|
35862
|
-
let stats = await
|
|
36578
|
+
let current = import_node_path13.default.resolve(startPath);
|
|
36579
|
+
let stats = await import_promises25.default.stat(current).catch(() => null);
|
|
35863
36580
|
if (stats?.isFile()) {
|
|
35864
|
-
current =
|
|
36581
|
+
current = import_node_path13.default.dirname(current);
|
|
35865
36582
|
}
|
|
35866
36583
|
while (true) {
|
|
35867
|
-
const bindingPath =
|
|
35868
|
-
const bindingStats = await
|
|
36584
|
+
const bindingPath = import_node_path13.default.join(current, ".remix", "config.json");
|
|
36585
|
+
const bindingStats = await import_promises25.default.stat(bindingPath).catch(() => null);
|
|
35869
36586
|
if (bindingStats?.isFile()) return current;
|
|
35870
|
-
const parent =
|
|
36587
|
+
const parent = import_node_path13.default.dirname(current);
|
|
35871
36588
|
if (parent === current) return null;
|
|
35872
36589
|
current = parent;
|
|
35873
36590
|
}
|
|
@@ -35886,23 +36603,26 @@ async function resolveBoundRepoSummary(startPath) {
|
|
|
35886
36603
|
}
|
|
35887
36604
|
|
|
35888
36605
|
// src/hook-stop-collab.ts
|
|
35889
|
-
var
|
|
36606
|
+
var HOOK_ACTOR2 = {
|
|
35890
36607
|
type: "agent",
|
|
35891
36608
|
name: "claude-code",
|
|
35892
36609
|
version: pluginMetadata.version,
|
|
35893
36610
|
provider: "anthropic"
|
|
35894
36611
|
};
|
|
35895
|
-
var
|
|
36612
|
+
var collabFinalizeTurn3 = collabFinalizeTurn;
|
|
35896
36613
|
function getErrorDetails(error) {
|
|
35897
36614
|
if (error instanceof Error) {
|
|
35898
36615
|
const hint = typeof error.hint === "string" ? String(error.hint) : null;
|
|
36616
|
+
const codeRaw = error.code;
|
|
36617
|
+
const preflightCode = isFinalizePreflightFailureCode(codeRaw) ? codeRaw : null;
|
|
35899
36618
|
return {
|
|
35900
36619
|
message: error.message || "Fallback Remix turn recording failed.",
|
|
35901
|
-
hint
|
|
36620
|
+
hint,
|
|
36621
|
+
preflightCode
|
|
35902
36622
|
};
|
|
35903
36623
|
}
|
|
35904
36624
|
const message = typeof error === "string" && error.trim() ? error.trim() : "Fallback Remix turn recording failed.";
|
|
35905
|
-
return { message, hint: null };
|
|
36625
|
+
return { message, hint: null, preflightCode: null };
|
|
35906
36626
|
}
|
|
35907
36627
|
function buildRepoIdempotencyKey(turnId, repo) {
|
|
35908
36628
|
const repoToken = repo.currentAppId?.trim() || repo.repoRoot;
|
|
@@ -35957,7 +36677,7 @@ function createFallbackTouchedRepo(params) {
|
|
|
35957
36677
|
};
|
|
35958
36678
|
}
|
|
35959
36679
|
var TRANSCRIPT_FLUSH_RETRY_DELAYS_MS = [50, 100, 200];
|
|
35960
|
-
function
|
|
36680
|
+
function sleep6(ms) {
|
|
35961
36681
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
35962
36682
|
}
|
|
35963
36683
|
async function harvestTurnUsage(params) {
|
|
@@ -36002,7 +36722,7 @@ async function harvestTurnUsage(params) {
|
|
|
36002
36722
|
while (!currentResult.ok && currentResult.reason === "no_messages_for_turn") {
|
|
36003
36723
|
if (retriesUsed >= TRANSCRIPT_FLUSH_RETRY_DELAYS_MS.length) break;
|
|
36004
36724
|
const delayMs = TRANSCRIPT_FLUSH_RETRY_DELAYS_MS[retriesUsed];
|
|
36005
|
-
await
|
|
36725
|
+
await sleep6(delayMs);
|
|
36006
36726
|
retriesUsed += 1;
|
|
36007
36727
|
totalBackoffMs += delayMs;
|
|
36008
36728
|
const reparsed = await readAndParseTranscript(transcriptPath);
|
|
@@ -36207,9 +36927,18 @@ async function recordTouchedRepo(params) {
|
|
|
36207
36927
|
try {
|
|
36208
36928
|
const binding = await readCollabBinding(repo.repoRoot).catch(() => null);
|
|
36209
36929
|
if (!binding) {
|
|
36210
|
-
|
|
36930
|
+
const failure = {
|
|
36931
|
+
repoRoot: repo.repoRoot,
|
|
36211
36932
|
message: "Fallback Remix turn recording failed because the repository is no longer bound to Remix.",
|
|
36212
|
-
hint: `Repo root: ${repo.repoRoot}
|
|
36933
|
+
hint: `Repo root: ${repo.repoRoot}`,
|
|
36934
|
+
// Equivalent to the not_bound preflight code — the binding
|
|
36935
|
+
// disappeared between touch-time and finalize-time. Reusing the
|
|
36936
|
+
// code lets the dispatcher route this through the same recovery.
|
|
36937
|
+
preflightCode: "not_bound"
|
|
36938
|
+
};
|
|
36939
|
+
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, {
|
|
36940
|
+
message: failure.message,
|
|
36941
|
+
hint: failure.hint
|
|
36213
36942
|
});
|
|
36214
36943
|
await appendHookDiagnosticsEvent({
|
|
36215
36944
|
hook,
|
|
@@ -36220,19 +36949,20 @@ async function recordTouchedRepo(params) {
|
|
|
36220
36949
|
reason: "repo_not_bound",
|
|
36221
36950
|
repoRoot: repo.repoRoot
|
|
36222
36951
|
});
|
|
36223
|
-
return { recorded: false, queued: false };
|
|
36952
|
+
return { recorded: false, queued: false, failure };
|
|
36224
36953
|
}
|
|
36225
|
-
const result = await
|
|
36954
|
+
const result = await collabFinalizeTurn3({
|
|
36226
36955
|
api,
|
|
36227
36956
|
cwd: repo.repoRoot,
|
|
36228
36957
|
prompt,
|
|
36229
36958
|
assistantResponse,
|
|
36230
36959
|
idempotencyKey: buildRepoIdempotencyKey(turnId, repo),
|
|
36231
|
-
actor:
|
|
36960
|
+
actor: HOOK_ACTOR2,
|
|
36232
36961
|
turnUsage,
|
|
36233
36962
|
promptedAt: promptedAt ?? null
|
|
36234
36963
|
});
|
|
36235
36964
|
await markTouchedRepoStopRecorded(sessionId, repo.repoRoot, { mode: result.mode });
|
|
36965
|
+
await clearFinalizeFailureMarker(repo.repoRoot).catch(() => void 0);
|
|
36236
36966
|
await appendHookDiagnosticsEvent({
|
|
36237
36967
|
hook,
|
|
36238
36968
|
sessionId,
|
|
@@ -36242,10 +36972,17 @@ async function recordTouchedRepo(params) {
|
|
|
36242
36972
|
reason: result.mode,
|
|
36243
36973
|
repoRoot: repo.repoRoot
|
|
36244
36974
|
});
|
|
36245
|
-
return {
|
|
36975
|
+
return {
|
|
36976
|
+
recorded: true,
|
|
36977
|
+
queued: result.queued === true,
|
|
36978
|
+
failure: null
|
|
36979
|
+
};
|
|
36246
36980
|
} catch (error) {
|
|
36247
36981
|
const details = getErrorDetails(error);
|
|
36248
|
-
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot,
|
|
36982
|
+
await markTouchedRepoRecordingFailure(sessionId, repo.repoRoot, {
|
|
36983
|
+
message: details.message,
|
|
36984
|
+
hint: details.hint
|
|
36985
|
+
});
|
|
36249
36986
|
await appendHookDiagnosticsEvent({
|
|
36250
36987
|
hook,
|
|
36251
36988
|
sessionId,
|
|
@@ -36256,68 +36993,246 @@ async function recordTouchedRepo(params) {
|
|
|
36256
36993
|
repoRoot: repo.repoRoot,
|
|
36257
36994
|
message: details.message,
|
|
36258
36995
|
fields: {
|
|
36259
|
-
hint: details.hint
|
|
36996
|
+
hint: details.hint,
|
|
36997
|
+
preflightCode: details.preflightCode
|
|
36260
36998
|
}
|
|
36261
36999
|
});
|
|
36262
|
-
return {
|
|
37000
|
+
return {
|
|
37001
|
+
recorded: false,
|
|
37002
|
+
queued: false,
|
|
37003
|
+
failure: {
|
|
37004
|
+
repoRoot: repo.repoRoot,
|
|
37005
|
+
message: details.message,
|
|
37006
|
+
hint: details.hint,
|
|
37007
|
+
preflightCode: details.preflightCode
|
|
37008
|
+
}
|
|
37009
|
+
};
|
|
36263
37010
|
}
|
|
36264
37011
|
}
|
|
36265
37012
|
function spawnFinalizeQueueDrainer() {
|
|
36266
37013
|
const entrypoint = process.argv[1];
|
|
36267
37014
|
if (!entrypoint) return;
|
|
36268
|
-
const child = (0,
|
|
37015
|
+
const child = (0, import_node_child_process9.spawn)(process.execPath, [...process.execArgv, entrypoint, "--drain-finalize-queue"], {
|
|
36269
37016
|
detached: true,
|
|
36270
37017
|
stdio: "ignore",
|
|
36271
37018
|
env: process.env
|
|
36272
37019
|
});
|
|
36273
37020
|
child.unref();
|
|
36274
37021
|
}
|
|
36275
|
-
var
|
|
36276
|
-
|
|
36277
|
-
|
|
37022
|
+
var DEFERRED_TURN_PRUNE_LOG_LIMIT = 5;
|
|
37023
|
+
async function drainDeferredTurnsForRepo(params) {
|
|
37024
|
+
const { hook, sessionId, triggerTurnId, repoRoot, api } = params;
|
|
37025
|
+
let entries = [];
|
|
36278
37026
|
try {
|
|
36279
|
-
|
|
36280
|
-
|
|
37027
|
+
entries = await listDeferredTurnsForRepo(repoRoot);
|
|
37028
|
+
} catch (listErr) {
|
|
37029
|
+
await appendHookDiagnosticsEvent({
|
|
37030
|
+
hook,
|
|
37031
|
+
sessionId,
|
|
37032
|
+
turnId: triggerTurnId,
|
|
37033
|
+
stage: "deferred_turn_list_failed",
|
|
37034
|
+
result: "error",
|
|
37035
|
+
reason: "exception",
|
|
37036
|
+
repoRoot,
|
|
37037
|
+
message: listErr instanceof Error ? listErr.message : String(listErr)
|
|
37038
|
+
});
|
|
37039
|
+
return;
|
|
37040
|
+
}
|
|
37041
|
+
if (entries.length === 0) return;
|
|
37042
|
+
const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
|
|
37043
|
+
const currentBranch = bindingState?.currentBranch ?? null;
|
|
37044
|
+
const isCurrentBranchBound = bindingState?.binding != null;
|
|
37045
|
+
await appendHookDiagnosticsEvent({
|
|
37046
|
+
hook,
|
|
37047
|
+
sessionId,
|
|
37048
|
+
turnId: triggerTurnId,
|
|
37049
|
+
stage: "deferred_turn_drain_started",
|
|
37050
|
+
result: "info",
|
|
37051
|
+
repoRoot,
|
|
37052
|
+
fields: {
|
|
37053
|
+
candidateCount: entries.length,
|
|
37054
|
+
currentBranch,
|
|
37055
|
+
currentBranchBound: isCurrentBranchBound
|
|
37056
|
+
}
|
|
37057
|
+
});
|
|
37058
|
+
let recordedCount = 0;
|
|
37059
|
+
let skippedCount = 0;
|
|
37060
|
+
let failedCount = 0;
|
|
37061
|
+
for (const entry of entries) {
|
|
37062
|
+
const { record, filePath } = entry;
|
|
37063
|
+
if (!isCurrentBranchBound || record.branchAtDefer && record.branchAtDefer !== currentBranch) {
|
|
37064
|
+
skippedCount += 1;
|
|
37065
|
+
await appendHookDiagnosticsEvent({
|
|
37066
|
+
hook,
|
|
37067
|
+
sessionId,
|
|
37068
|
+
turnId: triggerTurnId,
|
|
37069
|
+
stage: "deferred_turn_skipped",
|
|
37070
|
+
result: "info",
|
|
37071
|
+
reason: "branch_mismatch",
|
|
37072
|
+
repoRoot,
|
|
37073
|
+
fields: {
|
|
37074
|
+
deferredTurnId: record.turnId,
|
|
37075
|
+
deferredSessionId: record.sessionId,
|
|
37076
|
+
branchAtDefer: record.branchAtDefer,
|
|
37077
|
+
currentBranch
|
|
37078
|
+
}
|
|
37079
|
+
});
|
|
37080
|
+
continue;
|
|
37081
|
+
}
|
|
37082
|
+
try {
|
|
37083
|
+
const idempotencyKey = `${record.turnId}:${repoRoot}:finalize_turn`;
|
|
37084
|
+
await collabFinalizeTurn3({
|
|
37085
|
+
api,
|
|
37086
|
+
cwd: repoRoot,
|
|
37087
|
+
prompt: record.prompt,
|
|
37088
|
+
assistantResponse: record.assistantResponse,
|
|
37089
|
+
idempotencyKey,
|
|
37090
|
+
actor: HOOK_ACTOR2,
|
|
37091
|
+
turnUsage: null,
|
|
37092
|
+
promptedAt: record.submittedAt
|
|
37093
|
+
});
|
|
37094
|
+
await deleteDeferredTurnFile(filePath);
|
|
37095
|
+
recordedCount += 1;
|
|
37096
|
+
await appendHookDiagnosticsEvent({
|
|
37097
|
+
hook,
|
|
37098
|
+
sessionId,
|
|
37099
|
+
turnId: triggerTurnId,
|
|
37100
|
+
stage: "deferred_turn_recorded",
|
|
37101
|
+
result: "success",
|
|
37102
|
+
repoRoot,
|
|
37103
|
+
fields: {
|
|
37104
|
+
deferredTurnId: record.turnId,
|
|
37105
|
+
deferredSessionId: record.sessionId,
|
|
37106
|
+
deferredAt: record.deferredAt,
|
|
37107
|
+
submittedAt: record.submittedAt,
|
|
37108
|
+
recordingDelayMs: Math.max(0, Date.now() - Date.parse(record.deferredAt))
|
|
37109
|
+
}
|
|
37110
|
+
});
|
|
37111
|
+
} catch (recordErr) {
|
|
37112
|
+
failedCount += 1;
|
|
37113
|
+
await appendHookDiagnosticsEvent({
|
|
37114
|
+
hook,
|
|
37115
|
+
sessionId,
|
|
37116
|
+
turnId: triggerTurnId,
|
|
37117
|
+
stage: "deferred_turn_record_failed",
|
|
37118
|
+
result: "error",
|
|
37119
|
+
reason: "exception",
|
|
37120
|
+
repoRoot,
|
|
37121
|
+
message: recordErr instanceof Error ? recordErr.message : String(recordErr),
|
|
37122
|
+
fields: {
|
|
37123
|
+
deferredTurnId: record.turnId,
|
|
37124
|
+
deferredSessionId: record.sessionId
|
|
37125
|
+
}
|
|
37126
|
+
});
|
|
36281
37127
|
}
|
|
36282
|
-
} catch (markerErr) {
|
|
36283
|
-
return {
|
|
36284
|
-
spawned: false,
|
|
36285
|
-
reason: "marker_check_failed",
|
|
36286
|
-
message: markerErr instanceof Error ? markerErr.message : String(markerErr)
|
|
36287
|
-
};
|
|
36288
37128
|
}
|
|
36289
|
-
const remixDir = import_node_path10.default.join(repoRoot, ".remix");
|
|
36290
37129
|
try {
|
|
36291
|
-
|
|
37130
|
+
const pruned = await pruneStaleDeferredTurns();
|
|
37131
|
+
for (const prunedPath of pruned.slice(0, DEFERRED_TURN_PRUNE_LOG_LIMIT)) {
|
|
37132
|
+
await appendHookDiagnosticsEvent({
|
|
37133
|
+
hook,
|
|
37134
|
+
sessionId,
|
|
37135
|
+
turnId: triggerTurnId,
|
|
37136
|
+
stage: "deferred_turn_pruned",
|
|
37137
|
+
result: "info",
|
|
37138
|
+
reason: "stale",
|
|
37139
|
+
repoRoot,
|
|
37140
|
+
fields: { prunedFilePath: prunedPath }
|
|
37141
|
+
});
|
|
37142
|
+
}
|
|
37143
|
+
if (pruned.length > DEFERRED_TURN_PRUNE_LOG_LIMIT) {
|
|
37144
|
+
await appendHookDiagnosticsEvent({
|
|
37145
|
+
hook,
|
|
37146
|
+
sessionId,
|
|
37147
|
+
turnId: triggerTurnId,
|
|
37148
|
+
stage: "deferred_turn_pruned",
|
|
37149
|
+
result: "info",
|
|
37150
|
+
reason: "stale_truncated",
|
|
37151
|
+
repoRoot,
|
|
37152
|
+
fields: {
|
|
37153
|
+
totalPruned: pruned.length,
|
|
37154
|
+
loggedPruned: DEFERRED_TURN_PRUNE_LOG_LIMIT
|
|
37155
|
+
}
|
|
37156
|
+
});
|
|
37157
|
+
}
|
|
36292
37158
|
} catch {
|
|
36293
37159
|
}
|
|
36294
|
-
|
|
36295
|
-
|
|
36296
|
-
|
|
36297
|
-
|
|
36298
|
-
|
|
36299
|
-
|
|
36300
|
-
|
|
36301
|
-
|
|
36302
|
-
|
|
36303
|
-
|
|
36304
|
-
|
|
36305
|
-
}
|
|
36306
|
-
}
|
|
37160
|
+
await appendHookDiagnosticsEvent({
|
|
37161
|
+
hook,
|
|
37162
|
+
sessionId,
|
|
37163
|
+
turnId: triggerTurnId,
|
|
37164
|
+
stage: "deferred_turn_drain_completed",
|
|
37165
|
+
result: "info",
|
|
37166
|
+
repoRoot,
|
|
37167
|
+
fields: {
|
|
37168
|
+
recordedCount,
|
|
37169
|
+
skippedCount,
|
|
37170
|
+
failedCount
|
|
37171
|
+
}
|
|
37172
|
+
});
|
|
37173
|
+
}
|
|
37174
|
+
async function deferTurnForRecoveryInProgress(params) {
|
|
37175
|
+
const { hook, sessionId, turnId, repoRoot, prompt, assistantResponse, submittedAt, preflightCode } = params;
|
|
37176
|
+
const bindingState = await readCollabBindingState(repoRoot).catch(() => null);
|
|
37177
|
+
const branchAtDefer = bindingState?.currentBranch ?? null;
|
|
36307
37178
|
try {
|
|
36308
|
-
const
|
|
36309
|
-
|
|
36310
|
-
|
|
36311
|
-
|
|
37179
|
+
const deferredFilePath = await writeDeferredTurn(
|
|
37180
|
+
buildDeferredTurnRecord({
|
|
37181
|
+
sessionId,
|
|
37182
|
+
turnId,
|
|
37183
|
+
repoRoot,
|
|
37184
|
+
prompt,
|
|
37185
|
+
assistantResponse,
|
|
37186
|
+
submittedAt,
|
|
37187
|
+
branchAtDefer,
|
|
37188
|
+
reason: "recovery_in_progress"
|
|
37189
|
+
})
|
|
37190
|
+
);
|
|
37191
|
+
await appendHookDiagnosticsEvent({
|
|
37192
|
+
hook,
|
|
37193
|
+
sessionId,
|
|
37194
|
+
turnId,
|
|
37195
|
+
stage: "turn_deferred",
|
|
37196
|
+
result: "success",
|
|
37197
|
+
reason: "recovery_in_progress",
|
|
37198
|
+
repoRoot,
|
|
37199
|
+
fields: {
|
|
37200
|
+
deferredFilePath,
|
|
37201
|
+
promptLength: prompt.length,
|
|
37202
|
+
assistantResponseLength: assistantResponse.length,
|
|
37203
|
+
branchAtDefer,
|
|
37204
|
+
preflightCode
|
|
37205
|
+
}
|
|
36312
37206
|
});
|
|
36313
|
-
|
|
36314
|
-
|
|
36315
|
-
|
|
36316
|
-
|
|
36317
|
-
|
|
36318
|
-
|
|
36319
|
-
|
|
36320
|
-
|
|
37207
|
+
spawnDeferredTurnDrainer(repoRoot);
|
|
37208
|
+
await appendHookDiagnosticsEvent({
|
|
37209
|
+
hook,
|
|
37210
|
+
sessionId,
|
|
37211
|
+
turnId,
|
|
37212
|
+
stage: "deferred_turn_drainer_spawned",
|
|
37213
|
+
result: "info",
|
|
37214
|
+
repoRoot,
|
|
37215
|
+
fields: {
|
|
37216
|
+
triggeredBy: "recovery_in_progress",
|
|
37217
|
+
preflightCode
|
|
37218
|
+
}
|
|
37219
|
+
});
|
|
37220
|
+
return deferredFilePath;
|
|
37221
|
+
} catch (deferErr) {
|
|
37222
|
+
await appendHookDiagnosticsEvent({
|
|
37223
|
+
hook,
|
|
37224
|
+
sessionId,
|
|
37225
|
+
turnId,
|
|
37226
|
+
stage: "deferred_turn_write_failed",
|
|
37227
|
+
result: "error",
|
|
37228
|
+
reason: "exception",
|
|
37229
|
+
repoRoot,
|
|
37230
|
+
message: deferErr instanceof Error ? deferErr.message : String(deferErr),
|
|
37231
|
+
fields: {
|
|
37232
|
+
preflightCode
|
|
37233
|
+
}
|
|
37234
|
+
});
|
|
37235
|
+
return null;
|
|
36321
37236
|
}
|
|
36322
37237
|
}
|
|
36323
37238
|
async function runHookStopCollab(payload) {
|
|
@@ -36395,6 +37310,35 @@ async function runHookStopCollab(payload) {
|
|
|
36395
37310
|
unboundBranchKnownCount = knownBoundBranches.length;
|
|
36396
37311
|
}
|
|
36397
37312
|
}
|
|
37313
|
+
const promptTextForDefer = state.prompt.trim();
|
|
37314
|
+
const assistantResponseForDefer = (extractAssistantResponse(payload) || "").trim();
|
|
37315
|
+
let deferredFilePath = null;
|
|
37316
|
+
if (skipReason === "current_branch_unbound" && unboundBranchRepoRoot && promptTextForDefer && assistantResponseForDefer) {
|
|
37317
|
+
try {
|
|
37318
|
+
deferredFilePath = await writeDeferredTurn(
|
|
37319
|
+
buildDeferredTurnRecord({
|
|
37320
|
+
sessionId,
|
|
37321
|
+
turnId: state.turnId,
|
|
37322
|
+
repoRoot: unboundBranchRepoRoot,
|
|
37323
|
+
prompt: promptTextForDefer,
|
|
37324
|
+
assistantResponse: assistantResponseForDefer,
|
|
37325
|
+
submittedAt: state.submittedAt,
|
|
37326
|
+
branchAtDefer: unboundBranchName
|
|
37327
|
+
})
|
|
37328
|
+
);
|
|
37329
|
+
} catch (deferErr) {
|
|
37330
|
+
await appendHookDiagnosticsEvent({
|
|
37331
|
+
hook,
|
|
37332
|
+
sessionId,
|
|
37333
|
+
turnId: state.turnId,
|
|
37334
|
+
stage: "deferred_turn_write_failed",
|
|
37335
|
+
result: "error",
|
|
37336
|
+
reason: "exception",
|
|
37337
|
+
repoRoot: unboundBranchRepoRoot,
|
|
37338
|
+
message: deferErr instanceof Error ? deferErr.message : String(deferErr)
|
|
37339
|
+
});
|
|
37340
|
+
}
|
|
37341
|
+
}
|
|
36398
37342
|
await clearPendingTurnState(sessionId);
|
|
36399
37343
|
await appendHookDiagnosticsEvent({
|
|
36400
37344
|
hook,
|
|
@@ -36406,16 +37350,44 @@ async function runHookStopCollab(payload) {
|
|
|
36406
37350
|
repoRoot: unboundBranchRepoRoot,
|
|
36407
37351
|
fields: skipReason === "current_branch_unbound" ? {
|
|
36408
37352
|
currentBranch: unboundBranchName,
|
|
36409
|
-
knownBoundBranchCount: unboundBranchKnownCount
|
|
37353
|
+
knownBoundBranchCount: unboundBranchKnownCount,
|
|
37354
|
+
deferredForRetry: deferredFilePath !== null
|
|
36410
37355
|
} : {}
|
|
36411
37356
|
});
|
|
37357
|
+
if (deferredFilePath && unboundBranchRepoRoot) {
|
|
37358
|
+
await appendHookDiagnosticsEvent({
|
|
37359
|
+
hook,
|
|
37360
|
+
sessionId,
|
|
37361
|
+
turnId: state.turnId,
|
|
37362
|
+
stage: "turn_deferred",
|
|
37363
|
+
result: "success",
|
|
37364
|
+
reason: "current_branch_unbound",
|
|
37365
|
+
repoRoot: unboundBranchRepoRoot,
|
|
37366
|
+
fields: {
|
|
37367
|
+
deferredFilePath,
|
|
37368
|
+
promptLength: promptTextForDefer.length,
|
|
37369
|
+
assistantResponseLength: assistantResponseForDefer.length,
|
|
37370
|
+
branchAtDefer: unboundBranchName
|
|
37371
|
+
}
|
|
37372
|
+
});
|
|
37373
|
+
spawnDeferredTurnDrainer(unboundBranchRepoRoot);
|
|
37374
|
+
await appendHookDiagnosticsEvent({
|
|
37375
|
+
hook,
|
|
37376
|
+
sessionId,
|
|
37377
|
+
turnId: state.turnId,
|
|
37378
|
+
stage: "deferred_turn_drainer_spawned",
|
|
37379
|
+
result: "info",
|
|
37380
|
+
repoRoot: unboundBranchRepoRoot,
|
|
37381
|
+
fields: { triggeredBy: "defer" }
|
|
37382
|
+
});
|
|
37383
|
+
}
|
|
36412
37384
|
await appendHookDiagnosticsEvent({
|
|
36413
37385
|
hook,
|
|
36414
37386
|
sessionId,
|
|
36415
37387
|
turnId: state.turnId,
|
|
36416
37388
|
stage: "state_cleanup",
|
|
36417
37389
|
result: "success",
|
|
36418
|
-
reason: "cleared_without_bound_repo"
|
|
37390
|
+
reason: deferredFilePath ? "cleared_after_defer" : "cleared_without_bound_repo"
|
|
36419
37391
|
});
|
|
36420
37392
|
return;
|
|
36421
37393
|
}
|
|
@@ -36491,10 +37463,20 @@ async function runHookStopCollab(payload) {
|
|
|
36491
37463
|
state: { turnId: state.turnId, prompt, submittedAt: state.submittedAt },
|
|
36492
37464
|
payload
|
|
36493
37465
|
});
|
|
37466
|
+
for (const repo of touchedRepos) {
|
|
37467
|
+
await drainDeferredTurnsForRepo({
|
|
37468
|
+
hook,
|
|
37469
|
+
sessionId,
|
|
37470
|
+
triggerTurnId: state.turnId,
|
|
37471
|
+
repoRoot: repo.repoRoot,
|
|
37472
|
+
api
|
|
37473
|
+
}).catch(() => void 0);
|
|
37474
|
+
}
|
|
36494
37475
|
let hadFailure = false;
|
|
36495
37476
|
let queuedFinalizeWork = false;
|
|
36496
37477
|
let anyRecorded = false;
|
|
36497
37478
|
let anyTurnExists = false;
|
|
37479
|
+
const failures = [];
|
|
36498
37480
|
for (const repo of touchedRepos) {
|
|
36499
37481
|
if (shouldSkipStopRecording(repo)) {
|
|
36500
37482
|
const legacyMcpFinalizeQueued = repo.manuallyRecordedByTool === "remix_collab_finalize_turn" && repo.manualRecordingScope === "full_turn";
|
|
@@ -36541,31 +37523,11 @@ async function runHookStopCollab(payload) {
|
|
|
36541
37523
|
if (recording.recorded) {
|
|
36542
37524
|
anyRecorded = true;
|
|
36543
37525
|
anyTurnExists = true;
|
|
36544
|
-
const autoSpawn = maybeAutoSpawnHistoryImportFromStopHook(repo.repoRoot);
|
|
36545
|
-
if (autoSpawn.spawned) {
|
|
36546
|
-
await appendHookDiagnosticsEvent({
|
|
36547
|
-
hook,
|
|
36548
|
-
sessionId,
|
|
36549
|
-
turnId: state.turnId,
|
|
36550
|
-
stage: "history_import_auto_spawned_from_stop",
|
|
36551
|
-
result: "success",
|
|
36552
|
-
repoRoot: repo.repoRoot,
|
|
36553
|
-
fields: { pid: autoSpawn.pid ?? null, logPath: autoSpawn.logPath }
|
|
36554
|
-
});
|
|
36555
|
-
} else if (autoSpawn.reason !== "marker_present") {
|
|
36556
|
-
await appendHookDiagnosticsEvent({
|
|
36557
|
-
hook,
|
|
36558
|
-
sessionId,
|
|
36559
|
-
turnId: state.turnId,
|
|
36560
|
-
stage: "history_import_auto_spawn_skipped",
|
|
36561
|
-
result: "info",
|
|
36562
|
-
reason: autoSpawn.reason,
|
|
36563
|
-
repoRoot: repo.repoRoot,
|
|
36564
|
-
message: autoSpawn.message ?? null
|
|
36565
|
-
});
|
|
36566
|
-
}
|
|
36567
37526
|
} else {
|
|
36568
37527
|
hadFailure = true;
|
|
37528
|
+
if (recording.failure) {
|
|
37529
|
+
failures.push(recording.failure);
|
|
37530
|
+
}
|
|
36569
37531
|
}
|
|
36570
37532
|
}
|
|
36571
37533
|
if (anyRecorded || anyTurnExists) {
|
|
@@ -36574,7 +37536,48 @@ async function runHookStopCollab(payload) {
|
|
|
36574
37536
|
if (queuedFinalizeWork) {
|
|
36575
37537
|
spawnFinalizeQueueDrainer();
|
|
36576
37538
|
}
|
|
36577
|
-
|
|
37539
|
+
let deferredFailureCount = 0;
|
|
37540
|
+
let dispatchFailureCount = 0;
|
|
37541
|
+
for (const failure of failures) {
|
|
37542
|
+
const outcome = await dispatchFinalizeFailure({
|
|
37543
|
+
hook: "Stop",
|
|
37544
|
+
sessionId,
|
|
37545
|
+
turnId: state.turnId,
|
|
37546
|
+
repoRoot: failure.repoRoot,
|
|
37547
|
+
preflightCode: failure.preflightCode,
|
|
37548
|
+
message: failure.message,
|
|
37549
|
+
hint: failure.hint
|
|
37550
|
+
}).catch((dispatchErr) => {
|
|
37551
|
+
dispatchFailureCount += 1;
|
|
37552
|
+
return appendHookDiagnosticsEvent({
|
|
37553
|
+
hook,
|
|
37554
|
+
sessionId,
|
|
37555
|
+
turnId: state.turnId,
|
|
37556
|
+
stage: "auto_fix_dispatch_failed",
|
|
37557
|
+
result: "error",
|
|
37558
|
+
reason: "exception",
|
|
37559
|
+
repoRoot: failure.repoRoot,
|
|
37560
|
+
message: dispatchErr instanceof Error ? dispatchErr.message : String(dispatchErr)
|
|
37561
|
+
}).then(() => null);
|
|
37562
|
+
});
|
|
37563
|
+
if (outcome && isAutoFixableFinalizeFailureCode(failure.preflightCode) && (outcome.kind === "spawned" || outcome.kind === "spawn_throttled")) {
|
|
37564
|
+
const deferredFilePath = await deferTurnForRecoveryInProgress({
|
|
37565
|
+
hook,
|
|
37566
|
+
sessionId,
|
|
37567
|
+
turnId: state.turnId,
|
|
37568
|
+
repoRoot: failure.repoRoot,
|
|
37569
|
+
prompt,
|
|
37570
|
+
assistantResponse,
|
|
37571
|
+
submittedAt: state.submittedAt,
|
|
37572
|
+
preflightCode: failure.preflightCode
|
|
37573
|
+
});
|
|
37574
|
+
if (deferredFilePath) {
|
|
37575
|
+
deferredFailureCount += 1;
|
|
37576
|
+
}
|
|
37577
|
+
}
|
|
37578
|
+
}
|
|
37579
|
+
const allFailuresDeferred = failures.length > 0 && deferredFailureCount === failures.length && dispatchFailureCount === 0;
|
|
37580
|
+
if (!hadFailure || allFailuresDeferred) {
|
|
36578
37581
|
await clearPendingTurnState(sessionId);
|
|
36579
37582
|
await appendHookDiagnosticsEvent({
|
|
36580
37583
|
hook,
|
|
@@ -36582,7 +37585,10 @@ async function runHookStopCollab(payload) {
|
|
|
36582
37585
|
turnId: state.turnId,
|
|
36583
37586
|
stage: "state_cleanup",
|
|
36584
37587
|
result: "success",
|
|
36585
|
-
reason: "cleared_after_success"
|
|
37588
|
+
reason: allFailuresDeferred ? "cleared_after_recovery_defer" : "cleared_after_success",
|
|
37589
|
+
fields: allFailuresDeferred ? {
|
|
37590
|
+
deferredFailureCount
|
|
37591
|
+
} : void 0
|
|
36586
37592
|
});
|
|
36587
37593
|
return;
|
|
36588
37594
|
}
|
|
@@ -36596,7 +37602,7 @@ async function runHookStopCollab(payload) {
|
|
|
36596
37602
|
});
|
|
36597
37603
|
} catch (error) {
|
|
36598
37604
|
const details = getErrorDetails(error);
|
|
36599
|
-
await markPendingTurnFailure(sessionId, details);
|
|
37605
|
+
await markPendingTurnFailure(sessionId, { message: details.message, hint: details.hint });
|
|
36600
37606
|
await appendHookDiagnosticsEvent({
|
|
36601
37607
|
hook,
|
|
36602
37608
|
sessionId,
|
|
@@ -36606,17 +37612,21 @@ async function runHookStopCollab(payload) {
|
|
|
36606
37612
|
reason: "exception",
|
|
36607
37613
|
message: details.message,
|
|
36608
37614
|
fields: {
|
|
36609
|
-
hint: details.hint
|
|
37615
|
+
hint: details.hint,
|
|
37616
|
+
preflightCode: details.preflightCode
|
|
36610
37617
|
}
|
|
36611
37618
|
});
|
|
36612
37619
|
}
|
|
36613
37620
|
}
|
|
36614
37621
|
async function main() {
|
|
37622
|
+
if (await maybeRunDeferredTurnDrainerFromArgv()) {
|
|
37623
|
+
return;
|
|
37624
|
+
}
|
|
36615
37625
|
if (process.argv.includes("--drain-finalize-queue")) {
|
|
36616
37626
|
const api = await createHookCollabApiClient();
|
|
36617
|
-
const
|
|
36618
|
-
if (typeof
|
|
36619
|
-
await
|
|
37627
|
+
const drainPendingFinalizeQueue3 = drainPendingFinalizeQueue;
|
|
37628
|
+
if (typeof drainPendingFinalizeQueue3 === "function") {
|
|
37629
|
+
await drainPendingFinalizeQueue3({ api });
|
|
36620
37630
|
}
|
|
36621
37631
|
return;
|
|
36622
37632
|
}
|