linkshell-cli 0.5.26 → 0.5.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/src/commands/hook.d.ts +6 -0
- package/dist/cli/src/commands/hook.js +120 -0
- package/dist/cli/src/commands/hook.js.map +1 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js +122 -0
- package/dist/cli/src/runtime/acp/agent-workspace.js.map +1 -1
- package/dist/cli/src/runtime/acp/claude-sdk-client.d.ts +8 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js +43 -0
- package/dist/cli/src/runtime/acp/claude-sdk-client.js.map +1 -1
- package/dist/cli/src/runtime/bridge-session.js +33 -4
- package/dist/cli/src/runtime/bridge-session.js.map +1 -1
- package/dist/cli/src/runtime/hook-permission-server.d.ts +36 -0
- package/dist/cli/src/runtime/hook-permission-server.js +147 -0
- package/dist/cli/src/runtime/hook-permission-server.js.map +1 -0
- package/dist/cli/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/runtime/acp/agent-workspace.ts +97 -0
- package/src/runtime/acp/claude-sdk-client.ts +35 -0
- package/src/runtime/bridge-session.ts +37 -3
- package/web/assets/{index-BF_KJjhI.css → index-CRyJIhW8.css} +1 -1
- package/web/assets/{index-IawebWL8.js → index-DABStn7S.js} +33 -33
- package/web/index.html +2 -2
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare const HOOK_SOCKET_PATH: string;
|
|
2
|
+
/** `linkshell hook` — invoked by Claude Code's PreToolUse / Notification hooks.
|
|
3
|
+
* Reads the hook event JSON on stdin, relays it to the local bridge over a Unix
|
|
4
|
+
* socket, and (for PreToolUse) prints the permission decision the user made
|
|
5
|
+
* remotely. Fail-open everywhere. */
|
|
6
|
+
export declare function runHook(event: string): Promise<void>;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { createConnection } from "node:net";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
// Path of the local Unix socket the running `linkshell` bridge listens on to
|
|
5
|
+
// relay external-session permission requests to phone/web clients. Kept under
|
|
6
|
+
// the same ~/.linkshell dir as the daemon pid/log files.
|
|
7
|
+
export const HOOK_SOCKET_PATH = join(homedir(), ".linkshell", "hook.sock");
|
|
8
|
+
// How long the hook waits for a remote decision before giving up. On timeout we
|
|
9
|
+
// fail OPEN (emit no decision → Claude's normal permission flow takes over), so
|
|
10
|
+
// an unattended phone never blocks a tool call forever.
|
|
11
|
+
const DECISION_TIMEOUT_MS = 25_000;
|
|
12
|
+
// Connecting to the socket must be near-instant; if the bridge isn't running
|
|
13
|
+
// the socket file may exist stale, so cap the connect attempt tightly.
|
|
14
|
+
const CONNECT_TIMEOUT_MS = 1_500;
|
|
15
|
+
/** Emit "no decision" and exit 0. Claude Code treats an empty stdout on exit 0
|
|
16
|
+
* as "the hook abstained", so the normal permission flow (terminal prompt /
|
|
17
|
+
* configured mode) proceeds. This is the fail-open path taken on ANY error —
|
|
18
|
+
* bridge not running, socket refused, timeout, malformed input — so installing
|
|
19
|
+
* the hook can never break or block a session that LinkShell isn't watching. */
|
|
20
|
+
function passThrough() {
|
|
21
|
+
process.exit(0);
|
|
22
|
+
}
|
|
23
|
+
function readStdin() {
|
|
24
|
+
return new Promise((resolve) => {
|
|
25
|
+
let data = "";
|
|
26
|
+
let done = false;
|
|
27
|
+
const finish = () => { if (!done) {
|
|
28
|
+
done = true;
|
|
29
|
+
resolve(data);
|
|
30
|
+
} };
|
|
31
|
+
process.stdin.setEncoding("utf8");
|
|
32
|
+
process.stdin.on("data", (chunk) => { data += chunk; });
|
|
33
|
+
process.stdin.on("end", finish);
|
|
34
|
+
process.stdin.on("error", finish);
|
|
35
|
+
// Hooks always get stdin, but never hang forever waiting for it.
|
|
36
|
+
setTimeout(finish, 1_000);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/** `linkshell hook` — invoked by Claude Code's PreToolUse / Notification hooks.
|
|
40
|
+
* Reads the hook event JSON on stdin, relays it to the local bridge over a Unix
|
|
41
|
+
* socket, and (for PreToolUse) prints the permission decision the user made
|
|
42
|
+
* remotely. Fail-open everywhere. */
|
|
43
|
+
export async function runHook(event) {
|
|
44
|
+
let payload = {};
|
|
45
|
+
try {
|
|
46
|
+
const raw = (await readStdin()).trim();
|
|
47
|
+
if (raw)
|
|
48
|
+
payload = JSON.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return passThrough(); // unparseable stdin — abstain
|
|
52
|
+
}
|
|
53
|
+
const decision = await requestDecision(event, payload);
|
|
54
|
+
// Notification events (and any non-decision) produce no stdout — they're
|
|
55
|
+
// fire-and-forget "session needs attention" signals, not gates.
|
|
56
|
+
if (event === "PreToolUse" && decision && (decision === "allow" || decision === "deny" || decision === "ask")) {
|
|
57
|
+
process.stdout.write(JSON.stringify({
|
|
58
|
+
hookSpecificOutput: {
|
|
59
|
+
hookEventName: "PreToolUse",
|
|
60
|
+
permissionDecision: decision,
|
|
61
|
+
permissionDecisionReason: "LinkShell remote decision",
|
|
62
|
+
},
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
/** Connect to the bridge socket, send the event, wait for a decision line.
|
|
68
|
+
* Resolves to "allow"|"deny"|"ask" on success, or undefined on any failure
|
|
69
|
+
* (caller then abstains). Never throws. */
|
|
70
|
+
function requestDecision(event, payload) {
|
|
71
|
+
return new Promise((resolve) => {
|
|
72
|
+
let settled = false;
|
|
73
|
+
const done = (v) => {
|
|
74
|
+
if (settled)
|
|
75
|
+
return;
|
|
76
|
+
settled = true;
|
|
77
|
+
try {
|
|
78
|
+
socket.destroy();
|
|
79
|
+
}
|
|
80
|
+
catch { /* ignore */ }
|
|
81
|
+
clearTimeout(connectTimer);
|
|
82
|
+
clearTimeout(decisionTimer);
|
|
83
|
+
resolve(v);
|
|
84
|
+
};
|
|
85
|
+
const socket = createConnection(HOOK_SOCKET_PATH);
|
|
86
|
+
let buffer = "";
|
|
87
|
+
const connectTimer = setTimeout(() => done(undefined), CONNECT_TIMEOUT_MS);
|
|
88
|
+
// For Notification we don't need a decision; a short window to deliver the
|
|
89
|
+
// message is enough. For PreToolUse we wait for the human.
|
|
90
|
+
const decisionTimer = setTimeout(() => done(undefined), event === "PreToolUse" ? DECISION_TIMEOUT_MS : 2_000);
|
|
91
|
+
socket.on("connect", () => {
|
|
92
|
+
clearTimeout(connectTimer);
|
|
93
|
+
try {
|
|
94
|
+
socket.write(JSON.stringify({ type: "hook", event, payload }) + "\n");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
done(undefined);
|
|
98
|
+
}
|
|
99
|
+
// Notification: deliver and go; don't wait for a reply.
|
|
100
|
+
if (event !== "PreToolUse")
|
|
101
|
+
setTimeout(() => done(undefined), 300);
|
|
102
|
+
});
|
|
103
|
+
socket.on("data", (chunk) => {
|
|
104
|
+
buffer += chunk.toString("utf8");
|
|
105
|
+
const nl = buffer.indexOf("\n");
|
|
106
|
+
if (nl === -1)
|
|
107
|
+
return;
|
|
108
|
+
try {
|
|
109
|
+
const msg = JSON.parse(buffer.slice(0, nl));
|
|
110
|
+
done(typeof msg.decision === "string" ? msg.decision : undefined);
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
done(undefined);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
socket.on("error", () => done(undefined));
|
|
117
|
+
socket.on("close", () => done(undefined));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
//# sourceMappingURL=hook.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"hook.js","sourceRoot":"","sources":["../../../../src/commands/hook.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AAC5C,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAEjC,6EAA6E;AAC7E,8EAA8E;AAC9E,yDAAyD;AACzD,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;AAE3E,gFAAgF;AAChF,gFAAgF;AAChF,wDAAwD;AACxD,MAAM,mBAAmB,GAAG,MAAM,CAAC;AACnC,6EAA6E;AAC7E,uEAAuE;AACvE,MAAM,kBAAkB,GAAG,KAAK,CAAC;AAEjC;;;;iFAIiF;AACjF,SAAS,WAAW;IAClB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED,SAAS,SAAS;IAChB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,IAAI,GAAG,EAAE,CAAC;QACd,IAAI,IAAI,GAAG,KAAK,CAAC;QACjB,MAAM,MAAM,GAAG,GAAG,EAAE,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;YAAC,IAAI,GAAG,IAAI,CAAC;YAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QAAC,CAAC,CAAC,CAAC,CAAC;QACpE,OAAO,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAClC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,GAAG,IAAI,IAAI,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;QACxD,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAChC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;QAClC,iEAAiE;QACjE,UAAU,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;sCAGsC;AACtC,MAAM,CAAC,KAAK,UAAU,OAAO,CAAC,KAAa;IACzC,IAAI,OAAO,GAA4B,EAAE,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,GAAG,GAAG,CAAC,MAAM,SAAS,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;QACvC,IAAI,GAAG;YAAE,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAA4B,CAAC;IAChE,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,WAAW,EAAE,CAAC,CAAC,8BAA8B;IACtD,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;IACvD,yEAAyE;IACzE,gEAAgE;IAChE,IAAI,KAAK,KAAK,YAAY,IAAI,QAAQ,IAAI,CAAC,QAAQ,KAAK,OAAO,IAAI,QAAQ,KAAK,MAAM,IAAI,QAAQ,KAAK,KAAK,CAAC,EAAE,CAAC;QAC9G,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,IAAI,CAAC,SAAS,CAAC;YACb,kBAAkB,EAAE;gBAClB,aAAa,EAAE,YAAY;gBAC3B,kBAAkB,EAAE,QAAQ;gBAC5B,wBAAwB,EAAE,2BAA2B;aACtD;SACF,CAAC,CACH,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC;AAED;;4CAE4C;AAC5C,SAAS,eAAe,CAAC,KAAa,EAAE,OAAgC;IACtE,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,MAAM,IAAI,GAAG,CAAC,CAAqB,EAAE,EAAE;YACrC,IAAI,OAAO;gBAAE,OAAO;YACpB,OAAO,GAAG,IAAI,CAAC;YACf,IAAI,CAAC;gBAAC,MAAM,CAAC,OAAO,EAAE,CAAC;YAAC,CAAC;YAAC,MAAM,CAAC,CAAC,YAAY,CAAC,CAAC;YAChD,YAAY,CAAC,YAAY,CAAC,CAAC;YAC3B,YAAY,CAAC,aAAa,CAAC,CAAC;YAC5B,OAAO,CAAC,CAAC,CAAC,CAAC;QACb,CAAC,CAAC;QAEF,MAAM,MAAM,GAAG,gBAAgB,CAAC,gBAAgB,CAAC,CAAC;QAClD,IAAI,MAAM,GAAG,EAAE,CAAC;QAEhB,MAAM,YAAY,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,kBAAkB,CAAC,CAAC;QAC3E,2EAA2E;QAC3E,2DAA2D;QAC3D,MAAM,aAAa,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,KAAK,KAAK,YAAY,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;QAE9G,MAAM,CAAC,EAAE,CAAC,SAAS,EAAE,GAAG,EAAE;YACxB,YAAY,CAAC,YAAY,CAAC,CAAC;YAC3B,IAAI,CAAC;gBACH,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;YACxE,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,SAAS,CAAC,CAAC;YAClB,CAAC;YACD,wDAAwD;YACxD,IAAI,KAAK,KAAK,YAAY;gBAAE,UAAU,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,GAAG,CAAC,CAAC;QACrE,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE;YAC1B,MAAM,IAAI,KAAK,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YACjC,MAAM,EAAE,GAAG,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;YAChC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAAE,OAAO;YACtB,IAAI,CAAC;gBACH,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAA0B,CAAC;gBACrE,IAAI,CAAC,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;YACpE,CAAC;YAAC,MAAM,CAAC;gBACP,IAAI,CAAC,SAAS,CAAC,CAAC;YAClB,CAAC;QACH,CAAC,CAAC,CAAC;QAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;IAC5C,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -1215,6 +1215,98 @@ function runningCodexSessionIds(nowMs) {
|
|
|
1215
1215
|
walk(root);
|
|
1216
1216
|
return running;
|
|
1217
1217
|
}
|
|
1218
|
+
/** Read a Codex session's last-known settings (reasoning effort, plan mode,
|
|
1219
|
+
* permission/sandbox) from its on-disk rollout, so opening a session LinkShell
|
|
1220
|
+
* never drove shows the config it actually ran with. Codex records the live
|
|
1221
|
+
* turn config in `turn_context` records; the last one is the current config.
|
|
1222
|
+
* Matched by the filename session id (cheap — no per-file reads to locate it),
|
|
1223
|
+
* newest first. Only called on open (one file), never per-list. */
|
|
1224
|
+
function codexSessionConfig(sessionId) {
|
|
1225
|
+
const root = join(homedir(), ".codex", "sessions");
|
|
1226
|
+
if (!existsSync(root))
|
|
1227
|
+
return {};
|
|
1228
|
+
const ID_RE = /^rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-(.+)\.jsonl$/;
|
|
1229
|
+
const matches = [];
|
|
1230
|
+
const walk = (dir) => {
|
|
1231
|
+
let entries;
|
|
1232
|
+
try {
|
|
1233
|
+
entries = readdirSync(dir);
|
|
1234
|
+
}
|
|
1235
|
+
catch {
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
for (const name of entries) {
|
|
1239
|
+
const path = join(dir, name);
|
|
1240
|
+
let stat;
|
|
1241
|
+
try {
|
|
1242
|
+
stat = statSync(path);
|
|
1243
|
+
}
|
|
1244
|
+
catch {
|
|
1245
|
+
continue;
|
|
1246
|
+
}
|
|
1247
|
+
if (stat.isDirectory()) {
|
|
1248
|
+
walk(path);
|
|
1249
|
+
continue;
|
|
1250
|
+
}
|
|
1251
|
+
const match = ID_RE.exec(name);
|
|
1252
|
+
if (match && match[1] === sessionId)
|
|
1253
|
+
matches.push({ path, mt: stat.mtimeMs });
|
|
1254
|
+
}
|
|
1255
|
+
};
|
|
1256
|
+
walk(root);
|
|
1257
|
+
if (matches.length === 0)
|
|
1258
|
+
return {};
|
|
1259
|
+
matches.sort((a, b) => b.mt - a.mt);
|
|
1260
|
+
const newest = matches[0];
|
|
1261
|
+
if (!newest)
|
|
1262
|
+
return {};
|
|
1263
|
+
let lastTurnContext;
|
|
1264
|
+
try {
|
|
1265
|
+
const text = readFileSync(newest.path, "utf8");
|
|
1266
|
+
for (const line of text.split(/\r?\n/)) {
|
|
1267
|
+
if (!line.trim())
|
|
1268
|
+
continue;
|
|
1269
|
+
let record;
|
|
1270
|
+
try {
|
|
1271
|
+
record = asRecord(JSON.parse(line));
|
|
1272
|
+
}
|
|
1273
|
+
catch {
|
|
1274
|
+
continue;
|
|
1275
|
+
}
|
|
1276
|
+
if (record?.type === "turn_context") {
|
|
1277
|
+
lastTurnContext = asRecord(record.payload) ?? record;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
catch {
|
|
1282
|
+
return {};
|
|
1283
|
+
}
|
|
1284
|
+
if (!lastTurnContext)
|
|
1285
|
+
return {};
|
|
1286
|
+
const cfg = {};
|
|
1287
|
+
const collab = asRecord(lastTurnContext.collaboration_mode);
|
|
1288
|
+
const effort = firstString(asRecord(collab?.settings), ["reasoning_effort"]);
|
|
1289
|
+
if (effort)
|
|
1290
|
+
cfg.reasoningEffort = effort;
|
|
1291
|
+
const mode = firstString(collab, ["mode"]);
|
|
1292
|
+
if (mode === "plan")
|
|
1293
|
+
cfg.collaborationMode = "plan";
|
|
1294
|
+
else if (mode)
|
|
1295
|
+
cfg.collaborationMode = "default";
|
|
1296
|
+
// Map Codex's sandbox policy (kebab-case, e.g. "danger-full-access") to ours.
|
|
1297
|
+
const sandbox = firstString(asRecord(lastTurnContext.sandbox_policy), ["type"]);
|
|
1298
|
+
if (sandbox) {
|
|
1299
|
+
const norm = sandbox.replace(/-/g, "").toLowerCase();
|
|
1300
|
+
cfg.permissionMode = norm.includes("readonly")
|
|
1301
|
+
? "read_only"
|
|
1302
|
+
: norm.includes("workspace")
|
|
1303
|
+
? "workspace_write"
|
|
1304
|
+
: norm.includes("danger") || norm.includes("full")
|
|
1305
|
+
? "full_access"
|
|
1306
|
+
: undefined;
|
|
1307
|
+
}
|
|
1308
|
+
return cfg;
|
|
1309
|
+
}
|
|
1218
1310
|
function parseRemoteSessions(value) {
|
|
1219
1311
|
const raw = asRecord(value);
|
|
1220
1312
|
const sessionsValue = Array.isArray(value) ? value :
|
|
@@ -2087,6 +2179,36 @@ export class AgentWorkspaceProxy {
|
|
|
2087
2179
|
const model = firstString(thread, ["model", "modelId", "currentModel"]);
|
|
2088
2180
|
if (model && !conversation.model)
|
|
2089
2181
|
conversation.model = model;
|
|
2182
|
+
// Fill the control settings (reasoning effort / permission / plan mode) from
|
|
2183
|
+
// the on-disk transcript when we don't already have them, so opening a
|
|
2184
|
+
// session LinkShell never drove shows what it actually ran with instead of
|
|
2185
|
+
// the UI's first-option default. Existing (LinkShell-set) values win — this
|
|
2186
|
+
// only backfills. One file read, on open only; never per-list.
|
|
2187
|
+
if (conversation.reasoningEffort === undefined &&
|
|
2188
|
+
conversation.permissionMode === undefined &&
|
|
2189
|
+
conversation.collaborationMode === undefined) {
|
|
2190
|
+
try {
|
|
2191
|
+
let cfg = {};
|
|
2192
|
+
if (conversation.provider === "codex") {
|
|
2193
|
+
cfg = codexSessionConfig(conversation.agentSessionId);
|
|
2194
|
+
}
|
|
2195
|
+
else {
|
|
2196
|
+
const readConfig = client.readSessionConfig;
|
|
2197
|
+
if (typeof readConfig === "function") {
|
|
2198
|
+
cfg = (asRecord(await readConfig.call(client, { sessionId: conversation.agentSessionId })) ?? {});
|
|
2199
|
+
}
|
|
2200
|
+
}
|
|
2201
|
+
if (cfg.reasoningEffort && !conversation.reasoningEffort)
|
|
2202
|
+
conversation.reasoningEffort = cfg.reasoningEffort;
|
|
2203
|
+
if (cfg.permissionMode && !conversation.permissionMode)
|
|
2204
|
+
conversation.permissionMode = cfg.permissionMode;
|
|
2205
|
+
if (cfg.collaborationMode && !conversation.collaborationMode)
|
|
2206
|
+
conversation.collaborationMode = cfg.collaborationMode;
|
|
2207
|
+
}
|
|
2208
|
+
catch {
|
|
2209
|
+
// Best-effort — a missing/unreadable transcript just leaves the pills at 默认.
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2090
2212
|
// Seed a scroll-back cursor for Claude, which hydrates via readSession (full
|
|
2091
2213
|
// transcript) yet pages via listTurns. The timeline only keeps the last
|
|
2092
2214
|
// MAX_TIMELINE_ITEMS, so anything older is dropped here but reachable via
|