offhands 0.1.4 → 0.1.7
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/approval-mcp.mjs +50 -10
- package/dist/daemon.mjs +1251 -189
- package/package.json +1 -1
- package/README.internal.md +0 -49
package/dist/daemon.mjs
CHANGED
|
@@ -79,9 +79,9 @@ var require_buffer_util = __commonJS({
|
|
|
79
79
|
}
|
|
80
80
|
return target;
|
|
81
81
|
}
|
|
82
|
-
function _mask(source, mask,
|
|
82
|
+
function _mask(source, mask, output2, offset, length) {
|
|
83
83
|
for (let i2 = 0; i2 < length; i2++) {
|
|
84
|
-
|
|
84
|
+
output2[offset + i2] = source[i2] ^ mask[i2 & 3];
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
function _unmask(buffer, mask) {
|
|
@@ -119,9 +119,9 @@ var require_buffer_util = __commonJS({
|
|
|
119
119
|
if (!process.env.WS_NO_BUFFER_UTIL) {
|
|
120
120
|
try {
|
|
121
121
|
const bufferUtil = __require("bufferutil");
|
|
122
|
-
module2.exports.mask = function(source, mask,
|
|
123
|
-
if (length < 48) _mask(source, mask,
|
|
124
|
-
else bufferUtil.mask(source, mask,
|
|
122
|
+
module2.exports.mask = function(source, mask, output2, offset, length) {
|
|
123
|
+
if (length < 48) _mask(source, mask, output2, offset, length);
|
|
124
|
+
else bufferUtil.mask(source, mask, output2, offset, length);
|
|
125
125
|
};
|
|
126
126
|
module2.exports.unmask = function(buffer, mask) {
|
|
127
127
|
if (buffer.length < 32) _unmask(buffer, mask);
|
|
@@ -4719,7 +4719,7 @@ var require_main = __commonJS({
|
|
|
4719
4719
|
var qrcode = new QRCode(-1, this.error);
|
|
4720
4720
|
qrcode.addData(input);
|
|
4721
4721
|
qrcode.make();
|
|
4722
|
-
var
|
|
4722
|
+
var output2 = "";
|
|
4723
4723
|
if (opts && opts.small) {
|
|
4724
4724
|
var BLACK = true, WHITE = false;
|
|
4725
4725
|
var moduleCount = qrcode.getModuleCount();
|
|
@@ -4736,37 +4736,37 @@ var require_main = __commonJS({
|
|
|
4736
4736
|
};
|
|
4737
4737
|
var borderTop = repeat(platte.BLACK_WHITE).times(moduleCount + 3);
|
|
4738
4738
|
var borderBottom = repeat(platte.WHITE_BLACK).times(moduleCount + 3);
|
|
4739
|
-
|
|
4739
|
+
output2 += borderTop + "\n";
|
|
4740
4740
|
for (var row = 0; row < moduleCount; row += 2) {
|
|
4741
|
-
|
|
4741
|
+
output2 += platte.WHITE_ALL;
|
|
4742
4742
|
for (var col = 0; col < moduleCount; col++) {
|
|
4743
4743
|
if (moduleData[row][col] === WHITE && moduleData[row + 1][col] === WHITE) {
|
|
4744
|
-
|
|
4744
|
+
output2 += platte.WHITE_ALL;
|
|
4745
4745
|
} else if (moduleData[row][col] === WHITE && moduleData[row + 1][col] === BLACK) {
|
|
4746
|
-
|
|
4746
|
+
output2 += platte.WHITE_BLACK;
|
|
4747
4747
|
} else if (moduleData[row][col] === BLACK && moduleData[row + 1][col] === WHITE) {
|
|
4748
|
-
|
|
4748
|
+
output2 += platte.BLACK_WHITE;
|
|
4749
4749
|
} else {
|
|
4750
|
-
|
|
4750
|
+
output2 += platte.BLACK_ALL;
|
|
4751
4751
|
}
|
|
4752
4752
|
}
|
|
4753
|
-
|
|
4753
|
+
output2 += platte.WHITE_ALL + "\n";
|
|
4754
4754
|
}
|
|
4755
4755
|
if (!oddRow) {
|
|
4756
|
-
|
|
4756
|
+
output2 += borderBottom;
|
|
4757
4757
|
}
|
|
4758
4758
|
} else {
|
|
4759
4759
|
var border = repeat(white).times(qrcode.getModuleCount() + 3);
|
|
4760
|
-
|
|
4760
|
+
output2 += border + "\n";
|
|
4761
4761
|
qrcode.modules.forEach(function(row2) {
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4762
|
+
output2 += white;
|
|
4763
|
+
output2 += row2.map(toCell).join("");
|
|
4764
|
+
output2 += white + "\n";
|
|
4765
4765
|
});
|
|
4766
|
-
|
|
4766
|
+
output2 += border;
|
|
4767
4767
|
}
|
|
4768
|
-
if (cb) cb(
|
|
4769
|
-
else console.log(
|
|
4768
|
+
if (cb) cb(output2);
|
|
4769
|
+
else console.log(output2);
|
|
4770
4770
|
},
|
|
4771
4771
|
setErrorLevel: function(error) {
|
|
4772
4772
|
this.error = QRErrorCorrectLevel[error] || this.error;
|
|
@@ -4777,7 +4777,7 @@ var require_main = __commonJS({
|
|
|
4777
4777
|
|
|
4778
4778
|
// ../daemon/src/index.ts
|
|
4779
4779
|
import { resolve as resolve5 } from "node:path";
|
|
4780
|
-
import { existsSync as
|
|
4780
|
+
import { existsSync as existsSync10 } from "node:fs";
|
|
4781
4781
|
|
|
4782
4782
|
// ../daemon/src/runners/claude-code.ts
|
|
4783
4783
|
import { spawn } from "node:child_process";
|
|
@@ -4862,10 +4862,12 @@ function mapClaudeEvent(value) {
|
|
|
4862
4862
|
for (const block of content) {
|
|
4863
4863
|
if (block?.type === "tool_use" && typeof block.name === "string") {
|
|
4864
4864
|
if (block.name.startsWith("mcp__offhand__")) continue;
|
|
4865
|
+
const todos = block.name === "TodoWrite" ? extractTodos(block.input) : void 0;
|
|
4865
4866
|
events.push({
|
|
4866
4867
|
type: "tool",
|
|
4867
4868
|
name: block.name,
|
|
4868
|
-
summary: summariseToolInput(block.name, block.input)
|
|
4869
|
+
summary: todos ? summariseTodos(todos) : summariseToolInput(block.name, block.input),
|
|
4870
|
+
...todos ? { todos } : {}
|
|
4869
4871
|
});
|
|
4870
4872
|
}
|
|
4871
4873
|
}
|
|
@@ -4894,6 +4896,29 @@ function contextUsage(usage, costUsd) {
|
|
|
4894
4896
|
if (contextTokens <= 0) return null;
|
|
4895
4897
|
return { type: "usage", contextTokens, contextWindow: 2e5, ...costUsd !== void 0 ? { costUsd } : {} };
|
|
4896
4898
|
}
|
|
4899
|
+
function extractTodos(input) {
|
|
4900
|
+
if (typeof input !== "object" || input === null) return void 0;
|
|
4901
|
+
const list = input.todos;
|
|
4902
|
+
if (!Array.isArray(list)) return void 0;
|
|
4903
|
+
const todos = [];
|
|
4904
|
+
for (const item of list) {
|
|
4905
|
+
if (typeof item !== "object" || item === null) return void 0;
|
|
4906
|
+
const t2 = item;
|
|
4907
|
+
if (typeof t2.content !== "string") return void 0;
|
|
4908
|
+
if (t2.status !== "pending" && t2.status !== "in_progress" && t2.status !== "completed") return void 0;
|
|
4909
|
+
todos.push({
|
|
4910
|
+
content: t2.content,
|
|
4911
|
+
status: t2.status,
|
|
4912
|
+
...typeof t2.activeForm === "string" ? { activeForm: t2.activeForm } : {}
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
return todos;
|
|
4916
|
+
}
|
|
4917
|
+
function summariseTodos(todos) {
|
|
4918
|
+
const active = todos.find((t2) => t2.status === "in_progress");
|
|
4919
|
+
const done = todos.filter((t2) => t2.status === "completed").length;
|
|
4920
|
+
return active ? `TodoWrite: ${active.activeForm ?? active.content} (${done}/${todos.length} done)` : `TodoWrite: ${done}/${todos.length} done`;
|
|
4921
|
+
}
|
|
4897
4922
|
function summariseToolInput(name, input) {
|
|
4898
4923
|
if (typeof input !== "object" || input === null) return name;
|
|
4899
4924
|
const i2 = input;
|
|
@@ -4941,8 +4966,16 @@ var ClaudeCodeRunner = class {
|
|
|
4941
4966
|
}
|
|
4942
4967
|
id = "claude-code";
|
|
4943
4968
|
name = "Claude Code";
|
|
4944
|
-
|
|
4969
|
+
// No enumeration command exists (checked `claude --help` in full against
|
|
4970
|
+
// CLI v2.1.263 — no `models` subcommand, no --list-models flag). These are
|
|
4971
|
+
// the aliases `--model`'s own help text names as examples ('fable', 'opus',
|
|
4972
|
+
// 'sonnet'), plus 'haiku' (long-standing, not superseded by anything found
|
|
4973
|
+
// here) — hardcoded because there is genuinely nothing to query, not out
|
|
4974
|
+
// of laziness. Re-verify against `claude --help` when bumping this list.
|
|
4975
|
+
models = ["sonnet", "opus", "haiku", "fable"];
|
|
4945
4976
|
supportsApprovals = true;
|
|
4977
|
+
// Real: every tier maps to a MAX_THINKING_TOKENS value below (see start()).
|
|
4978
|
+
effortTiers = ["low", "medium", "high", "max"];
|
|
4946
4979
|
loggedIn() {
|
|
4947
4980
|
return existsSync(join(homedir(), ".claude", ".credentials.json"));
|
|
4948
4981
|
}
|
|
@@ -4988,7 +5021,15 @@ var ClaudeCodeRunner = class {
|
|
|
4988
5021
|
offhand: {
|
|
4989
5022
|
command: process.execPath,
|
|
4990
5023
|
args: [APPROVAL_MCP_PATH],
|
|
4991
|
-
|
|
5024
|
+
// OFFHAND_STATUS_URL is derived from approvalUrl rather than a
|
|
5025
|
+
// second constructor param — same daemon, same port, one fewer
|
|
5026
|
+
// thing to keep in sync as this.approvalUrl already carries the
|
|
5027
|
+
// host:port everything else here needs.
|
|
5028
|
+
env: {
|
|
5029
|
+
OFFHAND_APPROVAL_URL: this.approvalUrl,
|
|
5030
|
+
OFFHAND_STATUS_URL: this.approvalUrl.replace(/\/approval$/, "/status"),
|
|
5031
|
+
OFFHAND_SESSION_ID: run.sessionId
|
|
5032
|
+
}
|
|
4992
5033
|
}
|
|
4993
5034
|
}
|
|
4994
5035
|
};
|
|
@@ -5174,6 +5215,10 @@ var CopilotCliRunner = class {
|
|
|
5174
5215
|
models = [];
|
|
5175
5216
|
// no headless enumeration in 1.0.31; CLI default
|
|
5176
5217
|
supportsApprovals = false;
|
|
5218
|
+
// No effortTiers: start() never reads run.effort — there's no thinking-
|
|
5219
|
+
// budget flag in Copilot CLI 1.0.31's non-interactive mode. Leaving this
|
|
5220
|
+
// undefined (not an empty array) is what tells the phone to hide the
|
|
5221
|
+
// effort control here rather than show one that silently does nothing.
|
|
5177
5222
|
/** [command, ...prefixArgs] resolved once. */
|
|
5178
5223
|
resolved = null;
|
|
5179
5224
|
resolveCommand() {
|
|
@@ -5299,6 +5344,7 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5299
5344
|
|
|
5300
5345
|
// ../daemon/src/runners/opencode-cli.ts
|
|
5301
5346
|
import { spawn as spawn4 } from "node:child_process";
|
|
5347
|
+
import { createServer } from "node:net";
|
|
5302
5348
|
import { existsSync as existsSync3 } from "node:fs";
|
|
5303
5349
|
import { homedir as homedir3 } from "node:os";
|
|
5304
5350
|
import { delimiter as delimiter2, join as join3 } from "node:path";
|
|
@@ -5307,79 +5353,61 @@ import { delimiter as delimiter2, join as join3 } from "node:path";
|
|
|
5307
5353
|
function mapOpenCodeEvent(value) {
|
|
5308
5354
|
if (typeof value !== "object" || value === null) return [];
|
|
5309
5355
|
const v2 = value;
|
|
5310
|
-
const type = typeof v2.type === "string" ? v2.type : "";
|
|
5311
5356
|
const part = v2.part ?? {};
|
|
5312
|
-
const partType = typeof part.type === "string" ? part.type : "";
|
|
5313
|
-
if (
|
|
5357
|
+
const partType = typeof part.type === "string" ? part.type : typeof v2.type === "string" ? v2.type : "";
|
|
5358
|
+
if (partType === "text") {
|
|
5314
5359
|
const text = typeof part.text === "string" ? part.text : "";
|
|
5315
5360
|
return text ? [{ type: "text", chunk: text }] : [];
|
|
5316
5361
|
}
|
|
5317
|
-
if (
|
|
5318
|
-
const thinking = typeof part.
|
|
5362
|
+
if (partType === "reasoning") {
|
|
5363
|
+
const thinking = typeof part.text === "string" ? part.text : "";
|
|
5319
5364
|
return thinking ? [{ type: "thinking", chunk: thinking }] : [];
|
|
5320
5365
|
}
|
|
5321
|
-
if (
|
|
5322
|
-
const name =
|
|
5323
|
-
const
|
|
5324
|
-
const
|
|
5366
|
+
if (partType === "tool") {
|
|
5367
|
+
const name = typeof part.tool === "string" ? part.tool : "tool";
|
|
5368
|
+
const state = part.state ?? {};
|
|
5369
|
+
const input = state.input;
|
|
5370
|
+
const summary = input ? firstString2(input, "command", "filePath", "path", "file", "pattern", "query", "url") ?? "" : "";
|
|
5371
|
+
const status = typeof state.status === "string" ? state.status : void 0;
|
|
5372
|
+
if (status === "error") {
|
|
5373
|
+
const errMsg = typeof state.error === "string" ? state.error : "tool call failed";
|
|
5374
|
+
return [{ type: "tool", name, summary: `${name}: ${truncate3(errMsg, 120)}` }];
|
|
5375
|
+
}
|
|
5325
5376
|
return [{ type: "tool", name, summary: summary ? `${name}: ${truncate3(summary, 120)}` : name }];
|
|
5326
5377
|
}
|
|
5327
|
-
if (
|
|
5328
|
-
|
|
5329
|
-
}
|
|
5330
|
-
if (type === "step_finish" || partType === "step-finish") {
|
|
5378
|
+
if (partType === "step-start") return [];
|
|
5379
|
+
if (partType === "step-finish") {
|
|
5331
5380
|
const reason = typeof part.reason === "string" ? part.reason : "stop";
|
|
5332
|
-
const isError = reason === "error" || reason === "failed";
|
|
5333
5381
|
const usage = extractUsageFromPart(part);
|
|
5334
|
-
if (
|
|
5335
|
-
const message =
|
|
5382
|
+
if (reason === "error") {
|
|
5383
|
+
const message = firstString2(part, "error", "message") ?? "run failed";
|
|
5336
5384
|
const events2 = [{ type: "error", message }];
|
|
5337
5385
|
if (usage) events2.push(usage);
|
|
5338
5386
|
return events2;
|
|
5339
5387
|
}
|
|
5388
|
+
if (reason !== "stop") return usage ? [usage] : [];
|
|
5340
5389
|
const events = [{ type: "done", summary: "" }];
|
|
5341
5390
|
if (usage) events.push(usage);
|
|
5342
5391
|
return events;
|
|
5343
5392
|
}
|
|
5344
|
-
if (type === "
|
|
5345
|
-
const
|
|
5346
|
-
const id = firstString2(partData, "id", "permissionId", "id") ?? crypto.randomUUID();
|
|
5347
|
-
const action = firstString2(partData, "toolName", "action", "command") ?? "action";
|
|
5348
|
-
const detail = firstString2(partData, "detail", "description", "message") ?? "";
|
|
5349
|
-
const risk = firstString2(partData, "risk") === "high" ? "high" : "low";
|
|
5350
|
-
const preview = firstString2(partData, "preview", "diff", "command") ?? void 0;
|
|
5351
|
-
const question = partData?.question ? mapQuestion(partData.question) : void 0;
|
|
5352
|
-
return [{
|
|
5353
|
-
type: "approval",
|
|
5354
|
-
id,
|
|
5355
|
-
action,
|
|
5356
|
-
detail,
|
|
5357
|
-
risk,
|
|
5358
|
-
preview,
|
|
5359
|
-
question
|
|
5360
|
-
}];
|
|
5361
|
-
}
|
|
5362
|
-
if (type === "error") {
|
|
5363
|
-
const message = firstString2(v2, "message", "error", "detail", "result") ?? firstString2(part, "message", "error") ?? "unknown error";
|
|
5393
|
+
if (!part.type && typeof v2.type === "string" && v2.type === "error") {
|
|
5394
|
+
const message = firstString2(v2, "message", "error", "detail", "result") ?? "unknown error";
|
|
5364
5395
|
return [{ type: "error", message }];
|
|
5365
5396
|
}
|
|
5366
|
-
if (type === "session_created" || type === "session.updated" || type === "session.created") {
|
|
5367
|
-
return [];
|
|
5368
|
-
}
|
|
5369
5397
|
return [];
|
|
5370
5398
|
}
|
|
5371
5399
|
function extractOpenCodeSessionId(value) {
|
|
5372
5400
|
if (typeof value !== "object" || value === null) return null;
|
|
5373
5401
|
const v2 = value;
|
|
5374
5402
|
const part = v2.part ?? {};
|
|
5375
|
-
return firstString2(v2, "
|
|
5403
|
+
return firstString2(v2, "sessionID", "sessionId", "session_id") ?? firstString2(part, "sessionID", "sessionId", "session_id") ?? null;
|
|
5376
5404
|
}
|
|
5377
5405
|
function extractUsageFromPart(part) {
|
|
5378
5406
|
const tokens = part.tokens;
|
|
5379
|
-
const costUsd = typeof part.cost === "number" ? part.cost :
|
|
5407
|
+
const costUsd = typeof part.cost === "number" ? part.cost : void 0;
|
|
5380
5408
|
if (!tokens && costUsd === void 0) return null;
|
|
5381
5409
|
const n2 = (k2) => typeof tokens?.[k2] === "number" ? tokens[k2] : 0;
|
|
5382
|
-
const contextTokens =
|
|
5410
|
+
const contextTokens = tokens && typeof tokens.total === "number" ? tokens.total : n2("input") + n2("output") + n2("reasoning");
|
|
5383
5411
|
if (contextTokens <= 0 && costUsd === void 0) return null;
|
|
5384
5412
|
return {
|
|
5385
5413
|
type: "usage",
|
|
@@ -5388,19 +5416,8 @@ function extractUsageFromPart(part) {
|
|
|
5388
5416
|
...costUsd !== void 0 ? { costUsd } : {}
|
|
5389
5417
|
};
|
|
5390
5418
|
}
|
|
5391
|
-
function mapQuestion(q2) {
|
|
5392
|
-
if (typeof q2 !== "object" || q2 === null) return void 0;
|
|
5393
|
-
const qq = q2;
|
|
5394
|
-
const text = typeof qq.text === "string" ? qq.text : "";
|
|
5395
|
-
const options = Array.isArray(qq.options) ? qq.options.filter((o2) => typeof o2 === "object" && o2 !== null).map((o2) => ({
|
|
5396
|
-
label: typeof o2.label === "string" ? o2.label : "",
|
|
5397
|
-
description: typeof o2.description === "string" ? o2.description : void 0
|
|
5398
|
-
})) : [];
|
|
5399
|
-
const multiSelect = typeof qq.multiSelect === "boolean" ? qq.multiSelect : void 0;
|
|
5400
|
-
if (!text && options.length === 0) return void 0;
|
|
5401
|
-
return { text, options, multiSelect };
|
|
5402
|
-
}
|
|
5403
5419
|
function firstString2(obj, ...keys) {
|
|
5420
|
+
if (!obj) return void 0;
|
|
5404
5421
|
for (const k2 of keys) {
|
|
5405
5422
|
const val = obj[k2];
|
|
5406
5423
|
if (typeof val === "string" && val !== "") return val;
|
|
@@ -5412,15 +5429,27 @@ function truncate3(s2, max) {
|
|
|
5412
5429
|
}
|
|
5413
5430
|
|
|
5414
5431
|
// ../daemon/src/runners/opencode-cli.ts
|
|
5432
|
+
var POLL_INTERVAL_MS = 500;
|
|
5415
5433
|
var OpenCodeRunner = class {
|
|
5434
|
+
constructor(broker2) {
|
|
5435
|
+
this.broker = broker2;
|
|
5436
|
+
}
|
|
5416
5437
|
id = "opencode";
|
|
5417
5438
|
name = "OpenCode";
|
|
5418
5439
|
supportsApprovals = true;
|
|
5419
5440
|
models = [];
|
|
5420
5441
|
// populated dynamically via `opencode models`
|
|
5442
|
+
// Real: every tier maps to a --variant value below (see startViaCli()) —
|
|
5443
|
+
// low/medium collide onto minimal/high internally, but all four inputs
|
|
5444
|
+
// are genuinely honored, just not 1:1 named.
|
|
5445
|
+
effortTiers = ["low", "medium", "high", "max"];
|
|
5421
5446
|
/** [command, ...prefixArgs] resolved once. */
|
|
5422
5447
|
resolved = null;
|
|
5423
5448
|
modelsCache = [];
|
|
5449
|
+
// Lazily-started, shared `opencode serve` instance backing startViaHttpApi.
|
|
5450
|
+
serverPort = null;
|
|
5451
|
+
serverProcess = null;
|
|
5452
|
+
serverStarting = null;
|
|
5424
5453
|
resolveCommand() {
|
|
5425
5454
|
if (this.resolved) return this.resolved;
|
|
5426
5455
|
if (process.platform === "win32") {
|
|
@@ -5482,16 +5511,16 @@ var OpenCodeRunner = class {
|
|
|
5482
5511
|
stdio: ["ignore", "pipe", "ignore"],
|
|
5483
5512
|
shell: process.platform === "win32"
|
|
5484
5513
|
});
|
|
5485
|
-
let
|
|
5514
|
+
let output2 = "";
|
|
5486
5515
|
p2.stdout.setEncoding("utf8");
|
|
5487
5516
|
p2.stdout.on("data", (chunk) => {
|
|
5488
|
-
|
|
5517
|
+
output2 += chunk;
|
|
5489
5518
|
});
|
|
5490
5519
|
p2.on("close", (code) => {
|
|
5491
5520
|
if (code === 0) {
|
|
5492
5521
|
try {
|
|
5493
|
-
const lines =
|
|
5494
|
-
const models = lines.map((l2) => l2.trim()).filter((l2) => l2
|
|
5522
|
+
const lines = output2.trim().split("\n");
|
|
5523
|
+
const models = lines.map((l2) => l2.trim()).filter((l2) => l2.length > 0);
|
|
5495
5524
|
this.modelsCache = models;
|
|
5496
5525
|
Object.defineProperty(this, "models", {
|
|
5497
5526
|
value: models,
|
|
@@ -5513,6 +5542,13 @@ var OpenCodeRunner = class {
|
|
|
5513
5542
|
return existsSync3(join3(homedir3(), ".local", "share", "opencode", "auth.json"));
|
|
5514
5543
|
}
|
|
5515
5544
|
start(run, callbacks) {
|
|
5545
|
+
const mode = run.permissionMode ?? "guarded";
|
|
5546
|
+
if (this.broker && (mode === "guarded" || mode === "acceptEdits")) {
|
|
5547
|
+
return this.startViaHttpApi(run, mode, callbacks);
|
|
5548
|
+
}
|
|
5549
|
+
return this.startViaCli(run, mode, callbacks);
|
|
5550
|
+
}
|
|
5551
|
+
startViaCli(run, mode, callbacks) {
|
|
5516
5552
|
const queue = new AsyncEventQueue();
|
|
5517
5553
|
const parser = new NdjsonParser();
|
|
5518
5554
|
let child;
|
|
@@ -5537,8 +5573,7 @@ var OpenCodeRunner = class {
|
|
|
5537
5573
|
...cmd.slice(1),
|
|
5538
5574
|
"run",
|
|
5539
5575
|
"--format",
|
|
5540
|
-
"json"
|
|
5541
|
-
"--no-color"
|
|
5576
|
+
"json"
|
|
5542
5577
|
];
|
|
5543
5578
|
if (run.model) {
|
|
5544
5579
|
args2.push("--model", run.model);
|
|
@@ -5546,12 +5581,10 @@ var OpenCodeRunner = class {
|
|
|
5546
5581
|
if (run.resumeConversationId) {
|
|
5547
5582
|
args2.push("--session", run.resumeConversationId);
|
|
5548
5583
|
}
|
|
5549
|
-
|
|
5550
|
-
if (mode === "bypass") {
|
|
5551
|
-
args2.push("--auto");
|
|
5552
|
-
} else if (mode === "plan") {
|
|
5584
|
+
if (mode === "plan") {
|
|
5553
5585
|
args2.push("--agent", "plan");
|
|
5554
5586
|
} else {
|
|
5587
|
+
args2.push("--auto");
|
|
5555
5588
|
}
|
|
5556
5589
|
if (run.effort) {
|
|
5557
5590
|
const variantMap = { low: "minimal", medium: "high", high: "max", max: "max" };
|
|
@@ -5622,17 +5655,304 @@ stderr: ${stderrTail}` : ""}`
|
|
|
5622
5655
|
});
|
|
5623
5656
|
return {
|
|
5624
5657
|
events: queue,
|
|
5625
|
-
|
|
5658
|
+
// No broker (or bypass/plan, which never ask) — nothing to answer.
|
|
5659
|
+
respond: () => {
|
|
5626
5660
|
},
|
|
5627
5661
|
cancel: () => child.kill()
|
|
5628
5662
|
};
|
|
5629
5663
|
}
|
|
5664
|
+
startViaHttpApi(run, mode, callbacks) {
|
|
5665
|
+
const queue = new AsyncEventQueue();
|
|
5666
|
+
const emit = (events) => {
|
|
5667
|
+
for (const e of events) queue.push(e);
|
|
5668
|
+
};
|
|
5669
|
+
const broker2 = this.broker;
|
|
5670
|
+
const directory = run.workspace;
|
|
5671
|
+
const encDir = encodeURIComponent(directory);
|
|
5672
|
+
let cancelled = false;
|
|
5673
|
+
let sessionId = run.resumeConversationId ?? null;
|
|
5674
|
+
let port2 = null;
|
|
5675
|
+
let detachBroker;
|
|
5676
|
+
const pendingQuestions = /* @__PURE__ */ new Map();
|
|
5677
|
+
const cleanup = () => {
|
|
5678
|
+
detachBroker?.();
|
|
5679
|
+
queue.close();
|
|
5680
|
+
};
|
|
5681
|
+
void (async () => {
|
|
5682
|
+
try {
|
|
5683
|
+
port2 = await this.ensureServer();
|
|
5684
|
+
} catch (e) {
|
|
5685
|
+
emit([{ type: "error", message: `failed to start opencode server: ${String(e)}` }]);
|
|
5686
|
+
cleanup();
|
|
5687
|
+
return;
|
|
5688
|
+
}
|
|
5689
|
+
if (cancelled) {
|
|
5690
|
+
cleanup();
|
|
5691
|
+
return;
|
|
5692
|
+
}
|
|
5693
|
+
const base = `http://127.0.0.1:${port2}`;
|
|
5694
|
+
detachBroker = broker2.attach((ev) => queue.push(ev));
|
|
5695
|
+
try {
|
|
5696
|
+
if (!sessionId) {
|
|
5697
|
+
const modelRef2 = parseModelRef(run.model);
|
|
5698
|
+
const body = { permission: buildPermissionRuleset(mode) };
|
|
5699
|
+
if (modelRef2) body.model = { id: modelRef2.modelId, providerID: modelRef2.providerId };
|
|
5700
|
+
const session = await httpJson(`${base}/session?directory=${encDir}`, "POST", body);
|
|
5701
|
+
sessionId = session.id;
|
|
5702
|
+
}
|
|
5703
|
+
callbacks?.onConversationId?.(sessionId);
|
|
5704
|
+
const baselineMessageIds = /* @__PURE__ */ new Set();
|
|
5705
|
+
if (run.resumeConversationId) {
|
|
5706
|
+
try {
|
|
5707
|
+
const existing = await httpJson(`${base}/session/${sessionId}/message?directory=${encDir}`, "GET");
|
|
5708
|
+
for (const row of existing) baselineMessageIds.add(row.info.id);
|
|
5709
|
+
} catch {
|
|
5710
|
+
}
|
|
5711
|
+
}
|
|
5712
|
+
const parts = [{ type: "text", text: run.prompt }];
|
|
5713
|
+
for (const file of run.attachments ?? []) {
|
|
5714
|
+
parts.push({ type: "file", mime: "application/octet-stream", url: fileUrl(file) });
|
|
5715
|
+
}
|
|
5716
|
+
const promptBody = { parts };
|
|
5717
|
+
const modelRef = parseModelRef(run.model);
|
|
5718
|
+
if (modelRef) promptBody.model = { providerID: modelRef.providerId, modelID: modelRef.modelId };
|
|
5719
|
+
await httpJson(`${base}/session/${sessionId}/prompt_async?directory=${encDir}`, "POST", promptBody, true);
|
|
5720
|
+
await this.pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, baselineMessageIds, () => cancelled);
|
|
5721
|
+
} catch (e) {
|
|
5722
|
+
if (!cancelled) emit([{ type: "error", message: `opencode HTTP API error: ${String(e)}` }]);
|
|
5723
|
+
} finally {
|
|
5724
|
+
cleanup();
|
|
5725
|
+
}
|
|
5726
|
+
})();
|
|
5727
|
+
return {
|
|
5728
|
+
events: queue,
|
|
5729
|
+
respond: (approvalId, ok, answer) => {
|
|
5730
|
+
const settleQuestion = pendingQuestions.get(approvalId);
|
|
5731
|
+
if (settleQuestion) {
|
|
5732
|
+
pendingQuestions.delete(approvalId);
|
|
5733
|
+
settleQuestion(ok, answer);
|
|
5734
|
+
return;
|
|
5735
|
+
}
|
|
5736
|
+
broker2.resolve(approvalId, ok, answer);
|
|
5737
|
+
},
|
|
5738
|
+
cancel: () => {
|
|
5739
|
+
cancelled = true;
|
|
5740
|
+
if (port2 && sessionId) {
|
|
5741
|
+
const abortUrl = `http://127.0.0.1:${port2}/session/${sessionId}/abort?directory=${encDir}`;
|
|
5742
|
+
void httpJson(abortUrl, "POST", {}, true).catch(() => {
|
|
5743
|
+
});
|
|
5744
|
+
}
|
|
5745
|
+
}
|
|
5746
|
+
};
|
|
5747
|
+
}
|
|
5748
|
+
/** Poll session messages + pending permissions/questions until the turn finishes. */
|
|
5749
|
+
async pollUntilDone(base, sessionId, encDir, broker2, emit, pendingQuestions, baselineMessageIds, isCancelled) {
|
|
5750
|
+
const seenPermissions = /* @__PURE__ */ new Set();
|
|
5751
|
+
const seenQuestions = /* @__PURE__ */ new Set();
|
|
5752
|
+
const emittedParts = /* @__PURE__ */ new Set();
|
|
5753
|
+
let sawTerminal = false;
|
|
5754
|
+
while (!isCancelled() && !sawTerminal) {
|
|
5755
|
+
await sleep(POLL_INTERVAL_MS);
|
|
5756
|
+
if (isCancelled()) break;
|
|
5757
|
+
try {
|
|
5758
|
+
const pending = await httpJson(`${base}/permission?directory=${encDir}`, "GET");
|
|
5759
|
+
for (const p2 of pending) {
|
|
5760
|
+
if (p2.sessionID !== sessionId || seenPermissions.has(p2.id)) continue;
|
|
5761
|
+
seenPermissions.add(p2.id);
|
|
5762
|
+
void this.answerPermission(base, encDir, p2, broker2);
|
|
5763
|
+
}
|
|
5764
|
+
} catch {
|
|
5765
|
+
}
|
|
5766
|
+
try {
|
|
5767
|
+
const pendingQ = await httpJson(`${base}/question?directory=${encDir}`, "GET");
|
|
5768
|
+
for (const q2 of pendingQ) {
|
|
5769
|
+
if (q2.sessionID !== sessionId || seenQuestions.has(q2.id)) continue;
|
|
5770
|
+
seenQuestions.add(q2.id);
|
|
5771
|
+
void this.answerQuestion(base, encDir, q2, emit, pendingQuestions);
|
|
5772
|
+
}
|
|
5773
|
+
} catch {
|
|
5774
|
+
}
|
|
5775
|
+
let messages;
|
|
5776
|
+
try {
|
|
5777
|
+
messages = await httpJson(`${base}/session/${sessionId}/message?directory=${encDir}`, "GET");
|
|
5778
|
+
} catch {
|
|
5779
|
+
continue;
|
|
5780
|
+
}
|
|
5781
|
+
const result = processMessages(messages, baselineMessageIds, emittedParts);
|
|
5782
|
+
emit(result.events);
|
|
5783
|
+
if (result.sawTerminal) sawTerminal = true;
|
|
5784
|
+
}
|
|
5785
|
+
}
|
|
5786
|
+
/** Route one pending permission through the shared broker, then reply. */
|
|
5787
|
+
async answerPermission(base, encDir, p2, broker2) {
|
|
5788
|
+
const input = { ...p2.metadata };
|
|
5789
|
+
const verdict = await broker2.submit(p2.permission, input);
|
|
5790
|
+
try {
|
|
5791
|
+
await httpJson(`${base}/permission/${p2.id}/reply?directory=${encDir}`, "POST", {
|
|
5792
|
+
reply: verdict.approve ? "once" : "reject",
|
|
5793
|
+
...verdict.message ? { message: verdict.message } : {}
|
|
5794
|
+
}, true);
|
|
5795
|
+
} catch {
|
|
5796
|
+
}
|
|
5797
|
+
}
|
|
5798
|
+
/** Surface an OpenCode "question" (its AskUserQuestion equivalent) as a
|
|
5799
|
+
* real approval-with-options card, wait for the phone's pick, then answer
|
|
5800
|
+
* through the question API's own reply/reject endpoints. */
|
|
5801
|
+
async answerQuestion(base, encDir, q2, emit, pendingQuestions) {
|
|
5802
|
+
const first = q2.questions[0];
|
|
5803
|
+
if (!first) return;
|
|
5804
|
+
const options = first.options.map((o2) => ({ label: o2.label, description: o2.description }));
|
|
5805
|
+
const preview = [first.question, ...options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)].join("\n");
|
|
5806
|
+
emit([{
|
|
5807
|
+
type: "approval",
|
|
5808
|
+
id: q2.id,
|
|
5809
|
+
action: "question",
|
|
5810
|
+
detail: truncate4(first.question, 200),
|
|
5811
|
+
risk: "low",
|
|
5812
|
+
preview: truncate4(preview, 600),
|
|
5813
|
+
question: { text: first.question, options, multiSelect: first.multiple }
|
|
5814
|
+
}]);
|
|
5815
|
+
const verdict = await new Promise((resolve6) => {
|
|
5816
|
+
pendingQuestions.set(q2.id, (ok, answer) => resolve6({ ok, answer }));
|
|
5817
|
+
});
|
|
5818
|
+
emit([{
|
|
5819
|
+
type: "approval-result",
|
|
5820
|
+
id: q2.id,
|
|
5821
|
+
approve: verdict.ok,
|
|
5822
|
+
...verdict.answer ? { answer: verdict.answer } : {}
|
|
5823
|
+
}]);
|
|
5824
|
+
try {
|
|
5825
|
+
if (verdict.ok && verdict.answer) {
|
|
5826
|
+
await httpJson(`${base}/question/${q2.id}/reply?directory=${encDir}`, "POST", { answers: [[verdict.answer]] }, true);
|
|
5827
|
+
} else {
|
|
5828
|
+
await httpJson(`${base}/question/${q2.id}/reject?directory=${encDir}`, "POST", {}, true);
|
|
5829
|
+
}
|
|
5830
|
+
} catch {
|
|
5831
|
+
}
|
|
5832
|
+
}
|
|
5833
|
+
/** Lazily start (once) the shared `opencode serve` instance used for
|
|
5834
|
+
* broker-driven runs, and wait for it to accept connections. */
|
|
5835
|
+
async ensureServer() {
|
|
5836
|
+
if (this.serverPort) return this.serverPort;
|
|
5837
|
+
if (this.serverStarting) return this.serverStarting;
|
|
5838
|
+
this.serverStarting = (async () => {
|
|
5839
|
+
const cmd = this.resolveCommand();
|
|
5840
|
+
if (!cmd) throw new Error("opencode CLI not found");
|
|
5841
|
+
const port2 = await findFreePort();
|
|
5842
|
+
const proc = spawn4(cmd[0], [...cmd.slice(1), "serve", "--port", String(port2), "--hostname", "127.0.0.1"], {
|
|
5843
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
5844
|
+
shell: process.platform === "win32"
|
|
5845
|
+
});
|
|
5846
|
+
proc.on("exit", () => {
|
|
5847
|
+
if (this.serverProcess === proc) {
|
|
5848
|
+
this.serverProcess = null;
|
|
5849
|
+
this.serverPort = null;
|
|
5850
|
+
}
|
|
5851
|
+
});
|
|
5852
|
+
await waitForServerReady(port2);
|
|
5853
|
+
this.serverProcess = proc;
|
|
5854
|
+
this.serverPort = port2;
|
|
5855
|
+
return port2;
|
|
5856
|
+
})();
|
|
5857
|
+
try {
|
|
5858
|
+
return await this.serverStarting;
|
|
5859
|
+
} finally {
|
|
5860
|
+
this.serverStarting = null;
|
|
5861
|
+
}
|
|
5862
|
+
}
|
|
5630
5863
|
};
|
|
5864
|
+
function processMessages(messages, baselineMessageIds, emittedParts) {
|
|
5865
|
+
const events = [];
|
|
5866
|
+
let sawTerminal = false;
|
|
5867
|
+
for (const row of messages) {
|
|
5868
|
+
if (row.info.role !== "assistant" || baselineMessageIds.has(row.info.id)) continue;
|
|
5869
|
+
for (const part of row.parts) {
|
|
5870
|
+
if (!part.id || emittedParts.has(part.id)) continue;
|
|
5871
|
+
if ((part.type === "text" || part.type === "reasoning") && !part.time?.end) continue;
|
|
5872
|
+
emittedParts.add(part.id);
|
|
5873
|
+
const partEvents = mapOpenCodeEvent({ part });
|
|
5874
|
+
events.push(...partEvents);
|
|
5875
|
+
if (partEvents.some((e) => e.type === "done" || e.type === "error")) sawTerminal = true;
|
|
5876
|
+
}
|
|
5877
|
+
if (!sawTerminal && row.info.error) {
|
|
5878
|
+
sawTerminal = true;
|
|
5879
|
+
events.push({ type: "error", message: row.info.error.data?.message ?? "run failed" });
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
return { events, sawTerminal };
|
|
5883
|
+
}
|
|
5884
|
+
function buildPermissionRuleset(mode) {
|
|
5885
|
+
const base = [{ permission: "*", pattern: "*", action: "allow" }];
|
|
5886
|
+
if (mode === "acceptEdits") {
|
|
5887
|
+
return [
|
|
5888
|
+
...base,
|
|
5889
|
+
{ permission: "edit", pattern: "*", action: "allow" },
|
|
5890
|
+
{ permission: "bash", pattern: "*", action: "ask" },
|
|
5891
|
+
{ permission: "webfetch", pattern: "*", action: "ask" }
|
|
5892
|
+
];
|
|
5893
|
+
}
|
|
5894
|
+
return [
|
|
5895
|
+
...base,
|
|
5896
|
+
{ permission: "edit", pattern: "*", action: "ask" },
|
|
5897
|
+
{ permission: "bash", pattern: "*", action: "ask" },
|
|
5898
|
+
{ permission: "webfetch", pattern: "*", action: "ask" }
|
|
5899
|
+
];
|
|
5900
|
+
}
|
|
5901
|
+
function parseModelRef(model) {
|
|
5902
|
+
if (!model) return null;
|
|
5903
|
+
const idx = model.indexOf("/");
|
|
5904
|
+
if (idx < 0) return null;
|
|
5905
|
+
return { providerId: model.slice(0, idx), modelId: model.slice(idx + 1) };
|
|
5906
|
+
}
|
|
5907
|
+
function fileUrl(path) {
|
|
5908
|
+
const normalised = path.replace(/\\/g, "/");
|
|
5909
|
+
return `file://${normalised.startsWith("/") ? "" : "/"}${normalised}`;
|
|
5910
|
+
}
|
|
5911
|
+
function truncate4(s2, max) {
|
|
5912
|
+
return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
|
|
5913
|
+
}
|
|
5914
|
+
async function httpJson(url, method, body, expectNoContent = false) {
|
|
5915
|
+
const res = await fetch(url, {
|
|
5916
|
+
method,
|
|
5917
|
+
headers: body !== void 0 ? { "content-type": "application/json" } : void 0,
|
|
5918
|
+
body: body !== void 0 ? JSON.stringify(body) : void 0
|
|
5919
|
+
});
|
|
5920
|
+
if (!res.ok) throw new Error(`opencode API ${method} ${url} -> ${res.status}`);
|
|
5921
|
+
if (expectNoContent || res.status === 204) return void 0;
|
|
5922
|
+
return res.json();
|
|
5923
|
+
}
|
|
5924
|
+
function findFreePort() {
|
|
5925
|
+
return new Promise((resolve6, reject) => {
|
|
5926
|
+
const srv = createServer();
|
|
5927
|
+
srv.unref();
|
|
5928
|
+
srv.on("error", reject);
|
|
5929
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
5930
|
+
const address = srv.address();
|
|
5931
|
+
const port2 = typeof address === "object" && address ? address.port : 0;
|
|
5932
|
+
srv.close(() => resolve6(port2));
|
|
5933
|
+
});
|
|
5934
|
+
});
|
|
5935
|
+
}
|
|
5936
|
+
async function waitForServerReady(port2, timeoutMs = 2e4) {
|
|
5937
|
+
const deadline = Date.now() + timeoutMs;
|
|
5938
|
+
while (Date.now() < deadline) {
|
|
5939
|
+
try {
|
|
5940
|
+
const res = await fetch(`http://127.0.0.1:${port2}/doc`);
|
|
5941
|
+
if (res.ok) return;
|
|
5942
|
+
} catch {
|
|
5943
|
+
}
|
|
5944
|
+
await sleep(150);
|
|
5945
|
+
}
|
|
5946
|
+
throw new Error(`opencode serve did not become ready on port ${port2}`);
|
|
5947
|
+
}
|
|
5948
|
+
function sleep(ms) {
|
|
5949
|
+
return new Promise((resolve6) => setTimeout(resolve6, ms));
|
|
5950
|
+
}
|
|
5631
5951
|
|
|
5632
5952
|
// ../daemon/src/session-manager.ts
|
|
5633
5953
|
import { randomUUID } from "node:crypto";
|
|
5634
|
-
import { hostname, platform, tmpdir } from "node:os";
|
|
5635
|
-
import { existsSync as
|
|
5954
|
+
import { hostname, platform as platform2, tmpdir } from "node:os";
|
|
5955
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync2, readdirSync as readdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "node:fs";
|
|
5636
5956
|
import { dirname as dirname2, join as join6, basename as basename3, parse as parse2, resolve as resolve3 } from "node:path";
|
|
5637
5957
|
import { fileURLToPath as fileURLToPath2 } from "node:url";
|
|
5638
5958
|
|
|
@@ -5748,7 +6068,7 @@ function summarizeConversation(path, workspace) {
|
|
|
5748
6068
|
return {
|
|
5749
6069
|
conversationId: basename(path, ".jsonl"),
|
|
5750
6070
|
workspace,
|
|
5751
|
-
firstPrompt:
|
|
6071
|
+
firstPrompt: truncate5(firstPrompt || "(empty prompt)", 120),
|
|
5752
6072
|
lastActiveMs: Math.floor(statSync(path).mtimeMs),
|
|
5753
6073
|
messageCount
|
|
5754
6074
|
};
|
|
@@ -5774,7 +6094,7 @@ function userMessageText(value) {
|
|
|
5774
6094
|
function collapseText(text) {
|
|
5775
6095
|
return text.replace(/\s+/g, " ").trim();
|
|
5776
6096
|
}
|
|
5777
|
-
function
|
|
6097
|
+
function truncate5(text, max) {
|
|
5778
6098
|
return text.length <= max ? text : `${text.slice(0, max - 1)}\u2026`;
|
|
5779
6099
|
}
|
|
5780
6100
|
function safeReadDir(path) {
|
|
@@ -5979,6 +6299,267 @@ function mimeFromName(path) {
|
|
|
5979
6299
|
}[ext] ?? "application/octet-stream";
|
|
5980
6300
|
}
|
|
5981
6301
|
|
|
6302
|
+
// ../daemon/src/handover.ts
|
|
6303
|
+
var MAX_DIFF_BYTES2 = 48 * 1024;
|
|
6304
|
+
var MAX_TOOL_SUMMARIES = 30;
|
|
6305
|
+
var MAX_RECENT_MESSAGES = 10;
|
|
6306
|
+
async function liveDiff(workspace) {
|
|
6307
|
+
const diff = await git(workspace, "diff", "HEAD");
|
|
6308
|
+
if (!diff) return "";
|
|
6309
|
+
return diff.length > MAX_DIFF_BYTES2 ? diff.slice(0, MAX_DIFF_BYTES2) + "\n\u2026 diff truncated \u2026" : diff;
|
|
6310
|
+
}
|
|
6311
|
+
function assembleHandoverPacket(items, diff) {
|
|
6312
|
+
const toolSummaries = [];
|
|
6313
|
+
let todos;
|
|
6314
|
+
const recentMessages = [];
|
|
6315
|
+
const resolvedApprovalIds = /* @__PURE__ */ new Set();
|
|
6316
|
+
const approvalsById = /* @__PURE__ */ new Map();
|
|
6317
|
+
for (const msg of items) {
|
|
6318
|
+
if (msg.type === "run-started") {
|
|
6319
|
+
recentMessages.push({ role: "user", text: msg.prompt });
|
|
6320
|
+
continue;
|
|
6321
|
+
}
|
|
6322
|
+
if (msg.type !== "run-event") continue;
|
|
6323
|
+
const ev = msg.event;
|
|
6324
|
+
if (ev.type === "tool") {
|
|
6325
|
+
toolSummaries.push(ev.summary);
|
|
6326
|
+
if (ev.todos) todos = ev.todos;
|
|
6327
|
+
} else if (ev.type === "text") {
|
|
6328
|
+
const last = recentMessages[recentMessages.length - 1];
|
|
6329
|
+
if (last?.role === "assistant") last.text += ev.chunk;
|
|
6330
|
+
else recentMessages.push({ role: "assistant", text: ev.chunk });
|
|
6331
|
+
} else if (ev.type === "approval") {
|
|
6332
|
+
approvalsById.set(ev.id, { action: ev.action, detail: ev.detail, risk: ev.risk });
|
|
6333
|
+
} else if (ev.type === "approval-result") {
|
|
6334
|
+
resolvedApprovalIds.add(ev.id);
|
|
6335
|
+
}
|
|
6336
|
+
}
|
|
6337
|
+
const pendingApprovals = [...approvalsById.entries()].filter(([id]) => !resolvedApprovalIds.has(id)).map(([, a2]) => a2);
|
|
6338
|
+
return {
|
|
6339
|
+
diff,
|
|
6340
|
+
toolSummaries: toolSummaries.slice(-MAX_TOOL_SUMMARIES),
|
|
6341
|
+
...todos ? { todos } : {},
|
|
6342
|
+
pendingApprovals,
|
|
6343
|
+
recentMessages: recentMessages.slice(-MAX_RECENT_MESSAGES)
|
|
6344
|
+
};
|
|
6345
|
+
}
|
|
6346
|
+
function translatePacketToPrompt(packet, fromRunnerName) {
|
|
6347
|
+
const sections = [
|
|
6348
|
+
`You're continuing a task handed off from ${fromRunnerName}, which hit its usage limit mid-task. Here is the context so far.`
|
|
6349
|
+
];
|
|
6350
|
+
if (packet.recentMessages.length > 0) {
|
|
6351
|
+
sections.push(
|
|
6352
|
+
"## Recent conversation\n" + packet.recentMessages.map((m3) => `**${m3.role}:** ${m3.text}`).join("\n\n")
|
|
6353
|
+
);
|
|
6354
|
+
}
|
|
6355
|
+
if (packet.toolSummaries.length > 0) {
|
|
6356
|
+
sections.push("## Actions already taken\n" + packet.toolSummaries.map((t2) => `- ${t2}`).join("\n"));
|
|
6357
|
+
}
|
|
6358
|
+
if (packet.todos && packet.todos.length > 0) {
|
|
6359
|
+
sections.push(
|
|
6360
|
+
"## Remaining plan\n" + packet.todos.map((t2) => `- [${t2.status === "completed" ? "x" : " "}] ${t2.content}`).join("\n")
|
|
6361
|
+
);
|
|
6362
|
+
}
|
|
6363
|
+
if (packet.pendingApprovals.length > 0) {
|
|
6364
|
+
sections.push(
|
|
6365
|
+
"## Not yet approved (ask again if still needed)\n" + packet.pendingApprovals.map((a2) => `- ${a2.action}: ${a2.detail} (${a2.risk} risk)`).join("\n")
|
|
6366
|
+
);
|
|
6367
|
+
}
|
|
6368
|
+
if (packet.diff.trim()) {
|
|
6369
|
+
sections.push("## Uncommitted changes so far (git diff)\n```diff\n" + packet.diff + "\n```");
|
|
6370
|
+
}
|
|
6371
|
+
if (packet.agentSummary) {
|
|
6372
|
+
sections.push("## Outgoing agent's own summary\n" + packet.agentSummary);
|
|
6373
|
+
}
|
|
6374
|
+
sections.push("Please continue the task from here.");
|
|
6375
|
+
return sections.join("\n\n");
|
|
6376
|
+
}
|
|
6377
|
+
|
|
6378
|
+
// ../daemon/src/wol.ts
|
|
6379
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
6380
|
+
import { promisify as promisify2 } from "node:util";
|
|
6381
|
+
import { networkInterfaces, platform } from "node:os";
|
|
6382
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
6383
|
+
var exec2 = promisify2(execFile2);
|
|
6384
|
+
function pickPrimaryInterface(ifaces) {
|
|
6385
|
+
for (const [name, infos] of Object.entries(ifaces)) {
|
|
6386
|
+
if (!infos) continue;
|
|
6387
|
+
const ipv4 = infos.find((i2) => i2.family === "IPv4" && !i2.internal);
|
|
6388
|
+
if (ipv4?.mac && ipv4.mac !== "00:00:00:00:00:00") {
|
|
6389
|
+
return { name, mac: ipv4.mac, address: ipv4.address, netmask: ipv4.netmask };
|
|
6390
|
+
}
|
|
6391
|
+
}
|
|
6392
|
+
return null;
|
|
6393
|
+
}
|
|
6394
|
+
function pickInterfaceByName(ifaces, name) {
|
|
6395
|
+
const infos = ifaces[name];
|
|
6396
|
+
if (!infos) return null;
|
|
6397
|
+
const ipv4 = infos.find((i2) => i2.family === "IPv4" && !i2.internal);
|
|
6398
|
+
if (!ipv4?.mac || ipv4.mac === "00:00:00:00:00:00") return null;
|
|
6399
|
+
return { name, mac: ipv4.mac, address: ipv4.address, netmask: ipv4.netmask };
|
|
6400
|
+
}
|
|
6401
|
+
function computeBroadcastAddress(address, netmask) {
|
|
6402
|
+
const a2 = address.split(".").map(Number);
|
|
6403
|
+
const m3 = netmask.split(".").map(Number);
|
|
6404
|
+
if (a2.length !== 4 || m3.length !== 4 || a2.some(Number.isNaN) || m3.some(Number.isNaN)) return null;
|
|
6405
|
+
return a2.map((octet, i2) => (octet | ~m3[i2] & 255) & 255).join(".");
|
|
6406
|
+
}
|
|
6407
|
+
function normalizeMac(mac) {
|
|
6408
|
+
return (mac ?? "").toLowerCase().replace(/[:-]/g, "");
|
|
6409
|
+
}
|
|
6410
|
+
function classifyWindowsMediaType(mediaType) {
|
|
6411
|
+
const t2 = (mediaType ?? "").toLowerCase();
|
|
6412
|
+
if (t2.includes("802.11") || t2.includes("wireless") || t2.includes("native 802.11")) return "wifi";
|
|
6413
|
+
if (t2.includes("802.3") || t2.includes("ethernet")) return "ethernet";
|
|
6414
|
+
return "unknown";
|
|
6415
|
+
}
|
|
6416
|
+
function parseWindowsAdapterJson(json) {
|
|
6417
|
+
let parsed;
|
|
6418
|
+
try {
|
|
6419
|
+
parsed = JSON.parse(json);
|
|
6420
|
+
} catch {
|
|
6421
|
+
return [];
|
|
6422
|
+
}
|
|
6423
|
+
const arr = Array.isArray(parsed) ? parsed : parsed ? [parsed] : [];
|
|
6424
|
+
return arr.filter((x2) => !!x2 && typeof x2 === "object").map((x2) => ({
|
|
6425
|
+
name: String(x2.Name ?? ""),
|
|
6426
|
+
macAddress: x2.MacAddress ? String(x2.MacAddress) : null,
|
|
6427
|
+
networkType: classifyWindowsMediaType(x2.MediaType),
|
|
6428
|
+
wakeOnMagicPacket: x2.WakeOnMagicPacket === "Enabled" || x2.WakeOnMagicPacket === "Disabled" || x2.WakeOnMagicPacket === "NotSupported" ? x2.WakeOnMagicPacket : null
|
|
6429
|
+
}));
|
|
6430
|
+
}
|
|
6431
|
+
function parseMacPmset(output2) {
|
|
6432
|
+
const m3 = output2.match(/^\s*womp\s+(\d)/m);
|
|
6433
|
+
return m3 ? m3[1] === "1" : false;
|
|
6434
|
+
}
|
|
6435
|
+
function classifyMacHardwarePort(listing, deviceName) {
|
|
6436
|
+
const blocks = listing.split(/\n\n+/);
|
|
6437
|
+
for (const block of blocks) {
|
|
6438
|
+
if (!new RegExp(`Device:\\s*${deviceName}\\b`).test(block)) continue;
|
|
6439
|
+
if (/Hardware Port:\s*Wi-Fi/i.test(block)) return "wifi";
|
|
6440
|
+
if (/Hardware Port:\s*(Ethernet|Thunderbolt Ethernet)/i.test(block)) return "ethernet";
|
|
6441
|
+
}
|
|
6442
|
+
return "unknown";
|
|
6443
|
+
}
|
|
6444
|
+
function parseLinuxEthtool(output2) {
|
|
6445
|
+
let supports = "";
|
|
6446
|
+
let current = "";
|
|
6447
|
+
for (const line of output2.split(/\r?\n/)) {
|
|
6448
|
+
const supportsMatch = line.match(/^\s*Supports Wake-on:\s*([a-zA-Z]*)/);
|
|
6449
|
+
if (supportsMatch) {
|
|
6450
|
+
supports = supportsMatch[1];
|
|
6451
|
+
continue;
|
|
6452
|
+
}
|
|
6453
|
+
const currentMatch = line.match(/^\s*Wake-on:\s*([a-zA-Z]*)/);
|
|
6454
|
+
if (currentMatch) current = currentMatch[1];
|
|
6455
|
+
}
|
|
6456
|
+
return { supportsWakeOnG: supports.toLowerCase().includes("g"), wakeOnGEnabled: current.toLowerCase().includes("g") };
|
|
6457
|
+
}
|
|
6458
|
+
function computeWolCapability(networkType, wolWorks) {
|
|
6459
|
+
if (!wolWorks) {
|
|
6460
|
+
return {
|
|
6461
|
+
capability: "unsupported",
|
|
6462
|
+
reason: networkType === "wifi" ? "Wake-on-Wireless-LAN isn't enabled on this network adapter." : "Wake-on-LAN isn't enabled (or isn't supported) on this network adapter."
|
|
6463
|
+
};
|
|
6464
|
+
}
|
|
6465
|
+
if (networkType === "wifi") {
|
|
6466
|
+
return {
|
|
6467
|
+
capability: "wifi-limited",
|
|
6468
|
+
reason: "Wake-on-Wireless-LAN is enabled, but WiFi wake support depends on the router too \u2014 less reliable than a wired connection."
|
|
6469
|
+
};
|
|
6470
|
+
}
|
|
6471
|
+
if (networkType === "ethernet") {
|
|
6472
|
+
return { capability: "supported", reason: "Wake-on-LAN is enabled on this wired connection." };
|
|
6473
|
+
}
|
|
6474
|
+
return { capability: "unsupported", reason: "Couldn't determine this machine's network connection type." };
|
|
6475
|
+
}
|
|
6476
|
+
var WINDOWS_ADAPTER_SCRIPT = "Get-NetAdapter -Physical | Where-Object Status -eq 'Up' | ForEach-Object { $pm = Get-NetAdapterPowerManagement -Name $_.Name -ErrorAction SilentlyContinue; [PSCustomObject]@{ Name = $_.Name; MediaType = [string]$_.PhysicalMediaType; MacAddress = $_.MacAddress; WakeOnMagicPacket = [string]$pm.WakeOnMagicPacket } } | ConvertTo-Json";
|
|
6477
|
+
async function detectWindows(primary) {
|
|
6478
|
+
const { stdout } = await exec2("powershell", ["-NoProfile", "-NonInteractive", "-Command", WINDOWS_ADAPTER_SCRIPT], {
|
|
6479
|
+
timeout: 8e3
|
|
6480
|
+
});
|
|
6481
|
+
const adapters = parseWindowsAdapterJson(stdout);
|
|
6482
|
+
const match = adapters.find((a2) => normalizeMac(a2.macAddress) === normalizeMac(primary.mac)) ?? adapters[0];
|
|
6483
|
+
if (!match) throw new Error("no active physical adapter reported by Get-NetAdapter");
|
|
6484
|
+
return { networkType: match.networkType, wolWorks: match.wakeOnMagicPacket === "Enabled" };
|
|
6485
|
+
}
|
|
6486
|
+
async function detectMac(primary) {
|
|
6487
|
+
const { stdout: hardwarePorts } = await exec2("networksetup", ["-listallhardwareports"], { timeout: 8e3 });
|
|
6488
|
+
const networkType = classifyMacHardwarePort(hardwarePorts, primary.name);
|
|
6489
|
+
const { stdout: pmsetOut } = await exec2("pmset", ["-g"], { timeout: 8e3 });
|
|
6490
|
+
return { networkType, wolWorks: parseMacPmset(pmsetOut) };
|
|
6491
|
+
}
|
|
6492
|
+
async function detectLinux(primary) {
|
|
6493
|
+
const networkType = existsSync6(`/sys/class/net/${primary.name}/wireless`) ? "wifi" : "ethernet";
|
|
6494
|
+
const { stdout } = await exec2("ethtool", [primary.name], { timeout: 8e3 });
|
|
6495
|
+
const { supportsWakeOnG, wakeOnGEnabled } = parseLinuxEthtool(stdout);
|
|
6496
|
+
return { networkType, wolWorks: supportsWakeOnG && wakeOnGEnabled };
|
|
6497
|
+
}
|
|
6498
|
+
async function getDefaultRouteInterfaceName(plat) {
|
|
6499
|
+
try {
|
|
6500
|
+
if (plat === "win32") {
|
|
6501
|
+
const { stdout } = await exec2(
|
|
6502
|
+
"powershell",
|
|
6503
|
+
[
|
|
6504
|
+
"-NoProfile",
|
|
6505
|
+
"-NonInteractive",
|
|
6506
|
+
"-Command",
|
|
6507
|
+
"Get-NetRoute -DestinationPrefix '0.0.0.0/0' -ErrorAction SilentlyContinue | Sort-Object RouteMetric | Select-Object -First 1 -ExpandProperty InterfaceAlias"
|
|
6508
|
+
],
|
|
6509
|
+
{ timeout: 8e3 }
|
|
6510
|
+
);
|
|
6511
|
+
return stdout.trim() || null;
|
|
6512
|
+
}
|
|
6513
|
+
if (plat === "darwin") {
|
|
6514
|
+
const { stdout } = await exec2("route", ["-n", "get", "default"], { timeout: 8e3 });
|
|
6515
|
+
const m3 = stdout.match(/interface:\s*(\S+)/);
|
|
6516
|
+
return m3 ? m3[1] : null;
|
|
6517
|
+
}
|
|
6518
|
+
if (plat === "linux") {
|
|
6519
|
+
const { stdout } = await exec2("ip", ["route", "show", "default"], { timeout: 8e3 });
|
|
6520
|
+
const m3 = stdout.match(/\bdev\s+(\S+)/);
|
|
6521
|
+
return m3 ? m3[1] : null;
|
|
6522
|
+
}
|
|
6523
|
+
return null;
|
|
6524
|
+
} catch {
|
|
6525
|
+
return null;
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
async function probeWolCapability() {
|
|
6529
|
+
const checkedAtMs = Date.now();
|
|
6530
|
+
const plat = platform();
|
|
6531
|
+
const ifaces = networkInterfaces();
|
|
6532
|
+
const routeIfaceName = await getDefaultRouteInterfaceName(plat);
|
|
6533
|
+
const primary = routeIfaceName && pickInterfaceByName(ifaces, routeIfaceName) || pickPrimaryInterface(ifaces);
|
|
6534
|
+
if (!primary) {
|
|
6535
|
+
return {
|
|
6536
|
+
capability: "unsupported",
|
|
6537
|
+
reason: "Couldn't find an active network interface to check.",
|
|
6538
|
+
networkType: "unknown",
|
|
6539
|
+
mac: null,
|
|
6540
|
+
broadcast: null,
|
|
6541
|
+
checkedAtMs
|
|
6542
|
+
};
|
|
6543
|
+
}
|
|
6544
|
+
const broadcast = computeBroadcastAddress(primary.address, primary.netmask);
|
|
6545
|
+
try {
|
|
6546
|
+
const detected = plat === "win32" ? await detectWindows(primary) : plat === "darwin" ? await detectMac(primary) : plat === "linux" ? await detectLinux(primary) : (() => {
|
|
6547
|
+
throw new Error(`unsupported OS: ${plat}`);
|
|
6548
|
+
})();
|
|
6549
|
+
const { capability, reason } = computeWolCapability(detected.networkType, detected.wolWorks);
|
|
6550
|
+
return { capability, reason, networkType: detected.networkType, mac: primary.mac, broadcast, checkedAtMs };
|
|
6551
|
+
} catch (e) {
|
|
6552
|
+
return {
|
|
6553
|
+
capability: "unsupported",
|
|
6554
|
+
reason: `Couldn't check Wake-on-LAN support: ${e instanceof Error ? e.message : String(e)}`,
|
|
6555
|
+
networkType: "unknown",
|
|
6556
|
+
mac: primary.mac,
|
|
6557
|
+
broadcast,
|
|
6558
|
+
checkedAtMs
|
|
6559
|
+
};
|
|
6560
|
+
}
|
|
6561
|
+
}
|
|
6562
|
+
|
|
5982
6563
|
// ../daemon/src/session-manager.ts
|
|
5983
6564
|
function resolveDaemonVersion() {
|
|
5984
6565
|
try {
|
|
@@ -5990,6 +6571,9 @@ function resolveDaemonVersion() {
|
|
|
5990
6571
|
}
|
|
5991
6572
|
}
|
|
5992
6573
|
var DAEMON_VERSION = resolveDaemonVersion();
|
|
6574
|
+
var SYSTEM_LOG_SESSION_ID = "system";
|
|
6575
|
+
var MOBILE_SESSION_PREAMBLE_BASE = "You're being driven remotely from a phone via offhand \u2014 the person who sent this prompt isn't at this keyboard and can't see this terminal. Lean on visual proof (screenshots, diffs, clear file paths) over long text explanations where you can, since they're reading this on a phone screen, not a terminal.";
|
|
6576
|
+
var MOBILE_SESSION_PREAMBLE_STATUS_TOOL = " You have an offhand_status_update tool available \u2014 call it with a short plain-English note at meaningful checkpoints (starting a long step, hitting a snag, finishing a phase) so they see progress without waiting for the whole run to end.";
|
|
5993
6577
|
var SessionManager = class {
|
|
5994
6578
|
constructor(store2, runners2) {
|
|
5995
6579
|
this.store = store2;
|
|
@@ -6004,6 +6588,14 @@ var SessionManager = class {
|
|
|
6004
6588
|
// sessionId → queued prompts
|
|
6005
6589
|
runners = /* @__PURE__ */ new Map();
|
|
6006
6590
|
runnerAvailability = /* @__PURE__ */ new Map();
|
|
6591
|
+
/** handoverId → what was proposed, so `handover-response` (which only
|
|
6592
|
+
* carries the id back) knows what to act on. */
|
|
6593
|
+
pendingHandovers = /* @__PURE__ */ new Map();
|
|
6594
|
+
/** sessionId → the most recent offhand_status_update note (Execution Plan
|
|
6595
|
+
* 5) — becomes a Handover Packet's optional agentSummary when a handover
|
|
6596
|
+
* is proposed afterward. Still zero hosted AI: the agent chose to send
|
|
6597
|
+
* this, on its own tokens, no different from any other tool call. */
|
|
6598
|
+
latestStatusUpdate = /* @__PURE__ */ new Map();
|
|
6007
6599
|
/** Optional artifact plumbing (set by index when capture is possible). */
|
|
6008
6600
|
uploader = null;
|
|
6009
6601
|
capture = null;
|
|
@@ -6021,6 +6613,21 @@ var SessionManager = class {
|
|
|
6021
6613
|
for (const [id, r2] of this.runners) {
|
|
6022
6614
|
this.runnerAvailability.set(id, await r2.detect());
|
|
6023
6615
|
}
|
|
6616
|
+
void this.recheckWolCapability();
|
|
6617
|
+
}
|
|
6618
|
+
/** Runs the real Wake-on-LAN/WiFi probe and persists the result — called
|
|
6619
|
+
* once at startup and again on-demand via the phone's "check again"
|
|
6620
|
+
* action (Execution Plan 7), since drivers/BIOS/network type can change. */
|
|
6621
|
+
async recheckWolCapability() {
|
|
6622
|
+
const result = await probeWolCapability();
|
|
6623
|
+
this.store.setWolCapability({
|
|
6624
|
+
capability: result.capability,
|
|
6625
|
+
reason: result.reason,
|
|
6626
|
+
networkType: result.networkType,
|
|
6627
|
+
mac: result.mac,
|
|
6628
|
+
broadcast: result.broadcast,
|
|
6629
|
+
checkedAtMs: result.checkedAtMs
|
|
6630
|
+
});
|
|
6024
6631
|
}
|
|
6025
6632
|
// ---- transport attachment --------------------------------------------------
|
|
6026
6633
|
attach(sink) {
|
|
@@ -6028,10 +6635,16 @@ var SessionManager = class {
|
|
|
6028
6635
|
return () => this.sinks.delete(sink);
|
|
6029
6636
|
}
|
|
6030
6637
|
hello() {
|
|
6638
|
+
const wol = this.store.getWolCapability();
|
|
6031
6639
|
return {
|
|
6032
6640
|
type: "hello",
|
|
6033
6641
|
protocol: 2,
|
|
6034
|
-
host: {
|
|
6642
|
+
host: {
|
|
6643
|
+
hostname: hostname(),
|
|
6644
|
+
os: platform2(),
|
|
6645
|
+
daemonVersion: DAEMON_VERSION,
|
|
6646
|
+
...wol ? { wolCapability: wol.capability, wolReason: wol.reason, wolCheckedAtMs: wol.checkedAtMs } : {}
|
|
6647
|
+
},
|
|
6035
6648
|
lastSeq: this.store.lastSeq()
|
|
6036
6649
|
};
|
|
6037
6650
|
}
|
|
@@ -6042,7 +6655,8 @@ var SessionManager = class {
|
|
|
6042
6655
|
available: this.runnerAvailability.get(r2.id) ?? false,
|
|
6043
6656
|
...r2.loggedIn ? { loggedIn: r2.loggedIn() } : {},
|
|
6044
6657
|
models: r2.models,
|
|
6045
|
-
supportsApprovals: r2.supportsApprovals
|
|
6658
|
+
supportsApprovals: r2.supportsApprovals,
|
|
6659
|
+
...r2.effortTiers ? { effortTiers: [...r2.effortTiers] } : {}
|
|
6046
6660
|
}));
|
|
6047
6661
|
const workspaces = await Promise.all(
|
|
6048
6662
|
this.store.listWorkspaces().map((w2) => workspaceInfo(w2))
|
|
@@ -6137,6 +6751,41 @@ var SessionManager = class {
|
|
|
6137
6751
|
case "resume":
|
|
6138
6752
|
for (const m3 of this.store.replayAfter(msg.afterSeq)) reply(m3);
|
|
6139
6753
|
return;
|
|
6754
|
+
case "handover-response": {
|
|
6755
|
+
const pending = this.pendingHandovers.get(msg.handoverId);
|
|
6756
|
+
if (!pending || pending.sessionId !== msg.sessionId) return;
|
|
6757
|
+
this.pendingHandovers.delete(msg.handoverId);
|
|
6758
|
+
if (!msg.accept) {
|
|
6759
|
+
this.record(msg.sessionId, (seq) => ({
|
|
6760
|
+
type: "run-event",
|
|
6761
|
+
sessionId: msg.sessionId,
|
|
6762
|
+
runId: "",
|
|
6763
|
+
seq,
|
|
6764
|
+
event: { type: "handover-declined", id: msg.handoverId }
|
|
6765
|
+
}));
|
|
6766
|
+
return;
|
|
6767
|
+
}
|
|
6768
|
+
const session = this.store.getSession(msg.sessionId);
|
|
6769
|
+
const toRunner = this.runners.get(pending.toRunnerId);
|
|
6770
|
+
if (!session || !toRunner) return;
|
|
6771
|
+
const fromRunnerId = session.runnerId;
|
|
6772
|
+
const fromRunner = this.runners.get(fromRunnerId);
|
|
6773
|
+
const prompt = translatePacketToPrompt(pending.packet, fromRunner?.name ?? fromRunnerId);
|
|
6774
|
+
this.store.updateSession(session.id, { runnerId: pending.toRunnerId, conversationId: null });
|
|
6775
|
+
this.record(session.id, (seq) => ({
|
|
6776
|
+
type: "run-event",
|
|
6777
|
+
sessionId: session.id,
|
|
6778
|
+
runId: "",
|
|
6779
|
+
seq,
|
|
6780
|
+
event: { type: "handover-accepted", id: msg.handoverId, fromRunnerId, toRunnerId: pending.toRunnerId }
|
|
6781
|
+
}));
|
|
6782
|
+
const q2 = this.queues.get(session.id) ?? [];
|
|
6783
|
+
q2.push(prompt);
|
|
6784
|
+
this.queues.set(session.id, q2);
|
|
6785
|
+
this.pump(session.id);
|
|
6786
|
+
await this.broadcastManifest();
|
|
6787
|
+
return;
|
|
6788
|
+
}
|
|
6140
6789
|
case "session-create": {
|
|
6141
6790
|
const row = this.store.createSession(msg.workspace, msg.runnerId, msg.model, msg.label);
|
|
6142
6791
|
this.store.upsertWorkspace(msg.workspace);
|
|
@@ -6160,7 +6809,9 @@ var SessionManager = class {
|
|
|
6160
6809
|
...msg.model !== void 0 ? { model: msg.model || null } : {},
|
|
6161
6810
|
...msg.archived !== void 0 ? { archived: msg.archived } : {},
|
|
6162
6811
|
...msg.permissionMode !== void 0 ? { permissionMode: msg.permissionMode } : {},
|
|
6163
|
-
...msg.effort !== void 0 ? { effort: msg.effort } : {}
|
|
6812
|
+
...msg.effort !== void 0 ? { effort: msg.effort } : {},
|
|
6813
|
+
...msg.budgetCostUsdCap !== void 0 ? { budgetCostUsdCap: msg.budgetCostUsdCap } : {},
|
|
6814
|
+
...msg.budgetMinutesCap !== void 0 ? { budgetMinutesCap: msg.budgetMinutesCap } : {}
|
|
6164
6815
|
});
|
|
6165
6816
|
await this.broadcastManifest();
|
|
6166
6817
|
return;
|
|
@@ -6193,8 +6844,56 @@ var SessionManager = class {
|
|
|
6193
6844
|
case "fs-list":
|
|
6194
6845
|
reply({ type: "fs-response", rpcId: msg.rpcId, ...listFolders(msg.path) });
|
|
6195
6846
|
return;
|
|
6847
|
+
case "fs-create-folder": {
|
|
6848
|
+
try {
|
|
6849
|
+
const safeName = safeDropName(msg.name);
|
|
6850
|
+
mkdirSync2(join6(resolve3(msg.path), safeName), { recursive: false });
|
|
6851
|
+
} catch (e) {
|
|
6852
|
+
reply({
|
|
6853
|
+
type: "fs-response",
|
|
6854
|
+
rpcId: msg.rpcId,
|
|
6855
|
+
...listFolders(msg.path),
|
|
6856
|
+
error: `couldn't create folder: ${e instanceof Error ? e.message : String(e)}`
|
|
6857
|
+
});
|
|
6858
|
+
return;
|
|
6859
|
+
}
|
|
6860
|
+
reply({ type: "fs-response", rpcId: msg.rpcId, ...listFolders(msg.path) });
|
|
6861
|
+
return;
|
|
6862
|
+
}
|
|
6863
|
+
case "wol-recheck":
|
|
6864
|
+
await this.recheckWolCapability();
|
|
6865
|
+
reply(this.hello());
|
|
6866
|
+
return;
|
|
6196
6867
|
}
|
|
6197
6868
|
}
|
|
6869
|
+
/** The daemon's own relay connection dropped — logged durably so a phone
|
|
6870
|
+
* that wasn't listening live can still see it in the "left off" digest
|
|
6871
|
+
* (Execution Plan 6), unlike the relay's ephemeral `presence` frame. */
|
|
6872
|
+
recordDeviceOffline() {
|
|
6873
|
+
this.record(SYSTEM_LOG_SESSION_ID, (seq) => ({ type: "device-offline", seq, atMs: Date.now() }));
|
|
6874
|
+
}
|
|
6875
|
+
/** A running agent proactively pushed a status note (Execution Plan 5's
|
|
6876
|
+
* offhand_status_update MCP tool). Recorded into the normal session log
|
|
6877
|
+
* (so it shows in the transcript and the run timeline) and cached as the
|
|
6878
|
+
* latest note for this session, ready to enrich a Handover Packet later. */
|
|
6879
|
+
recordStatusUpdate(sessionId, text) {
|
|
6880
|
+
this.latestStatusUpdate.set(sessionId, text);
|
|
6881
|
+
this.record(sessionId, (seq) => ({
|
|
6882
|
+
type: "run-event",
|
|
6883
|
+
sessionId,
|
|
6884
|
+
runId: "",
|
|
6885
|
+
seq,
|
|
6886
|
+
event: { type: "status-update", text }
|
|
6887
|
+
}));
|
|
6888
|
+
}
|
|
6889
|
+
recordDeviceReconnected(offlineForMs) {
|
|
6890
|
+
this.record(SYSTEM_LOG_SESSION_ID, (seq) => ({
|
|
6891
|
+
type: "device-reconnected",
|
|
6892
|
+
seq,
|
|
6893
|
+
atMs: Date.now(),
|
|
6894
|
+
offlineForMs
|
|
6895
|
+
}));
|
|
6896
|
+
}
|
|
6198
6897
|
async sendDropToPhone(path) {
|
|
6199
6898
|
if (!this.uploader) throw new Error("drop upload unavailable: start daemon with --relay");
|
|
6200
6899
|
const file = readOutgoingDrop(path);
|
|
@@ -6237,10 +6936,17 @@ var SessionManager = class {
|
|
|
6237
6936
|
const touchedFiles = [];
|
|
6238
6937
|
let toolCount = 0;
|
|
6239
6938
|
let succeeded = false;
|
|
6939
|
+
let limitHitEmitted = false;
|
|
6940
|
+
let budgetWarned = false;
|
|
6941
|
+
const hasStatusTool = runner.id === "claude-code" && session.permissionMode !== "bypass";
|
|
6942
|
+
const sentPrompt = session.conversationId ? prompt : `${MOBILE_SESSION_PREAMBLE_BASE}${hasStatusTool ? MOBILE_SESSION_PREAMBLE_STATUS_TOOL : ""}
|
|
6943
|
+
|
|
6944
|
+
${prompt}`;
|
|
6240
6945
|
const handle = runner.start(
|
|
6241
6946
|
{
|
|
6242
6947
|
runId,
|
|
6243
|
-
|
|
6948
|
+
sessionId: session.id,
|
|
6949
|
+
prompt: sentPrompt,
|
|
6244
6950
|
workspace: session.workspace,
|
|
6245
6951
|
...session.model ? { model: session.model } : {},
|
|
6246
6952
|
...session.conversationId ? { resumeConversationId: session.conversationId } : {},
|
|
@@ -6273,6 +6979,17 @@ var SessionManager = class {
|
|
|
6273
6979
|
seq,
|
|
6274
6980
|
event
|
|
6275
6981
|
}));
|
|
6982
|
+
const limitHit = checkLimitHit(event, limitHitEmitted);
|
|
6983
|
+
if (limitHit) {
|
|
6984
|
+
limitHitEmitted = true;
|
|
6985
|
+
this.record(session.id, (seq) => ({ type: "run-event", sessionId: session.id, runId, seq, event: limitHit }));
|
|
6986
|
+
void this.proposeHandover(session, runId, limitHit.reason);
|
|
6987
|
+
}
|
|
6988
|
+
const budgetHit = checkBudgetWarning(event, session, budgetWarned) ?? checkTimeBudget(session, Date.now() - startedAt, budgetWarned);
|
|
6989
|
+
if (budgetHit) {
|
|
6990
|
+
budgetWarned = true;
|
|
6991
|
+
this.record(session.id, (seq) => ({ type: "run-event", sessionId: session.id, runId, seq, event: budgetHit }));
|
|
6992
|
+
}
|
|
6276
6993
|
}
|
|
6277
6994
|
} finally {
|
|
6278
6995
|
this.active.delete(session.id);
|
|
@@ -6304,7 +7021,65 @@ var SessionManager = class {
|
|
|
6304
7021
|
}));
|
|
6305
7022
|
}
|
|
6306
7023
|
}
|
|
7024
|
+
/** Builds and offers a Handover Packet once a limit-hit fires. Entirely
|
|
7025
|
+
* deterministic (live git diff + this session's own log) — no cooperation
|
|
7026
|
+
* needed from the agent that just hit its limit, which is the point:
|
|
7027
|
+
* this must still work when that agent is already dead or blocked, not
|
|
7028
|
+
* only the graceful case. Silent (no card at all) when there's no other
|
|
7029
|
+
* detected, available runner to hand off to — never a dead-end proposal. */
|
|
7030
|
+
async proposeHandover(session, runId, reason) {
|
|
7031
|
+
const target = this.pickHandoverTarget(session.runnerId);
|
|
7032
|
+
if (!target) return;
|
|
7033
|
+
const diff = await liveDiff(session.workspace);
|
|
7034
|
+
const { items } = this.store.historyPage(session.id, 0, 500);
|
|
7035
|
+
const packet = assembleHandoverPacket(items, diff);
|
|
7036
|
+
const agentSummary = this.latestStatusUpdate.get(session.id);
|
|
7037
|
+
if (agentSummary) packet.agentSummary = agentSummary;
|
|
7038
|
+
const id = randomUUID();
|
|
7039
|
+
this.pendingHandovers.set(id, { sessionId: session.id, toRunnerId: target.id, packet });
|
|
7040
|
+
this.record(session.id, (seq) => ({
|
|
7041
|
+
type: "run-event",
|
|
7042
|
+
sessionId: session.id,
|
|
7043
|
+
runId,
|
|
7044
|
+
seq,
|
|
7045
|
+
event: { type: "handover-proposed", id, fromRunnerId: session.runnerId, toRunnerId: target.id, reason, packet }
|
|
7046
|
+
}));
|
|
7047
|
+
}
|
|
7048
|
+
/** First other detected+available runner — good enough for v1 (single
|
|
7049
|
+
* candidate, not a picker); a real choice among several becomes relevant
|
|
7050
|
+
* once more than one alternative is commonly available at once. */
|
|
7051
|
+
pickHandoverTarget(currentRunnerId) {
|
|
7052
|
+
for (const [id, runner] of this.runners) {
|
|
7053
|
+
if (id === currentRunnerId) continue;
|
|
7054
|
+
if (!(this.runnerAvailability.get(id) ?? false)) continue;
|
|
7055
|
+
return runner;
|
|
7056
|
+
}
|
|
7057
|
+
return null;
|
|
7058
|
+
}
|
|
6307
7059
|
};
|
|
7060
|
+
var LIMIT_HIT_THRESHOLD = 0.9;
|
|
7061
|
+
function checkLimitHit(event, alreadyEmitted) {
|
|
7062
|
+
if (alreadyEmitted || event.type !== "rate-limit") return null;
|
|
7063
|
+
const utilization = Math.max(event.fiveHourUtilization, event.sevenDayUtilization);
|
|
7064
|
+
if (utilization < LIMIT_HIT_THRESHOLD) return null;
|
|
7065
|
+
return { type: "limit-hit", reason: "rate_limit", utilization };
|
|
7066
|
+
}
|
|
7067
|
+
function checkBudgetWarning(event, session, alreadyWarned) {
|
|
7068
|
+
if (alreadyWarned || event.type !== "usage" || !session.budgetCostUsdCap) return null;
|
|
7069
|
+
if (event.costUsd === void 0 || event.costUsd < session.budgetCostUsdCap) return null;
|
|
7070
|
+
return { type: "budget-warning", kind: "cost", capUsd: session.budgetCostUsdCap, actualUsd: event.costUsd };
|
|
7071
|
+
}
|
|
7072
|
+
function checkTimeBudget(session, elapsedMs, alreadyWarned) {
|
|
7073
|
+
if (alreadyWarned || !session.budgetMinutesCap) return null;
|
|
7074
|
+
const minutes = elapsedMs / 6e4;
|
|
7075
|
+
if (minutes < session.budgetMinutesCap) return null;
|
|
7076
|
+
return {
|
|
7077
|
+
type: "budget-warning",
|
|
7078
|
+
kind: "time",
|
|
7079
|
+
capMinutes: session.budgetMinutesCap,
|
|
7080
|
+
actualMinutes: Math.round(minutes)
|
|
7081
|
+
};
|
|
7082
|
+
}
|
|
6308
7083
|
function trackTouches(event, touched) {
|
|
6309
7084
|
if (event.type !== "tool") return;
|
|
6310
7085
|
if (!/^(Edit|Write|MultiEdit|NotebookEdit)/.test(event.name)) return;
|
|
@@ -6321,16 +7096,16 @@ function listFolders(path) {
|
|
|
6321
7096
|
const current = resolve3(path);
|
|
6322
7097
|
const dirs = safeReadDir2(current).filter((entry) => entry.isDirectory() && !skipDir(entry.name)).map((entry) => {
|
|
6323
7098
|
const full = join6(current, entry.name);
|
|
6324
|
-
return { name: entry.name, path: full, isGit:
|
|
7099
|
+
return { name: entry.name, path: full, isGit: existsSync7(join6(full, ".git")) };
|
|
6325
7100
|
}).sort((a2, b2) => Number(b2.isGit) - Number(a2.isGit) || a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
6326
7101
|
return { path: current, parent: isRoot(current) ? null : dirname2(current), dirs };
|
|
6327
7102
|
}
|
|
6328
7103
|
function driveRoots() {
|
|
6329
|
-
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit:
|
|
7104
|
+
if (process.platform !== "win32") return [{ name: "/", path: "/", isGit: existsSync7("/.git") }];
|
|
6330
7105
|
const roots = [];
|
|
6331
7106
|
for (let code = 67; code <= 90; code++) {
|
|
6332
7107
|
const name = `${String.fromCharCode(code)}:\\`;
|
|
6333
|
-
if (
|
|
7108
|
+
if (existsSync7(name)) roots.push({ name, path: name, isGit: existsSync7(join6(name, ".git")) });
|
|
6334
7109
|
}
|
|
6335
7110
|
return roots.sort((a2, b2) => a2.name.localeCompare(b2.name)).slice(0, 200);
|
|
6336
7111
|
}
|
|
@@ -6395,6 +7170,15 @@ var Store = class {
|
|
|
6395
7170
|
dev_url TEXT
|
|
6396
7171
|
);
|
|
6397
7172
|
CREATE VIRTUAL TABLE IF NOT EXISTS log_fts USING fts5(text, session_id UNINDEXED, seq UNINDEXED);
|
|
7173
|
+
CREATE TABLE IF NOT EXISTS device (
|
|
7174
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
7175
|
+
wol_capability TEXT,
|
|
7176
|
+
wol_reason TEXT,
|
|
7177
|
+
wol_network_type TEXT,
|
|
7178
|
+
wol_mac TEXT,
|
|
7179
|
+
wol_broadcast TEXT,
|
|
7180
|
+
wol_checked_at_ms INTEGER
|
|
7181
|
+
);
|
|
6398
7182
|
`);
|
|
6399
7183
|
try {
|
|
6400
7184
|
this.db.exec(`ALTER TABLE workspaces ADD COLUMN policy TEXT NOT NULL DEFAULT 'balanced'`);
|
|
@@ -6408,6 +7192,14 @@ var Store = class {
|
|
|
6408
7192
|
this.db.exec(`ALTER TABLE sessions ADD COLUMN effort TEXT`);
|
|
6409
7193
|
} catch {
|
|
6410
7194
|
}
|
|
7195
|
+
try {
|
|
7196
|
+
this.db.exec(`ALTER TABLE sessions ADD COLUMN budget_cost_usd_cap REAL`);
|
|
7197
|
+
} catch {
|
|
7198
|
+
}
|
|
7199
|
+
try {
|
|
7200
|
+
this.db.exec(`ALTER TABLE sessions ADD COLUMN budget_minutes_cap INTEGER`);
|
|
7201
|
+
} catch {
|
|
7202
|
+
}
|
|
6411
7203
|
}
|
|
6412
7204
|
// ---- sessions -------------------------------------------------------------
|
|
6413
7205
|
createSession(workspace, runnerId, model, label) {
|
|
@@ -6421,7 +7213,9 @@ var Store = class {
|
|
|
6421
7213
|
archived: false,
|
|
6422
7214
|
conversationId: null,
|
|
6423
7215
|
permissionMode: "guarded",
|
|
6424
|
-
effort: null
|
|
7216
|
+
effort: null,
|
|
7217
|
+
budgetCostUsdCap: null,
|
|
7218
|
+
budgetMinutesCap: null
|
|
6425
7219
|
};
|
|
6426
7220
|
this.db.prepare(
|
|
6427
7221
|
`INSERT INTO sessions (id, workspace, runner_id, model, label, created_at_ms, archived, conversation_id)
|
|
@@ -6441,7 +7235,9 @@ var Store = class {
|
|
|
6441
7235
|
archived: Boolean(r2.archived),
|
|
6442
7236
|
conversationId: r2.conversation_id ?? null,
|
|
6443
7237
|
permissionMode: r2.permission_mode ?? "guarded",
|
|
6444
|
-
effort: r2.effort ?? null
|
|
7238
|
+
effort: r2.effort ?? null,
|
|
7239
|
+
budgetCostUsdCap: r2.budget_cost_usd_cap ?? null,
|
|
7240
|
+
budgetMinutesCap: r2.budget_minutes_cap ?? null
|
|
6445
7241
|
}));
|
|
6446
7242
|
}
|
|
6447
7243
|
getSession(id) {
|
|
@@ -6452,8 +7248,20 @@ var Store = class {
|
|
|
6452
7248
|
if (!current) return;
|
|
6453
7249
|
const next = { ...current, ...patch };
|
|
6454
7250
|
this.db.prepare(
|
|
6455
|
-
`UPDATE sessions SET label = ?, model = ?, archived = ?, conversation_id = ?, permission_mode = ?, effort =
|
|
6456
|
-
|
|
7251
|
+
`UPDATE sessions SET label = ?, model = ?, archived = ?, conversation_id = ?, permission_mode = ?, effort = ?,
|
|
7252
|
+
budget_cost_usd_cap = ?, budget_minutes_cap = ?, runner_id = ? WHERE id = ?`
|
|
7253
|
+
).run(
|
|
7254
|
+
next.label,
|
|
7255
|
+
next.model,
|
|
7256
|
+
next.archived ? 1 : 0,
|
|
7257
|
+
next.conversationId,
|
|
7258
|
+
next.permissionMode,
|
|
7259
|
+
next.effort,
|
|
7260
|
+
next.budgetCostUsdCap,
|
|
7261
|
+
next.budgetMinutesCap,
|
|
7262
|
+
next.runnerId,
|
|
7263
|
+
id
|
|
7264
|
+
);
|
|
6457
7265
|
}
|
|
6458
7266
|
toSessionInfo(row, busy, queued) {
|
|
6459
7267
|
return {
|
|
@@ -6467,7 +7275,9 @@ var Store = class {
|
|
|
6467
7275
|
permissionMode: row.permissionMode,
|
|
6468
7276
|
effort: row.effort ?? void 0,
|
|
6469
7277
|
busy,
|
|
6470
|
-
queuedPrompts: queued
|
|
7278
|
+
queuedPrompts: queued,
|
|
7279
|
+
budgetCostUsdCap: row.budgetCostUsdCap ?? void 0,
|
|
7280
|
+
budgetMinutesCap: row.budgetMinutesCap ?? void 0
|
|
6471
7281
|
};
|
|
6472
7282
|
}
|
|
6473
7283
|
// ---- transcript log ---------------------------------------------------------
|
|
@@ -6538,6 +7348,32 @@ var Store = class {
|
|
|
6538
7348
|
setWorkspacePolicy(path, policy) {
|
|
6539
7349
|
this.db.prepare(`UPDATE workspaces SET policy = ? WHERE path = ?`).run(policy, path);
|
|
6540
7350
|
}
|
|
7351
|
+
// ---- device capability (Execution Plan 7) ----------------------------------
|
|
7352
|
+
getWolCapability() {
|
|
7353
|
+
const row = this.db.prepare(`SELECT * FROM device WHERE id = 1`).get();
|
|
7354
|
+
if (!row || row.wol_capability == null) return null;
|
|
7355
|
+
return {
|
|
7356
|
+
capability: row.wol_capability,
|
|
7357
|
+
reason: row.wol_reason,
|
|
7358
|
+
networkType: row.wol_network_type,
|
|
7359
|
+
mac: row.wol_mac ?? null,
|
|
7360
|
+
broadcast: row.wol_broadcast ?? null,
|
|
7361
|
+
checkedAtMs: row.wol_checked_at_ms
|
|
7362
|
+
};
|
|
7363
|
+
}
|
|
7364
|
+
setWolCapability(v2) {
|
|
7365
|
+
this.db.prepare(
|
|
7366
|
+
`INSERT INTO device (id, wol_capability, wol_reason, wol_network_type, wol_mac, wol_broadcast, wol_checked_at_ms)
|
|
7367
|
+
VALUES (1, ?, ?, ?, ?, ?, ?)
|
|
7368
|
+
ON CONFLICT(id) DO UPDATE SET
|
|
7369
|
+
wol_capability = excluded.wol_capability,
|
|
7370
|
+
wol_reason = excluded.wol_reason,
|
|
7371
|
+
wol_network_type = excluded.wol_network_type,
|
|
7372
|
+
wol_mac = excluded.wol_mac,
|
|
7373
|
+
wol_broadcast = excluded.wol_broadcast,
|
|
7374
|
+
wol_checked_at_ms = excluded.wol_checked_at_ms`
|
|
7375
|
+
).run(v2.capability, v2.reason, v2.networkType, v2.mac, v2.broadcast, v2.checkedAtMs);
|
|
7376
|
+
}
|
|
6541
7377
|
};
|
|
6542
7378
|
function searchableText(msg) {
|
|
6543
7379
|
switch (msg.type) {
|
|
@@ -6548,6 +7384,7 @@ function searchableText(msg) {
|
|
|
6548
7384
|
if (msg.event.type === "tool") return msg.event.summary;
|
|
6549
7385
|
if (msg.event.type === "done") return msg.event.summary;
|
|
6550
7386
|
if (msg.event.type === "error") return msg.event.message;
|
|
7387
|
+
if (msg.event.type === "status-update") return msg.event.text;
|
|
6551
7388
|
return "";
|
|
6552
7389
|
case "drop":
|
|
6553
7390
|
return msg.name;
|
|
@@ -6557,7 +7394,7 @@ function searchableText(msg) {
|
|
|
6557
7394
|
}
|
|
6558
7395
|
|
|
6559
7396
|
// ../daemon/src/local-server.ts
|
|
6560
|
-
import { createServer } from "node:http";
|
|
7397
|
+
import { createServer as createServer2 } from "node:http";
|
|
6561
7398
|
|
|
6562
7399
|
// ../node_modules/.pnpm/ws@8.21.3/node_modules/ws/wrapper.mjs
|
|
6563
7400
|
var import_stream = __toESM(require_stream(), 1);
|
|
@@ -10617,11 +11454,42 @@ var ApprovalQuestionSchema = external_exports.object({
|
|
|
10617
11454
|
options: external_exports.array(external_exports.object({ label: external_exports.string(), description: external_exports.string().optional() })),
|
|
10618
11455
|
multiSelect: external_exports.boolean().optional()
|
|
10619
11456
|
});
|
|
11457
|
+
var TodoItemSchema = external_exports.object({
|
|
11458
|
+
content: external_exports.string(),
|
|
11459
|
+
status: external_exports.enum(["pending", "in_progress", "completed"]),
|
|
11460
|
+
activeForm: external_exports.string().optional()
|
|
11461
|
+
});
|
|
11462
|
+
var HandoverPacketSchema = external_exports.object({
|
|
11463
|
+
/** Live `git diff HEAD` against the workspace, captured at handover time. */
|
|
11464
|
+
diff: external_exports.string(),
|
|
11465
|
+
/** Tool-call summaries this session made, oldest first, capped to the most recent. */
|
|
11466
|
+
toolSummaries: external_exports.array(external_exports.string()),
|
|
11467
|
+
/** The last TodoWrite call's task list, verbatim — never summarized. */
|
|
11468
|
+
todos: external_exports.array(TodoItemSchema).optional(),
|
|
11469
|
+
/** Approvals raised but never resolved (still pending or timed out) at handover time. */
|
|
11470
|
+
pendingApprovals: external_exports.array(
|
|
11471
|
+
external_exports.object({ action: external_exports.string(), detail: external_exports.string(), risk: external_exports.enum(["low", "high"]) })
|
|
11472
|
+
),
|
|
11473
|
+
/** Recent prompt/response turns, oldest first, capped — for tone/context continuity. */
|
|
11474
|
+
recentMessages: external_exports.array(external_exports.object({ role: external_exports.enum(["user", "assistant"]), text: external_exports.string() })),
|
|
11475
|
+
/** Optional free-text self-summary from the outgoing agent — enrichment
|
|
11476
|
+
* only, attempted when there was runway before a hard cutoff. Its absence
|
|
11477
|
+
* changes nothing; the packet above is already sufficient on its own. */
|
|
11478
|
+
agentSummary: external_exports.string().optional()
|
|
11479
|
+
});
|
|
10620
11480
|
var RunEventSchema = external_exports.discriminatedUnion("type", [
|
|
10621
11481
|
external_exports.object({ type: external_exports.literal("text"), chunk: external_exports.string() }),
|
|
10622
11482
|
/** Extended-thinking content, streamed the same way as text (medium+ effort tiers). */
|
|
10623
11483
|
external_exports.object({ type: external_exports.literal("thinking"), chunk: external_exports.string() }),
|
|
10624
|
-
external_exports.object({
|
|
11484
|
+
external_exports.object({
|
|
11485
|
+
type: external_exports.literal("tool"),
|
|
11486
|
+
name: external_exports.string(),
|
|
11487
|
+
summary: external_exports.string(),
|
|
11488
|
+
/** Structured payload for calls that matter to continuity (TodoWrite's
|
|
11489
|
+
* actual task list). Optional: most tool calls have none, and omitting
|
|
11490
|
+
* it is always safe — `summary` alone still renders the transcript. */
|
|
11491
|
+
todos: external_exports.array(TodoItemSchema).optional()
|
|
11492
|
+
}),
|
|
10625
11493
|
external_exports.object({
|
|
10626
11494
|
type: external_exports.literal("approval"),
|
|
10627
11495
|
id: external_exports.string(),
|
|
@@ -10666,12 +11534,67 @@ var RunEventSchema = external_exports.discriminatedUnion("type", [
|
|
|
10666
11534
|
fiveHourResetsAtMs: external_exports.number(),
|
|
10667
11535
|
sevenDayUtilization: external_exports.number(),
|
|
10668
11536
|
sevenDayResetsAtMs: external_exports.number()
|
|
11537
|
+
}),
|
|
11538
|
+
/** Emitted once, when the daemon's own threshold check crosses before the
|
|
11539
|
+
* CLI itself refuses — the phone's cue to propose a handover (Execution
|
|
11540
|
+
* Plan 5) while there's still runway to do it gracefully. */
|
|
11541
|
+
external_exports.object({
|
|
11542
|
+
type: external_exports.literal("limit-hit"),
|
|
11543
|
+
reason: external_exports.enum(["rate_limit", "context_window", "cost_cap"]),
|
|
11544
|
+
/** 0-1 utilization that crossed the threshold, when known. */
|
|
11545
|
+
utilization: external_exports.number().optional()
|
|
11546
|
+
}),
|
|
11547
|
+
/** Session cost or wall-clock time crossed the cap set on it (Execution
|
|
11548
|
+
* Plan 1's budget feature) — informational, the run is not cancelled. */
|
|
11549
|
+
external_exports.object({
|
|
11550
|
+
type: external_exports.literal("budget-warning"),
|
|
11551
|
+
kind: external_exports.enum(["cost", "time"]),
|
|
11552
|
+
capUsd: external_exports.number().optional(),
|
|
11553
|
+
capMinutes: external_exports.number().optional(),
|
|
11554
|
+
actualUsd: external_exports.number().optional(),
|
|
11555
|
+
actualMinutes: external_exports.number().optional()
|
|
11556
|
+
}),
|
|
11557
|
+
/** The daemon is offering to move this session to another runner
|
|
11558
|
+
* (Execution Plan 4) — built from the log/diff alone, never from asking
|
|
11559
|
+
* this agent to cooperate (it may already be unable to). */
|
|
11560
|
+
external_exports.object({
|
|
11561
|
+
type: external_exports.literal("handover-proposed"),
|
|
11562
|
+
id: external_exports.string(),
|
|
11563
|
+
fromRunnerId: external_exports.string(),
|
|
11564
|
+
toRunnerId: external_exports.string(),
|
|
11565
|
+
reason: external_exports.enum(["rate_limit", "context_window", "cost_cap", "crash", "manual"]),
|
|
11566
|
+
packet: HandoverPacketSchema
|
|
11567
|
+
}),
|
|
11568
|
+
external_exports.object({
|
|
11569
|
+
type: external_exports.literal("handover-declined"),
|
|
11570
|
+
id: external_exports.string()
|
|
11571
|
+
}),
|
|
11572
|
+
external_exports.object({
|
|
11573
|
+
type: external_exports.literal("handover-accepted"),
|
|
11574
|
+
id: external_exports.string(),
|
|
11575
|
+
fromRunnerId: external_exports.string(),
|
|
11576
|
+
toRunnerId: external_exports.string()
|
|
11577
|
+
}),
|
|
11578
|
+
/** A proactive note the agent chose to push mid-run (Execution Plan 5's
|
|
11579
|
+
* offhand_status_update MCP tool) — outside the normal streamed
|
|
11580
|
+
* text/thinking, and never fabricated: only exists when the agent itself
|
|
11581
|
+
* called the tool. The most recent one on a session also becomes the
|
|
11582
|
+
* Handover Packet's optional `agentSummary` (Execution Plan 4) when a
|
|
11583
|
+
* handover is proposed afterward — still zero hosted AI, since it's the
|
|
11584
|
+
* same agent, on its own tokens, choosing to leave a note. */
|
|
11585
|
+
external_exports.object({
|
|
11586
|
+
type: external_exports.literal("status-update"),
|
|
11587
|
+
text: external_exports.string()
|
|
10669
11588
|
})
|
|
10670
11589
|
]);
|
|
10671
11590
|
var PermissionModeSchema = external_exports.enum(["guarded", "plan", "acceptEdits", "bypass"]);
|
|
10672
11591
|
var EffortSchema = external_exports.enum(["low", "medium", "high", "max"]);
|
|
10673
11592
|
var RunSpecSchema = external_exports.object({
|
|
10674
11593
|
runId: external_exports.string(),
|
|
11594
|
+
/** The offhand session this run belongs to — lets an MCP bridge tool
|
|
11595
|
+
* (Execution Plan 5) tag a proactive call (status update, dropped file)
|
|
11596
|
+
* with the right session without a global single-listener broker. */
|
|
11597
|
+
sessionId: external_exports.string(),
|
|
10675
11598
|
prompt: external_exports.string().min(1),
|
|
10676
11599
|
/** Absolute path of the workspace the agent runs in. */
|
|
10677
11600
|
workspace: external_exports.string(),
|
|
@@ -10696,7 +11619,11 @@ var RunnerInfoSchema = external_exports.object({
|
|
|
10696
11619
|
version: external_exports.string().optional(),
|
|
10697
11620
|
/** Model ids the phone may offer; empty = CLI default only. */
|
|
10698
11621
|
models: external_exports.array(external_exports.string()),
|
|
10699
|
-
supportsApprovals: external_exports.boolean()
|
|
11622
|
+
supportsApprovals: external_exports.boolean(),
|
|
11623
|
+
/** Thinking-budget tiers this runner actually honors (Execution Plan 3) —
|
|
11624
|
+
* undefined means the phone should hide the effort control entirely for
|
|
11625
|
+
* this runner, not show one that silently does nothing. */
|
|
11626
|
+
effortTiers: external_exports.array(external_exports.enum(["low", "medium", "high", "max"])).optional()
|
|
10700
11627
|
});
|
|
10701
11628
|
var ApprovalPolicySchema = external_exports.enum(["paranoid", "balanced", "trusting"]);
|
|
10702
11629
|
var WorkspaceInfoSchema = external_exports.object({
|
|
@@ -10723,12 +11650,23 @@ var SessionInfoSchema = external_exports.object({
|
|
|
10723
11650
|
effort: external_exports.enum(["low", "medium", "high", "max"]).optional(),
|
|
10724
11651
|
/** Whether a run is active or prompts are queued right now. */
|
|
10725
11652
|
busy: external_exports.boolean(),
|
|
10726
|
-
queuedPrompts: external_exports.number().int()
|
|
11653
|
+
queuedPrompts: external_exports.number().int(),
|
|
11654
|
+
/** Per-session budget caps (Execution Plan 1) — undefined means no cap set. */
|
|
11655
|
+
budgetCostUsdCap: external_exports.number().optional(),
|
|
11656
|
+
budgetMinutesCap: external_exports.number().int().optional()
|
|
10727
11657
|
});
|
|
10728
11658
|
var HostInfoSchema = external_exports.object({
|
|
10729
11659
|
hostname: external_exports.string(),
|
|
10730
11660
|
os: external_exports.string(),
|
|
10731
|
-
daemonVersion: external_exports.string()
|
|
11661
|
+
daemonVersion: external_exports.string(),
|
|
11662
|
+
/** Wake-on-LAN/WiFi capability, detected per device (Execution Plan 7) —
|
|
11663
|
+
* never assumed from the OS alone. Absent only if no probe has completed
|
|
11664
|
+
* yet (e.g. mid-startup). */
|
|
11665
|
+
wolCapability: external_exports.enum(["supported", "wifi-limited", "unsupported"]).optional(),
|
|
11666
|
+
/** Human-readable reason behind wolCapability — always present alongside
|
|
11667
|
+
* it, since a bare enum can't explain itself. */
|
|
11668
|
+
wolReason: external_exports.string().optional(),
|
|
11669
|
+
wolCheckedAtMs: external_exports.number().int().optional()
|
|
10732
11670
|
});
|
|
10733
11671
|
|
|
10734
11672
|
// ../shared/src/protocol-v2.ts
|
|
@@ -10762,6 +11700,12 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
10762
11700
|
answer: external_exports.string().optional()
|
|
10763
11701
|
}),
|
|
10764
11702
|
external_exports.object({ type: external_exports.literal("resume"), afterSeq: external_exports.number().int().nonnegative() }),
|
|
11703
|
+
external_exports.object({
|
|
11704
|
+
type: external_exports.literal("handover-response"),
|
|
11705
|
+
sessionId: external_exports.string(),
|
|
11706
|
+
handoverId: external_exports.string(),
|
|
11707
|
+
accept: external_exports.boolean()
|
|
11708
|
+
}),
|
|
10765
11709
|
external_exports.object({
|
|
10766
11710
|
type: external_exports.literal("drop-send"),
|
|
10767
11711
|
blobId: external_exports.string(),
|
|
@@ -10792,7 +11736,10 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
10792
11736
|
model: external_exports.string().optional(),
|
|
10793
11737
|
archived: external_exports.boolean().optional(),
|
|
10794
11738
|
permissionMode: external_exports.enum(["guarded", "plan", "acceptEdits", "bypass"]).optional(),
|
|
10795
|
-
effort: external_exports.enum(["low", "medium", "high", "max"]).optional()
|
|
11739
|
+
effort: external_exports.enum(["low", "medium", "high", "max"]).optional(),
|
|
11740
|
+
/** Per-session budget caps (Execution Plan 1). null clears an existing cap. */
|
|
11741
|
+
budgetCostUsdCap: external_exports.number().positive().nullable().optional(),
|
|
11742
|
+
budgetMinutesCap: external_exports.number().int().positive().nullable().optional()
|
|
10796
11743
|
}),
|
|
10797
11744
|
/** Start a fresh agent conversation inside the session. */
|
|
10798
11745
|
external_exports.object({ type: external_exports.literal("session-reset"), sessionId: external_exports.string() }),
|
|
@@ -10826,7 +11773,19 @@ var ClientMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
10826
11773
|
type: external_exports.literal("fs-list"),
|
|
10827
11774
|
rpcId: external_exports.string(),
|
|
10828
11775
|
path: external_exports.string().optional()
|
|
10829
|
-
})
|
|
11776
|
+
}),
|
|
11777
|
+
/** Mobile-first session bootstrap (Execution Plan 5): create a new project
|
|
11778
|
+
* folder from the phone's workspace browser, no laptop needed first. */
|
|
11779
|
+
external_exports.object({
|
|
11780
|
+
type: external_exports.literal("fs-create-folder"),
|
|
11781
|
+
rpcId: external_exports.string(),
|
|
11782
|
+
path: external_exports.string(),
|
|
11783
|
+
name: external_exports.string().min(1)
|
|
11784
|
+
}),
|
|
11785
|
+
/** Re-run the Wake-on-LAN/WiFi capability probe (Execution Plan 7) — an
|
|
11786
|
+
* explicit "check again" action, since drivers/BIOS settings/network type
|
|
11787
|
+
* can change after first setup. */
|
|
11788
|
+
external_exports.object({ type: external_exports.literal("wol-recheck") })
|
|
10830
11789
|
]);
|
|
10831
11790
|
var ReceiptSchema = external_exports.object({
|
|
10832
11791
|
runId: external_exports.string(),
|
|
@@ -10884,6 +11843,17 @@ var ServerMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
10884
11843
|
size: external_exports.number().int().nonnegative(),
|
|
10885
11844
|
direction: external_exports.enum(["to-phone", "to-pc"])
|
|
10886
11845
|
}),
|
|
11846
|
+
/** The daemon's own relay connection dropped/came back — logged durably
|
|
11847
|
+
* (unlike the relay's live `presence` frame) so a phone that wasn't
|
|
11848
|
+
* listening at the time can still reconstruct "laptop asleep since 11pm"
|
|
11849
|
+
* from history (Execution Plan 6's digest). Not tied to any run. */
|
|
11850
|
+
external_exports.object({ type: external_exports.literal("device-offline"), seq: external_exports.number().int(), atMs: external_exports.number().int() }),
|
|
11851
|
+
external_exports.object({
|
|
11852
|
+
type: external_exports.literal("device-reconnected"),
|
|
11853
|
+
seq: external_exports.number().int(),
|
|
11854
|
+
atMs: external_exports.number().int(),
|
|
11855
|
+
offlineForMs: external_exports.number().int().nonnegative()
|
|
11856
|
+
}),
|
|
10887
11857
|
// RPC responses (not seq-logged)
|
|
10888
11858
|
external_exports.object({
|
|
10889
11859
|
type: external_exports.literal("history-response"),
|
|
@@ -10909,7 +11879,12 @@ var ServerMessageSchema = external_exports.discriminatedUnion("type", [
|
|
|
10909
11879
|
rpcId: external_exports.string(),
|
|
10910
11880
|
path: external_exports.string(),
|
|
10911
11881
|
parent: external_exports.string().nullable(),
|
|
10912
|
-
dirs: external_exports.array(FsDirSchema)
|
|
11882
|
+
dirs: external_exports.array(FsDirSchema),
|
|
11883
|
+
/** Set only by fs-create-folder on failure — the generic top-level
|
|
11884
|
+
* `error` message carries no rpcId, so it can never resolve a specific
|
|
11885
|
+
* pending RPC (see client.ts's rpc()); this keeps the failure correctly
|
|
11886
|
+
* routed back to whoever asked, with the listing unchanged either way. */
|
|
11887
|
+
error: external_exports.string().optional()
|
|
10913
11888
|
}),
|
|
10914
11889
|
external_exports.object({ type: external_exports.literal("error"), message: external_exports.string() })
|
|
10915
11890
|
]);
|
|
@@ -10933,7 +11908,8 @@ var RelayFrameSchema = external_exports.discriminatedUnion("kind", [
|
|
|
10933
11908
|
*/
|
|
10934
11909
|
external_exports.object({
|
|
10935
11910
|
kind: external_exports.literal("notify"),
|
|
10936
|
-
notice: external_exports.enum(["approval", "drop", "done", "error", "progress"]),
|
|
11911
|
+
notice: external_exports.enum(["approval", "drop", "done", "error", "progress", "handover", "limit-hit"]),
|
|
11912
|
+
/** 'approval'/'handover' only — the approval/handover id being acted on. */
|
|
10937
11913
|
id: external_exports.string().optional(),
|
|
10938
11914
|
sessionId: external_exports.string().optional(),
|
|
10939
11915
|
/** 'progress' only — a bare action count, never the action itself (no content in push). */
|
|
@@ -10941,6 +11917,16 @@ var RelayFrameSchema = external_exports.discriminatedUnion("kind", [
|
|
|
10941
11917
|
}),
|
|
10942
11918
|
/** relay → daemon: verdict delivered via push-notification action button. */
|
|
10943
11919
|
external_exports.object({ kind: external_exports.literal("verdict"), approvalId: external_exports.string(), approve: external_exports.boolean() }),
|
|
11920
|
+
/** relay → daemon: handover accept/decline from a push-notification action
|
|
11921
|
+
* button (Execution Plan 6) — same "opaque ids only" shape as `verdict`,
|
|
11922
|
+
* with `chatSessionId` since handover-response needs it to look up the
|
|
11923
|
+
* pending proposal. */
|
|
11924
|
+
external_exports.object({
|
|
11925
|
+
kind: external_exports.literal("handover-verdict"),
|
|
11926
|
+
handoverId: external_exports.string(),
|
|
11927
|
+
chatSessionId: external_exports.string(),
|
|
11928
|
+
accept: external_exports.boolean()
|
|
11929
|
+
}),
|
|
10944
11930
|
external_exports.object({ kind: external_exports.literal("ping") }),
|
|
10945
11931
|
external_exports.object({ kind: external_exports.literal("pong") })
|
|
10946
11932
|
]);
|
|
@@ -11014,9 +12000,9 @@ if (typeof Module.getRandomValue === "undefined") {
|
|
|
11014
12000
|
Module.getRandomValue = randomValuesStandard;
|
|
11015
12001
|
} catch (e) {
|
|
11016
12002
|
try {
|
|
11017
|
-
|
|
12003
|
+
crypto = null;
|
|
11018
12004
|
randomValueNodeJS = function() {
|
|
11019
|
-
var buf =
|
|
12005
|
+
var buf = crypto["randomBytes"](4);
|
|
11020
12006
|
return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
11021
12007
|
};
|
|
11022
12008
|
randomValueNodeJS();
|
|
@@ -11029,7 +12015,7 @@ if (typeof Module.getRandomValue === "undefined") {
|
|
|
11029
12015
|
var window_;
|
|
11030
12016
|
var crypto_;
|
|
11031
12017
|
var randomValuesStandard;
|
|
11032
|
-
var
|
|
12018
|
+
var crypto;
|
|
11033
12019
|
var randomValueNodeJS;
|
|
11034
12020
|
var _Module = Module;
|
|
11035
12021
|
Module.ready = new Promise(function(resolve6, reject) {
|
|
@@ -37923,7 +38909,7 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
37923
38909
|
try {
|
|
37924
38910
|
var window_ = "object" === typeof window ? window : self;
|
|
37925
38911
|
var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
|
|
37926
|
-
crypto_ = crypto_ === void 0 ?
|
|
38912
|
+
crypto_ = crypto_ === void 0 ? crypto : crypto_;
|
|
37927
38913
|
var randomValuesStandard = function() {
|
|
37928
38914
|
var buf = new Uint32Array(1);
|
|
37929
38915
|
crypto_.getRandomValues(buf);
|
|
@@ -37933,9 +38919,9 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
37933
38919
|
Module3.getRandomValue = randomValuesStandard;
|
|
37934
38920
|
} catch (e) {
|
|
37935
38921
|
try {
|
|
37936
|
-
var
|
|
38922
|
+
var crypto = null;
|
|
37937
38923
|
var randomValueNodeJS = function() {
|
|
37938
|
-
var buf =
|
|
38924
|
+
var buf = crypto["randomBytes"](4);
|
|
37939
38925
|
return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
37940
38926
|
};
|
|
37941
38927
|
randomValueNodeJS();
|
|
@@ -38581,7 +39567,7 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
38581
39567
|
try {
|
|
38582
39568
|
var window_ = "object" === typeof window ? window : self;
|
|
38583
39569
|
var crypto_ = typeof window_.crypto !== "undefined" ? window_.crypto : window_.msCrypto;
|
|
38584
|
-
crypto_ = crypto_ === void 0 ?
|
|
39570
|
+
crypto_ = crypto_ === void 0 ? crypto : crypto_;
|
|
38585
39571
|
var randomValuesStandard = function() {
|
|
38586
39572
|
var buf = new Uint32Array(1);
|
|
38587
39573
|
crypto_.getRandomValues(buf);
|
|
@@ -38591,9 +39577,9 @@ Module.ready = new Promise(function(resolve6, reject) {
|
|
|
38591
39577
|
Module2.getRandomValue = randomValuesStandard;
|
|
38592
39578
|
} catch (e) {
|
|
38593
39579
|
try {
|
|
38594
|
-
var
|
|
39580
|
+
var crypto = null;
|
|
38595
39581
|
var randomValueNodeJS = function() {
|
|
38596
|
-
var buf =
|
|
39582
|
+
var buf = crypto["randomBytes"](4);
|
|
38597
39583
|
return (buf[0] << 24 | buf[1] << 16 | buf[2] << 8 | buf[3]) >>> 0;
|
|
38598
39584
|
};
|
|
38599
39585
|
randomValueNodeJS();
|
|
@@ -41515,7 +42501,7 @@ var LocalSessionServer = class {
|
|
|
41515
42501
|
constructor(manager2, port2, broker2) {
|
|
41516
42502
|
this.manager = manager2;
|
|
41517
42503
|
this.broker = broker2;
|
|
41518
|
-
const http =
|
|
42504
|
+
const http = createServer2((req, res) => void this.onRequest(req, res));
|
|
41519
42505
|
this.wss = new import_websocket_server.default({ server: http });
|
|
41520
42506
|
this.wss.on("connection", (ws) => this.onConnection(ws));
|
|
41521
42507
|
http.on("error", (err) => {
|
|
@@ -41548,6 +42534,22 @@ var LocalSessionServer = class {
|
|
|
41548
42534
|
}
|
|
41549
42535
|
return;
|
|
41550
42536
|
}
|
|
42537
|
+
if (req.method === "POST" && req.url === "/status") {
|
|
42538
|
+
try {
|
|
42539
|
+
const chunks = [];
|
|
42540
|
+
for await (const c2 of req) chunks.push(c2);
|
|
42541
|
+
const body = JSON.parse(Buffer.concat(chunks).toString());
|
|
42542
|
+
if (typeof body.sessionId === "string" && typeof body.text === "string" && body.text.trim()) {
|
|
42543
|
+
this.manager.recordStatusUpdate(body.sessionId, body.text.trim());
|
|
42544
|
+
}
|
|
42545
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
42546
|
+
res.end(JSON.stringify({ ok: true }));
|
|
42547
|
+
} catch (e) {
|
|
42548
|
+
res.writeHead(400, { "content-type": "application/json" });
|
|
42549
|
+
res.end(JSON.stringify({ ok: false, message: `bad status request: ${String(e)}` }));
|
|
42550
|
+
}
|
|
42551
|
+
return;
|
|
42552
|
+
}
|
|
41551
42553
|
res.writeHead(404);
|
|
41552
42554
|
res.end();
|
|
41553
42555
|
}
|
|
@@ -41583,8 +42585,19 @@ var RelayClient = class {
|
|
|
41583
42585
|
heartbeat = null;
|
|
41584
42586
|
detach = null;
|
|
41585
42587
|
stopped = false;
|
|
42588
|
+
/** Set when a heartbeat ping goes out; cleared by ANY inbound frame. If
|
|
42589
|
+
* still set on the next tick, nothing has answered in a full interval —
|
|
42590
|
+
* the connection is a zombie (readyState still reports OPEN even though
|
|
42591
|
+
* the underlying TCP connection died silently, e.g. after laptop sleep or
|
|
42592
|
+
* a NAT/firewall idle timeout — no close/error event fires on its own). */
|
|
42593
|
+
awaitingPongSince = null;
|
|
41586
42594
|
/** sessionId → tool-call count for the in-flight run (opaque count only, never the action). */
|
|
41587
42595
|
toolCounts = /* @__PURE__ */ new Map();
|
|
42596
|
+
/** True once we've been online at least once — a failed first connection
|
|
42597
|
+
* attempt isn't "going offline" (there was nothing to lose), so the very
|
|
42598
|
+
* first successful open never logs a reconnect. */
|
|
42599
|
+
everConnected = false;
|
|
42600
|
+
offlineSinceMs = null;
|
|
41588
42601
|
start() {
|
|
41589
42602
|
this.connect();
|
|
41590
42603
|
}
|
|
@@ -41599,8 +42612,13 @@ var RelayClient = class {
|
|
|
41599
42612
|
}
|
|
41600
42613
|
connect() {
|
|
41601
42614
|
if (this.stopped) return;
|
|
42615
|
+
console.log(`relay: connecting (${this.relayUrl})`);
|
|
41602
42616
|
const ws = new wrapper_default(this.url());
|
|
41603
42617
|
this.ws = ws;
|
|
42618
|
+
const slowStartTimer = setTimeout(() => {
|
|
42619
|
+
console.log("relay: still connecting \u2014 the hosted relay may be waking up from a cold start (can take up to a minute)");
|
|
42620
|
+
}, 8e3);
|
|
42621
|
+
const clearSlowStartTimer = () => clearTimeout(slowStartTimer);
|
|
41604
42622
|
const send = (msg) => {
|
|
41605
42623
|
if (ws.readyState === wrapper_default.OPEN) {
|
|
41606
42624
|
const envelope = seal(msg, this.keys.tx);
|
|
@@ -41640,24 +42658,52 @@ var RelayClient = class {
|
|
|
41640
42658
|
JSON.stringify({ kind: "notify", notice: "error", sessionId: msg.sessionId })
|
|
41641
42659
|
);
|
|
41642
42660
|
}
|
|
42661
|
+
if (msg.type === "run-event" && msg.event.type === "handover-proposed") {
|
|
42662
|
+
ws.send(
|
|
42663
|
+
JSON.stringify({
|
|
42664
|
+
kind: "notify",
|
|
42665
|
+
notice: "handover",
|
|
42666
|
+
id: msg.event.id,
|
|
42667
|
+
sessionId: msg.sessionId
|
|
42668
|
+
})
|
|
42669
|
+
);
|
|
42670
|
+
}
|
|
42671
|
+
if (msg.type === "run-event" && msg.event.type === "limit-hit") {
|
|
42672
|
+
ws.send(
|
|
42673
|
+
JSON.stringify({ kind: "notify", notice: "limit-hit", sessionId: msg.sessionId })
|
|
42674
|
+
);
|
|
42675
|
+
}
|
|
41643
42676
|
if (msg.type === "drop" && msg.direction === "to-phone") {
|
|
41644
42677
|
ws.send(JSON.stringify({ kind: "notify", notice: "drop" }));
|
|
41645
42678
|
}
|
|
41646
42679
|
}
|
|
41647
42680
|
};
|
|
41648
42681
|
ws.on("open", () => {
|
|
42682
|
+
clearSlowStartTimer();
|
|
41649
42683
|
this.backoffMs = 500;
|
|
42684
|
+
this.awaitingPongSince = null;
|
|
41650
42685
|
console.log(`relay: connected (${this.relayUrl})`);
|
|
42686
|
+
if (this.everConnected && this.offlineSinceMs !== null) {
|
|
42687
|
+
this.manager.recordDeviceReconnected(Date.now() - this.offlineSinceMs);
|
|
42688
|
+
}
|
|
42689
|
+
this.everConnected = true;
|
|
42690
|
+
this.offlineSinceMs = null;
|
|
41651
42691
|
this.detach = this.manager.attach(send);
|
|
41652
42692
|
send(this.manager.hello());
|
|
41653
42693
|
void this.manager.manifest().then(send);
|
|
41654
42694
|
this.heartbeat = setInterval(() => {
|
|
41655
|
-
if (ws.readyState
|
|
41656
|
-
|
|
42695
|
+
if (ws.readyState !== wrapper_default.OPEN) return;
|
|
42696
|
+
if (this.awaitingPongSince !== null) {
|
|
42697
|
+
console.log("relay: no response since last heartbeat \u2014 reconnecting");
|
|
42698
|
+
ws.terminate();
|
|
42699
|
+
return;
|
|
41657
42700
|
}
|
|
42701
|
+
this.awaitingPongSince = Date.now();
|
|
42702
|
+
ws.send(JSON.stringify({ kind: "ping" }));
|
|
41658
42703
|
}, HEARTBEAT_MS);
|
|
41659
42704
|
});
|
|
41660
42705
|
ws.on("message", (raw) => {
|
|
42706
|
+
this.awaitingPongSince = null;
|
|
41661
42707
|
let frame;
|
|
41662
42708
|
try {
|
|
41663
42709
|
frame = parseRelayFrame(raw.toString());
|
|
@@ -41671,6 +42717,18 @@ var RelayClient = class {
|
|
|
41671
42717
|
);
|
|
41672
42718
|
return;
|
|
41673
42719
|
}
|
|
42720
|
+
if (frame.kind === "handover-verdict") {
|
|
42721
|
+
this.manager.handle(
|
|
42722
|
+
{
|
|
42723
|
+
type: "handover-response",
|
|
42724
|
+
sessionId: frame.chatSessionId,
|
|
42725
|
+
handoverId: frame.handoverId,
|
|
42726
|
+
accept: frame.accept
|
|
42727
|
+
},
|
|
42728
|
+
send
|
|
42729
|
+
);
|
|
42730
|
+
return;
|
|
42731
|
+
}
|
|
41674
42732
|
if (frame.kind !== "peer") return;
|
|
41675
42733
|
try {
|
|
41676
42734
|
const envelope = EnvelopeSchema.parse(frame.payload);
|
|
@@ -41683,7 +42741,12 @@ var RelayClient = class {
|
|
|
41683
42741
|
console.error(`relay: ${err.message}`);
|
|
41684
42742
|
});
|
|
41685
42743
|
ws.on("close", () => {
|
|
42744
|
+
clearSlowStartTimer();
|
|
41686
42745
|
this.cleanup();
|
|
42746
|
+
if (this.everConnected && this.offlineSinceMs === null) {
|
|
42747
|
+
this.offlineSinceMs = Date.now();
|
|
42748
|
+
this.manager.recordDeviceOffline();
|
|
42749
|
+
}
|
|
41687
42750
|
if (this.stopped) return;
|
|
41688
42751
|
console.log(`relay: disconnected \u2014 retrying in ${this.backoffMs}ms`);
|
|
41689
42752
|
setTimeout(() => this.connect(), this.backoffMs);
|
|
@@ -41699,7 +42762,7 @@ var RelayClient = class {
|
|
|
41699
42762
|
};
|
|
41700
42763
|
|
|
41701
42764
|
// ../daemon/src/pairing.ts
|
|
41702
|
-
import { existsSync as
|
|
42765
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync4 } from "node:fs";
|
|
41703
42766
|
import { homedir as homedir7 } from "node:os";
|
|
41704
42767
|
import { join as join8 } from "node:path";
|
|
41705
42768
|
import { randomInt } from "node:crypto";
|
|
@@ -41709,7 +42772,7 @@ var POLL_MS = 2e3;
|
|
|
41709
42772
|
var POLL_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
41710
42773
|
async function ensurePairing(relayUrl2, forceNew = false, webUrl2 = "https://offhand-web.onrender.com") {
|
|
41711
42774
|
await ready;
|
|
41712
|
-
if (!forceNew &&
|
|
42775
|
+
if (!forceNew && existsSync8(PAIRING_PATH)) {
|
|
41713
42776
|
const f2 = JSON.parse(readFileSync4(PAIRING_PATH, "utf8"));
|
|
41714
42777
|
const kp2 = { publicKey: fromB64u(f2.daemonPublicKey), secretKey: fromB64u(f2.daemonSecretKey) };
|
|
41715
42778
|
const phonePk = fromB64u(f2.phonePublicKey);
|
|
@@ -41864,7 +42927,7 @@ var ApprovalBroker = class {
|
|
|
41864
42927
|
};
|
|
41865
42928
|
function classifyRisk(toolName, input) {
|
|
41866
42929
|
const i2 = input ?? {};
|
|
41867
|
-
const text = [toolName, i2.command, i2.file_path, i2.path].filter((x2) => typeof x2 === "string").join(" ");
|
|
42930
|
+
const text = [toolName, i2.command, i2.file_path, i2.path, i2.filepath].filter((x2) => typeof x2 === "string").join(" ");
|
|
41868
42931
|
return /\b(rm|del|rmdir|rd|format|mkfs|shutdown|reboot|kill|drop\s+table|truncate|git\s+push\s+--force|--hard)\b/i.test(
|
|
41869
42932
|
text
|
|
41870
42933
|
) ? "high" : "low";
|
|
@@ -41873,11 +42936,11 @@ function summariseInput(input) {
|
|
|
41873
42936
|
if (typeof input !== "object" || input === null) return String(input ?? "");
|
|
41874
42937
|
const i2 = input;
|
|
41875
42938
|
const q2 = firstQuestion(i2);
|
|
41876
|
-
if (q2) return
|
|
41877
|
-
for (const key of ["command", "file_path", "path", "url", "pattern", "description"]) {
|
|
41878
|
-
if (typeof i2[key] === "string" && i2[key] !== "") return
|
|
42939
|
+
if (q2) return truncate6(q2.text, 200);
|
|
42940
|
+
for (const key of ["command", "file_path", "filepath", "path", "url", "pattern", "description"]) {
|
|
42941
|
+
if (typeof i2[key] === "string" && i2[key] !== "") return truncate6(`${key}: ${i2[key]}`, 200);
|
|
41879
42942
|
}
|
|
41880
|
-
return
|
|
42943
|
+
return truncate6(JSON.stringify(input), 200);
|
|
41881
42944
|
}
|
|
41882
42945
|
function firstQuestion(i2) {
|
|
41883
42946
|
if (!Array.isArray(i2.questions) || i2.questions.length === 0) return void 0;
|
|
@@ -41895,7 +42958,7 @@ function buildPreview(toolName, input) {
|
|
|
41895
42958
|
const q2 = firstQuestion(i2);
|
|
41896
42959
|
if (q2) {
|
|
41897
42960
|
const lines = [q2.text, ...q2.options.map((o2, n2) => `${n2 + 1}. ${o2.label}${o2.description ? ` \u2014 ${o2.description}` : ""}`)];
|
|
41898
|
-
return
|
|
42961
|
+
return truncate6(lines.join("\n"), 600);
|
|
41899
42962
|
}
|
|
41900
42963
|
if (/^(Edit|MultiEdit)/.test(toolName) && typeof i2.new_string === "string") {
|
|
41901
42964
|
const oldS = typeof i2.old_string === "string" ? i2.old_string : "";
|
|
@@ -41903,15 +42966,24 @@ function buildPreview(toolName, input) {
|
|
|
41903
42966
|
...oldS.split("\n").map((l2) => `- ${l2}`),
|
|
41904
42967
|
...i2.new_string.split("\n").map((l2) => `+ ${l2}`)
|
|
41905
42968
|
];
|
|
41906
|
-
return
|
|
42969
|
+
return truncate6(lines.join("\n"), 600);
|
|
41907
42970
|
}
|
|
41908
42971
|
if (/^Write/.test(toolName) && typeof i2.content === "string") {
|
|
41909
|
-
return
|
|
42972
|
+
return truncate6(i2.content.split("\n").map((l2) => `+ ${l2}`).join("\n"), 600);
|
|
42973
|
+
}
|
|
42974
|
+
if (typeof i2.diff === "string" && i2.diff.trim()) {
|
|
42975
|
+
return truncate6(formatUnifiedDiff(i2.diff), 600);
|
|
41910
42976
|
}
|
|
41911
|
-
if (typeof i2.
|
|
42977
|
+
if (typeof i2.url === "string") return truncate6(`\u2192 ${i2.url}`, 600);
|
|
42978
|
+
if (typeof i2.command === "string") return truncate6(`$ ${i2.command}`, 600);
|
|
41912
42979
|
return void 0;
|
|
41913
42980
|
}
|
|
41914
|
-
function
|
|
42981
|
+
function formatUnifiedDiff(diff) {
|
|
42982
|
+
return diff.split("\n").filter(
|
|
42983
|
+
(l2) => !l2.startsWith("index ") && !l2.startsWith("Index:") && !/^={3,}$/.test(l2) && !/^(new|deleted) file mode /.test(l2) && !/^(old|new) mode /.test(l2)
|
|
42984
|
+
).join("\n").trimEnd();
|
|
42985
|
+
}
|
|
42986
|
+
function truncate6(s2, max) {
|
|
41915
42987
|
return s2.length <= max ? s2 : s2.slice(0, max - 1) + "\u2026";
|
|
41916
42988
|
}
|
|
41917
42989
|
|
|
@@ -41950,7 +43022,7 @@ async function downloadArtifact(relayUrl2, sessionId, blobId, keys) {
|
|
|
41950
43022
|
// ../daemon/src/autostart.ts
|
|
41951
43023
|
import { homedir as homedir8 } from "node:os";
|
|
41952
43024
|
import { join as join10, resolve as resolve4, dirname as dirname3 } from "node:path";
|
|
41953
|
-
import { existsSync as
|
|
43025
|
+
import { existsSync as existsSync9, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync5, rmSync } from "node:fs";
|
|
41954
43026
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
41955
43027
|
import { spawnSync } from "node:child_process";
|
|
41956
43028
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
@@ -41971,27 +43043,23 @@ function getWrapperPath() {
|
|
|
41971
43043
|
function getShortcutPath() {
|
|
41972
43044
|
return join10(getStartupDir(), SHORTCUT_NAME);
|
|
41973
43045
|
}
|
|
41974
|
-
function buildWrapperScript(
|
|
41975
|
-
const
|
|
41976
|
-
const
|
|
41977
|
-
const
|
|
41978
|
-
const
|
|
41979
|
-
const
|
|
41980
|
-
const
|
|
41981
|
-
const
|
|
41982
|
-
|
|
41983
|
-
|
|
41984
|
-
|
|
41985
|
-
|
|
41986
|
-
|
|
41987
|
-
|
|
41988
|
-
|
|
41989
|
-
|
|
41990
|
-
|
|
41991
|
-
const cmd = parts.join(" ");
|
|
41992
|
-
return `@echo off
|
|
41993
|
-
cd /d "${projectRoot}"
|
|
41994
|
-
${cmd}
|
|
43046
|
+
function buildWrapperScript(config2, entry = process.argv[1] ?? "", execPath = process.execPath) {
|
|
43047
|
+
const wsArgs2 = config2.workspaces.map((w2) => `--workspace "${w2}"`).join(" ");
|
|
43048
|
+
const relayArg = config2.relayUrl ? `--relay "${config2.relayUrl}"` : "";
|
|
43049
|
+
const devUrlArg = config2.devUrl ? `--dev-url "${config2.devUrl}"` : "";
|
|
43050
|
+
const portArg = `--port ${config2.port}`;
|
|
43051
|
+
const approvalArg = `--approval-timeout ${config2.approvalTimeout ?? 300}`;
|
|
43052
|
+
const webUrlArg = `--web-url "${config2.webUrl ?? "https://offhand-web.onrender.com"}"`;
|
|
43053
|
+
const args2 = [wsArgs2, relayArg, devUrlArg, portArg, approvalArg, webUrlArg].filter(Boolean).join(" ");
|
|
43054
|
+
if (entry.endsWith(".ts")) {
|
|
43055
|
+
const projectRoot = resolve4(__dirname2, "..", "..");
|
|
43056
|
+
return `@echo off\r
|
|
43057
|
+
cd /d "${projectRoot}"\r
|
|
43058
|
+
pnpm --filter @offhand/daemon dev -- ${args2}\r
|
|
43059
|
+
`;
|
|
43060
|
+
}
|
|
43061
|
+
return `@echo off\r
|
|
43062
|
+
"${execPath}" "${entry}" ${args2}\r
|
|
41995
43063
|
`;
|
|
41996
43064
|
}
|
|
41997
43065
|
function createShortcut(targetPath, shortcutPath) {
|
|
@@ -42012,11 +43080,11 @@ $Shortcut.Save()`;
|
|
|
42012
43080
|
return { success: result.status === 0, output: result.stdout + result.stderr };
|
|
42013
43081
|
}
|
|
42014
43082
|
function checkShortcutExists(shortcutPath) {
|
|
42015
|
-
return
|
|
43083
|
+
return existsSync9(shortcutPath);
|
|
42016
43084
|
}
|
|
42017
|
-
function installAutostart(
|
|
43085
|
+
function installAutostart(config2) {
|
|
42018
43086
|
const wrapperPath = getWrapperPath();
|
|
42019
|
-
const script = buildWrapperScript(
|
|
43087
|
+
const script = buildWrapperScript(config2);
|
|
42020
43088
|
writeFileSync4(wrapperPath, script);
|
|
42021
43089
|
const shortcutPath = getShortcutPath();
|
|
42022
43090
|
const result = createShortcut(wrapperPath, shortcutPath);
|
|
@@ -42032,7 +43100,7 @@ function getAutostartStatus() {
|
|
|
42032
43100
|
const shortcutPath = getShortcutPath();
|
|
42033
43101
|
const installed = checkShortcutExists(shortcutPath);
|
|
42034
43102
|
const wrapperPath = getWrapperPath();
|
|
42035
|
-
const wrapperExists =
|
|
43103
|
+
const wrapperExists = existsSync9(wrapperPath);
|
|
42036
43104
|
let details = `Shortcut: ${shortcutPath} (${installed ? "EXISTS" : "MISSING"})`;
|
|
42037
43105
|
if (wrapperExists) {
|
|
42038
43106
|
details += `
|
|
@@ -42040,14 +43108,13 @@ Wrapper: ${wrapperPath} (EXISTS)`;
|
|
|
42040
43108
|
}
|
|
42041
43109
|
return { installed, details };
|
|
42042
43110
|
}
|
|
42043
|
-
function saveConfig(
|
|
43111
|
+
function saveConfig(config2) {
|
|
42044
43112
|
const configPath = join10(OFFHAND_HOME, "autostart.json");
|
|
42045
|
-
writeFileSync4(configPath, JSON.stringify(
|
|
43113
|
+
writeFileSync4(configPath, JSON.stringify(config2, null, 2));
|
|
42046
43114
|
}
|
|
42047
43115
|
function getConfigFromStore() {
|
|
42048
|
-
const workspaces = ["C:\\Users\\udbha\\dev\\offhand"];
|
|
42049
43116
|
return {
|
|
42050
|
-
workspaces,
|
|
43117
|
+
workspaces: [],
|
|
42051
43118
|
port: 4317,
|
|
42052
43119
|
relayUrl: void 0,
|
|
42053
43120
|
devUrl: void 0,
|
|
@@ -42090,7 +43157,7 @@ var devUrl = argValue("--dev-url");
|
|
|
42090
43157
|
var store = new Store();
|
|
42091
43158
|
var wsArgs = argValues("--workspace").map((w2) => resolve5(w2));
|
|
42092
43159
|
for (const w2 of wsArgs) {
|
|
42093
|
-
if (!
|
|
43160
|
+
if (!existsSync10(w2)) {
|
|
42094
43161
|
console.error(`workspace does not exist: ${w2}`);
|
|
42095
43162
|
process.exit(1);
|
|
42096
43163
|
}
|
|
@@ -42102,7 +43169,7 @@ var approvalUrl = `http://127.0.0.1:${port}/approval`;
|
|
|
42102
43169
|
var runners = [
|
|
42103
43170
|
new ClaudeCodeRunner(broker, approvalUrl),
|
|
42104
43171
|
new CopilotCliRunner(),
|
|
42105
|
-
new OpenCodeRunner(),
|
|
43172
|
+
new OpenCodeRunner(broker),
|
|
42106
43173
|
new CodexCliRunner(),
|
|
42107
43174
|
new CursorAgentRunner(),
|
|
42108
43175
|
new GeminiCliRunner()
|
|
@@ -42123,25 +43190,20 @@ if (m2.type === "manifest") {
|
|
|
42123
43190
|
new LocalSessionServer(manager, port, broker);
|
|
42124
43191
|
console.log(` local : ws://127.0.0.1:${port}`);
|
|
42125
43192
|
console.log(` approvals : timeout ${approvalTimeoutMs / 1e3}s then auto-deny`);
|
|
42126
|
-
var
|
|
42127
|
-
|
|
42128
|
-
|
|
42129
|
-
|
|
42130
|
-
|
|
42131
|
-
|
|
42132
|
-
|
|
42133
|
-
|
|
42134
|
-
|
|
42135
|
-
|
|
42136
|
-
|
|
42137
|
-
|
|
42138
|
-
if (success) {
|
|
42139
|
-
console.log(` autostart : installed (runs on login)`);
|
|
42140
|
-
} else {
|
|
42141
|
-
console.warn(` autostart : failed to install: ${output}`);
|
|
42142
|
-
}
|
|
43193
|
+
var wasInstalled = getAutostartStatus().installed;
|
|
43194
|
+
var config = getConfigFromStore();
|
|
43195
|
+
config.workspaces = store.listWorkspaces().map((w2) => w2.path);
|
|
43196
|
+
config.port = port;
|
|
43197
|
+
config.relayUrl = relayUrl;
|
|
43198
|
+
config.devUrl = devUrl;
|
|
43199
|
+
config.approvalTimeout = approvalTimeoutMs / 1e3;
|
|
43200
|
+
config.webUrl = webUrl;
|
|
43201
|
+
saveConfig(config);
|
|
43202
|
+
var { success, output } = installAutostart(config);
|
|
43203
|
+
if (success) {
|
|
43204
|
+
console.log(` autostart : ${wasInstalled ? "refreshed" : "installed"} (runs on login)`);
|
|
42143
43205
|
} else {
|
|
42144
|
-
console.
|
|
43206
|
+
console.warn(` autostart : failed to ${wasInstalled ? "refresh" : "install"}: ${output}`);
|
|
42145
43207
|
}
|
|
42146
43208
|
if (relayUrl) {
|
|
42147
43209
|
const pairing = await ensurePairing(relayUrl, forceRepair, webUrl);
|