@paradow/ledge 0.27.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-7SJJTIEA.js → action-NQRSZEC5.js} +4 -4
- package/dist/cli/{attach-V7UPHVB2.js → attach-KXOSFUHO.js} +8 -8
- package/dist/cli/{bridge-JK3PMG7T.js → bridge-XCTXVWKH.js} +1 -1
- package/dist/cli/{chunk-RA3OIXEQ.js → chunk-3CF4FQWG.js} +2 -2
- package/dist/cli/{chunk-7OGFH6KJ.js → chunk-3NQNWGDX.js} +2 -2
- package/dist/cli/{chunk-TOSPL5H2.js → chunk-CIQAFCX3.js} +3 -3
- package/dist/cli/{chunk-IGAGJVRZ.js → chunk-CR3LGTZQ.js} +3 -3
- package/dist/cli/{chunk-UPKJO3BZ.js → chunk-JHFKBKSO.js} +1 -1
- package/dist/cli/{chunk-2N7PSP7C.js → chunk-JLUHY5AB.js} +3 -3
- package/dist/cli/{chunk-ADAGKOFO.js → chunk-LG3XFIPV.js} +1 -1
- package/dist/cli/{chunk-CVHX7GUB.js → chunk-OYDTU6AP.js} +302 -46
- package/dist/cli/{chunk-YLN3VOGV.js → chunk-QSRB3F5Y.js} +2 -2
- package/dist/cli/{chunk-ISNHG5SJ.js → chunk-RYZ6FHYT.js} +5 -5
- package/dist/cli/chunk-UME2F7VM.js +6 -0
- package/dist/cli/{chunk-37G5F75H.js → chunk-VKRFHDRK.js} +124 -9
- package/dist/cli/{chunk-7GIUG73R.js → chunk-VMGACWNP.js} +1 -1
- package/dist/cli/{chunk-7JRQCKBP.js → chunk-ZI47U5CQ.js} +2 -2
- package/dist/cli/{daemon-YUPB5NUG.js → daemon-ZUHJKLSL.js} +7 -7
- package/dist/cli/{decide-HFI2WQXI.js → decide-OSSTI3FE.js} +3 -3
- package/dist/cli/{handover-GKORMXZ2.js → handover-2G7OEWIG.js} +4 -4
- package/dist/cli/{hook-TJJEFV54.js → hook-XY4E5H7O.js} +9 -9
- package/dist/cli/index.js +16 -16
- package/dist/cli/{launch-QKQZZSIM.js → launch-7WOARLZM.js} +8 -8
- package/dist/cli/{send-RI7JOO5K.js → send-RUHGKLMA.js} +4 -4
- package/dist/cli/{status-RQRS26S2.js → status-4Z2VJFID.js} +2 -2
- package/dist/cli/{task-U2XHBUKW.js → task-2GESYTOL.js} +4 -4
- package/dist/cli/{team-NFC4XEIA.js → team-MTSKNMAE.js} +6 -6
- package/dist/cli/{tui-UWR7SDYF.js → tui-4IRYQXYU.js} +8 -8
- package/dist/cli/{version-YYNMMWKJ.js → version-L2Y7MRH6.js} +1 -1
- package/dist/cli/{who-WW6IL4KD.js → who-PSKHASMK.js} +3 -3
- package/package.json +1 -1
- package/dist/cli/chunk-HMTOPOJG.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,
|
|
@@ -103,6 +107,7 @@ import {
|
|
|
103
107
|
THREAD_BUDGET,
|
|
104
108
|
THREAD_ITEMS_MAX,
|
|
105
109
|
THREAD_TEXT_MAX,
|
|
110
|
+
TODO_TOOLS,
|
|
106
111
|
TOOL_TEXT_MAX,
|
|
107
112
|
TOPIC_MAX,
|
|
108
113
|
TeamSchema,
|
|
@@ -114,6 +119,7 @@ import {
|
|
|
114
119
|
backendOf,
|
|
115
120
|
canonicalJson,
|
|
116
121
|
contextNear,
|
|
122
|
+
doneCount,
|
|
117
123
|
ensureDaemonToken,
|
|
118
124
|
extractProposal,
|
|
119
125
|
givenByLedge,
|
|
@@ -121,6 +127,7 @@ import {
|
|
|
121
127
|
isArchived,
|
|
122
128
|
isTeamLive,
|
|
123
129
|
isTeamsOperation,
|
|
130
|
+
latestTodos,
|
|
124
131
|
offers,
|
|
125
132
|
ompApprovalFor,
|
|
126
133
|
padPlain,
|
|
@@ -147,7 +154,7 @@ import {
|
|
|
147
154
|
teamsPurpose,
|
|
148
155
|
tokenEquals,
|
|
149
156
|
transmissions
|
|
150
|
-
} from "./chunk-
|
|
157
|
+
} from "./chunk-VKRFHDRK.js";
|
|
151
158
|
import {
|
|
152
159
|
log
|
|
153
160
|
} from "./chunk-6IYPRUQG.js";
|
|
@@ -2241,7 +2248,7 @@ function teamsJournal(path) {
|
|
|
2241
2248
|
|
|
2242
2249
|
// src/daemon/server.ts
|
|
2243
2250
|
import { createServer } from "http";
|
|
2244
|
-
import { randomUUID as
|
|
2251
|
+
import { randomUUID as randomUUID6 } from "crypto";
|
|
2245
2252
|
import { homedir as homedir7 } from "os";
|
|
2246
2253
|
import { basename as basename6, join as join18, resolve as resolve8 } from "path";
|
|
2247
2254
|
import { existsSync } from "fs";
|
|
@@ -5675,6 +5682,7 @@ function serveSsh(ws, alias, opts = {}) {
|
|
|
5675
5682
|
|
|
5676
5683
|
// src/mobile/gateway.ts
|
|
5677
5684
|
import { hostname } from "os";
|
|
5685
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
5678
5686
|
|
|
5679
5687
|
// ../ui/src/semver.ts
|
|
5680
5688
|
var parts = (v) => v.split("-")[0].split(".").map((n) => Number.parseInt(n, 10) || 0);
|
|
@@ -6025,6 +6033,8 @@ function fingerprint(m) {
|
|
|
6025
6033
|
f.rows ?? null,
|
|
6026
6034
|
...f.row !== void 0 || f.team !== void 0 || f.task !== void 0 ? [f.row ?? null, f.team ?? null, f.task ?? null] : [],
|
|
6027
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] : [],
|
|
6028
6038
|
// A role or team operation is its whole content: the same id with another prompt, seats or base is another operation.
|
|
6029
6039
|
...isTeamsOperation(m) ? [teamsContent(m)] : []
|
|
6030
6040
|
]);
|
|
@@ -6052,12 +6062,14 @@ var OperationRegistry = class {
|
|
|
6052
6062
|
return { kind: "fresh", record: { ...record } };
|
|
6053
6063
|
}
|
|
6054
6064
|
/** An operation already forgotten (expired) is left forgotten. */
|
|
6055
|
-
finish(op, status, reason) {
|
|
6065
|
+
finish(op, status, reason, models) {
|
|
6056
6066
|
const r = this.records.get(op);
|
|
6057
6067
|
if (!r) return;
|
|
6058
6068
|
r.status = status;
|
|
6059
6069
|
if (reason) r.reason = reason;
|
|
6060
6070
|
else delete r.reason;
|
|
6071
|
+
if (models) r.models = models;
|
|
6072
|
+
else delete r.models;
|
|
6061
6073
|
}
|
|
6062
6074
|
get(op) {
|
|
6063
6075
|
this.expire();
|
|
@@ -6209,7 +6221,27 @@ function toPhoneRequest(s, bodyMax) {
|
|
|
6209
6221
|
return toPhonePending(s.pending, pendingKey(s.pending), verdict.risk, verdict.reason, bodyMax);
|
|
6210
6222
|
}
|
|
6211
6223
|
var toPhoneFullRequest = (s) => toPhoneRequest(s, PENDING_BODY_MAX);
|
|
6212
|
-
|
|
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) {
|
|
6213
6245
|
const pending = toPhoneRequest(s, SNAPSHOT_BODY_MAX);
|
|
6214
6246
|
return {
|
|
6215
6247
|
id: s.id,
|
|
@@ -6225,10 +6257,19 @@ function toPhoneSession(s, readAt2) {
|
|
|
6225
6257
|
...pickSet(s, ["repo", "branch", "worktree", "role", "roleLabel", "team", "context", "relieves"]),
|
|
6226
6258
|
// Its relief's state only (context handover spec §3.2): the phone shows it, the PC starts it.
|
|
6227
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 } : {},
|
|
6228
6268
|
helpers: s.helpers.map((h) => ({ id: h.id, kind: h.kind, task: h.task, state: h.state, ...pickSet(h, ["finishedAt"]) })),
|
|
6229
6269
|
// A preview only: the whole reply is in the thread.
|
|
6230
6270
|
...s.lastReply ? { lastReply: { text: clip(s.lastReply.text, TOOL_TEXT_MAX).text, at: s.lastReply.at, ...pickSet(s.lastReply, ["plan"]) } } : {},
|
|
6231
6271
|
...pending ? { pending } : {},
|
|
6272
|
+
...checklist ? { checklist } : {},
|
|
6232
6273
|
unread: !!s.lastReply && s.lastReply.at > readAt2
|
|
6233
6274
|
};
|
|
6234
6275
|
}
|
|
@@ -6449,10 +6490,39 @@ function toPhoneTask(x, owner, teamName) {
|
|
|
6449
6490
|
};
|
|
6450
6491
|
}
|
|
6451
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
|
+
}
|
|
6452
6515
|
|
|
6453
6516
|
// src/mobile/teamsOps.ts
|
|
6454
6517
|
var FAILED = { status: "failed" };
|
|
6455
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
|
+
}
|
|
6456
6526
|
function createTeamsOps(deps) {
|
|
6457
6527
|
const listing = async () => {
|
|
6458
6528
|
let found;
|
|
@@ -6506,10 +6576,12 @@ function createTeamsOps(deps) {
|
|
|
6506
6576
|
if (!m.code) return true;
|
|
6507
6577
|
if (m.code === "missing") return FAILED;
|
|
6508
6578
|
return { status: "rejected", reason: m.code };
|
|
6509
|
-
case "team.changed":
|
|
6579
|
+
case "team.changed": {
|
|
6510
6580
|
if (m.rid !== rid) return void 0;
|
|
6511
|
-
|
|
6512
|
-
|
|
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
|
+
}
|
|
6513
6585
|
case "team.launched":
|
|
6514
6586
|
return m.rid === rid ? true : void 0;
|
|
6515
6587
|
case "team.failed":
|
|
@@ -6704,12 +6776,14 @@ function verifyAssertion(a, cred, rp, challenge) {
|
|
|
6704
6776
|
// src/mobile/gateway.ts
|
|
6705
6777
|
var FLUSH_BEFORE_REVOKE_MS = 5e3;
|
|
6706
6778
|
var REQUEST_NAME = /^[a-z][a-z.]{0,39}$/;
|
|
6779
|
+
var MODELS_WAIT_MS = 2e4;
|
|
6707
6780
|
function createPhoneGateway(deps) {
|
|
6708
6781
|
const { mobile, log: log2 } = deps;
|
|
6709
6782
|
const startedAt = new Date(deps.now ? deps.now() : Date.now()).toISOString();
|
|
6710
6783
|
const registry = new OperationRegistry();
|
|
6711
6784
|
const followers = /* @__PURE__ */ new Map();
|
|
6712
6785
|
let stopped2 = false;
|
|
6786
|
+
const modelFailures = /* @__PURE__ */ new Map();
|
|
6713
6787
|
const follower = (deviceId) => {
|
|
6714
6788
|
let f = followers.get(deviceId);
|
|
6715
6789
|
if (!f) followers.set(deviceId, f = { subscribed: false, watching: null, reads: /* @__PURE__ */ new Map(), visible: false });
|
|
@@ -6752,7 +6826,17 @@ function createPhoneGateway(deps) {
|
|
|
6752
6826
|
const s = deps.session(id);
|
|
6753
6827
|
return s && !isArchived(s) ? s : void 0;
|
|
6754
6828
|
};
|
|
6755
|
-
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);
|
|
6756
6840
|
const pushStatus = (deviceId) => {
|
|
6757
6841
|
const key = mobile.relayPushKey();
|
|
6758
6842
|
if (key === null) return "unavailable";
|
|
@@ -6764,14 +6848,15 @@ function createPhoneGateway(deps) {
|
|
|
6764
6848
|
const push2 = pushStatus(deviceId);
|
|
6765
6849
|
const f = followers.get(deviceId);
|
|
6766
6850
|
if (f) f.pushStatus = push2;
|
|
6767
|
-
|
|
6851
|
+
const message = {
|
|
6768
6852
|
type: "snapshot",
|
|
6769
6853
|
protocol: PHONE_PROTOCOL,
|
|
6770
6854
|
sessions: deps.sessions().filter((s) => !isArchived(s)).map((s) => projected(deviceId, s)),
|
|
6771
6855
|
capabilities: capabilities(),
|
|
6772
|
-
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 } : {} },
|
|
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 },
|
|
6773
6857
|
device: { confirm: deps.credential(deviceId) !== null, push: push2, ...deps.chats ? { chat: true } : {}, ...deps.contextNear ? { contextNear: deps.contextNear() } : {} }
|
|
6774
6858
|
};
|
|
6859
|
+
return deps.models ? { ...message, daemon: { ...message.daemon, models: true } } : message;
|
|
6775
6860
|
};
|
|
6776
6861
|
const phoneTeams = () => (deps.teams?.() ?? []).flatMap((t) => {
|
|
6777
6862
|
const x = toPhoneTeam(t, deps.tasks?.() ?? [], deps.projectName ?? (() => null));
|
|
@@ -6793,6 +6878,8 @@ function createPhoneGateway(deps) {
|
|
|
6793
6878
|
if (followers.get(deviceId)?.subscribed) {
|
|
6794
6879
|
outbox.push(deviceId, snapshot(deviceId), "snapshot");
|
|
6795
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}`);
|
|
6796
6883
|
}
|
|
6797
6884
|
},
|
|
6798
6885
|
log: log2,
|
|
@@ -6801,9 +6888,27 @@ function createPhoneGateway(deps) {
|
|
|
6801
6888
|
const push = (deviceId, msg, key) => {
|
|
6802
6889
|
if (!stopped2) outbox.push(deviceId, msg, key);
|
|
6803
6890
|
};
|
|
6804
|
-
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) => {
|
|
6805
6910
|
if (stopped2 || !approved(deviceId)) return;
|
|
6806
|
-
push(deviceId, { type: "operation.result", op, status, ...reason ? { reason } : {} });
|
|
6911
|
+
push(deviceId, { type: "operation.result", op, status, ...reason ? { reason } : {}, ...models ? { models } : {} });
|
|
6807
6912
|
};
|
|
6808
6913
|
const needsAssertion = (msg, s) => {
|
|
6809
6914
|
if (msg.type === "session.decide") return msg.decision !== "deny" && riskOf2(s)?.risk !== "low";
|
|
@@ -6865,6 +6970,9 @@ function createPhoneGateway(deps) {
|
|
|
6865
6970
|
return s.pending.requestId === msg.requestId ? void 0 : { status: "rejected", reason: "stale" };
|
|
6866
6971
|
case "session.interrupt":
|
|
6867
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" };
|
|
6868
6976
|
}
|
|
6869
6977
|
};
|
|
6870
6978
|
const local = (msg) => {
|
|
@@ -6877,6 +6985,8 @@ function createPhoneGateway(deps) {
|
|
|
6877
6985
|
return { type: "session.answer", id: msg.id, requestId: msg.requestId, answers: msg.answers, ...msg.cancel ? { cancel: true } : {} };
|
|
6878
6986
|
case "session.interrupt":
|
|
6879
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 };
|
|
6880
6990
|
}
|
|
6881
6991
|
};
|
|
6882
6992
|
const outcomeOf = (code) => {
|
|
@@ -6998,6 +7108,41 @@ function createPhoneGateway(deps) {
|
|
|
6998
7108
|
});
|
|
6999
7109
|
return outcome ?? opts.silent ?? { status: "applied" };
|
|
7000
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
|
+
};
|
|
7001
7146
|
const chatTeam = async (deviceId, msg) => {
|
|
7002
7147
|
const turn = ownTurn(deviceId, msg.qid);
|
|
7003
7148
|
if (!turn?.team || !deps.teams) return { status: "rejected", reason: "unknown_session" };
|
|
@@ -7078,6 +7223,7 @@ function createPhoneGateway(deps) {
|
|
|
7078
7223
|
const refused = precheck(deviceId, msg);
|
|
7079
7224
|
if (refused) return refused;
|
|
7080
7225
|
const out = local(msg);
|
|
7226
|
+
if (out.type === "session.model") return modelChange(out);
|
|
7081
7227
|
let error;
|
|
7082
7228
|
await deps.dispatch(out, (m) => {
|
|
7083
7229
|
if (m.type === "error" && (m.request === void 0 || m.request === out.type)) error ??= { code: m.code };
|
|
@@ -7087,7 +7233,7 @@ function createPhoneGateway(deps) {
|
|
|
7087
7233
|
const write = async (deviceId, msg) => {
|
|
7088
7234
|
const begun = registry.begin(deviceId, msg.op, fingerprint(msg));
|
|
7089
7235
|
if (begun.kind === "conflict") return result(deviceId, msg.op, "rejected", "conflict");
|
|
7090
|
-
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);
|
|
7091
7237
|
let outcome;
|
|
7092
7238
|
try {
|
|
7093
7239
|
result(deviceId, msg.op, "received");
|
|
@@ -7096,8 +7242,8 @@ function createPhoneGateway(deps) {
|
|
|
7096
7242
|
log2(`phone ${msg.type} failed (${e instanceof Error ? e.name : "error"})`);
|
|
7097
7243
|
outcome = { status: "failed" };
|
|
7098
7244
|
}
|
|
7099
|
-
registry.finish(msg.op, outcome.status, outcome.reason);
|
|
7100
|
-
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);
|
|
7101
7247
|
if (msg.type === "credential.register" && outcome.status === "applied" && approved(deviceId)) push(deviceId, snapshot(deviceId), "snapshot");
|
|
7102
7248
|
if (msg.type === "device.forget" && outcome.status === "applied") {
|
|
7103
7249
|
let timer;
|
|
@@ -7184,13 +7330,19 @@ function createPhoneGateway(deps) {
|
|
|
7184
7330
|
case "presence":
|
|
7185
7331
|
follower(deviceId).visible = msg.visible;
|
|
7186
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;
|
|
7187
7337
|
case "snapshot.request":
|
|
7188
7338
|
follower(deviceId).subscribed = true;
|
|
7189
7339
|
push(deviceId, snapshot(deviceId), "snapshot");
|
|
7340
|
+
replayModelFailures(deviceId);
|
|
7190
7341
|
return teamsAndTasks(deviceId);
|
|
7191
7342
|
case "session.thread": {
|
|
7192
7343
|
if (!live3(msg.id)) return push(deviceId, { type: "error", code: "unknown_session", request: msg.type });
|
|
7193
|
-
|
|
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}`);
|
|
7194
7346
|
}
|
|
7195
7347
|
case "session.pending": {
|
|
7196
7348
|
const session = live3(msg.id);
|
|
@@ -7291,6 +7443,7 @@ function createPhoneGateway(deps) {
|
|
|
7291
7443
|
const request = typeof type === "string" && REQUEST_NAME.test(type) ? type : void 0;
|
|
7292
7444
|
log2(`phone message refused: ${request ?? "?"}`);
|
|
7293
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" });
|
|
7294
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" });
|
|
7295
7448
|
return push(deviceId, { type: "error", code: "invalid", ...request !== void 0 ? { request } : {} });
|
|
7296
7449
|
}
|
|
@@ -7324,17 +7477,40 @@ function createPhoneGateway(deps) {
|
|
|
7324
7477
|
}
|
|
7325
7478
|
};
|
|
7326
7479
|
const removed = (id) => {
|
|
7480
|
+
checklists.delete(id);
|
|
7327
7481
|
for (const [deviceId, f] of followers) {
|
|
7328
7482
|
f.reads.delete(id);
|
|
7329
7483
|
if (f.watching === id) f.watching = null;
|
|
7330
7484
|
if (f.subscribed) push(deviceId, { type: "session.removed", id });
|
|
7331
7485
|
}
|
|
7332
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
|
+
};
|
|
7333
7500
|
const unsubscribeMessage = mobile.onMessage(receive);
|
|
7334
7501
|
const unsubscribeChange = mobile.onChange(pruneRevoked);
|
|
7335
7502
|
return {
|
|
7336
7503
|
forward(m) {
|
|
7337
|
-
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
|
+
}
|
|
7338
7514
|
try {
|
|
7339
7515
|
switch (m.type) {
|
|
7340
7516
|
case "session.updated":
|
|
@@ -7345,6 +7521,12 @@ function createPhoneGateway(deps) {
|
|
|
7345
7521
|
return;
|
|
7346
7522
|
case "session.removed":
|
|
7347
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
|
+
}
|
|
7348
7530
|
case "context.prefs":
|
|
7349
7531
|
for (const [deviceId, f] of followers) if (f.subscribed) push(deviceId, snapshot(deviceId), "snapshot");
|
|
7350
7532
|
return;
|
|
@@ -7376,6 +7558,7 @@ function createPhoneGateway(deps) {
|
|
|
7376
7558
|
}
|
|
7377
7559
|
return;
|
|
7378
7560
|
case "session.thread.item": {
|
|
7561
|
+
checklistChanged(m.id, m.item);
|
|
7379
7562
|
const watchers = [...followers].filter(([, f]) => f.watching === m.id);
|
|
7380
7563
|
if (watchers.length === 0) return;
|
|
7381
7564
|
const { item, cut: cut2 } = toPhoneItem(m.item);
|
|
@@ -8156,6 +8339,7 @@ async function startServer(opts) {
|
|
|
8156
8339
|
async function applyOmpModel(s) {
|
|
8157
8340
|
const next2 = s.modelNext;
|
|
8158
8341
|
typing.add(s.id);
|
|
8342
|
+
applyingModel.set(s.id, null);
|
|
8159
8343
|
try {
|
|
8160
8344
|
await omp.setModel(s.id, next2);
|
|
8161
8345
|
log(`model of ${s.id} is now ${next2.model ?? "unchanged"}${next2.effort ? ` \xB7 ${next2.effort}` : ""}`);
|
|
@@ -8163,32 +8347,91 @@ async function startServer(opts) {
|
|
|
8163
8347
|
const message = e.message;
|
|
8164
8348
|
log(`model change of ${s.id} failed: ${message}`);
|
|
8165
8349
|
store.patch(s.id, { modelNext: void 0 });
|
|
8166
|
-
|
|
8350
|
+
modelFailed(s.id, message, "refused");
|
|
8167
8351
|
} finally {
|
|
8168
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);
|
|
8169
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 };
|
|
8170
8400
|
}
|
|
8171
8401
|
async function applyModelNext(id) {
|
|
8172
8402
|
const s = store.get(id);
|
|
8173
|
-
if (!s?.modelNext || !
|
|
8403
|
+
if (!s?.modelNext || !canApplyNow(id)) return;
|
|
8174
8404
|
if (backendOf(s) === "omp-rpc") return applyOmpModel(s);
|
|
8175
|
-
const
|
|
8176
|
-
|
|
8405
|
+
const { model, effort } = s.modelNext;
|
|
8406
|
+
const refusal2 = modelRefusal(s, effort ?? null);
|
|
8407
|
+
if (refusal2) {
|
|
8177
8408
|
store.patch(id, { modelNext: void 0 });
|
|
8178
|
-
return;
|
|
8409
|
+
return modelFailed(id, refusal2.message, refusal2.refused);
|
|
8179
8410
|
}
|
|
8180
|
-
const
|
|
8411
|
+
const key = resumeKey(s);
|
|
8181
8412
|
typing.add(id);
|
|
8413
|
+
applyingModel.set(id, null);
|
|
8182
8414
|
try {
|
|
8183
8415
|
for (const [k, at] of respawned) if (Date.now() - at > RESPAWN_HOOKS_MS) respawned.delete(k);
|
|
8184
8416
|
respawned.set(id, Date.now());
|
|
8185
8417
|
await endSession(s);
|
|
8186
8418
|
const ended = store.get(id);
|
|
8187
|
-
if (!ended || ended.archivedAt !== void 0)
|
|
8419
|
+
if (!ended || ended.archivedAt !== void 0) {
|
|
8420
|
+
store.patch(id, { modelNext: void 0 });
|
|
8421
|
+
return;
|
|
8422
|
+
}
|
|
8188
8423
|
const refused = await respawn({ ...ended, modelNext: void 0 }, key, model, effort);
|
|
8189
|
-
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");
|
|
8190
8432
|
} finally {
|
|
8191
8433
|
typing.delete(id);
|
|
8434
|
+
afterModelApply(id);
|
|
8192
8435
|
}
|
|
8193
8436
|
}
|
|
8194
8437
|
const chatLaunches = /* @__PURE__ */ new Set();
|
|
@@ -8451,6 +8694,26 @@ ${now2.prompt}` });
|
|
|
8451
8694
|
for (const id of leaverRefused) if (!leaving.has(id)) leaverRefused.delete(id);
|
|
8452
8695
|
}
|
|
8453
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
|
+
}
|
|
8454
8717
|
async function composeTeam(msg) {
|
|
8455
8718
|
const changed = (extra) => ({ type: "team.changed", rid: msg.rid, ...extra });
|
|
8456
8719
|
const t = teams.get(msg.team);
|
|
@@ -8467,12 +8730,14 @@ ${now2.prompt}` });
|
|
|
8467
8730
|
if (busy) return changed({ reason: `@${busy.role} : une op\xE9ration est d\xE9j\xE0 en cours sur ce si\xE8ge` });
|
|
8468
8731
|
for (const k of keys) seatsInFlight.add(k);
|
|
8469
8732
|
try {
|
|
8733
|
+
const before = new Map(t.seats.map((s) => [s.role, s]));
|
|
8470
8734
|
const r = teams.compose(msg.team, msg.seats);
|
|
8471
8735
|
if (typeof r === "string") return changed({ reason: r });
|
|
8472
8736
|
log(`team ${t.id} composed: +${r.added.join(",") || "-"} \u2212${r.removed.join(",") || "-"}`);
|
|
8473
8737
|
const failed = r.added.length ? await startSeats(r.team, msg.seats.filter((s) => r.added.includes(s.role))) : null;
|
|
8474
8738
|
closeLeavers();
|
|
8475
|
-
|
|
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 } : {} });
|
|
8476
8741
|
} finally {
|
|
8477
8742
|
for (const k of keys) seatsInFlight.delete(k);
|
|
8478
8743
|
}
|
|
@@ -8813,21 +9078,10 @@ ${now2.prompt}` });
|
|
|
8813
9078
|
return;
|
|
8814
9079
|
}
|
|
8815
9080
|
case "session.model": {
|
|
8816
|
-
|
|
8817
|
-
|
|
8818
|
-
if (
|
|
8819
|
-
|
|
8820
|
-
if (!resumeKey(s)) return refuse("no conversation to resume", msg.id);
|
|
8821
|
-
if (backendOf(s) === "omp-rpc" && !omp.isLive(s.id)) return refuse("the session is not running", msg.id);
|
|
8822
|
-
if (msg.effort && backendOf(s) !== "omp-rpc" && !EFFORT_ARGS[s.tool]) return refuse(`${s.tool} takes its effort in the model id`, msg.id);
|
|
8823
|
-
const next2 = { ...msg.model ? { model: msg.model } : {}, ...msg.effort ? { effort: msg.effort } : {} };
|
|
8824
|
-
if (next2.model === s.model && next2.effort === s.effort) {
|
|
8825
|
-
if (s.modelNext) store.patch(msg.id, { modelNext: void 0 });
|
|
8826
|
-
return;
|
|
8827
|
-
}
|
|
8828
|
-
store.patch(msg.id, { modelNext: next2 });
|
|
8829
|
-
await applyModelNext(msg.id);
|
|
8830
|
-
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 });
|
|
8831
9085
|
}
|
|
8832
9086
|
case "session.delete": {
|
|
8833
9087
|
const s = store.get(msg.id);
|
|
@@ -9093,7 +9347,7 @@ ${now2.prompt}` });
|
|
|
9093
9347
|
},
|
|
9094
9348
|
onChatEnd: (deviceId, end) => notifyChatEnd?.(deviceId, end.code),
|
|
9095
9349
|
version: opts.daemon?.version ?? "unknown",
|
|
9096
|
-
boot:
|
|
9350
|
+
boot: randomUUID6(),
|
|
9097
9351
|
log,
|
|
9098
9352
|
now,
|
|
9099
9353
|
rp: () => rpOf(mobile.state().relayUrl),
|
|
@@ -9107,6 +9361,8 @@ ${now2.prompt}` });
|
|
|
9107
9361
|
// A project's folder is its first local repo: only a project with one can be named by the phone.
|
|
9108
9362
|
projectByName: (name) => projects.projects.find((p) => p.name === name && p.repos.some((r) => r.machine === "local"))?.id ?? null,
|
|
9109
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,
|
|
9110
9366
|
// The phone's « Mettre à jour le PC » and « Redémarrer le daemon »: the updater Desktop's `update.daemon` uses, progress broadcast.
|
|
9111
9367
|
...opts.updater && opts.daemon ? {
|
|
9112
9368
|
pc: {
|