@paradow/ledge 0.26.0 → 0.28.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/{action-SCMMRWYI.js → action-NQRSZEC5.js} +4 -4
- package/dist/cli/{attach-EFIN5VU6.js → attach-KXOSFUHO.js} +8 -8
- package/dist/cli/{bridge-QT5X6TWJ.js → bridge-XCTXVWKH.js} +1 -1
- package/dist/cli/{chunk-6F6JTPMC.js → chunk-3CF4FQWG.js} +2 -2
- package/dist/cli/{chunk-JHUIHBC7.js → chunk-3NQNWGDX.js} +2 -2
- package/dist/cli/{chunk-5PTC732E.js → chunk-CIQAFCX3.js} +3 -3
- package/dist/cli/{chunk-SYXW4YB6.js → chunk-CR3LGTZQ.js} +3 -3
- package/dist/cli/{chunk-II6VRGLU.js → chunk-JHFKBKSO.js} +1 -1
- package/dist/cli/{chunk-L3GYGJRE.js → chunk-JLUHY5AB.js} +3 -3
- package/dist/cli/{chunk-PTY4LXTG.js → chunk-LG3XFIPV.js} +1 -1
- package/dist/cli/{chunk-2V2OJR2N.js → chunk-OYDTU6AP.js} +373 -58
- package/dist/cli/{chunk-HCTXTN2X.js → chunk-QSRB3F5Y.js} +2 -2
- package/dist/cli/{chunk-WYVMCF2K.js → chunk-RYZ6FHYT.js} +5 -5
- package/dist/cli/chunk-UME2F7VM.js +6 -0
- package/dist/cli/{chunk-QRD74KPO.js → chunk-VKRFHDRK.js} +151 -13
- package/dist/cli/{chunk-NYGLT5G2.js → chunk-VMGACWNP.js} +1 -1
- package/dist/cli/{chunk-TYNRKZFE.js → chunk-ZI47U5CQ.js} +2 -2
- package/dist/cli/{daemon-U6NA6PVD.js → daemon-ZUHJKLSL.js} +34 -15
- package/dist/cli/{decide-XOUTZT3S.js → decide-OSSTI3FE.js} +3 -3
- package/dist/cli/{handover-6CQKUZJV.js → handover-2G7OEWIG.js} +4 -4
- package/dist/cli/{hook-ZIJACTCF.js → hook-XY4E5H7O.js} +9 -9
- package/dist/cli/index.js +16 -16
- package/dist/cli/{launch-QWPA2P66.js → launch-7WOARLZM.js} +8 -8
- package/dist/cli/{send-T5IOOVGX.js → send-RUHGKLMA.js} +4 -4
- package/dist/cli/{status-EZPFE5CL.js → status-4Z2VJFID.js} +2 -2
- package/dist/cli/{task-B2QVGHDF.js → task-2GESYTOL.js} +4 -4
- package/dist/cli/{team-V6VPO3CB.js → team-MTSKNMAE.js} +6 -6
- package/dist/cli/{tui-MM64KSQ2.js → tui-4IRYQXYU.js} +8 -8
- package/dist/cli/{version-SK4VC4AE.js → version-L2Y7MRH6.js} +1 -1
- package/dist/cli/{who-XXZUCJPD.js → who-PSKHASMK.js} +3 -3
- package/package.json +1 -1
- package/dist/cli/chunk-3PLC745L.js +0 -6
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
templateExists,
|
|
11
11
|
templateId,
|
|
12
12
|
templateText
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-QSRB3F5Y.js";
|
|
14
14
|
import {
|
|
15
15
|
NO_ALWAYS,
|
|
16
16
|
NO_NOTE,
|
|
@@ -39,7 +39,7 @@ import {
|
|
|
39
39
|
shownCommand,
|
|
40
40
|
teammates,
|
|
41
41
|
withImages
|
|
42
|
-
} from "./chunk-
|
|
42
|
+
} from "./chunk-ZI47U5CQ.js";
|
|
43
43
|
import {
|
|
44
44
|
definitionFiles,
|
|
45
45
|
definitionHash,
|
|
@@ -47,7 +47,7 @@ import {
|
|
|
47
47
|
loadRole,
|
|
48
48
|
quoteScalar,
|
|
49
49
|
repoRoot
|
|
50
|
-
} from "./chunk-
|
|
50
|
+
} from "./chunk-JHFKBKSO.js";
|
|
51
51
|
import {
|
|
52
52
|
PARENT_AGENT_VARS,
|
|
53
53
|
agentEnv,
|
|
@@ -57,7 +57,7 @@ import {
|
|
|
57
57
|
labelTranscript,
|
|
58
58
|
resumeLabel,
|
|
59
59
|
spawnSession
|
|
60
|
-
} from "./chunk-
|
|
60
|
+
} from "./chunk-LG3XFIPV.js";
|
|
61
61
|
import {
|
|
62
62
|
ACTIONS,
|
|
63
63
|
ATTACHMENT_ID,
|
|
@@ -66,6 +66,8 @@ import {
|
|
|
66
66
|
AttachmentRefSchema,
|
|
67
67
|
BRIEF_MAX,
|
|
68
68
|
CHAT_HISTORY_BUDGET,
|
|
69
|
+
CHECKLIST_TASKS_MAX,
|
|
70
|
+
CHECKLIST_TEXT_MAX,
|
|
69
71
|
CONTEXT_NEAR,
|
|
70
72
|
ContextPrefsSchema,
|
|
71
73
|
DEFAULT_PROJECTS,
|
|
@@ -80,6 +82,7 @@ import {
|
|
|
80
82
|
MESSAGE_MAX,
|
|
81
83
|
MachinesSchema,
|
|
82
84
|
ModelIdSchema,
|
|
85
|
+
ModelRefusalSchema,
|
|
83
86
|
NEVER_PROPOSED,
|
|
84
87
|
NOTE_NOT_TYPED,
|
|
85
88
|
OperationIdSchema,
|
|
@@ -88,6 +91,7 @@ import {
|
|
|
88
91
|
PREVIEW_MAX,
|
|
89
92
|
PROTOCOL_PORT,
|
|
90
93
|
PhoneClientMessageSchema,
|
|
94
|
+
PhoneModelChoiceSchema,
|
|
91
95
|
PhoneRoleSchema,
|
|
92
96
|
PhoneTemplateSchema,
|
|
93
97
|
ProjectsSchema,
|
|
@@ -95,6 +99,7 @@ import {
|
|
|
95
99
|
RefusalCodeSchema,
|
|
96
100
|
RoleIdSchema,
|
|
97
101
|
RouteResultSchema,
|
|
102
|
+
SEMVER,
|
|
98
103
|
SESSION_KEY_VAR,
|
|
99
104
|
SNAPSHOT_BODY_MAX,
|
|
100
105
|
SshAliasSchema,
|
|
@@ -102,6 +107,7 @@ import {
|
|
|
102
107
|
THREAD_BUDGET,
|
|
103
108
|
THREAD_ITEMS_MAX,
|
|
104
109
|
THREAD_TEXT_MAX,
|
|
110
|
+
TODO_TOOLS,
|
|
105
111
|
TOOL_TEXT_MAX,
|
|
106
112
|
TOPIC_MAX,
|
|
107
113
|
TeamSchema,
|
|
@@ -113,6 +119,7 @@ import {
|
|
|
113
119
|
backendOf,
|
|
114
120
|
canonicalJson,
|
|
115
121
|
contextNear,
|
|
122
|
+
doneCount,
|
|
116
123
|
ensureDaemonToken,
|
|
117
124
|
extractProposal,
|
|
118
125
|
givenByLedge,
|
|
@@ -120,6 +127,7 @@ import {
|
|
|
120
127
|
isArchived,
|
|
121
128
|
isTeamLive,
|
|
122
129
|
isTeamsOperation,
|
|
130
|
+
latestTodos,
|
|
123
131
|
offers,
|
|
124
132
|
ompApprovalFor,
|
|
125
133
|
padPlain,
|
|
@@ -131,6 +139,7 @@ import {
|
|
|
131
139
|
parseServerMessage,
|
|
132
140
|
parseTaskBlock,
|
|
133
141
|
parseTeamBlock,
|
|
142
|
+
pcSubject,
|
|
134
143
|
pendingKey,
|
|
135
144
|
pickName,
|
|
136
145
|
projectOf,
|
|
@@ -145,7 +154,7 @@ import {
|
|
|
145
154
|
teamsPurpose,
|
|
146
155
|
tokenEquals,
|
|
147
156
|
transmissions
|
|
148
|
-
} from "./chunk-
|
|
157
|
+
} from "./chunk-VKRFHDRK.js";
|
|
149
158
|
import {
|
|
150
159
|
log
|
|
151
160
|
} from "./chunk-6IYPRUQG.js";
|
|
@@ -2239,7 +2248,7 @@ function teamsJournal(path) {
|
|
|
2239
2248
|
|
|
2240
2249
|
// src/daemon/server.ts
|
|
2241
2250
|
import { createServer } from "http";
|
|
2242
|
-
import { randomUUID as
|
|
2251
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2243
2252
|
import { homedir as homedir7 } from "os";
|
|
2244
2253
|
import { basename as basename6, join as join18, resolve as resolve8 } from "path";
|
|
2245
2254
|
import { existsSync } from "fs";
|
|
@@ -2276,12 +2285,12 @@ var RpcError = class extends Error {
|
|
|
2276
2285
|
};
|
|
2277
2286
|
function lineReader(max, onLine, onBad) {
|
|
2278
2287
|
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
2279
|
-
let
|
|
2288
|
+
let parts2 = [];
|
|
2280
2289
|
let size = 0;
|
|
2281
2290
|
let skipping = false;
|
|
2282
2291
|
const flush = () => {
|
|
2283
|
-
const whole =
|
|
2284
|
-
|
|
2292
|
+
const whole = parts2.length === 1 ? parts2[0] : Buffer.concat(parts2);
|
|
2293
|
+
parts2 = [];
|
|
2285
2294
|
size = 0;
|
|
2286
2295
|
let text;
|
|
2287
2296
|
try {
|
|
@@ -2301,10 +2310,10 @@ function lineReader(max, onLine, onBad) {
|
|
|
2301
2310
|
size += end - from;
|
|
2302
2311
|
if (size > max) {
|
|
2303
2312
|
skipping = true;
|
|
2304
|
-
|
|
2313
|
+
parts2 = [];
|
|
2305
2314
|
size = 0;
|
|
2306
2315
|
onBad(`frame over ${max} bytes`);
|
|
2307
|
-
} else if (end > from)
|
|
2316
|
+
} else if (end > from) parts2.push(chunk.subarray(from, end));
|
|
2308
2317
|
}
|
|
2309
2318
|
if (nl === -1) return;
|
|
2310
2319
|
if (skipping) skipping = false;
|
|
@@ -3959,9 +3968,9 @@ function createForge(deps = {}) {
|
|
|
3959
3968
|
index.push(`- ${name}: no repository on this machine`);
|
|
3960
3969
|
return;
|
|
3961
3970
|
}
|
|
3962
|
-
const
|
|
3963
|
-
await writeFile5(join9(ctx.dir, file),
|
|
3964
|
-
const sources =
|
|
3971
|
+
const parts2 = await Promise.all(local.map((r) => section(r.path)));
|
|
3972
|
+
await writeFile5(join9(ctx.dir, file), parts2.flatMap((s) => s.lines).join("\n"));
|
|
3973
|
+
const sources = parts2.flatMap((s) => s.source ? [s.source] : []);
|
|
3965
3974
|
ctx.steps.set(file, { kind: "repo", name, path: sources.join(", ") || file });
|
|
3966
3975
|
index.push(`- ${name}: ${file}`);
|
|
3967
3976
|
}));
|
|
@@ -5061,18 +5070,18 @@ async function readAt(request, path) {
|
|
|
5061
5070
|
if (!stat4.isFile()) return failure(request, "not_file");
|
|
5062
5071
|
if (stat4.size > DOCUMENT_MAX_BYTES) return failure(request, "too_large");
|
|
5063
5072
|
const chunk = Buffer.allocUnsafe(64 * 1024);
|
|
5064
|
-
const
|
|
5073
|
+
const parts2 = [];
|
|
5065
5074
|
let bytes4 = 0;
|
|
5066
5075
|
while (bytes4 <= DOCUMENT_MAX_BYTES) {
|
|
5067
5076
|
const { bytesRead } = await file.read(chunk, 0, Math.min(chunk.length, DOCUMENT_MAX_BYTES + 1 - bytes4), null);
|
|
5068
5077
|
if (!bytesRead) break;
|
|
5069
|
-
|
|
5078
|
+
parts2.push(Buffer.from(chunk.subarray(0, bytesRead)));
|
|
5070
5079
|
bytes4 += bytesRead;
|
|
5071
5080
|
}
|
|
5072
5081
|
if (bytes4 > DOCUMENT_MAX_BYTES) return failure(request, "too_large");
|
|
5073
5082
|
let text;
|
|
5074
5083
|
try {
|
|
5075
|
-
text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(
|
|
5084
|
+
text = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(parts2, bytes4));
|
|
5076
5085
|
} catch {
|
|
5077
5086
|
return failure(request, "invalid_utf8");
|
|
5078
5087
|
}
|
|
@@ -5673,6 +5682,19 @@ function serveSsh(ws, alias, opts = {}) {
|
|
|
5673
5682
|
|
|
5674
5683
|
// src/mobile/gateway.ts
|
|
5675
5684
|
import { hostname } from "os";
|
|
5685
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
5686
|
+
|
|
5687
|
+
// ../ui/src/semver.ts
|
|
5688
|
+
var parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
5689
|
+
function compareVersions(a, b2) {
|
|
5690
|
+
const x = parts(a);
|
|
5691
|
+
const y = parts(b2);
|
|
5692
|
+
for (let i = 0; i < 3; i++) {
|
|
5693
|
+
const d = (x[i] ?? 0) - (y[i] ?? 0);
|
|
5694
|
+
if (d) return d > 0 ? 1 : -1;
|
|
5695
|
+
}
|
|
5696
|
+
return a === b2 ? 0 : a.includes("-") ? -1 : b2.includes("-") ? 1 : 0;
|
|
5697
|
+
}
|
|
5676
5698
|
|
|
5677
5699
|
// src/mobile/chat.ts
|
|
5678
5700
|
var HISTORY_TURNS = 50;
|
|
@@ -6011,6 +6033,8 @@ function fingerprint(m) {
|
|
|
6011
6033
|
f.rows ?? null,
|
|
6012
6034
|
...f.row !== void 0 || f.team !== void 0 || f.task !== void 0 ? [f.row ?? null, f.team ?? null, f.task ?? null] : [],
|
|
6013
6035
|
...f.attachments?.length ? [f.attachments] : [],
|
|
6036
|
+
// A model change is its model and effort: the same id for another model is another operation (null, the agent's default, too).
|
|
6037
|
+
...m.type === "session.model" ? [m.model, m.effort] : [],
|
|
6014
6038
|
// A role or team operation is its whole content: the same id with another prompt, seats or base is another operation.
|
|
6015
6039
|
...isTeamsOperation(m) ? [teamsContent(m)] : []
|
|
6016
6040
|
]);
|
|
@@ -6038,12 +6062,14 @@ var OperationRegistry = class {
|
|
|
6038
6062
|
return { kind: "fresh", record: { ...record } };
|
|
6039
6063
|
}
|
|
6040
6064
|
/** An operation already forgotten (expired) is left forgotten. */
|
|
6041
|
-
finish(op, status, reason) {
|
|
6065
|
+
finish(op, status, reason, models) {
|
|
6042
6066
|
const r = this.records.get(op);
|
|
6043
6067
|
if (!r) return;
|
|
6044
6068
|
r.status = status;
|
|
6045
6069
|
if (reason) r.reason = reason;
|
|
6046
6070
|
else delete r.reason;
|
|
6071
|
+
if (models) r.models = models;
|
|
6072
|
+
else delete r.models;
|
|
6047
6073
|
}
|
|
6048
6074
|
get(op) {
|
|
6049
6075
|
this.expire();
|
|
@@ -6195,7 +6221,27 @@ function toPhoneRequest(s, bodyMax) {
|
|
|
6195
6221
|
return toPhonePending(s.pending, pendingKey(s.pending), verdict.risk, verdict.reason, bodyMax);
|
|
6196
6222
|
}
|
|
6197
6223
|
var toPhoneFullRequest = (s) => toPhoneRequest(s, PENDING_BODY_MAX);
|
|
6198
|
-
|
|
6224
|
+
var checklistCut = (todos) => todos.length > CHECKLIST_TASKS_MAX || todos.some((t) => t.text.length > CHECKLIST_TEXT_MAX);
|
|
6225
|
+
function toPhoneChecklist(todos) {
|
|
6226
|
+
if (!todos) return null;
|
|
6227
|
+
return {
|
|
6228
|
+
tasks: todos.slice(0, CHECKLIST_TASKS_MAX).map((t) => ({ text: clip(t.text, CHECKLIST_TEXT_MAX).text, status: t.status })),
|
|
6229
|
+
done: doneCount(todos),
|
|
6230
|
+
total: todos.length,
|
|
6231
|
+
...checklistCut(todos) ? { cut: true } : {}
|
|
6232
|
+
};
|
|
6233
|
+
}
|
|
6234
|
+
function checklistSummary(todos) {
|
|
6235
|
+
if (!todos) return void 0;
|
|
6236
|
+
const current2 = todos.find((t) => t.status === "in_progress");
|
|
6237
|
+
return {
|
|
6238
|
+
done: doneCount(todos),
|
|
6239
|
+
total: todos.length,
|
|
6240
|
+
...current2 ? { current: clip(current2.text, CHECKLIST_TEXT_MAX).text } : {},
|
|
6241
|
+
...checklistCut(todos) ? { cut: true } : {}
|
|
6242
|
+
};
|
|
6243
|
+
}
|
|
6244
|
+
function toPhoneSession(s, readAt2, checklist) {
|
|
6199
6245
|
const pending = toPhoneRequest(s, SNAPSHOT_BODY_MAX);
|
|
6200
6246
|
return {
|
|
6201
6247
|
id: s.id,
|
|
@@ -6211,10 +6257,19 @@ function toPhoneSession(s, readAt2) {
|
|
|
6211
6257
|
...pickSet(s, ["repo", "branch", "worktree", "role", "roleLabel", "team", "context", "relieves"]),
|
|
6212
6258
|
// Its relief's state only (context handover spec §3.2): the phone shows it, the PC starts it.
|
|
6213
6259
|
...s.handover ? { handover: { state: s.handover.state, ...s.handover.toName ? { toName: s.handover.toName.slice(0, 80) } : {} } } : {},
|
|
6260
|
+
// Its model, effort and the change waiting for its next free moment (#58), cut to the bounds of a model id and an effort.
|
|
6261
|
+
...s.model !== void 0 ? { model: clip(s.model, MODEL_MAX).text } : {},
|
|
6262
|
+
...s.effort !== void 0 ? { effort: clip(s.effort, EFFORT_MAX).text } : {},
|
|
6263
|
+
...s.modelNext ? { modelNext: {
|
|
6264
|
+
...s.modelNext.model !== void 0 ? { model: clip(s.modelNext.model, MODEL_MAX).text } : {},
|
|
6265
|
+
...s.modelNext.effort !== void 0 ? { effort: clip(s.modelNext.effort, EFFORT_MAX).text } : {}
|
|
6266
|
+
} } : {},
|
|
6267
|
+
...canSwitch(s) ? { canSwitch: true } : {},
|
|
6214
6268
|
helpers: s.helpers.map((h) => ({ id: h.id, kind: h.kind, task: h.task, state: h.state, ...pickSet(h, ["finishedAt"]) })),
|
|
6215
6269
|
// A preview only: the whole reply is in the thread.
|
|
6216
6270
|
...s.lastReply ? { lastReply: { text: clip(s.lastReply.text, TOOL_TEXT_MAX).text, at: s.lastReply.at, ...pickSet(s.lastReply, ["plan"]) } } : {},
|
|
6217
6271
|
...pending ? { pending } : {},
|
|
6272
|
+
...checklist ? { checklist } : {},
|
|
6218
6273
|
unread: !!s.lastReply && s.lastReply.at > readAt2
|
|
6219
6274
|
};
|
|
6220
6275
|
}
|
|
@@ -6435,10 +6490,39 @@ function toPhoneTask(x, owner, teamName) {
|
|
|
6435
6490
|
};
|
|
6436
6491
|
}
|
|
6437
6492
|
var phoneTasks = (tasks, now) => tasks.filter((x) => x.giver === LEDGE_GIVER && (!taskClosed(x) || !x.seen && now - Date.parse(x.updatedAt) < 864e5)).slice(-200);
|
|
6493
|
+
var MODEL_MAX = 200;
|
|
6494
|
+
var EFFORT_MAX = 20;
|
|
6495
|
+
var MODELS_MAX = 40;
|
|
6496
|
+
function canSwitch(s) {
|
|
6497
|
+
return s.source === "launcher" && s.state !== "exited";
|
|
6498
|
+
}
|
|
6499
|
+
function toPhoneModels(models) {
|
|
6500
|
+
const out = [];
|
|
6501
|
+
for (const m of models) {
|
|
6502
|
+
if (out.length >= MODELS_MAX) break;
|
|
6503
|
+
const efforts = m.efforts?.filter((e) => EffortSchema.safeParse(e).success).slice(0, 8);
|
|
6504
|
+
const c = PhoneModelChoiceSchema.safeParse({
|
|
6505
|
+
id: m.id,
|
|
6506
|
+
label: clip(m.label, 80).text,
|
|
6507
|
+
...m.description !== void 0 ? { description: clip(m.description, 200).text } : {},
|
|
6508
|
+
...efforts ? { efforts } : {},
|
|
6509
|
+
...pickSet(m, ["defaultEffort", "isDefault"])
|
|
6510
|
+
});
|
|
6511
|
+
if (c.success) out.push(c.data);
|
|
6512
|
+
}
|
|
6513
|
+
return out;
|
|
6514
|
+
}
|
|
6438
6515
|
|
|
6439
6516
|
// src/mobile/teamsOps.ts
|
|
6440
6517
|
var FAILED = { status: "failed" };
|
|
6441
6518
|
var invalid = { status: "rejected", reason: "invalid" };
|
|
6519
|
+
function withSeats(models) {
|
|
6520
|
+
const seats = (models ?? []).flatMap((x) => {
|
|
6521
|
+
if (x.code) return [{ role: x.role, session: x.session, status: "refused", reason: x.code }];
|
|
6522
|
+
return x.when ? [{ role: x.role, session: x.session, status: x.when === "now" ? "applied" : "scheduled" }] : [];
|
|
6523
|
+
});
|
|
6524
|
+
return seats.length ? { models: seats } : {};
|
|
6525
|
+
}
|
|
6442
6526
|
function createTeamsOps(deps) {
|
|
6443
6527
|
const listing = async () => {
|
|
6444
6528
|
let found;
|
|
@@ -6492,10 +6576,12 @@ function createTeamsOps(deps) {
|
|
|
6492
6576
|
if (!m.code) return true;
|
|
6493
6577
|
if (m.code === "missing") return FAILED;
|
|
6494
6578
|
return { status: "rejected", reason: m.code };
|
|
6495
|
-
case "team.changed":
|
|
6579
|
+
case "team.changed": {
|
|
6496
6580
|
if (m.rid !== rid) return void 0;
|
|
6497
|
-
|
|
6498
|
-
|
|
6581
|
+
const seats = withSeats(m.models);
|
|
6582
|
+
if (m.reason && m.team && type === "team.compose") return { status: "applied", reason: "seat_failed", ...seats };
|
|
6583
|
+
return m.reason ? FAILED : { status: "applied", ...seats };
|
|
6584
|
+
}
|
|
6499
6585
|
case "team.launched":
|
|
6500
6586
|
return m.rid === rid ? true : void 0;
|
|
6501
6587
|
case "team.failed":
|
|
@@ -6690,12 +6776,14 @@ function verifyAssertion(a, cred, rp, challenge) {
|
|
|
6690
6776
|
// src/mobile/gateway.ts
|
|
6691
6777
|
var FLUSH_BEFORE_REVOKE_MS = 5e3;
|
|
6692
6778
|
var REQUEST_NAME = /^[a-z][a-z.]{0,39}$/;
|
|
6779
|
+
var MODELS_WAIT_MS = 2e4;
|
|
6693
6780
|
function createPhoneGateway(deps) {
|
|
6694
6781
|
const { mobile, log: log2 } = deps;
|
|
6695
6782
|
const startedAt = new Date(deps.now ? deps.now() : Date.now()).toISOString();
|
|
6696
6783
|
const registry = new OperationRegistry();
|
|
6697
6784
|
const followers = /* @__PURE__ */ new Map();
|
|
6698
6785
|
let stopped2 = false;
|
|
6786
|
+
const modelFailures = /* @__PURE__ */ new Map();
|
|
6699
6787
|
const follower = (deviceId) => {
|
|
6700
6788
|
let f = followers.get(deviceId);
|
|
6701
6789
|
if (!f) followers.set(deviceId, f = { subscribed: false, watching: null, reads: /* @__PURE__ */ new Map(), visible: false });
|
|
@@ -6738,7 +6826,17 @@ function createPhoneGateway(deps) {
|
|
|
6738
6826
|
const s = deps.session(id);
|
|
6739
6827
|
return s && !isArchived(s) ? s : void 0;
|
|
6740
6828
|
};
|
|
6741
|
-
const
|
|
6829
|
+
const checklists = /* @__PURE__ */ new Map();
|
|
6830
|
+
const computeChecklist = (id) => {
|
|
6831
|
+
const todos = latestTodos(deps.thread(id) ?? []);
|
|
6832
|
+
return { list: toPhoneChecklist(todos), summary: checklistSummary(todos) };
|
|
6833
|
+
};
|
|
6834
|
+
const checklistOf = (id) => {
|
|
6835
|
+
let c = checklists.get(id);
|
|
6836
|
+
if (!c) checklists.set(id, c = computeChecklist(id));
|
|
6837
|
+
return c;
|
|
6838
|
+
};
|
|
6839
|
+
const projected = (deviceId, s) => toPhoneSession(s, followers.get(deviceId)?.reads.get(s.id) ?? startedAt, checklistOf(s.id).summary);
|
|
6742
6840
|
const pushStatus = (deviceId) => {
|
|
6743
6841
|
const key = mobile.relayPushKey();
|
|
6744
6842
|
if (key === null) return "unavailable";
|
|
@@ -6750,14 +6848,15 @@ function createPhoneGateway(deps) {
|
|
|
6750
6848
|
const push2 = pushStatus(deviceId);
|
|
6751
6849
|
const f = followers.get(deviceId);
|
|
6752
6850
|
if (f) f.pushStatus = push2;
|
|
6753
|
-
|
|
6851
|
+
const message = {
|
|
6754
6852
|
type: "snapshot",
|
|
6755
6853
|
protocol: PHONE_PROTOCOL,
|
|
6756
6854
|
sessions: deps.sessions().filter((s) => !isArchived(s)).map((s) => projected(deviceId, s)),
|
|
6757
6855
|
capabilities: capabilities(),
|
|
6758
|
-
daemon: { version: deps.version, boot: deps.boot, name: clip(hostname(), 40).text },
|
|
6856
|
+
daemon: { version: deps.version, boot: deps.boot, name: clip(hostname(), 40).text, ...teamsOps ? { teams: true } : {}, ...deps.pc?.canUpdate ? { update: true } : {}, ...deps.pc?.canRestart ? { restart: true } : {}, checklist: true },
|
|
6759
6857
|
device: { confirm: deps.credential(deviceId) !== null, push: push2, ...deps.chats ? { chat: true } : {}, ...deps.contextNear ? { contextNear: deps.contextNear() } : {} }
|
|
6760
6858
|
};
|
|
6859
|
+
return deps.models ? { ...message, daemon: { ...message.daemon, models: true } } : message;
|
|
6761
6860
|
};
|
|
6762
6861
|
const phoneTeams = () => (deps.teams?.() ?? []).flatMap((t) => {
|
|
6763
6862
|
const x = toPhoneTeam(t, deps.tasks?.() ?? [], deps.projectName ?? (() => null));
|
|
@@ -6779,6 +6878,8 @@ function createPhoneGateway(deps) {
|
|
|
6779
6878
|
if (followers.get(deviceId)?.subscribed) {
|
|
6780
6879
|
outbox.push(deviceId, snapshot(deviceId), "snapshot");
|
|
6781
6880
|
teamsAndTasks(deviceId);
|
|
6881
|
+
const watching = followers.get(deviceId)?.watching;
|
|
6882
|
+
if (watching) outbox.push(deviceId, { type: "session.checklist", id: watching, checklist: checklistOf(watching).list }, `session.checklist:${watching}`);
|
|
6782
6883
|
}
|
|
6783
6884
|
},
|
|
6784
6885
|
log: log2,
|
|
@@ -6787,9 +6888,27 @@ function createPhoneGateway(deps) {
|
|
|
6787
6888
|
const push = (deviceId, msg, key) => {
|
|
6788
6889
|
if (!stopped2) outbox.push(deviceId, msg, key);
|
|
6789
6890
|
};
|
|
6790
|
-
const
|
|
6891
|
+
const replayModelFailures = (deviceId) => {
|
|
6892
|
+
for (const [id, f] of modelFailures) if (live3(id)) push(deviceId, { type: "session.model.failed", id, reason: f.reason });
|
|
6893
|
+
};
|
|
6894
|
+
const keepModelFailure = (m) => {
|
|
6895
|
+
if (m.type === "session.removed") {
|
|
6896
|
+
modelFailures.delete(m.id);
|
|
6897
|
+
return;
|
|
6898
|
+
}
|
|
6899
|
+
if (m.type === "session.updated") {
|
|
6900
|
+
const f = modelFailures.get(m.session.id);
|
|
6901
|
+
if (f && (isArchived(m.session) || f.model !== m.session.model || f.effort !== m.session.effort)) modelFailures.delete(m.session.id);
|
|
6902
|
+
return;
|
|
6903
|
+
}
|
|
6904
|
+
if (m.type !== "error" || m.request !== "session.model" || m.id === void 0) return;
|
|
6905
|
+
const reason = ModelRefusalSchema.safeParse(m.code);
|
|
6906
|
+
const s = live3(m.id);
|
|
6907
|
+
if (reason.success && s) modelFailures.set(m.id, { reason: reason.data, model: s.model, effort: s.effort });
|
|
6908
|
+
};
|
|
6909
|
+
const result = (deviceId, op, status, reason, models) => {
|
|
6791
6910
|
if (stopped2 || !approved(deviceId)) return;
|
|
6792
|
-
push(deviceId, { type: "operation.result", op, status, ...reason ? { reason } : {} });
|
|
6911
|
+
push(deviceId, { type: "operation.result", op, status, ...reason ? { reason } : {}, ...models ? { models } : {} });
|
|
6793
6912
|
};
|
|
6794
6913
|
const needsAssertion = (msg, s) => {
|
|
6795
6914
|
if (msg.type === "session.decide") return msg.decision !== "deny" && riskOf2(s)?.risk !== "low";
|
|
@@ -6851,6 +6970,9 @@ function createPhoneGateway(deps) {
|
|
|
6851
6970
|
return s.pending.requestId === msg.requestId ? void 0 : { status: "rejected", reason: "stale" };
|
|
6852
6971
|
case "session.interrupt":
|
|
6853
6972
|
return s.tmuxSession || backendOf(s) === "omp-rpc" ? void 0 : { status: "rejected", reason: "read_only" };
|
|
6973
|
+
case "session.model":
|
|
6974
|
+
if (canSwitch(s)) return void 0;
|
|
6975
|
+
return { status: "rejected", reason: s.state === "exited" ? "ended" : "not_launched" };
|
|
6854
6976
|
}
|
|
6855
6977
|
};
|
|
6856
6978
|
const local = (msg) => {
|
|
@@ -6863,6 +6985,8 @@ function createPhoneGateway(deps) {
|
|
|
6863
6985
|
return { type: "session.answer", id: msg.id, requestId: msg.requestId, answers: msg.answers, ...msg.cancel ? { cancel: true } : {} };
|
|
6864
6986
|
case "session.interrupt":
|
|
6865
6987
|
return { type: "session.interrupt", id: msg.id };
|
|
6988
|
+
case "session.model":
|
|
6989
|
+
return { type: "session.model", id: msg.id, model: msg.model, effort: msg.effort };
|
|
6866
6990
|
}
|
|
6867
6991
|
};
|
|
6868
6992
|
const outcomeOf = (code) => {
|
|
@@ -6984,6 +7108,41 @@ function createPhoneGateway(deps) {
|
|
|
6984
7108
|
});
|
|
6985
7109
|
return outcome ?? opts.silent ?? { status: "applied" };
|
|
6986
7110
|
};
|
|
7111
|
+
const modelChange = async (out) => {
|
|
7112
|
+
let when;
|
|
7113
|
+
let refusal2;
|
|
7114
|
+
await deps.dispatch(out, (m) => {
|
|
7115
|
+
if (m.type === "session.model.accepted" && m.id === out.id) when ??= m.when;
|
|
7116
|
+
if (m.type === "error" && m.request === "session.model" && (m.id === void 0 || m.id === out.id)) refusal2 ??= { code: m.code };
|
|
7117
|
+
});
|
|
7118
|
+
if (when) modelFailures.delete(out.id);
|
|
7119
|
+
if (when) return when === "turn" ? { status: "applied", reason: "scheduled" } : { status: "applied" };
|
|
7120
|
+
const model = ModelRefusalSchema.safeParse(refusal2?.code);
|
|
7121
|
+
if (model.success) return { status: "rejected", reason: model.data };
|
|
7122
|
+
const known2 = RefusalCodeSchema.safeParse(refusal2?.code);
|
|
7123
|
+
return known2.success ? { status: "rejected", reason: known2.data } : { status: "failed" };
|
|
7124
|
+
};
|
|
7125
|
+
const modelsList = async (deviceId, tool) => {
|
|
7126
|
+
const requestId = randomUUID5();
|
|
7127
|
+
let listing;
|
|
7128
|
+
let timer;
|
|
7129
|
+
try {
|
|
7130
|
+
await Promise.race([
|
|
7131
|
+
deps.dispatch({ type: "models.list", requestId, tool }, (m) => {
|
|
7132
|
+
if (m.type === "models.listing" && m.requestId === requestId) listing ??= m;
|
|
7133
|
+
}),
|
|
7134
|
+
new Promise((r) => {
|
|
7135
|
+
timer = setTimeout(r, MODELS_WAIT_MS);
|
|
7136
|
+
})
|
|
7137
|
+
]);
|
|
7138
|
+
} catch (e) {
|
|
7139
|
+
log2(`phone models.list failed (${e instanceof Error ? e.name : "error"})`);
|
|
7140
|
+
} finally {
|
|
7141
|
+
clearTimeout(timer);
|
|
7142
|
+
}
|
|
7143
|
+
if (!listing) return push(deviceId, { type: "models.listing", tool, models: [], error: true });
|
|
7144
|
+
push(deviceId, { type: "models.listing", tool, models: toPhoneModels(listing.models), ...listing.error !== void 0 ? { error: true } : {} });
|
|
7145
|
+
};
|
|
6987
7146
|
const chatTeam = async (deviceId, msg) => {
|
|
6988
7147
|
const turn = ownTurn(deviceId, msg.qid);
|
|
6989
7148
|
if (!turn?.team || !deps.teams) return { status: "rejected", reason: "unknown_session" };
|
|
@@ -7019,7 +7178,23 @@ function createPhoneGateway(deps) {
|
|
|
7019
7178
|
push: (deviceId, m) => push(deviceId, m),
|
|
7020
7179
|
log: log2
|
|
7021
7180
|
}) : void 0;
|
|
7181
|
+
const pcOperation = (deviceId, msg) => {
|
|
7182
|
+
const pc = deps.pc;
|
|
7183
|
+
const update = msg.type === "daemon.update";
|
|
7184
|
+
if (!pc || !(update ? pc.canUpdate : pc.canRestart)) return { status: "rejected", reason: "unavailable" };
|
|
7185
|
+
if (update && (!SEMVER.test(deps.version) || compareVersions(msg.version, deps.version) <= 0)) return { status: "rejected", reason: "invalid" };
|
|
7186
|
+
const refused = checkAssertion(deviceId, msg.assertion, "pc", update ? pcSubject("update", msg.version) : pcSubject("restart"));
|
|
7187
|
+
if (refused) return refused;
|
|
7188
|
+
const reply = (m) => {
|
|
7189
|
+
if (m.type === "update.failed") push(deviceId, { type: "daemon.failed", version: m.version, reason: m.reason });
|
|
7190
|
+
};
|
|
7191
|
+
log2(`phone: device ${deviceId} asked the daemon to ${update ? `update to ${msg.version}` : "restart"}`);
|
|
7192
|
+
if (update) pc.update(msg.version, reply);
|
|
7193
|
+
else pc.restart(reply);
|
|
7194
|
+
return { status: "applied" };
|
|
7195
|
+
};
|
|
7022
7196
|
const run2 = async (deviceId, msg) => {
|
|
7197
|
+
if (msg.type === "daemon.update" || msg.type === "daemon.restart") return pcOperation(deviceId, msg);
|
|
7023
7198
|
if (isTeamsOperation(msg)) return teamsOps ? teamsOps.run(deviceId, msg) : { status: "rejected", reason: "unavailable" };
|
|
7024
7199
|
if (msg.type === "chat.ignore") return chatIgnore(deviceId, msg);
|
|
7025
7200
|
if (msg.type === "chat.team") return chatTeam(deviceId, msg);
|
|
@@ -7048,6 +7223,7 @@ function createPhoneGateway(deps) {
|
|
|
7048
7223
|
const refused = precheck(deviceId, msg);
|
|
7049
7224
|
if (refused) return refused;
|
|
7050
7225
|
const out = local(msg);
|
|
7226
|
+
if (out.type === "session.model") return modelChange(out);
|
|
7051
7227
|
let error;
|
|
7052
7228
|
await deps.dispatch(out, (m) => {
|
|
7053
7229
|
if (m.type === "error" && (m.request === void 0 || m.request === out.type)) error ??= { code: m.code };
|
|
@@ -7057,7 +7233,7 @@ function createPhoneGateway(deps) {
|
|
|
7057
7233
|
const write = async (deviceId, msg) => {
|
|
7058
7234
|
const begun = registry.begin(deviceId, msg.op, fingerprint(msg));
|
|
7059
7235
|
if (begun.kind === "conflict") return result(deviceId, msg.op, "rejected", "conflict");
|
|
7060
|
-
if (begun.kind === "replay") return result(deviceId, msg.op, begun.record.status, begun.record.reason);
|
|
7236
|
+
if (begun.kind === "replay") return result(deviceId, msg.op, begun.record.status, begun.record.reason, begun.record.models);
|
|
7061
7237
|
let outcome;
|
|
7062
7238
|
try {
|
|
7063
7239
|
result(deviceId, msg.op, "received");
|
|
@@ -7066,8 +7242,8 @@ function createPhoneGateway(deps) {
|
|
|
7066
7242
|
log2(`phone ${msg.type} failed (${e instanceof Error ? e.name : "error"})`);
|
|
7067
7243
|
outcome = { status: "failed" };
|
|
7068
7244
|
}
|
|
7069
|
-
registry.finish(msg.op, outcome.status, outcome.reason);
|
|
7070
|
-
result(deviceId, msg.op, outcome.status, outcome.reason);
|
|
7245
|
+
registry.finish(msg.op, outcome.status, outcome.reason, outcome.models);
|
|
7246
|
+
result(deviceId, msg.op, outcome.status, outcome.reason, outcome.models);
|
|
7071
7247
|
if (msg.type === "credential.register" && outcome.status === "applied" && approved(deviceId)) push(deviceId, snapshot(deviceId), "snapshot");
|
|
7072
7248
|
if (msg.type === "device.forget" && outcome.status === "applied") {
|
|
7073
7249
|
let timer;
|
|
@@ -7154,13 +7330,19 @@ function createPhoneGateway(deps) {
|
|
|
7154
7330
|
case "presence":
|
|
7155
7331
|
follower(deviceId).visible = msg.visible;
|
|
7156
7332
|
return;
|
|
7333
|
+
case "models.list":
|
|
7334
|
+
if (!deps.models) return;
|
|
7335
|
+
void modelsList(deviceId, msg.tool).catch((e) => log2(`phone models.list failed (${e instanceof Error ? e.name : "error"})`));
|
|
7336
|
+
return;
|
|
7157
7337
|
case "snapshot.request":
|
|
7158
7338
|
follower(deviceId).subscribed = true;
|
|
7159
7339
|
push(deviceId, snapshot(deviceId), "snapshot");
|
|
7340
|
+
replayModelFailures(deviceId);
|
|
7160
7341
|
return teamsAndTasks(deviceId);
|
|
7161
7342
|
case "session.thread": {
|
|
7162
7343
|
if (!live3(msg.id)) return push(deviceId, { type: "error", code: "unknown_session", request: msg.type });
|
|
7163
|
-
|
|
7344
|
+
push(deviceId, { type: "session.thread", id: msg.id, ...toPhoneThread(deps.thread(msg.id) ?? []) });
|
|
7345
|
+
return push(deviceId, { type: "session.checklist", id: msg.id, checklist: checklistOf(msg.id).list }, `session.checklist:${msg.id}`);
|
|
7164
7346
|
}
|
|
7165
7347
|
case "session.pending": {
|
|
7166
7348
|
const session = live3(msg.id);
|
|
@@ -7223,6 +7405,15 @@ function createPhoneGateway(deps) {
|
|
|
7223
7405
|
const challenge = deps.challenges.issue({ deviceId, purpose: msg.purpose, subject: msg.digest });
|
|
7224
7406
|
return push(deviceId, { type: "confirm.challenge", purpose: msg.purpose, challenge, rpId: rp.rpId, credentialId: cred.id, expiresInMs: CHALLENGE_TTL_MS, digest: msg.digest });
|
|
7225
7407
|
}
|
|
7408
|
+
if (msg.purpose === "pc") {
|
|
7409
|
+
const pc = deps.pc;
|
|
7410
|
+
if (!pc || !msg.action || msg.action === "update" !== (msg.version !== void 0)) return invalid2();
|
|
7411
|
+
if (!(msg.action === "update" ? pc.canUpdate : pc.canRestart)) return invalid2();
|
|
7412
|
+
const cred = deps.credential(deviceId);
|
|
7413
|
+
if (!cred || cred.rpId !== rp.rpId) return invalid2();
|
|
7414
|
+
const challenge = deps.challenges.issue({ deviceId, purpose: "pc", subject: pcSubject(msg.action, msg.version) });
|
|
7415
|
+
return push(deviceId, { type: "confirm.challenge", purpose: "pc", challenge, rpId: rp.rpId, credentialId: cred.id, expiresInMs: CHALLENGE_TTL_MS });
|
|
7416
|
+
}
|
|
7226
7417
|
const unhandled = msg.purpose;
|
|
7227
7418
|
void unhandled;
|
|
7228
7419
|
return invalid2();
|
|
@@ -7252,6 +7443,7 @@ function createPhoneGateway(deps) {
|
|
|
7252
7443
|
const request = typeof type === "string" && REQUEST_NAME.test(type) ? type : void 0;
|
|
7253
7444
|
log2(`phone message refused: ${request ?? "?"}`);
|
|
7254
7445
|
const op = OperationIdSchema.safeParse(raw?.op);
|
|
7446
|
+
if (request === "session.model" && op.success && approved(deviceId)) return push(deviceId, { type: "operation.result", op: op.data, status: "rejected", reason: "invalid" });
|
|
7255
7447
|
if ((request !== void 0 && TEAMS_OPERATION_TYPES.includes(request) || request === "push.subscribe" || request === "chat.launch" || request === "chat.close" || request === "chat.act" || request === "chat.ignore" || request === "chat.team" || request === "chat.task" || request === "team.resume" || request === "task.seen") && op.success && approved(deviceId)) return push(deviceId, { type: "operation.result", op: op.data, status: "rejected", reason: "invalid" });
|
|
7256
7448
|
return push(deviceId, { type: "error", code: "invalid", ...request !== void 0 ? { request } : {} });
|
|
7257
7449
|
}
|
|
@@ -7285,17 +7477,40 @@ function createPhoneGateway(deps) {
|
|
|
7285
7477
|
}
|
|
7286
7478
|
};
|
|
7287
7479
|
const removed = (id) => {
|
|
7480
|
+
checklists.delete(id);
|
|
7288
7481
|
for (const [deviceId, f] of followers) {
|
|
7289
7482
|
f.reads.delete(id);
|
|
7290
7483
|
if (f.watching === id) f.watching = null;
|
|
7291
7484
|
if (f.subscribed) push(deviceId, { type: "session.removed", id });
|
|
7292
7485
|
}
|
|
7293
7486
|
};
|
|
7487
|
+
const checklistChanged = (id, item) => {
|
|
7488
|
+
if (item.kind !== "tool" || item.helperId !== void 0 || !TODO_TOOLS.has(item.tool) || !checklists.has(id)) return;
|
|
7489
|
+
const before = checklists.get(id);
|
|
7490
|
+
const now = computeChecklist(id);
|
|
7491
|
+
checklists.set(id, now);
|
|
7492
|
+
if (JSON.stringify(now.list) !== JSON.stringify(before.list)) {
|
|
7493
|
+
for (const [deviceId, f] of followers) if (f.watching === id) push(deviceId, { type: "session.checklist", id, checklist: now.list }, `session.checklist:${id}`);
|
|
7494
|
+
}
|
|
7495
|
+
const s = live3(id);
|
|
7496
|
+
if (s && JSON.stringify(now.summary) !== JSON.stringify(before.summary)) {
|
|
7497
|
+
for (const [deviceId, f] of followers) if (f.subscribed) push(deviceId, { type: "session.updated", session: projected(deviceId, s) }, `session.updated:${id}`);
|
|
7498
|
+
}
|
|
7499
|
+
};
|
|
7294
7500
|
const unsubscribeMessage = mobile.onMessage(receive);
|
|
7295
7501
|
const unsubscribeChange = mobile.onChange(pruneRevoked);
|
|
7296
7502
|
return {
|
|
7297
7503
|
forward(m) {
|
|
7298
|
-
if (stopped2
|
|
7504
|
+
if (!stopped2) try {
|
|
7505
|
+
keepModelFailure(m);
|
|
7506
|
+
} catch (e) {
|
|
7507
|
+
log2(`phone model failure of ${m.type} failed (${e instanceof Error ? e.name : "error"})`);
|
|
7508
|
+
}
|
|
7509
|
+
if (stopped2) return;
|
|
7510
|
+
if (followers.size === 0) {
|
|
7511
|
+
checklists.clear();
|
|
7512
|
+
return;
|
|
7513
|
+
}
|
|
7299
7514
|
try {
|
|
7300
7515
|
switch (m.type) {
|
|
7301
7516
|
case "session.updated":
|
|
@@ -7306,6 +7521,12 @@ function createPhoneGateway(deps) {
|
|
|
7306
7521
|
return;
|
|
7307
7522
|
case "session.removed":
|
|
7308
7523
|
return removed(m.id);
|
|
7524
|
+
case "error": {
|
|
7525
|
+
const reason = ModelRefusalSchema.safeParse(m.code);
|
|
7526
|
+
if (m.request !== "session.model" || m.id === void 0 || !reason.success || !live3(m.id)) return;
|
|
7527
|
+
for (const [deviceId, f] of followers) if (f.subscribed) push(deviceId, { type: "session.model.failed", id: m.id, reason: reason.data });
|
|
7528
|
+
return;
|
|
7529
|
+
}
|
|
7309
7530
|
case "context.prefs":
|
|
7310
7531
|
for (const [deviceId, f] of followers) if (f.subscribed) push(deviceId, snapshot(deviceId), "snapshot");
|
|
7311
7532
|
return;
|
|
@@ -7324,12 +7545,20 @@ function createPhoneGateway(deps) {
|
|
|
7324
7545
|
}
|
|
7325
7546
|
return;
|
|
7326
7547
|
}
|
|
7548
|
+
// The daemon updating or restarting itself, whoever asked. Never npm's output nor a command: both hold the PC's paths.
|
|
7549
|
+
case "update.progress":
|
|
7550
|
+
for (const [deviceId, f] of followers) if (f.subscribed) push(deviceId, { type: "daemon.progress", version: m.version, step: m.step });
|
|
7551
|
+
return;
|
|
7552
|
+
case "update.failed":
|
|
7553
|
+
for (const [deviceId, f] of followers) if (f.subscribed) push(deviceId, { type: "daemon.failed", version: m.version, reason: m.reason });
|
|
7554
|
+
return;
|
|
7327
7555
|
case "session.preview":
|
|
7328
7556
|
for (const [deviceId, f] of followers) {
|
|
7329
7557
|
if (f.watching === m.id) push(deviceId, { type: "session.preview", id: m.id, lines: [...m.lines] }, `session.preview:${m.id}`);
|
|
7330
7558
|
}
|
|
7331
7559
|
return;
|
|
7332
7560
|
case "session.thread.item": {
|
|
7561
|
+
checklistChanged(m.id, m.item);
|
|
7333
7562
|
const watchers = [...followers].filter(([, f]) => f.watching === m.id);
|
|
7334
7563
|
if (watchers.length === 0) return;
|
|
7335
7564
|
const { item, cut: cut2 } = toPhoneItem(m.item);
|
|
@@ -8110,6 +8339,7 @@ async function startServer(opts) {
|
|
|
8110
8339
|
async function applyOmpModel(s) {
|
|
8111
8340
|
const next2 = s.modelNext;
|
|
8112
8341
|
typing.add(s.id);
|
|
8342
|
+
applyingModel.set(s.id, null);
|
|
8113
8343
|
try {
|
|
8114
8344
|
await omp.setModel(s.id, next2);
|
|
8115
8345
|
log(`model of ${s.id} is now ${next2.model ?? "unchanged"}${next2.effort ? ` \xB7 ${next2.effort}` : ""}`);
|
|
@@ -8117,32 +8347,91 @@ async function startServer(opts) {
|
|
|
8117
8347
|
const message = e.message;
|
|
8118
8348
|
log(`model change of ${s.id} failed: ${message}`);
|
|
8119
8349
|
store.patch(s.id, { modelNext: void 0 });
|
|
8120
|
-
|
|
8350
|
+
modelFailed(s.id, message, "refused");
|
|
8121
8351
|
} finally {
|
|
8122
8352
|
typing.delete(s.id);
|
|
8353
|
+
afterModelApply(s.id);
|
|
8354
|
+
}
|
|
8355
|
+
}
|
|
8356
|
+
const modelFailed = (id, message, code) => broadcast({ type: "error", id, message, request: "session.model", code });
|
|
8357
|
+
const applyingModel = /* @__PURE__ */ new Map();
|
|
8358
|
+
function afterModelApply(id) {
|
|
8359
|
+
const later = applyingModel.get(id);
|
|
8360
|
+
applyingModel.delete(id);
|
|
8361
|
+
const s = store.get(id);
|
|
8362
|
+
if (!later || !s) return;
|
|
8363
|
+
const refusal2 = modelRefusal(s, later.effort ?? null);
|
|
8364
|
+
if (refusal2) {
|
|
8365
|
+
store.patch(id, { modelNext: void 0 });
|
|
8366
|
+
return modelFailed(id, refusal2.message, refusal2.refused);
|
|
8123
8367
|
}
|
|
8368
|
+
store.patch(id, { modelNext: later.model === s.model && later.effort === s.effort ? void 0 : later });
|
|
8369
|
+
}
|
|
8370
|
+
function canApplyNow(id) {
|
|
8371
|
+
return free(store.get(id)) && !typing.has(id) && !closing.has(id) && !cardThreads.queuedRelays().some((q) => q.relay.id === id);
|
|
8372
|
+
}
|
|
8373
|
+
function modelRefusal(s, effort) {
|
|
8374
|
+
const inPlace = backendOf(s) === "omp-rpc";
|
|
8375
|
+
if (s.source === "hook_only") return { refused: "not_launched", message: READ_ONLY2 };
|
|
8376
|
+
if (s.state === "exited") return { refused: "ended", message: "the session has ended" };
|
|
8377
|
+
if (!resumeKey(s)) return { refused: "no_conversation", message: "no conversation to resume" };
|
|
8378
|
+
if (inPlace && !omp.isLive(s.id)) return { refused: "not_running", message: "the session is not running" };
|
|
8379
|
+
if (effort && !inPlace && !EFFORT_ARGS[s.tool]) return { refused: "effort_in_model", message: `${s.tool} takes its effort in the model id` };
|
|
8380
|
+
return null;
|
|
8381
|
+
}
|
|
8382
|
+
async function changeModel(id, model, effort) {
|
|
8383
|
+
const s = store.get(id);
|
|
8384
|
+
if (!s) throw unknownSession();
|
|
8385
|
+
const refusal2 = modelRefusal(s, effort);
|
|
8386
|
+
if (refusal2) return refusal2;
|
|
8387
|
+
const next2 = { ...model ? { model } : {}, ...effort ? { effort } : {} };
|
|
8388
|
+
if (applyingModel.has(id)) {
|
|
8389
|
+
applyingModel.set(id, next2);
|
|
8390
|
+
store.patch(id, { modelNext: next2 });
|
|
8391
|
+
return { when: "turn" };
|
|
8392
|
+
}
|
|
8393
|
+
if (next2.model === s.model && next2.effort === s.effort) {
|
|
8394
|
+
if (s.modelNext) store.patch(id, { modelNext: void 0 });
|
|
8395
|
+
return { when: "none" };
|
|
8396
|
+
}
|
|
8397
|
+
const when = canApplyNow(id) ? "now" : "turn";
|
|
8398
|
+
store.patch(id, { modelNext: next2 });
|
|
8399
|
+
return { when };
|
|
8124
8400
|
}
|
|
8125
8401
|
async function applyModelNext(id) {
|
|
8126
8402
|
const s = store.get(id);
|
|
8127
|
-
if (!s?.modelNext || !
|
|
8403
|
+
if (!s?.modelNext || !canApplyNow(id)) return;
|
|
8128
8404
|
if (backendOf(s) === "omp-rpc") return applyOmpModel(s);
|
|
8129
|
-
const
|
|
8130
|
-
|
|
8405
|
+
const { model, effort } = s.modelNext;
|
|
8406
|
+
const refusal2 = modelRefusal(s, effort ?? null);
|
|
8407
|
+
if (refusal2) {
|
|
8131
8408
|
store.patch(id, { modelNext: void 0 });
|
|
8132
|
-
return;
|
|
8409
|
+
return modelFailed(id, refusal2.message, refusal2.refused);
|
|
8133
8410
|
}
|
|
8134
|
-
const
|
|
8411
|
+
const key = resumeKey(s);
|
|
8135
8412
|
typing.add(id);
|
|
8413
|
+
applyingModel.set(id, null);
|
|
8136
8414
|
try {
|
|
8137
8415
|
for (const [k, at] of respawned) if (Date.now() - at > RESPAWN_HOOKS_MS) respawned.delete(k);
|
|
8138
8416
|
respawned.set(id, Date.now());
|
|
8139
8417
|
await endSession(s);
|
|
8140
8418
|
const ended = store.get(id);
|
|
8141
|
-
if (!ended || ended.archivedAt !== void 0)
|
|
8419
|
+
if (!ended || ended.archivedAt !== void 0) {
|
|
8420
|
+
store.patch(id, { modelNext: void 0 });
|
|
8421
|
+
return;
|
|
8422
|
+
}
|
|
8142
8423
|
const refused = await respawn({ ...ended, modelNext: void 0 }, key, model, effort);
|
|
8143
|
-
if (refused)
|
|
8424
|
+
if (refused) {
|
|
8425
|
+
log(`model change of ${id} failed: ${refused}`);
|
|
8426
|
+
modelFailed(id, refused, "restart_failed");
|
|
8427
|
+
}
|
|
8428
|
+
} catch (e) {
|
|
8429
|
+
log(`model change of ${id} failed: ${e.message}`);
|
|
8430
|
+
store.patch(id, { modelNext: void 0 });
|
|
8431
|
+
modelFailed(id, e.message, "restart_failed");
|
|
8144
8432
|
} finally {
|
|
8145
8433
|
typing.delete(id);
|
|
8434
|
+
afterModelApply(id);
|
|
8146
8435
|
}
|
|
8147
8436
|
}
|
|
8148
8437
|
const chatLaunches = /* @__PURE__ */ new Set();
|
|
@@ -8405,6 +8694,26 @@ ${now2.prompt}` });
|
|
|
8405
8694
|
for (const id of leaverRefused) if (!leaving.has(id)) leaverRefused.delete(id);
|
|
8406
8695
|
}
|
|
8407
8696
|
const seatsInFlight = /* @__PURE__ */ new Set();
|
|
8697
|
+
async function seatModels(before, team) {
|
|
8698
|
+
const out = [];
|
|
8699
|
+
const roles = listDefinitions(team.cwd).roles;
|
|
8700
|
+
for (const seat of team.seats) {
|
|
8701
|
+
const was = before.get(seat.role);
|
|
8702
|
+
if (!was || seat.leaving || was.tool !== seat.tool || was.model === seat.model && was.effort === seat.effort) continue;
|
|
8703
|
+
const holder = seat.sessionId ? store.get(seat.sessionId) : void 0;
|
|
8704
|
+
if (!holder) continue;
|
|
8705
|
+
const resolved = resolveSeats([seat], roles);
|
|
8706
|
+
const want = "unknown" in resolved ? seat : resolved[0];
|
|
8707
|
+
try {
|
|
8708
|
+
const r = await changeModel(holder.id, want.model ?? null, want.effort ?? null);
|
|
8709
|
+
if ("refused" in r) out.push({ role: seat.role, session: holder.id, code: r.refused });
|
|
8710
|
+
else if (r.when !== "none") out.push({ role: seat.role, session: holder.id, when: r.when });
|
|
8711
|
+
} catch (e) {
|
|
8712
|
+
log(`team ${team.id}: model of @${seat.role} (${holder.id}) not changed: ${e.message}`);
|
|
8713
|
+
}
|
|
8714
|
+
}
|
|
8715
|
+
return out;
|
|
8716
|
+
}
|
|
8408
8717
|
async function composeTeam(msg) {
|
|
8409
8718
|
const changed = (extra) => ({ type: "team.changed", rid: msg.rid, ...extra });
|
|
8410
8719
|
const t = teams.get(msg.team);
|
|
@@ -8421,12 +8730,14 @@ ${now2.prompt}` });
|
|
|
8421
8730
|
if (busy) return changed({ reason: `@${busy.role} : une op\xE9ration est d\xE9j\xE0 en cours sur ce si\xE8ge` });
|
|
8422
8731
|
for (const k of keys) seatsInFlight.add(k);
|
|
8423
8732
|
try {
|
|
8733
|
+
const before = new Map(t.seats.map((s) => [s.role, s]));
|
|
8424
8734
|
const r = teams.compose(msg.team, msg.seats);
|
|
8425
8735
|
if (typeof r === "string") return changed({ reason: r });
|
|
8426
8736
|
log(`team ${t.id} composed: +${r.added.join(",") || "-"} \u2212${r.removed.join(",") || "-"}`);
|
|
8427
8737
|
const failed = r.added.length ? await startSeats(r.team, msg.seats.filter((s) => r.added.includes(s.role))) : null;
|
|
8428
8738
|
closeLeavers();
|
|
8429
|
-
|
|
8739
|
+
const models2 = await seatModels(before, teams.get(t.id));
|
|
8740
|
+
return changed({ team: teams.get(t.id), ...failed ? { reason: failed } : {}, ...models2.length ? { models: models2 } : {} });
|
|
8430
8741
|
} finally {
|
|
8431
8742
|
for (const k of keys) seatsInFlight.delete(k);
|
|
8432
8743
|
}
|
|
@@ -8767,21 +9078,10 @@ ${now2.prompt}` });
|
|
|
8767
9078
|
return;
|
|
8768
9079
|
}
|
|
8769
9080
|
case "session.model": {
|
|
8770
|
-
|
|
8771
|
-
|
|
8772
|
-
if (
|
|
8773
|
-
|
|
8774
|
-
if (!resumeKey(s)) return refuse("no conversation to resume", msg.id);
|
|
8775
|
-
if (backendOf(s) === "omp-rpc" && !omp.isLive(s.id)) return refuse("the session is not running", msg.id);
|
|
8776
|
-
if (msg.effort && backendOf(s) !== "omp-rpc" && !EFFORT_ARGS[s.tool]) return refuse(`${s.tool} takes its effort in the model id`, msg.id);
|
|
8777
|
-
const next2 = { ...msg.model ? { model: msg.model } : {}, ...msg.effort ? { effort: msg.effort } : {} };
|
|
8778
|
-
if (next2.model === s.model && next2.effort === s.effort) {
|
|
8779
|
-
if (s.modelNext) store.patch(msg.id, { modelNext: void 0 });
|
|
8780
|
-
return;
|
|
8781
|
-
}
|
|
8782
|
-
store.patch(msg.id, { modelNext: next2 });
|
|
8783
|
-
await applyModelNext(msg.id);
|
|
8784
|
-
return;
|
|
9081
|
+
if (!store.get(msg.id)) return refuse("unknown session", msg.id);
|
|
9082
|
+
const r = await changeModel(msg.id, msg.model, msg.effort);
|
|
9083
|
+
if ("refused" in r) return refuse({ message: r.message, code: r.refused }, msg.id);
|
|
9084
|
+
return reply({ type: "session.model.accepted", id: msg.id, when: r.when });
|
|
8785
9085
|
}
|
|
8786
9086
|
case "session.delete": {
|
|
8787
9087
|
const s = store.get(msg.id);
|
|
@@ -9047,7 +9347,7 @@ ${now2.prompt}` });
|
|
|
9047
9347
|
},
|
|
9048
9348
|
onChatEnd: (deviceId, end) => notifyChatEnd?.(deviceId, end.code),
|
|
9049
9349
|
version: opts.daemon?.version ?? "unknown",
|
|
9050
|
-
boot:
|
|
9350
|
+
boot: randomUUID6(),
|
|
9051
9351
|
log,
|
|
9052
9352
|
now,
|
|
9053
9353
|
rp: () => rpOf(mobile.state().relayUrl),
|
|
@@ -9060,7 +9360,22 @@ ${now2.prompt}` });
|
|
|
9060
9360
|
projectName: (id) => id ? projects.projects.find((p) => p.id === id)?.name ?? null : null,
|
|
9061
9361
|
// A project's folder is its first local repo: only a project with one can be named by the phone.
|
|
9062
9362
|
projectByName: (name) => projects.projects.find((p) => p.name === name && p.repos.some((r) => r.machine === "local"))?.id ?? null,
|
|
9063
|
-
teamCatalog: true
|
|
9363
|
+
teamCatalog: true,
|
|
9364
|
+
// The phone's model switch (#58): `session.model` answers with a code, and `models.list` asks this machine's agents.
|
|
9365
|
+
models: true,
|
|
9366
|
+
// The phone's « Mettre à jour le PC » and « Redémarrer le daemon »: the updater Desktop's `update.daemon` uses, progress broadcast.
|
|
9367
|
+
...opts.updater && opts.daemon ? {
|
|
9368
|
+
pc: {
|
|
9369
|
+
canUpdate: opts.daemon.install === "npm" && opts.daemon.systemd,
|
|
9370
|
+
canRestart: opts.daemon.systemd,
|
|
9371
|
+
update: (version, reply) => {
|
|
9372
|
+
void opts.updater.update(version, broadcast, reply);
|
|
9373
|
+
},
|
|
9374
|
+
restart: (reply) => {
|
|
9375
|
+
void opts.updater.restart(broadcast, reply);
|
|
9376
|
+
}
|
|
9377
|
+
}
|
|
9378
|
+
} : {}
|
|
9064
9379
|
});
|
|
9065
9380
|
let unsubscribePush;
|
|
9066
9381
|
if (mobile) {
|