wicked-crew 0.16.12 → 0.16.13
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/README.md +2 -1
- package/dist/api/chat-history.d.ts +6 -0
- package/dist/api/chat-history.d.ts.map +1 -1
- package/dist/api/chat-history.js +21 -0
- package/dist/api/chat-history.js.map +1 -1
- package/dist/api/chat-proposals.d.ts +61 -6
- package/dist/api/chat-proposals.d.ts.map +1 -1
- package/dist/api/chat-proposals.js +252 -24
- package/dist/api/chat-proposals.js.map +1 -1
- package/dist/api/routes.d.ts +14 -2
- package/dist/api/routes.d.ts.map +1 -1
- package/dist/api/routes.js +119 -9
- package/dist/api/routes.js.map +1 -1
- package/dist/api/run-timing-index.d.ts +18 -0
- package/dist/api/run-timing-index.d.ts.map +1 -1
- package/dist/api/run-timing-index.js +34 -0
- package/dist/api/run-timing-index.js.map +1 -1
- package/dist/api/server.d.ts.map +1 -1
- package/dist/api/server.js +100 -1
- package/dist/api/server.js.map +1 -1
- package/dist/apps/spec.d.ts +3009 -0
- package/dist/apps/spec.d.ts.map +1 -0
- package/dist/apps/spec.js +870 -0
- package/dist/apps/spec.js.map +1 -0
- package/dist/cli/index.js +3 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/core/run-boot-resume.d.ts +4 -1
- package/dist/core/run-boot-resume.d.ts.map +1 -1
- package/dist/core/run-boot-resume.js +6 -1
- package/dist/core/run-boot-resume.js.map +1 -1
- package/dist/editors/csp.d.ts +24 -4
- package/dist/editors/csp.d.ts.map +1 -1
- package/dist/editors/csp.js +35 -8
- package/dist/editors/csp.js.map +1 -1
- package/dist/editors/routes.d.ts +4 -1
- package/dist/editors/routes.d.ts.map +1 -1
- package/dist/editors/routes.js +30 -3
- package/dist/editors/routes.js.map +1 -1
- package/dist/editors/webrtc-guard.d.ts +94 -0
- package/dist/editors/webrtc-guard.d.ts.map +1 -0
- package/dist/editors/webrtc-guard.js +506 -0
- package/dist/editors/webrtc-guard.js.map +1 -0
- package/dist/interactive/bridge-pool.d.ts +11 -2
- package/dist/interactive/bridge-pool.d.ts.map +1 -1
- package/dist/interactive/bridge-pool.js +10 -1
- package/dist/interactive/bridge-pool.js.map +1 -1
- package/dist/interactive/doc-binding.d.ts +156 -0
- package/dist/interactive/doc-binding.d.ts.map +1 -0
- package/dist/interactive/doc-binding.js +277 -0
- package/dist/interactive/doc-binding.js.map +1 -0
- package/dist/interactive/doc-delete-routes.d.ts +3 -0
- package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
- package/dist/interactive/doc-delete-routes.js +4 -0
- package/dist/interactive/doc-delete-routes.js.map +1 -1
- package/dist/interactive/draft-events.d.ts +89 -3
- package/dist/interactive/draft-events.d.ts.map +1 -1
- package/dist/interactive/draft-events.js +438 -27
- package/dist/interactive/draft-events.js.map +1 -1
- package/dist/interactive/edit-events.d.ts +2 -0
- package/dist/interactive/edit-events.d.ts.map +1 -1
- package/dist/interactive/edit-events.js +11 -0
- package/dist/interactive/edit-events.js.map +1 -1
- package/dist/interactive/proposal-create.d.ts +143 -0
- package/dist/interactive/proposal-create.d.ts.map +1 -0
- package/dist/interactive/proposal-create.js +318 -0
- package/dist/interactive/proposal-create.js.map +1 -0
- package/dist/interactive/proxy-routes.d.ts +60 -3
- package/dist/interactive/proxy-routes.d.ts.map +1 -1
- package/dist/interactive/proxy-routes.js +209 -3
- package/dist/interactive/proxy-routes.js.map +1 -1
- package/dist/interactive/source-packet.d.ts +126 -0
- package/dist/interactive/source-packet.d.ts.map +1 -0
- package/dist/interactive/source-packet.js +482 -0
- package/dist/interactive/source-packet.js.map +1 -0
- package/dist/studio/assets/{index-C_UIgTiy.js → index-u4O_7N-b.js} +1 -1
- package/dist/studio/editors/wicked-deck/editor.json +1 -1
- package/dist/studio/editors/wicked-deck/index.html +1 -1
- package/dist/studio/editors/wicked-doc/editor.json +1 -1
- package/dist/studio/editors/wicked-doc/index.html +1 -1
- package/dist/studio/editors/wicked-insights/editor.json +1 -1
- package/dist/studio/editors/wicked-insights/index.html +1 -1
- package/dist/studio/editors/wicked-page/editor.json +1 -1
- package/dist/studio/editors/wicked-page/index.html +1 -1
- package/dist/studio/editors/wicked-video/editor.json +1 -1
- package/dist/studio/editors/wicked-video/index.html +1 -1
- package/dist/studio/index.html +1 -1
- package/dist/studio/testid-inventory.json +1 -1
- package/endpoint-manifest.json +1 -1
- package/package.json +4 -3
|
@@ -24,11 +24,11 @@
|
|
|
24
24
|
* (the effective proposal and the identity derived from the pid), then the one launcher per run id
|
|
25
25
|
* ({@link LaunchOnceRegistry}), then `proposal_decided`. Not now runs no other check.
|
|
26
26
|
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* operation's resolution on and registers a {@link RecordingStubDispatcher}
|
|
31
|
-
* nothing.
|
|
27
|
+
* Each operation's launcher is registered by its own slice: A3's real `create` (`interactive/proposal-create.ts`)
|
|
28
|
+
* whenever the draft seam is armed, so production advertises `proposals` and `create`; refine (A5), theme (A7) and demo
|
|
29
|
+
* (A8) stay off there until their slices land. The TEST configuration (`WICKED_CHAT_ARTIFACTS_STUB=1`, DES §13 A2)
|
|
30
|
+
* turns every operation's resolution on and registers a {@link RecordingStubDispatcher} for the kinds that have no real
|
|
31
|
+
* launcher yet (`capabilities.chatArtifacts.stubOps`): those record a dispatch and start nothing.
|
|
32
32
|
*
|
|
33
33
|
* Records go to the chat's live transcript, or to its kept history copy once the chat has ended, so an open proposal
|
|
34
34
|
* stays answerable after a restart (§4.3).
|
|
@@ -236,11 +236,15 @@ export class LaunchOnceRegistry {
|
|
|
236
236
|
const d = this.dispatchers.get(kind);
|
|
237
237
|
return d?.live === undefined ? false : d.live(runId);
|
|
238
238
|
}
|
|
239
|
-
/** Whether a run this daemon started for the artifact is still live
|
|
239
|
+
/** Whether a run this daemon started for the artifact is still live, or a launcher's durable record says an attempt at
|
|
240
|
+
* it has no outcome yet (A3: across a restart too). */
|
|
240
241
|
async liveForArtifact(artifactRef) {
|
|
241
242
|
for (const r of this.byArtifact.get(artifactRef) ?? [])
|
|
242
243
|
if (await this.live(r.kind, r.runId))
|
|
243
244
|
return true;
|
|
245
|
+
for (const d of new Set(this.dispatchers.values()))
|
|
246
|
+
if (d.docBusy !== undefined && (await d.docBusy(artifactRef)))
|
|
247
|
+
return true;
|
|
244
248
|
return false;
|
|
245
249
|
}
|
|
246
250
|
launchOnce(dispatch) {
|
|
@@ -691,12 +695,23 @@ export class ChatProposals {
|
|
|
691
695
|
const style = seed !== undefined && seed.launch.kind === 'document' ? seed.launch.style : null;
|
|
692
696
|
const title = block.title !== undefined && block.title !== '' ? block.title : seed?.title ?? block.entry;
|
|
693
697
|
const repos = [];
|
|
698
|
+
const repoRefs = [];
|
|
694
699
|
for (const want of block.repos) {
|
|
695
|
-
const
|
|
696
|
-
if (
|
|
700
|
+
const hits = chat.repos.filter((r) => r.name === want || r.id === want);
|
|
701
|
+
if (hits.length === 0)
|
|
697
702
|
blocked.push(`${want} isn't in this chat's scope`);
|
|
698
|
-
else if (
|
|
699
|
-
|
|
703
|
+
else if (hits.length > 1)
|
|
704
|
+
blocked.push(`${want} names ${hits.length} repositories in this chat's scope (name it by id)`);
|
|
705
|
+
else if (!repoRefs.includes(hits[0].id)) {
|
|
706
|
+
repos.push(hits[0].name);
|
|
707
|
+
repoRefs.push(hits[0].id);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
// A3 (§5.2): a chat-made document is grounded on the chat's scope. A block that names none, in a chat scoped to ONE
|
|
711
|
+
// repository, is about that one; with several, nothing is guessed (the card says what it is about).
|
|
712
|
+
if (kind === 'create' && block.repos.length === 0 && chat.repos.length === 1) {
|
|
713
|
+
repos.push(chat.repos[0].name);
|
|
714
|
+
repoRefs.push(chat.repos[0].id);
|
|
700
715
|
}
|
|
701
716
|
let demo;
|
|
702
717
|
if (kind === 'demo') {
|
|
@@ -736,6 +751,7 @@ export class ChatProposals {
|
|
|
736
751
|
reason: block.reason ?? null,
|
|
737
752
|
sources,
|
|
738
753
|
repos,
|
|
754
|
+
...(repoRefs.length > 0 ? { repoRefs } : {}),
|
|
739
755
|
helpers,
|
|
740
756
|
time: await this.timeOf(kind, seed ?? null, chat.projectId),
|
|
741
757
|
stops: STOPS[kind],
|
|
@@ -851,10 +867,9 @@ export class ChatProposals {
|
|
|
851
867
|
continue;
|
|
852
868
|
}
|
|
853
869
|
if (token === 'page') {
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
strike(token, 'the page you were on', 'no page was shared with this message');
|
|
870
|
+
// A3: studio's page summary rides with its message and is not kept after it, so a document cannot be made from
|
|
871
|
+
// it later: struck with the reason, never promised on the card and then dropped at launch.
|
|
872
|
+
strike(token, 'the page you were on', ctx.turnHas && ctx.binding?.page === true ? "studio's page summary is not kept after the message" : 'no page was shared with this message');
|
|
858
873
|
continue;
|
|
859
874
|
}
|
|
860
875
|
const colon = token.indexOf(':');
|
|
@@ -877,6 +892,9 @@ export class ChatProposals {
|
|
|
877
892
|
const a = this.deps.attachments(ctx.chatId).find((x) => x.id === id);
|
|
878
893
|
if (a === undefined)
|
|
879
894
|
strike(`attachment:${id}`, `attachment ${id.slice(0, 12)}`, 'not a file attached to this chat');
|
|
895
|
+
// A3: the packet's per-file cap (512 KB): a file over it could never reach the draft.
|
|
896
|
+
else if (typeof a.bytes === 'number' && a.bytes > 512 * 1024)
|
|
897
|
+
strike(`attachment:${a.id}`, a.name, 'too large for a document\'s sources (over 512 KB)');
|
|
880
898
|
else
|
|
881
899
|
push({ token: `attachment:${a.id}`, label: a.name, status: 'ok' });
|
|
882
900
|
continue;
|
|
@@ -1064,7 +1082,7 @@ export class ChatProposals {
|
|
|
1064
1082
|
* proposals of one chat (or two chats changing one document) can never both pass the busy check.
|
|
1065
1083
|
*/
|
|
1066
1084
|
go(chatId, folded, resolution, edits, identity, actor, instance) {
|
|
1067
|
-
const docKey = resolution.kind === 'refine' && identity.artifactRef !== null ? identity.artifactRef : null;
|
|
1085
|
+
const docKey = (resolution.kind === 'refine' || (resolution.kind === 'create' && resolution.artifactRef !== undefined)) && identity.artifactRef !== null ? identity.artifactRef : null;
|
|
1068
1086
|
// Keyed by the chat's instance (shared by its original id and an ended chat's archived alias), else its id.
|
|
1069
1087
|
return this.lock.run(`chat\u0000${instance ?? chatId}`, () => docKey === null
|
|
1070
1088
|
? this.goLocked(chatId, folded, resolution, edits, identity, actor, instance)
|
|
@@ -1168,6 +1186,9 @@ export class ChatProposals {
|
|
|
1168
1186
|
if (p.kind === 'refine')
|
|
1169
1187
|
return { artifactRef: p.target?.ref ?? null, runId };
|
|
1170
1188
|
if (p.kind === 'create') {
|
|
1189
|
+
// A retry fills the same document again (§4.3: artifact identity and attempt identity are separate).
|
|
1190
|
+
if (p.artifactRef !== undefined)
|
|
1191
|
+
return { artifactRef: p.artifactRef, runId };
|
|
1171
1192
|
const mount = chatMount(p.filedIn.projectId) ?? UNFILED_MOUNT;
|
|
1172
1193
|
return { artifactRef: `${mount}/${proposalDocName(p.pid, p.what.title)}`, runId };
|
|
1173
1194
|
}
|
|
@@ -1179,6 +1200,13 @@ export class ChatProposals {
|
|
|
1179
1200
|
*/
|
|
1180
1201
|
async firstGone(chatId, p) {
|
|
1181
1202
|
let runs = null;
|
|
1203
|
+
// A3: a repository the card names must still be registered (never silently dropped from the launch).
|
|
1204
|
+
if ((p.repoRefs ?? []).length > 0 && this.deps.repoIds !== undefined) {
|
|
1205
|
+
const have = await this.deps.repoIds();
|
|
1206
|
+
const lost = (p.repoRefs ?? []).findIndex((id) => !have.has(id));
|
|
1207
|
+
if (lost !== -1)
|
|
1208
|
+
return `${p.repos[lost] ?? p.repoRefs[lost]} is no longer registered on this daemon; ask again in the chat`;
|
|
1209
|
+
}
|
|
1182
1210
|
for (const s of p.sources) {
|
|
1183
1211
|
if (s.status !== 'ok')
|
|
1184
1212
|
continue;
|
|
@@ -1225,16 +1253,16 @@ export class ChatProposals {
|
|
|
1225
1253
|
continue;
|
|
1226
1254
|
return { code: 'chat_busy', reason: `This chat already has a job running (run ${v.session.id.slice(0, 8)}). Wait, or stop that run.` };
|
|
1227
1255
|
}
|
|
1228
|
-
if (artifactRef !== null && p.kind === 'refine') {
|
|
1256
|
+
if (artifactRef !== null && (p.kind === 'refine' || (p.kind === 'create' && p.artifactRef !== undefined))) {
|
|
1229
1257
|
if (await this.deps.registry.liveForArtifact(artifactRef)) {
|
|
1230
|
-
return { code: 'document_busy', reason: `“${p.target?.title ??
|
|
1258
|
+
return { code: 'document_busy', reason: `“${p.target?.title ?? p.what.title}” is being ${p.kind === 'create' ? 'drafted' : 'revised'}. Wait, or stop that run.` };
|
|
1231
1259
|
}
|
|
1232
1260
|
const ref = parseArtifactRef(artifactRef);
|
|
1233
1261
|
if (ref !== null) {
|
|
1234
1262
|
for (const v of runs) {
|
|
1235
1263
|
const doc = v.session.document_id;
|
|
1236
1264
|
if (isLive(v) && doc === ref.doc && runMount(v) === ref.mount) {
|
|
1237
|
-
return { code: 'document_busy', reason: `“${p.target?.title ??
|
|
1265
|
+
return { code: 'document_busy', reason: `“${p.target?.title ?? p.what.title}” is being ${p.kind === 'create' ? 'drafted' : 'revised'}. Wait, or stop that run.` };
|
|
1238
1266
|
}
|
|
1239
1267
|
}
|
|
1240
1268
|
}
|
|
@@ -1252,7 +1280,7 @@ export class ChatProposals {
|
|
|
1252
1280
|
if (edits === undefined)
|
|
1253
1281
|
return p;
|
|
1254
1282
|
const allowed = {
|
|
1255
|
-
create: ['title', 'brief', 'sources', 'helpers'],
|
|
1283
|
+
create: p.artifactRef !== undefined ? ['title', 'brief', 'sources', 'helpers'] : ['title', 'brief', 'sources', 'helpers', 'repos', 'project'],
|
|
1256
1284
|
refine: ['ask', 'helpers'],
|
|
1257
1285
|
theme: ['name', 'patch'],
|
|
1258
1286
|
demo: ['audience', 'show', 'helpers'],
|
|
@@ -1306,13 +1334,52 @@ export class ChatProposals {
|
|
|
1306
1334
|
// Two helpers lift the one block a person can lift.
|
|
1307
1335
|
next = { ...next, helpers, blocked: next.blocked === NEEDS_HELPER ? null : next.blocked };
|
|
1308
1336
|
}
|
|
1337
|
+
// A3 (§4.2 Change something, create): the project — only in a chat filed in no project. Every source, repository
|
|
1338
|
+
// and turn is re-checked against the target below; what is outside it is struck (a `message:` whole).
|
|
1339
|
+
let recheck = false;
|
|
1340
|
+
if (edits.project !== undefined) {
|
|
1341
|
+
const chatProject = this.deps.chat(chatId)?.projectId ?? this.deps.projectOf(chatId);
|
|
1342
|
+
if (chatProject !== null)
|
|
1343
|
+
throw new EditRefusal("this chat is filed in a project: its documents stay there");
|
|
1344
|
+
const target = edits.project === null || edits.project === '' || edits.project === 'default' ? null : edits.project;
|
|
1345
|
+
if (target !== null && (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(target) || (await this.deps.docsRoot(target)) === null)) {
|
|
1346
|
+
throw new EditRefusal(`there is no project ${target.slice(0, 80)} on this daemon`);
|
|
1347
|
+
}
|
|
1348
|
+
if (target !== p.filedIn.projectId) {
|
|
1349
|
+
recheck = true;
|
|
1350
|
+
next = { ...next, filedIn: { projectId: target, label: target === null ? 'Not in a project · this chat' : ((await this.deps.projectName(target)) ?? target) } };
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
if (edits.repos !== undefined) {
|
|
1354
|
+
if (edits.repos.length > 8)
|
|
1355
|
+
throw new EditRefusal('at most 8 repositories');
|
|
1356
|
+
// In the chat's scope: the live chat's, else (an ended chat) the repositories the proposal was resolved with.
|
|
1357
|
+
const scope = this.deps.chat(chatId)?.repos ?? (p.repoRefs ?? []).map((id, i) => ({ id, name: p.repos[i] ?? id }));
|
|
1358
|
+
const repos = [];
|
|
1359
|
+
const repoRefs = [];
|
|
1360
|
+
for (const want of edits.repos) {
|
|
1361
|
+
const hits = scope.filter((r) => r.id === want || r.name === want);
|
|
1362
|
+
if (hits.length !== 1)
|
|
1363
|
+
throw new EditRefusal(`${want.slice(0, 120)} ${hits.length === 0 ? "isn't in this chat's scope" : 'names several repositories (name it by id)'}`);
|
|
1364
|
+
if (!repoRefs.includes(hits[0].id)) {
|
|
1365
|
+
repos.push(hits[0].name);
|
|
1366
|
+
repoRefs.push(hits[0].id);
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
next = { ...next, repos, ...(repoRefs.length > 0 ? { repoRefs } : {}) };
|
|
1370
|
+
if (repoRefs.length === 0)
|
|
1371
|
+
delete next.repoRefs;
|
|
1372
|
+
}
|
|
1373
|
+
if (recheck)
|
|
1374
|
+
next = await this.recheckProject(next, edits.repos !== undefined);
|
|
1309
1375
|
if (edits.sources !== undefined) {
|
|
1310
1376
|
if (edits.sources.length > PROPOSAL_SOURCES_MAX)
|
|
1311
1377
|
throw new EditRefusal(`at most ${PROPOSAL_SOURCES_MAX} sources`);
|
|
1312
1378
|
const records = this.deps.records(chatId);
|
|
1313
1379
|
const { binding, turnHas } = bindingOf(records, f.turnId);
|
|
1314
|
-
// A source the person adds counts as named by them, and is
|
|
1315
|
-
|
|
1380
|
+
// A source the person adds counts as named by them, and is checked against the chat's own scope first; then the
|
|
1381
|
+
// project re-check (when the document moves) strikes what is outside the target.
|
|
1382
|
+
const resolved = await this.resolveSources(edits.sources, {
|
|
1316
1383
|
chatId,
|
|
1317
1384
|
turnId: f.turnId,
|
|
1318
1385
|
records,
|
|
@@ -1322,13 +1389,174 @@ export class ChatProposals {
|
|
|
1322
1389
|
words: personWords(records, f.turnId),
|
|
1323
1390
|
personNamed: true,
|
|
1324
1391
|
});
|
|
1325
|
-
|
|
1326
|
-
|
|
1392
|
+
next = { ...next, sources: resolved };
|
|
1393
|
+
// Re-checked whenever the document is filed outside the chat's own scope (a moved document, or a retry of one).
|
|
1394
|
+
const chatScope = this.deps.chat(chatId)?.projectId ?? this.deps.projectOf(chatId);
|
|
1395
|
+
if (recheck || next.filedIn.projectId !== chatScope)
|
|
1396
|
+
next = await this.recheckProject(next, edits.repos !== undefined);
|
|
1397
|
+
// A source the person ADDED (not one the proposal already held) that cannot be used is said back, never silently
|
|
1398
|
+
// dropped from a launch; one it already held is struck with the reason, as the project change promises.
|
|
1399
|
+
const had = new Set(p.sources.map((s) => s.token));
|
|
1400
|
+
const added = next.sources.find((src) => src.status === 'struck' && !had.has(src.token));
|
|
1327
1401
|
if (added !== undefined)
|
|
1328
1402
|
throw new EditRefusal(`${added.label}: ${added.reason ?? 'it cannot be used'}`);
|
|
1329
|
-
|
|
1403
|
+
// The attachments together must fit the packet (1 MB with room for the rest), or one would be left out at launch.
|
|
1404
|
+
const files = this.deps.attachments(chatId);
|
|
1405
|
+
const attached = next.sources
|
|
1406
|
+
.filter((s) => s.status === 'ok' && s.token.startsWith('attachment:'))
|
|
1407
|
+
.reduce((n, s) => n + (files.find((a) => `attachment:${a.id}` === s.token)?.bytes ?? 0), 0);
|
|
1408
|
+
if (attached > 768 * 1024)
|
|
1409
|
+
throw new EditRefusal(`the attached files together are ${Math.round(attached / 1024)} KB: a document's sources take at most 768 KB of attachments; leave one out`);
|
|
1330
1410
|
}
|
|
1331
1411
|
return next;
|
|
1332
1412
|
}
|
|
1413
|
+
/**
|
|
1414
|
+
* A3 (§4.2): re-check a create against the project it is now filed in. A run or document source from elsewhere is
|
|
1415
|
+
* struck; a `message:` source is struck whole when any record its answer was given is elsewhere (the conversation
|
|
1416
|
+
* keeps only what is in the project, at packet time); repositories outside the project's members are cut (or, when
|
|
1417
|
+
* the person chose them, refused).
|
|
1418
|
+
*/
|
|
1419
|
+
async recheckProject(p, personRepos) {
|
|
1420
|
+
const target = p.filedIn.projectId;
|
|
1421
|
+
const label = target === null ? 'Unfiled' : p.filedIn.label;
|
|
1422
|
+
const records = this.deps.records(p.chatId);
|
|
1423
|
+
let runs = null;
|
|
1424
|
+
const runProject = async (id) => {
|
|
1425
|
+
runs ??= await this.deps.runs();
|
|
1426
|
+
const v = runs.find((x) => x.session.id === id);
|
|
1427
|
+
return v === undefined ? undefined : projectOfMount(runMount(v));
|
|
1428
|
+
};
|
|
1429
|
+
const sources = [];
|
|
1430
|
+
for (const s of p.sources) {
|
|
1431
|
+
if (s.status !== 'ok') {
|
|
1432
|
+
sources.push(s);
|
|
1433
|
+
continue;
|
|
1434
|
+
}
|
|
1435
|
+
if (s.token.startsWith('run:') || s.token.startsWith('doc:')) {
|
|
1436
|
+
const proj = s.projectId !== undefined ? s.projectId : s.token.startsWith('run:') ? await runProject(s.token.slice(4)) : projectOfMount(parseArtifactRef(s.token.slice(4))?.mount ?? '');
|
|
1437
|
+
sources.push(proj === target ? s : { ...s, status: 'struck', reason: `not in ${label}` });
|
|
1438
|
+
continue;
|
|
1439
|
+
}
|
|
1440
|
+
if (s.token.startsWith('message:')) {
|
|
1441
|
+
const turn = s.token.slice('message:'.length);
|
|
1442
|
+
const ctx = records.find((r) => r.kind === 'context' && r.turnId === turn);
|
|
1443
|
+
let inside = true;
|
|
1444
|
+
for (const r of ctx?.runs ?? [])
|
|
1445
|
+
if ((await runProject(r.id)) !== target)
|
|
1446
|
+
inside = false;
|
|
1447
|
+
if (ctx?.artifact !== undefined && projectOfMount(parseArtifactRef(ctx.artifact.ref)?.mount ?? '') !== target)
|
|
1448
|
+
inside = false;
|
|
1449
|
+
sources.push(inside ? s : { ...s, status: 'struck', reason: `it was answered from records outside ${label}` });
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
sources.push(s);
|
|
1453
|
+
}
|
|
1454
|
+
let repos = p.repos;
|
|
1455
|
+
let repoRefs = p.repoRefs ?? [];
|
|
1456
|
+
if (target !== null && repoRefs.length > 0) {
|
|
1457
|
+
const members = (await this.deps.projectRepos?.(target)) ?? [];
|
|
1458
|
+
const outside = repoRefs.filter((id) => !members.some((m) => m.id === id));
|
|
1459
|
+
if (outside.length > 0 && personRepos) {
|
|
1460
|
+
throw new EditRefusal(`${outside.map((id) => p.repos[repoRefs.indexOf(id)] ?? id).join(', ')} ${outside.length === 1 ? 'is' : 'are'} not in ${label}`);
|
|
1461
|
+
}
|
|
1462
|
+
repos = repos.filter((_, i) => !outside.includes(repoRefs[i]));
|
|
1463
|
+
repoRefs = repoRefs.filter((id) => !outside.includes(id));
|
|
1464
|
+
}
|
|
1465
|
+
const out = { ...p, sources, repos, ...(repoRefs.length > 0 ? { repoRefs } : {}) };
|
|
1466
|
+
if (repoRefs.length === 0)
|
|
1467
|
+
delete out.repoRefs;
|
|
1468
|
+
return out;
|
|
1469
|
+
}
|
|
1470
|
+
// ── A3: what the create launcher and the daemon need from the proposals ────────────────────────────────────────────
|
|
1471
|
+
/**
|
|
1472
|
+
* The accepted proposal a create's `chat_id` + `proposal_id` names, for this actor (§5.2): accepting (or decided go),
|
|
1473
|
+
* accepted by them, a create. `null` otherwise — and then the create is refused.
|
|
1474
|
+
*/
|
|
1475
|
+
grantFor(chatId, pid, actorId) {
|
|
1476
|
+
const f = foldProposals(this.deps.records(chatId)).get(pid);
|
|
1477
|
+
const acc = f?.accepting;
|
|
1478
|
+
if (f === undefined || acc === undefined || acc.actor !== actorId || acc.resolution.kind !== 'create')
|
|
1479
|
+
return null;
|
|
1480
|
+
if (!(f.state === 'accepting' || (f.state === 'decided' && f.decision === 'go')))
|
|
1481
|
+
return null;
|
|
1482
|
+
const ref = acc.identity.artifactRef === null ? null : parseArtifactRef(acc.identity.artifactRef);
|
|
1483
|
+
if (ref === null)
|
|
1484
|
+
return null;
|
|
1485
|
+
const ids = acc.resolution.repoRefs ?? [];
|
|
1486
|
+
return { doc: ref.doc, projectId: ref.projectId, repos: ids.map((id, i) => ({ id, name: acc.resolution.repos[i] ?? id })) };
|
|
1487
|
+
}
|
|
1488
|
+
/** The accepted resolution and identity of a proposal (the launcher's bus trigger rebuilds its dispatch from it). */
|
|
1489
|
+
acceptedOf(chatId, pid) {
|
|
1490
|
+
const f = foldProposals(this.deps.records(chatId)).get(pid);
|
|
1491
|
+
if (f?.accepting === undefined)
|
|
1492
|
+
return null;
|
|
1493
|
+
return { actor: f.accepting.actor, resolution: f.accepting.resolution, identity: f.accepting.identity, turnId: f.turnId, state: f.state };
|
|
1494
|
+
}
|
|
1495
|
+
/**
|
|
1496
|
+
* §6.2: a proposal-bound attempt ended without landing. Crew appends `<chatId>:retry:<runId>` — the same artifact, the
|
|
1497
|
+
* failed attempt's effective resolution (edits, helpers, sources; the conversation cutoff stays the binding's), a new
|
|
1498
|
+
* run id derived from its own pid — to the live chat or its kept history. Once per failed run.
|
|
1499
|
+
*/
|
|
1500
|
+
async appendRetry(chatId, failedPid, runId, artifactRef, reason, bound) {
|
|
1501
|
+
const instance = bound ?? this.deps.instanceOf(chatId);
|
|
1502
|
+
return this.lock.run(`chat\u0000${instance ?? chatId}`, async () => {
|
|
1503
|
+
// Checked under the lock: only on the chat instance the document was made in, never on another chat that reused
|
|
1504
|
+
// the id meanwhile (the caller resolves the instance's current id again and retries).
|
|
1505
|
+
if (bound !== undefined && bound !== null && this.deps.instanceOf(chatId) !== bound) {
|
|
1506
|
+
throw new Error(`chat ${chatId} is no longer the chat run ${runId} was made from`);
|
|
1507
|
+
}
|
|
1508
|
+
const records = this.deps.records(chatId);
|
|
1509
|
+
const folded = foldProposals(records);
|
|
1510
|
+
const failed = folded.get(failedPid);
|
|
1511
|
+
const base = failed?.accepting?.resolution ?? failed?.proposal;
|
|
1512
|
+
if (failed === undefined || base === undefined) {
|
|
1513
|
+
this.log(`[proposals] chat ${chatId}: run ${runId} failed but proposal ${failedPid} is not in its records — no retry card`);
|
|
1514
|
+
return null;
|
|
1515
|
+
}
|
|
1516
|
+
// Named by the chat's own id (an ended chat's archive alias still holds the same proposals).
|
|
1517
|
+
const pid = `${base.chatId}:retry:${runId}`;
|
|
1518
|
+
if (folded.has(pid))
|
|
1519
|
+
return pid;
|
|
1520
|
+
const retry = {
|
|
1521
|
+
...base,
|
|
1522
|
+
pid,
|
|
1523
|
+
turnId: null,
|
|
1524
|
+
sourceRunId: runId,
|
|
1525
|
+
artifactRef,
|
|
1526
|
+
reason: `The last attempt ended without a draft: ${reason}. Try again fills the same document.`,
|
|
1527
|
+
blocked: null,
|
|
1528
|
+
};
|
|
1529
|
+
return this.write(chatId, { at: this.now(), turnId: failed.turnId, kind: 'proposal', proposal: retry }, instance) ? pid : null;
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* The artifact job a chat's proposal started that is still live (busy until its outcome is recorded), as the card
|
|
1534
|
+
* names it, or `null`. A work launch filed under the chat (`POST /runs {chatId}`) is refused while one is: one live
|
|
1535
|
+
* job per chat, work and artifact runs together (DES §4.3).
|
|
1536
|
+
*/
|
|
1537
|
+
async liveJobOf(chatId) {
|
|
1538
|
+
for (const f of foldProposals(this.deps.records(chatId)).values()) {
|
|
1539
|
+
const runId = f.state === 'decided' && f.decision === 'go' ? f.result?.runId ?? null : f.state === 'accepting' ? f.accepting?.identity.runId ?? null : null;
|
|
1540
|
+
if (runId !== null && (await this.deps.registry.live(f.proposal.kind, runId)))
|
|
1541
|
+
return f.proposal.what.label;
|
|
1542
|
+
}
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
/** At daemon start (§6.2): an `accepting` proposal whose run exists is decided (adopted). `true` when it recorded. */
|
|
1546
|
+
async settleAccepting(chatId, pid) {
|
|
1547
|
+
const instance = this.deps.instanceOf(chatId);
|
|
1548
|
+
return this.lock.run(`pid\u0000${pid}`, async () => {
|
|
1549
|
+
const f = foldProposals(this.deps.records(chatId)).get(pid);
|
|
1550
|
+
const acc = f?.accepting;
|
|
1551
|
+
if (f === undefined || acc === undefined || f.state !== 'accepting')
|
|
1552
|
+
return false;
|
|
1553
|
+
const found = await this.deps.registry.find(f.proposal.kind, acc.identity);
|
|
1554
|
+
if (found === null)
|
|
1555
|
+
return false;
|
|
1556
|
+
const result = { runId: acc.identity.runId, artifactRef: acc.identity.artifactRef, adopted: true, stub: found.stub };
|
|
1557
|
+
const actor = { id: acc.actor, kind: 'human', trust: 'operator' };
|
|
1558
|
+
return this.decided(chatId, f.turnId, pid, actor, 'go', result, acc.resolution, acc.edits, instance);
|
|
1559
|
+
});
|
|
1560
|
+
}
|
|
1333
1561
|
}
|
|
1334
1562
|
//# sourceMappingURL=chat-proposals.js.map
|