@paradow/ledge 0.27.0 → 0.29.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-MAPPYLUM.js} +4 -4
- package/dist/cli/{attach-V7UPHVB2.js → attach-OC64CLB2.js} +8 -8
- package/dist/cli/{bridge-JK3PMG7T.js → bridge-TWX4V37E.js} +1 -1
- package/dist/cli/{chunk-IGAGJVRZ.js → chunk-353WRRGG.js} +3 -3
- package/dist/cli/{chunk-UPKJO3BZ.js → chunk-73YNZ3BN.js} +1 -1
- package/dist/cli/{chunk-37G5F75H.js → chunk-7S7ZLGIY.js} +150 -13
- package/dist/cli/{chunk-TOSPL5H2.js → chunk-ETGOWBUL.js} +3 -3
- package/dist/cli/chunk-JZEWCO6X.js +6 -0
- package/dist/cli/{chunk-ADAGKOFO.js → chunk-K3SDHHZF.js} +1 -1
- package/dist/cli/{chunk-RA3OIXEQ.js → chunk-MZVMW356.js} +2 -2
- package/dist/cli/{chunk-7GIUG73R.js → chunk-RFFTWAAF.js} +1 -1
- package/dist/cli/{chunk-7OGFH6KJ.js → chunk-TNYBWWRO.js} +2 -2
- package/dist/cli/{chunk-CVHX7GUB.js → chunk-TREGH6WM.js} +353 -49
- package/dist/cli/{chunk-7JRQCKBP.js → chunk-UWDE365N.js} +2 -2
- package/dist/cli/{chunk-YLN3VOGV.js → chunk-WCBXS3EU.js} +2 -2
- package/dist/cli/{chunk-ISNHG5SJ.js → chunk-WIUJNSAZ.js} +5 -5
- package/dist/cli/{chunk-2N7PSP7C.js → chunk-XZYQMKKO.js} +3 -3
- package/dist/cli/{daemon-YUPB5NUG.js → daemon-ZTZ7VMYA.js} +7 -7
- package/dist/cli/{decide-HFI2WQXI.js → decide-4BIYS3Y2.js} +3 -3
- package/dist/cli/{handover-GKORMXZ2.js → handover-IS7BJYGZ.js} +4 -4
- package/dist/cli/{hook-TJJEFV54.js → hook-3VKVD4IP.js} +9 -9
- package/dist/cli/index.js +16 -16
- package/dist/cli/{launch-QKQZZSIM.js → launch-XAPN2M3Q.js} +8 -8
- package/dist/cli/{send-RI7JOO5K.js → send-ZLF2ZXMJ.js} +4 -4
- package/dist/cli/{status-RQRS26S2.js → status-NONO6E2S.js} +2 -2
- package/dist/cli/{task-U2XHBUKW.js → task-47MVWDCY.js} +4 -4
- package/dist/cli/{team-NFC4XEIA.js → team-EJ4YIXJH.js} +6 -6
- package/dist/cli/{tui-UWR7SDYF.js → tui-CAP4O4YQ.js} +8 -8
- package/dist/cli/{version-YYNMMWKJ.js → version-EHBPJO7K.js} +1 -1
- package/dist/cli/{who-WW6IL4KD.js → who-FHAXYCM2.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-WCBXS3EU.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-UWDE365N.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-73YNZ3BN.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-K3SDHHZF.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-7S7ZLGIY.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,16 @@ 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, how) {
|
|
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;
|
|
6073
|
+
if (how) r.how = how;
|
|
6074
|
+
else delete r.how;
|
|
6061
6075
|
}
|
|
6062
6076
|
get(op) {
|
|
6063
6077
|
this.expire();
|
|
@@ -6209,7 +6223,27 @@ function toPhoneRequest(s, bodyMax) {
|
|
|
6209
6223
|
return toPhonePending(s.pending, pendingKey(s.pending), verdict.risk, verdict.reason, bodyMax);
|
|
6210
6224
|
}
|
|
6211
6225
|
var toPhoneFullRequest = (s) => toPhoneRequest(s, PENDING_BODY_MAX);
|
|
6212
|
-
|
|
6226
|
+
var checklistCut = (todos) => todos.length > CHECKLIST_TASKS_MAX || todos.some((t) => t.text.length > CHECKLIST_TEXT_MAX);
|
|
6227
|
+
function toPhoneChecklist(todos) {
|
|
6228
|
+
if (!todos) return null;
|
|
6229
|
+
return {
|
|
6230
|
+
tasks: todos.slice(0, CHECKLIST_TASKS_MAX).map((t) => ({ text: clip(t.text, CHECKLIST_TEXT_MAX).text, status: t.status })),
|
|
6231
|
+
done: doneCount(todos),
|
|
6232
|
+
total: todos.length,
|
|
6233
|
+
...checklistCut(todos) ? { cut: true } : {}
|
|
6234
|
+
};
|
|
6235
|
+
}
|
|
6236
|
+
function checklistSummary(todos) {
|
|
6237
|
+
if (!todos) return void 0;
|
|
6238
|
+
const current2 = todos.find((t) => t.status === "in_progress");
|
|
6239
|
+
return {
|
|
6240
|
+
done: doneCount(todos),
|
|
6241
|
+
total: todos.length,
|
|
6242
|
+
...current2 ? { current: clip(current2.text, CHECKLIST_TEXT_MAX).text } : {},
|
|
6243
|
+
...checklistCut(todos) ? { cut: true } : {}
|
|
6244
|
+
};
|
|
6245
|
+
}
|
|
6246
|
+
function toPhoneSession(s, readAt2, checklist) {
|
|
6213
6247
|
const pending = toPhoneRequest(s, SNAPSHOT_BODY_MAX);
|
|
6214
6248
|
return {
|
|
6215
6249
|
id: s.id,
|
|
@@ -6222,13 +6256,24 @@ function toPhoneSession(s, readAt2) {
|
|
|
6222
6256
|
lastActivityAt: s.lastActivityAt,
|
|
6223
6257
|
// Topics are already cut to TOPIC_MAX where they are set; clipped again so no source can swell a snapshot.
|
|
6224
6258
|
...s.topic !== void 0 ? { topic: clip(s.topic, TOPIC_MAX).text } : {},
|
|
6225
|
-
...pickSet(s, ["repo", "branch", "worktree", "role", "roleLabel", "team", "context", "relieves"]),
|
|
6259
|
+
...pickSet(s, ["repo", "branch", "worktree", "role", "roleLabel", "team", "context", "relieves", "exitedAt"]),
|
|
6260
|
+
// Known up front: the phone shows neither Close nor Resume for it, rather than learning it from a refusal.
|
|
6261
|
+
...s.source === "hook_only" ? { readOnly: true } : {},
|
|
6226
6262
|
// Its relief's state only (context handover spec §3.2): the phone shows it, the PC starts it.
|
|
6227
6263
|
...s.handover ? { handover: { state: s.handover.state, ...s.handover.toName ? { toName: s.handover.toName.slice(0, 80) } : {} } } : {},
|
|
6264
|
+
// Its model, effort and the change waiting for its next free moment (#58), cut to the bounds of a model id and an effort.
|
|
6265
|
+
...s.model !== void 0 ? { model: clip(s.model, MODEL_MAX).text } : {},
|
|
6266
|
+
...s.effort !== void 0 ? { effort: clip(s.effort, EFFORT_MAX).text } : {},
|
|
6267
|
+
...s.modelNext ? { modelNext: {
|
|
6268
|
+
...s.modelNext.model !== void 0 ? { model: clip(s.modelNext.model, MODEL_MAX).text } : {},
|
|
6269
|
+
...s.modelNext.effort !== void 0 ? { effort: clip(s.modelNext.effort, EFFORT_MAX).text } : {}
|
|
6270
|
+
} } : {},
|
|
6271
|
+
...canSwitch(s) ? { canSwitch: true } : {},
|
|
6228
6272
|
helpers: s.helpers.map((h) => ({ id: h.id, kind: h.kind, task: h.task, state: h.state, ...pickSet(h, ["finishedAt"]) })),
|
|
6229
6273
|
// A preview only: the whole reply is in the thread.
|
|
6230
6274
|
...s.lastReply ? { lastReply: { text: clip(s.lastReply.text, TOOL_TEXT_MAX).text, at: s.lastReply.at, ...pickSet(s.lastReply, ["plan"]) } } : {},
|
|
6231
6275
|
...pending ? { pending } : {},
|
|
6276
|
+
...checklist ? { checklist } : {},
|
|
6232
6277
|
unread: !!s.lastReply && s.lastReply.at > readAt2
|
|
6233
6278
|
};
|
|
6234
6279
|
}
|
|
@@ -6449,10 +6494,39 @@ function toPhoneTask(x, owner, teamName) {
|
|
|
6449
6494
|
};
|
|
6450
6495
|
}
|
|
6451
6496
|
var phoneTasks = (tasks, now) => tasks.filter((x) => x.giver === LEDGE_GIVER && (!taskClosed(x) || !x.seen && now - Date.parse(x.updatedAt) < 864e5)).slice(-200);
|
|
6497
|
+
var MODEL_MAX = 200;
|
|
6498
|
+
var EFFORT_MAX = 20;
|
|
6499
|
+
var MODELS_MAX = 40;
|
|
6500
|
+
function canSwitch(s) {
|
|
6501
|
+
return s.source === "launcher" && s.state !== "exited";
|
|
6502
|
+
}
|
|
6503
|
+
function toPhoneModels(models) {
|
|
6504
|
+
const out = [];
|
|
6505
|
+
for (const m of models) {
|
|
6506
|
+
if (out.length >= MODELS_MAX) break;
|
|
6507
|
+
const efforts = m.efforts?.filter((e) => EffortSchema.safeParse(e).success).slice(0, 8);
|
|
6508
|
+
const c = PhoneModelChoiceSchema.safeParse({
|
|
6509
|
+
id: m.id,
|
|
6510
|
+
label: clip(m.label, 80).text,
|
|
6511
|
+
...m.description !== void 0 ? { description: clip(m.description, 200).text } : {},
|
|
6512
|
+
...efforts ? { efforts } : {},
|
|
6513
|
+
...pickSet(m, ["defaultEffort", "isDefault"])
|
|
6514
|
+
});
|
|
6515
|
+
if (c.success) out.push(c.data);
|
|
6516
|
+
}
|
|
6517
|
+
return out;
|
|
6518
|
+
}
|
|
6452
6519
|
|
|
6453
6520
|
// src/mobile/teamsOps.ts
|
|
6454
6521
|
var FAILED = { status: "failed" };
|
|
6455
6522
|
var invalid = { status: "rejected", reason: "invalid" };
|
|
6523
|
+
function withSeats(models) {
|
|
6524
|
+
const seats = (models ?? []).flatMap((x) => {
|
|
6525
|
+
if (x.code) return [{ role: x.role, session: x.session, status: "refused", reason: x.code }];
|
|
6526
|
+
return x.when ? [{ role: x.role, session: x.session, status: x.when === "now" ? "applied" : "scheduled" }] : [];
|
|
6527
|
+
});
|
|
6528
|
+
return seats.length ? { models: seats } : {};
|
|
6529
|
+
}
|
|
6456
6530
|
function createTeamsOps(deps) {
|
|
6457
6531
|
const listing = async () => {
|
|
6458
6532
|
let found;
|
|
@@ -6506,10 +6580,12 @@ function createTeamsOps(deps) {
|
|
|
6506
6580
|
if (!m.code) return true;
|
|
6507
6581
|
if (m.code === "missing") return FAILED;
|
|
6508
6582
|
return { status: "rejected", reason: m.code };
|
|
6509
|
-
case "team.changed":
|
|
6583
|
+
case "team.changed": {
|
|
6510
6584
|
if (m.rid !== rid) return void 0;
|
|
6511
|
-
|
|
6512
|
-
|
|
6585
|
+
const seats = withSeats(m.models);
|
|
6586
|
+
if (m.reason && m.team && type === "team.compose") return { status: "applied", reason: "seat_failed", ...seats };
|
|
6587
|
+
return m.reason ? FAILED : { status: "applied", ...seats };
|
|
6588
|
+
}
|
|
6513
6589
|
case "team.launched":
|
|
6514
6590
|
return m.rid === rid ? true : void 0;
|
|
6515
6591
|
case "team.failed":
|
|
@@ -6704,12 +6780,14 @@ function verifyAssertion(a, cred, rp, challenge) {
|
|
|
6704
6780
|
// src/mobile/gateway.ts
|
|
6705
6781
|
var FLUSH_BEFORE_REVOKE_MS = 5e3;
|
|
6706
6782
|
var REQUEST_NAME = /^[a-z][a-z.]{0,39}$/;
|
|
6783
|
+
var MODELS_WAIT_MS = 2e4;
|
|
6707
6784
|
function createPhoneGateway(deps) {
|
|
6708
6785
|
const { mobile, log: log2 } = deps;
|
|
6709
6786
|
const startedAt = new Date(deps.now ? deps.now() : Date.now()).toISOString();
|
|
6710
6787
|
const registry = new OperationRegistry();
|
|
6711
6788
|
const followers = /* @__PURE__ */ new Map();
|
|
6712
6789
|
let stopped2 = false;
|
|
6790
|
+
const modelFailures = /* @__PURE__ */ new Map();
|
|
6713
6791
|
const follower = (deviceId) => {
|
|
6714
6792
|
let f = followers.get(deviceId);
|
|
6715
6793
|
if (!f) followers.set(deviceId, f = { subscribed: false, watching: null, reads: /* @__PURE__ */ new Map(), visible: false });
|
|
@@ -6752,7 +6830,17 @@ function createPhoneGateway(deps) {
|
|
|
6752
6830
|
const s = deps.session(id);
|
|
6753
6831
|
return s && !isArchived(s) ? s : void 0;
|
|
6754
6832
|
};
|
|
6755
|
-
const
|
|
6833
|
+
const checklists = /* @__PURE__ */ new Map();
|
|
6834
|
+
const computeChecklist = (id) => {
|
|
6835
|
+
const todos = latestTodos(deps.thread(id) ?? []);
|
|
6836
|
+
return { list: toPhoneChecklist(todos), summary: checklistSummary(todos) };
|
|
6837
|
+
};
|
|
6838
|
+
const checklistOf = (id) => {
|
|
6839
|
+
let c = checklists.get(id);
|
|
6840
|
+
if (!c) checklists.set(id, c = computeChecklist(id));
|
|
6841
|
+
return c;
|
|
6842
|
+
};
|
|
6843
|
+
const projected = (deviceId, s) => toPhoneSession(s, followers.get(deviceId)?.reads.get(s.id) ?? startedAt, checklistOf(s.id).summary);
|
|
6756
6844
|
const pushStatus = (deviceId) => {
|
|
6757
6845
|
const key = mobile.relayPushKey();
|
|
6758
6846
|
if (key === null) return "unavailable";
|
|
@@ -6764,14 +6852,15 @@ function createPhoneGateway(deps) {
|
|
|
6764
6852
|
const push2 = pushStatus(deviceId);
|
|
6765
6853
|
const f = followers.get(deviceId);
|
|
6766
6854
|
if (f) f.pushStatus = push2;
|
|
6767
|
-
|
|
6855
|
+
const message = {
|
|
6768
6856
|
type: "snapshot",
|
|
6769
6857
|
protocol: PHONE_PROTOCOL,
|
|
6770
6858
|
sessions: deps.sessions().filter((s) => !isArchived(s)).map((s) => projected(deviceId, s)),
|
|
6771
6859
|
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 } : {} },
|
|
6860
|
+
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, ...deps.closeSession ? { sessionClose: true } : {} },
|
|
6773
6861
|
device: { confirm: deps.credential(deviceId) !== null, push: push2, ...deps.chats ? { chat: true } : {}, ...deps.contextNear ? { contextNear: deps.contextNear() } : {} }
|
|
6774
6862
|
};
|
|
6863
|
+
return deps.models ? { ...message, daemon: { ...message.daemon, models: true } } : message;
|
|
6775
6864
|
};
|
|
6776
6865
|
const phoneTeams = () => (deps.teams?.() ?? []).flatMap((t) => {
|
|
6777
6866
|
const x = toPhoneTeam(t, deps.tasks?.() ?? [], deps.projectName ?? (() => null));
|
|
@@ -6793,6 +6882,8 @@ function createPhoneGateway(deps) {
|
|
|
6793
6882
|
if (followers.get(deviceId)?.subscribed) {
|
|
6794
6883
|
outbox.push(deviceId, snapshot(deviceId), "snapshot");
|
|
6795
6884
|
teamsAndTasks(deviceId);
|
|
6885
|
+
const watching = followers.get(deviceId)?.watching;
|
|
6886
|
+
if (watching) outbox.push(deviceId, { type: "session.checklist", id: watching, checklist: checklistOf(watching).list }, `session.checklist:${watching}`);
|
|
6796
6887
|
}
|
|
6797
6888
|
},
|
|
6798
6889
|
log: log2,
|
|
@@ -6801,9 +6892,27 @@ function createPhoneGateway(deps) {
|
|
|
6801
6892
|
const push = (deviceId, msg, key) => {
|
|
6802
6893
|
if (!stopped2) outbox.push(deviceId, msg, key);
|
|
6803
6894
|
};
|
|
6804
|
-
const
|
|
6895
|
+
const replayModelFailures = (deviceId) => {
|
|
6896
|
+
for (const [id, f] of modelFailures) if (live3(id)) push(deviceId, { type: "session.model.failed", id, reason: f.reason });
|
|
6897
|
+
};
|
|
6898
|
+
const keepModelFailure = (m) => {
|
|
6899
|
+
if (m.type === "session.removed") {
|
|
6900
|
+
modelFailures.delete(m.id);
|
|
6901
|
+
return;
|
|
6902
|
+
}
|
|
6903
|
+
if (m.type === "session.updated") {
|
|
6904
|
+
const f = modelFailures.get(m.session.id);
|
|
6905
|
+
if (f && (isArchived(m.session) || f.model !== m.session.model || f.effort !== m.session.effort)) modelFailures.delete(m.session.id);
|
|
6906
|
+
return;
|
|
6907
|
+
}
|
|
6908
|
+
if (m.type !== "error" || m.request !== "session.model" || m.id === void 0) return;
|
|
6909
|
+
const reason = ModelRefusalSchema.safeParse(m.code);
|
|
6910
|
+
const s = live3(m.id);
|
|
6911
|
+
if (reason.success && s) modelFailures.set(m.id, { reason: reason.data, model: s.model, effort: s.effort });
|
|
6912
|
+
};
|
|
6913
|
+
const result = (deviceId, op, status, reason, models, how) => {
|
|
6805
6914
|
if (stopped2 || !approved(deviceId)) return;
|
|
6806
|
-
push(deviceId, { type: "operation.result", op, status, ...reason ? { reason } : {} });
|
|
6915
|
+
push(deviceId, { type: "operation.result", op, status, ...reason ? { reason } : {}, ...models ? { models } : {}, ...how ? { how } : {} });
|
|
6807
6916
|
};
|
|
6808
6917
|
const needsAssertion = (msg, s) => {
|
|
6809
6918
|
if (msg.type === "session.decide") return msg.decision !== "deny" && riskOf2(s)?.risk !== "low";
|
|
@@ -6865,6 +6974,9 @@ function createPhoneGateway(deps) {
|
|
|
6865
6974
|
return s.pending.requestId === msg.requestId ? void 0 : { status: "rejected", reason: "stale" };
|
|
6866
6975
|
case "session.interrupt":
|
|
6867
6976
|
return s.tmuxSession || backendOf(s) === "omp-rpc" ? void 0 : { status: "rejected", reason: "read_only" };
|
|
6977
|
+
case "session.model":
|
|
6978
|
+
if (canSwitch(s)) return void 0;
|
|
6979
|
+
return { status: "rejected", reason: s.state === "exited" ? "ended" : "not_launched" };
|
|
6868
6980
|
}
|
|
6869
6981
|
};
|
|
6870
6982
|
const local = (msg) => {
|
|
@@ -6877,6 +6989,8 @@ function createPhoneGateway(deps) {
|
|
|
6877
6989
|
return { type: "session.answer", id: msg.id, requestId: msg.requestId, answers: msg.answers, ...msg.cancel ? { cancel: true } : {} };
|
|
6878
6990
|
case "session.interrupt":
|
|
6879
6991
|
return { type: "session.interrupt", id: msg.id };
|
|
6992
|
+
case "session.model":
|
|
6993
|
+
return { type: "session.model", id: msg.id, model: msg.model, effort: msg.effort };
|
|
6880
6994
|
}
|
|
6881
6995
|
};
|
|
6882
6996
|
const outcomeOf = (code) => {
|
|
@@ -6985,6 +7099,39 @@ function createPhoneGateway(deps) {
|
|
|
6985
7099
|
throw e;
|
|
6986
7100
|
}
|
|
6987
7101
|
};
|
|
7102
|
+
const sessionSubject = (id) => `session:${id}`;
|
|
7103
|
+
const sessionEnd = async (deviceId, msg) => {
|
|
7104
|
+
const close = deps.closeSession;
|
|
7105
|
+
if (!close) return { status: "rejected", reason: "unavailable" };
|
|
7106
|
+
const closing = msg.type === "session.close";
|
|
7107
|
+
const s = live3(msg.id);
|
|
7108
|
+
if (!s) return { status: "rejected", reason: "unknown_session" };
|
|
7109
|
+
if (s.source === "hook_only") return { status: "rejected", reason: "read_only" };
|
|
7110
|
+
if (closing && s.state === "exited") return { status: "rejected", reason: "ended" };
|
|
7111
|
+
if (!closing && s.state !== "exited") return { status: "rejected", reason: "busy" };
|
|
7112
|
+
if (deps.credential(deviceId) === null) return { status: "rejected", reason: "confirm_required" };
|
|
7113
|
+
const refused = checkAssertion(deviceId, msg.assertion, closing ? "close" : "act", sessionSubject(msg.id));
|
|
7114
|
+
if (refused) return refused;
|
|
7115
|
+
if (!closing) {
|
|
7116
|
+
let error;
|
|
7117
|
+
await deps.dispatch({ type: "session.resume", id: msg.id }, (m) => {
|
|
7118
|
+
if (m.type === "error" && m.request === "session.resume") error ??= { code: m.code };
|
|
7119
|
+
});
|
|
7120
|
+
if (!error) return { status: "applied" };
|
|
7121
|
+
return error.code === "no_conversation" ? { status: "rejected", reason: "no_conversation" } : outcomeOf(error.code);
|
|
7122
|
+
}
|
|
7123
|
+
try {
|
|
7124
|
+
const { how } = await close(msg.id);
|
|
7125
|
+
log2(`phone: device ${deviceId} closed ${msg.id} (${how})`);
|
|
7126
|
+
return { status: "applied", how };
|
|
7127
|
+
} catch (e) {
|
|
7128
|
+
if (live3(msg.id)?.state === "exited") return { status: "rejected", reason: "ended" };
|
|
7129
|
+
const code = RefusalCodeSchema.safeParse(e instanceof Error ? e.code : void 0);
|
|
7130
|
+
if (code.success) return { status: "rejected", reason: code.data };
|
|
7131
|
+
log2(`phone session.close failed (${e instanceof Error ? e.name : "error"})`);
|
|
7132
|
+
return { status: "failed" };
|
|
7133
|
+
}
|
|
7134
|
+
};
|
|
6988
7135
|
const ask = async (out, ok2, opts = {}) => {
|
|
6989
7136
|
let outcome;
|
|
6990
7137
|
await deps.dispatch(out, (m) => {
|
|
@@ -6998,6 +7145,41 @@ function createPhoneGateway(deps) {
|
|
|
6998
7145
|
});
|
|
6999
7146
|
return outcome ?? opts.silent ?? { status: "applied" };
|
|
7000
7147
|
};
|
|
7148
|
+
const modelChange = async (out) => {
|
|
7149
|
+
let when;
|
|
7150
|
+
let refusal2;
|
|
7151
|
+
await deps.dispatch(out, (m) => {
|
|
7152
|
+
if (m.type === "session.model.accepted" && m.id === out.id) when ??= m.when;
|
|
7153
|
+
if (m.type === "error" && m.request === "session.model" && (m.id === void 0 || m.id === out.id)) refusal2 ??= { code: m.code };
|
|
7154
|
+
});
|
|
7155
|
+
if (when) modelFailures.delete(out.id);
|
|
7156
|
+
if (when) return when === "turn" ? { status: "applied", reason: "scheduled" } : { status: "applied" };
|
|
7157
|
+
const model = ModelRefusalSchema.safeParse(refusal2?.code);
|
|
7158
|
+
if (model.success) return { status: "rejected", reason: model.data };
|
|
7159
|
+
const known2 = RefusalCodeSchema.safeParse(refusal2?.code);
|
|
7160
|
+
return known2.success ? { status: "rejected", reason: known2.data } : { status: "failed" };
|
|
7161
|
+
};
|
|
7162
|
+
const modelsList = async (deviceId, tool) => {
|
|
7163
|
+
const requestId = randomUUID5();
|
|
7164
|
+
let listing;
|
|
7165
|
+
let timer;
|
|
7166
|
+
try {
|
|
7167
|
+
await Promise.race([
|
|
7168
|
+
deps.dispatch({ type: "models.list", requestId, tool }, (m) => {
|
|
7169
|
+
if (m.type === "models.listing" && m.requestId === requestId) listing ??= m;
|
|
7170
|
+
}),
|
|
7171
|
+
new Promise((r) => {
|
|
7172
|
+
timer = setTimeout(r, MODELS_WAIT_MS);
|
|
7173
|
+
})
|
|
7174
|
+
]);
|
|
7175
|
+
} catch (e) {
|
|
7176
|
+
log2(`phone models.list failed (${e instanceof Error ? e.name : "error"})`);
|
|
7177
|
+
} finally {
|
|
7178
|
+
clearTimeout(timer);
|
|
7179
|
+
}
|
|
7180
|
+
if (!listing) return push(deviceId, { type: "models.listing", tool, models: [], error: true });
|
|
7181
|
+
push(deviceId, { type: "models.listing", tool, models: toPhoneModels(listing.models), ...listing.error !== void 0 ? { error: true } : {} });
|
|
7182
|
+
};
|
|
7001
7183
|
const chatTeam = async (deviceId, msg) => {
|
|
7002
7184
|
const turn = ownTurn(deviceId, msg.qid);
|
|
7003
7185
|
if (!turn?.team || !deps.teams) return { status: "rejected", reason: "unknown_session" };
|
|
@@ -7075,9 +7257,11 @@ function createPhoneGateway(deps) {
|
|
|
7075
7257
|
if (msg.type === "chat.launch") return chatLaunch(deviceId, msg);
|
|
7076
7258
|
if (msg.type === "chat.close") return chatClose(deviceId, msg);
|
|
7077
7259
|
if (msg.type === "chat.act") return chatAct(deviceId, msg);
|
|
7260
|
+
if (msg.type === "session.close" || msg.type === "session.resume") return sessionEnd(deviceId, msg);
|
|
7078
7261
|
const refused = precheck(deviceId, msg);
|
|
7079
7262
|
if (refused) return refused;
|
|
7080
7263
|
const out = local(msg);
|
|
7264
|
+
if (out.type === "session.model") return modelChange(out);
|
|
7081
7265
|
let error;
|
|
7082
7266
|
await deps.dispatch(out, (m) => {
|
|
7083
7267
|
if (m.type === "error" && (m.request === void 0 || m.request === out.type)) error ??= { code: m.code };
|
|
@@ -7087,7 +7271,7 @@ function createPhoneGateway(deps) {
|
|
|
7087
7271
|
const write = async (deviceId, msg) => {
|
|
7088
7272
|
const begun = registry.begin(deviceId, msg.op, fingerprint(msg));
|
|
7089
7273
|
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);
|
|
7274
|
+
if (begun.kind === "replay") return result(deviceId, msg.op, begun.record.status, begun.record.reason, begun.record.models, begun.record.how);
|
|
7091
7275
|
let outcome;
|
|
7092
7276
|
try {
|
|
7093
7277
|
result(deviceId, msg.op, "received");
|
|
@@ -7096,8 +7280,8 @@ function createPhoneGateway(deps) {
|
|
|
7096
7280
|
log2(`phone ${msg.type} failed (${e instanceof Error ? e.name : "error"})`);
|
|
7097
7281
|
outcome = { status: "failed" };
|
|
7098
7282
|
}
|
|
7099
|
-
registry.finish(msg.op, outcome.status, outcome.reason);
|
|
7100
|
-
result(deviceId, msg.op, outcome.status, outcome.reason);
|
|
7283
|
+
registry.finish(msg.op, outcome.status, outcome.reason, outcome.models, outcome.how);
|
|
7284
|
+
result(deviceId, msg.op, outcome.status, outcome.reason, outcome.models, outcome.how);
|
|
7101
7285
|
if (msg.type === "credential.register" && outcome.status === "applied" && approved(deviceId)) push(deviceId, snapshot(deviceId), "snapshot");
|
|
7102
7286
|
if (msg.type === "device.forget" && outcome.status === "applied") {
|
|
7103
7287
|
let timer;
|
|
@@ -7184,13 +7368,19 @@ function createPhoneGateway(deps) {
|
|
|
7184
7368
|
case "presence":
|
|
7185
7369
|
follower(deviceId).visible = msg.visible;
|
|
7186
7370
|
return;
|
|
7371
|
+
case "models.list":
|
|
7372
|
+
if (!deps.models) return;
|
|
7373
|
+
void modelsList(deviceId, msg.tool).catch((e) => log2(`phone models.list failed (${e instanceof Error ? e.name : "error"})`));
|
|
7374
|
+
return;
|
|
7187
7375
|
case "snapshot.request":
|
|
7188
7376
|
follower(deviceId).subscribed = true;
|
|
7189
7377
|
push(deviceId, snapshot(deviceId), "snapshot");
|
|
7378
|
+
replayModelFailures(deviceId);
|
|
7190
7379
|
return teamsAndTasks(deviceId);
|
|
7191
7380
|
case "session.thread": {
|
|
7192
7381
|
if (!live3(msg.id)) return push(deviceId, { type: "error", code: "unknown_session", request: msg.type });
|
|
7193
|
-
|
|
7382
|
+
push(deviceId, { type: "session.thread", id: msg.id, ...toPhoneThread(deps.thread(msg.id) ?? []) });
|
|
7383
|
+
return push(deviceId, { type: "session.checklist", id: msg.id, checklist: checklistOf(msg.id).list }, `session.checklist:${msg.id}`);
|
|
7194
7384
|
}
|
|
7195
7385
|
case "session.pending": {
|
|
7196
7386
|
const session = live3(msg.id);
|
|
@@ -7228,6 +7418,14 @@ function createPhoneGateway(deps) {
|
|
|
7228
7418
|
const challenge = deps.challenges.issue({ deviceId, purpose: "launch", subject: msg.qid });
|
|
7229
7419
|
return push(deviceId, { type: "confirm.challenge", purpose: "launch", challenge, rpId: rp.rpId, credentialId: cred.id, expiresInMs: CHALLENGE_TTL_MS });
|
|
7230
7420
|
}
|
|
7421
|
+
if ((msg.purpose === "close" || msg.purpose === "act") && msg.qid === void 0 && msg.id !== void 0) {
|
|
7422
|
+
if (!deps.closeSession) return invalid2();
|
|
7423
|
+
if (!live3(msg.id)) return push(deviceId, { type: "error", code: "unknown_session", request: msg.type });
|
|
7424
|
+
const cred = deps.credential(deviceId);
|
|
7425
|
+
if (!cred || cred.rpId !== rp.rpId) return invalid2();
|
|
7426
|
+
const challenge = deps.challenges.issue({ deviceId, purpose: msg.purpose, subject: sessionSubject(msg.id) });
|
|
7427
|
+
return push(deviceId, { type: "confirm.challenge", purpose: msg.purpose, challenge, rpId: rp.rpId, credentialId: cred.id, expiresInMs: CHALLENGE_TTL_MS });
|
|
7428
|
+
}
|
|
7231
7429
|
if (msg.purpose === "close") {
|
|
7232
7430
|
if (msg.qid === void 0 || !ownTurn(deviceId, msg.qid)) return invalid2();
|
|
7233
7431
|
const cred = deps.credential(deviceId);
|
|
@@ -7291,7 +7489,8 @@ function createPhoneGateway(deps) {
|
|
|
7291
7489
|
const request = typeof type === "string" && REQUEST_NAME.test(type) ? type : void 0;
|
|
7292
7490
|
log2(`phone message refused: ${request ?? "?"}`);
|
|
7293
7491
|
const op = OperationIdSchema.safeParse(raw?.op);
|
|
7294
|
-
if (
|
|
7492
|
+
if (request === "session.model" && op.success && approved(deviceId)) return push(deviceId, { type: "operation.result", op: op.data, status: "rejected", reason: "invalid" });
|
|
7493
|
+
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" || request === "session.close" || request === "session.resume") && op.success && approved(deviceId)) return push(deviceId, { type: "operation.result", op: op.data, status: "rejected", reason: "invalid" });
|
|
7295
7494
|
return push(deviceId, { type: "error", code: "invalid", ...request !== void 0 ? { request } : {} });
|
|
7296
7495
|
}
|
|
7297
7496
|
const msg = parsed.data;
|
|
@@ -7324,17 +7523,40 @@ function createPhoneGateway(deps) {
|
|
|
7324
7523
|
}
|
|
7325
7524
|
};
|
|
7326
7525
|
const removed = (id) => {
|
|
7526
|
+
checklists.delete(id);
|
|
7327
7527
|
for (const [deviceId, f] of followers) {
|
|
7328
7528
|
f.reads.delete(id);
|
|
7329
7529
|
if (f.watching === id) f.watching = null;
|
|
7330
7530
|
if (f.subscribed) push(deviceId, { type: "session.removed", id });
|
|
7331
7531
|
}
|
|
7332
7532
|
};
|
|
7533
|
+
const checklistChanged = (id, item) => {
|
|
7534
|
+
if (item.kind !== "tool" || item.helperId !== void 0 || !TODO_TOOLS.has(item.tool) || !checklists.has(id)) return;
|
|
7535
|
+
const before = checklists.get(id);
|
|
7536
|
+
const now = computeChecklist(id);
|
|
7537
|
+
checklists.set(id, now);
|
|
7538
|
+
if (JSON.stringify(now.list) !== JSON.stringify(before.list)) {
|
|
7539
|
+
for (const [deviceId, f] of followers) if (f.watching === id) push(deviceId, { type: "session.checklist", id, checklist: now.list }, `session.checklist:${id}`);
|
|
7540
|
+
}
|
|
7541
|
+
const s = live3(id);
|
|
7542
|
+
if (s && JSON.stringify(now.summary) !== JSON.stringify(before.summary)) {
|
|
7543
|
+
for (const [deviceId, f] of followers) if (f.subscribed) push(deviceId, { type: "session.updated", session: projected(deviceId, s) }, `session.updated:${id}`);
|
|
7544
|
+
}
|
|
7545
|
+
};
|
|
7333
7546
|
const unsubscribeMessage = mobile.onMessage(receive);
|
|
7334
7547
|
const unsubscribeChange = mobile.onChange(pruneRevoked);
|
|
7335
7548
|
return {
|
|
7336
7549
|
forward(m) {
|
|
7337
|
-
if (stopped2
|
|
7550
|
+
if (!stopped2) try {
|
|
7551
|
+
keepModelFailure(m);
|
|
7552
|
+
} catch (e) {
|
|
7553
|
+
log2(`phone model failure of ${m.type} failed (${e instanceof Error ? e.name : "error"})`);
|
|
7554
|
+
}
|
|
7555
|
+
if (stopped2) return;
|
|
7556
|
+
if (followers.size === 0) {
|
|
7557
|
+
checklists.clear();
|
|
7558
|
+
return;
|
|
7559
|
+
}
|
|
7338
7560
|
try {
|
|
7339
7561
|
switch (m.type) {
|
|
7340
7562
|
case "session.updated":
|
|
@@ -7345,6 +7567,12 @@ function createPhoneGateway(deps) {
|
|
|
7345
7567
|
return;
|
|
7346
7568
|
case "session.removed":
|
|
7347
7569
|
return removed(m.id);
|
|
7570
|
+
case "error": {
|
|
7571
|
+
const reason = ModelRefusalSchema.safeParse(m.code);
|
|
7572
|
+
if (m.request !== "session.model" || m.id === void 0 || !reason.success || !live3(m.id)) return;
|
|
7573
|
+
for (const [deviceId, f] of followers) if (f.subscribed) push(deviceId, { type: "session.model.failed", id: m.id, reason: reason.data });
|
|
7574
|
+
return;
|
|
7575
|
+
}
|
|
7348
7576
|
case "context.prefs":
|
|
7349
7577
|
for (const [deviceId, f] of followers) if (f.subscribed) push(deviceId, snapshot(deviceId), "snapshot");
|
|
7350
7578
|
return;
|
|
@@ -7376,6 +7604,7 @@ function createPhoneGateway(deps) {
|
|
|
7376
7604
|
}
|
|
7377
7605
|
return;
|
|
7378
7606
|
case "session.thread.item": {
|
|
7607
|
+
checklistChanged(m.id, m.item);
|
|
7379
7608
|
const watchers = [...followers].filter(([, f]) => f.watching === m.id);
|
|
7380
7609
|
if (watchers.length === 0) return;
|
|
7381
7610
|
const { item, cut: cut2 } = toPhoneItem(m.item);
|
|
@@ -8156,6 +8385,7 @@ async function startServer(opts) {
|
|
|
8156
8385
|
async function applyOmpModel(s) {
|
|
8157
8386
|
const next2 = s.modelNext;
|
|
8158
8387
|
typing.add(s.id);
|
|
8388
|
+
applyingModel.set(s.id, null);
|
|
8159
8389
|
try {
|
|
8160
8390
|
await omp.setModel(s.id, next2);
|
|
8161
8391
|
log(`model of ${s.id} is now ${next2.model ?? "unchanged"}${next2.effort ? ` \xB7 ${next2.effort}` : ""}`);
|
|
@@ -8163,32 +8393,91 @@ async function startServer(opts) {
|
|
|
8163
8393
|
const message = e.message;
|
|
8164
8394
|
log(`model change of ${s.id} failed: ${message}`);
|
|
8165
8395
|
store.patch(s.id, { modelNext: void 0 });
|
|
8166
|
-
|
|
8396
|
+
modelFailed(s.id, message, "refused");
|
|
8167
8397
|
} finally {
|
|
8168
8398
|
typing.delete(s.id);
|
|
8399
|
+
afterModelApply(s.id);
|
|
8400
|
+
}
|
|
8401
|
+
}
|
|
8402
|
+
const modelFailed = (id, message, code) => broadcast({ type: "error", id, message, request: "session.model", code });
|
|
8403
|
+
const applyingModel = /* @__PURE__ */ new Map();
|
|
8404
|
+
function afterModelApply(id) {
|
|
8405
|
+
const later = applyingModel.get(id);
|
|
8406
|
+
applyingModel.delete(id);
|
|
8407
|
+
const s = store.get(id);
|
|
8408
|
+
if (!later || !s) return;
|
|
8409
|
+
const refusal2 = modelRefusal(s, later.effort ?? null);
|
|
8410
|
+
if (refusal2) {
|
|
8411
|
+
store.patch(id, { modelNext: void 0 });
|
|
8412
|
+
return modelFailed(id, refusal2.message, refusal2.refused);
|
|
8169
8413
|
}
|
|
8414
|
+
store.patch(id, { modelNext: later.model === s.model && later.effort === s.effort ? void 0 : later });
|
|
8415
|
+
}
|
|
8416
|
+
function canApplyNow(id) {
|
|
8417
|
+
return free(store.get(id)) && !typing.has(id) && !closing.has(id) && !cardThreads.queuedRelays().some((q) => q.relay.id === id);
|
|
8418
|
+
}
|
|
8419
|
+
function modelRefusal(s, effort) {
|
|
8420
|
+
const inPlace = backendOf(s) === "omp-rpc";
|
|
8421
|
+
if (s.source === "hook_only") return { refused: "not_launched", message: READ_ONLY2 };
|
|
8422
|
+
if (s.state === "exited") return { refused: "ended", message: "the session has ended" };
|
|
8423
|
+
if (!resumeKey(s)) return { refused: "no_conversation", message: "no conversation to resume" };
|
|
8424
|
+
if (inPlace && !omp.isLive(s.id)) return { refused: "not_running", message: "the session is not running" };
|
|
8425
|
+
if (effort && !inPlace && !EFFORT_ARGS[s.tool]) return { refused: "effort_in_model", message: `${s.tool} takes its effort in the model id` };
|
|
8426
|
+
return null;
|
|
8427
|
+
}
|
|
8428
|
+
async function changeModel(id, model, effort) {
|
|
8429
|
+
const s = store.get(id);
|
|
8430
|
+
if (!s) throw unknownSession();
|
|
8431
|
+
const refusal2 = modelRefusal(s, effort);
|
|
8432
|
+
if (refusal2) return refusal2;
|
|
8433
|
+
const next2 = { ...model ? { model } : {}, ...effort ? { effort } : {} };
|
|
8434
|
+
if (applyingModel.has(id)) {
|
|
8435
|
+
applyingModel.set(id, next2);
|
|
8436
|
+
store.patch(id, { modelNext: next2 });
|
|
8437
|
+
return { when: "turn" };
|
|
8438
|
+
}
|
|
8439
|
+
if (next2.model === s.model && next2.effort === s.effort) {
|
|
8440
|
+
if (s.modelNext) store.patch(id, { modelNext: void 0 });
|
|
8441
|
+
return { when: "none" };
|
|
8442
|
+
}
|
|
8443
|
+
const when = canApplyNow(id) ? "now" : "turn";
|
|
8444
|
+
store.patch(id, { modelNext: next2 });
|
|
8445
|
+
return { when };
|
|
8170
8446
|
}
|
|
8171
8447
|
async function applyModelNext(id) {
|
|
8172
8448
|
const s = store.get(id);
|
|
8173
|
-
if (!s?.modelNext || !
|
|
8449
|
+
if (!s?.modelNext || !canApplyNow(id)) return;
|
|
8174
8450
|
if (backendOf(s) === "omp-rpc") return applyOmpModel(s);
|
|
8175
|
-
const
|
|
8176
|
-
|
|
8451
|
+
const { model, effort } = s.modelNext;
|
|
8452
|
+
const refusal2 = modelRefusal(s, effort ?? null);
|
|
8453
|
+
if (refusal2) {
|
|
8177
8454
|
store.patch(id, { modelNext: void 0 });
|
|
8178
|
-
return;
|
|
8455
|
+
return modelFailed(id, refusal2.message, refusal2.refused);
|
|
8179
8456
|
}
|
|
8180
|
-
const
|
|
8457
|
+
const key = resumeKey(s);
|
|
8181
8458
|
typing.add(id);
|
|
8459
|
+
applyingModel.set(id, null);
|
|
8182
8460
|
try {
|
|
8183
8461
|
for (const [k, at] of respawned) if (Date.now() - at > RESPAWN_HOOKS_MS) respawned.delete(k);
|
|
8184
8462
|
respawned.set(id, Date.now());
|
|
8185
8463
|
await endSession(s);
|
|
8186
8464
|
const ended = store.get(id);
|
|
8187
|
-
if (!ended || ended.archivedAt !== void 0)
|
|
8465
|
+
if (!ended || ended.archivedAt !== void 0) {
|
|
8466
|
+
store.patch(id, { modelNext: void 0 });
|
|
8467
|
+
return;
|
|
8468
|
+
}
|
|
8188
8469
|
const refused = await respawn({ ...ended, modelNext: void 0 }, key, model, effort);
|
|
8189
|
-
if (refused)
|
|
8470
|
+
if (refused) {
|
|
8471
|
+
log(`model change of ${id} failed: ${refused}`);
|
|
8472
|
+
modelFailed(id, refused, "restart_failed");
|
|
8473
|
+
}
|
|
8474
|
+
} catch (e) {
|
|
8475
|
+
log(`model change of ${id} failed: ${e.message}`);
|
|
8476
|
+
store.patch(id, { modelNext: void 0 });
|
|
8477
|
+
modelFailed(id, e.message, "restart_failed");
|
|
8190
8478
|
} finally {
|
|
8191
8479
|
typing.delete(id);
|
|
8480
|
+
afterModelApply(id);
|
|
8192
8481
|
}
|
|
8193
8482
|
}
|
|
8194
8483
|
const chatLaunches = /* @__PURE__ */ new Set();
|
|
@@ -8451,6 +8740,26 @@ ${now2.prompt}` });
|
|
|
8451
8740
|
for (const id of leaverRefused) if (!leaving.has(id)) leaverRefused.delete(id);
|
|
8452
8741
|
}
|
|
8453
8742
|
const seatsInFlight = /* @__PURE__ */ new Set();
|
|
8743
|
+
async function seatModels(before, team) {
|
|
8744
|
+
const out = [];
|
|
8745
|
+
const roles = listDefinitions(team.cwd).roles;
|
|
8746
|
+
for (const seat of team.seats) {
|
|
8747
|
+
const was = before.get(seat.role);
|
|
8748
|
+
if (!was || seat.leaving || was.tool !== seat.tool || was.model === seat.model && was.effort === seat.effort) continue;
|
|
8749
|
+
const holder = seat.sessionId ? store.get(seat.sessionId) : void 0;
|
|
8750
|
+
if (!holder) continue;
|
|
8751
|
+
const resolved = resolveSeats([seat], roles);
|
|
8752
|
+
const want = "unknown" in resolved ? seat : resolved[0];
|
|
8753
|
+
try {
|
|
8754
|
+
const r = await changeModel(holder.id, want.model ?? null, want.effort ?? null);
|
|
8755
|
+
if ("refused" in r) out.push({ role: seat.role, session: holder.id, code: r.refused });
|
|
8756
|
+
else if (r.when !== "none") out.push({ role: seat.role, session: holder.id, when: r.when });
|
|
8757
|
+
} catch (e) {
|
|
8758
|
+
log(`team ${team.id}: model of @${seat.role} (${holder.id}) not changed: ${e.message}`);
|
|
8759
|
+
}
|
|
8760
|
+
}
|
|
8761
|
+
return out;
|
|
8762
|
+
}
|
|
8454
8763
|
async function composeTeam(msg) {
|
|
8455
8764
|
const changed = (extra) => ({ type: "team.changed", rid: msg.rid, ...extra });
|
|
8456
8765
|
const t = teams.get(msg.team);
|
|
@@ -8467,12 +8776,14 @@ ${now2.prompt}` });
|
|
|
8467
8776
|
if (busy) return changed({ reason: `@${busy.role} : une op\xE9ration est d\xE9j\xE0 en cours sur ce si\xE8ge` });
|
|
8468
8777
|
for (const k of keys) seatsInFlight.add(k);
|
|
8469
8778
|
try {
|
|
8779
|
+
const before = new Map(t.seats.map((s) => [s.role, s]));
|
|
8470
8780
|
const r = teams.compose(msg.team, msg.seats);
|
|
8471
8781
|
if (typeof r === "string") return changed({ reason: r });
|
|
8472
8782
|
log(`team ${t.id} composed: +${r.added.join(",") || "-"} \u2212${r.removed.join(",") || "-"}`);
|
|
8473
8783
|
const failed = r.added.length ? await startSeats(r.team, msg.seats.filter((s) => r.added.includes(s.role))) : null;
|
|
8474
8784
|
closeLeavers();
|
|
8475
|
-
|
|
8785
|
+
const models2 = await seatModels(before, teams.get(t.id));
|
|
8786
|
+
return changed({ team: teams.get(t.id), ...failed ? { reason: failed } : {}, ...models2.length ? { models: models2 } : {} });
|
|
8476
8787
|
} finally {
|
|
8477
8788
|
for (const k of keys) seatsInFlight.delete(k);
|
|
8478
8789
|
}
|
|
@@ -8807,27 +9118,16 @@ ${now2.prompt}` });
|
|
|
8807
9118
|
if (!s) return refuse(unknownSession(), msg.id);
|
|
8808
9119
|
if (s.state !== "exited") return refuse(new Refusal("the session is still running", "busy"), msg.id);
|
|
8809
9120
|
const key = resumeKey(s);
|
|
8810
|
-
if (!key) return refuse("no conversation to resume", msg.id);
|
|
9121
|
+
if (!key) return refuse({ message: "no conversation to resume", code: "no_conversation" }, msg.id);
|
|
8811
9122
|
const refused = await respawn(s, key, s.model, s.effort);
|
|
8812
9123
|
if (refused) refuse(refused, msg.id);
|
|
8813
9124
|
return;
|
|
8814
9125
|
}
|
|
8815
9126
|
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;
|
|
9127
|
+
if (!store.get(msg.id)) return refuse("unknown session", msg.id);
|
|
9128
|
+
const r = await changeModel(msg.id, msg.model, msg.effort);
|
|
9129
|
+
if ("refused" in r) return refuse({ message: r.message, code: r.refused }, msg.id);
|
|
9130
|
+
return reply({ type: "session.model.accepted", id: msg.id, when: r.when });
|
|
8831
9131
|
}
|
|
8832
9132
|
case "session.delete": {
|
|
8833
9133
|
const s = store.get(msg.id);
|
|
@@ -9093,7 +9393,7 @@ ${now2.prompt}` });
|
|
|
9093
9393
|
},
|
|
9094
9394
|
onChatEnd: (deviceId, end) => notifyChatEnd?.(deviceId, end.code),
|
|
9095
9395
|
version: opts.daemon?.version ?? "unknown",
|
|
9096
|
-
boot:
|
|
9396
|
+
boot: randomUUID6(),
|
|
9097
9397
|
log,
|
|
9098
9398
|
now,
|
|
9099
9399
|
rp: () => rpOf(mobile.state().relayUrl),
|
|
@@ -9107,6 +9407,10 @@ ${now2.prompt}` });
|
|
|
9107
9407
|
// A project's folder is its first local repo: only a project with one can be named by the phone.
|
|
9108
9408
|
projectByName: (name) => projects.projects.find((p) => p.name === name && p.repos.some((r) => r.machine === "local"))?.id ?? null,
|
|
9109
9409
|
teamCatalog: true,
|
|
9410
|
+
// The phone's model switch (#58): `session.model` answers with a code, and `models.list` asks this machine's agents.
|
|
9411
|
+
models: true,
|
|
9412
|
+
// A session's « Fermer » on the phone: the cards' close, so a close under way (a card's, another phone's) is shared.
|
|
9413
|
+
closeSession: closeForCard,
|
|
9110
9414
|
// The phone's « Mettre à jour le PC » and « Redémarrer le daemon »: the updater Desktop's `update.daemon` uses, progress broadcast.
|
|
9111
9415
|
...opts.updater && opts.daemon ? {
|
|
9112
9416
|
pc: {
|