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.
Files changed (89) hide show
  1. package/README.md +2 -1
  2. package/dist/api/chat-history.d.ts +6 -0
  3. package/dist/api/chat-history.d.ts.map +1 -1
  4. package/dist/api/chat-history.js +21 -0
  5. package/dist/api/chat-history.js.map +1 -1
  6. package/dist/api/chat-proposals.d.ts +61 -6
  7. package/dist/api/chat-proposals.d.ts.map +1 -1
  8. package/dist/api/chat-proposals.js +252 -24
  9. package/dist/api/chat-proposals.js.map +1 -1
  10. package/dist/api/routes.d.ts +14 -2
  11. package/dist/api/routes.d.ts.map +1 -1
  12. package/dist/api/routes.js +119 -9
  13. package/dist/api/routes.js.map +1 -1
  14. package/dist/api/run-timing-index.d.ts +18 -0
  15. package/dist/api/run-timing-index.d.ts.map +1 -1
  16. package/dist/api/run-timing-index.js +34 -0
  17. package/dist/api/run-timing-index.js.map +1 -1
  18. package/dist/api/server.d.ts.map +1 -1
  19. package/dist/api/server.js +100 -1
  20. package/dist/api/server.js.map +1 -1
  21. package/dist/apps/spec.d.ts +3009 -0
  22. package/dist/apps/spec.d.ts.map +1 -0
  23. package/dist/apps/spec.js +870 -0
  24. package/dist/apps/spec.js.map +1 -0
  25. package/dist/cli/index.js +3 -3
  26. package/dist/cli/index.js.map +1 -1
  27. package/dist/core/run-boot-resume.d.ts +4 -1
  28. package/dist/core/run-boot-resume.d.ts.map +1 -1
  29. package/dist/core/run-boot-resume.js +6 -1
  30. package/dist/core/run-boot-resume.js.map +1 -1
  31. package/dist/editors/csp.d.ts +24 -4
  32. package/dist/editors/csp.d.ts.map +1 -1
  33. package/dist/editors/csp.js +35 -8
  34. package/dist/editors/csp.js.map +1 -1
  35. package/dist/editors/routes.d.ts +4 -1
  36. package/dist/editors/routes.d.ts.map +1 -1
  37. package/dist/editors/routes.js +30 -3
  38. package/dist/editors/routes.js.map +1 -1
  39. package/dist/editors/webrtc-guard.d.ts +94 -0
  40. package/dist/editors/webrtc-guard.d.ts.map +1 -0
  41. package/dist/editors/webrtc-guard.js +506 -0
  42. package/dist/editors/webrtc-guard.js.map +1 -0
  43. package/dist/interactive/bridge-pool.d.ts +11 -2
  44. package/dist/interactive/bridge-pool.d.ts.map +1 -1
  45. package/dist/interactive/bridge-pool.js +10 -1
  46. package/dist/interactive/bridge-pool.js.map +1 -1
  47. package/dist/interactive/doc-binding.d.ts +156 -0
  48. package/dist/interactive/doc-binding.d.ts.map +1 -0
  49. package/dist/interactive/doc-binding.js +277 -0
  50. package/dist/interactive/doc-binding.js.map +1 -0
  51. package/dist/interactive/doc-delete-routes.d.ts +3 -0
  52. package/dist/interactive/doc-delete-routes.d.ts.map +1 -1
  53. package/dist/interactive/doc-delete-routes.js +4 -0
  54. package/dist/interactive/doc-delete-routes.js.map +1 -1
  55. package/dist/interactive/draft-events.d.ts +89 -3
  56. package/dist/interactive/draft-events.d.ts.map +1 -1
  57. package/dist/interactive/draft-events.js +438 -27
  58. package/dist/interactive/draft-events.js.map +1 -1
  59. package/dist/interactive/edit-events.d.ts +2 -0
  60. package/dist/interactive/edit-events.d.ts.map +1 -1
  61. package/dist/interactive/edit-events.js +11 -0
  62. package/dist/interactive/edit-events.js.map +1 -1
  63. package/dist/interactive/proposal-create.d.ts +143 -0
  64. package/dist/interactive/proposal-create.d.ts.map +1 -0
  65. package/dist/interactive/proposal-create.js +318 -0
  66. package/dist/interactive/proposal-create.js.map +1 -0
  67. package/dist/interactive/proxy-routes.d.ts +60 -3
  68. package/dist/interactive/proxy-routes.d.ts.map +1 -1
  69. package/dist/interactive/proxy-routes.js +209 -3
  70. package/dist/interactive/proxy-routes.js.map +1 -1
  71. package/dist/interactive/source-packet.d.ts +126 -0
  72. package/dist/interactive/source-packet.d.ts.map +1 -0
  73. package/dist/interactive/source-packet.js +482 -0
  74. package/dist/interactive/source-packet.js.map +1 -0
  75. package/dist/studio/assets/{index-C_UIgTiy.js → index-u4O_7N-b.js} +1 -1
  76. package/dist/studio/editors/wicked-deck/editor.json +1 -1
  77. package/dist/studio/editors/wicked-deck/index.html +1 -1
  78. package/dist/studio/editors/wicked-doc/editor.json +1 -1
  79. package/dist/studio/editors/wicked-doc/index.html +1 -1
  80. package/dist/studio/editors/wicked-insights/editor.json +1 -1
  81. package/dist/studio/editors/wicked-insights/index.html +1 -1
  82. package/dist/studio/editors/wicked-page/editor.json +1 -1
  83. package/dist/studio/editors/wicked-page/index.html +1 -1
  84. package/dist/studio/editors/wicked-video/editor.json +1 -1
  85. package/dist/studio/editors/wicked-video/index.html +1 -1
  86. package/dist/studio/index.html +1 -1
  87. package/dist/studio/testid-inventory.json +1 -1
  88. package/endpoint-manifest.json +1 -1
  89. 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
- * Nothing in this slice starts work: each operation's launcher is registered by its own slice (A3 create, A5 refine,
28
- * A7 theme, A8 demo). Production advertises `proposals` with every operation off, so seats are told about nothing and
29
- * the resolver refuses any block. The TEST configuration (`WICKED_CHAT_ARTIFACTS_STUB=1`, DES §13 A2) turns every
30
- * operation's resolution on and registers a {@link RecordingStubDispatcher}, which records a dispatch and starts
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 hit = chat.repos.find((r) => r.name === want || r.id === want);
696
- if (hit === undefined)
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 (!repos.includes(hit.name))
699
- repos.push(hit.name);
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
- if (ctx.turnHas && ctx.binding?.page === true)
855
- push({ token, label: 'the page you were on', status: 'ok' });
856
- else
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 ?? artifactRef}” is being revised. Wait, or stop that run.` };
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 ?? ref.doc}” is being revised. Wait, or stop that run.` };
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 still checked against the scope.
1315
- const sources = await this.resolveSources(edits.sources, {
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
- // A source the person adds that cannot be used is said back, never silently dropped from a launch.
1326
- const added = sources.find((src) => src.status === 'struck' && !p.sources.some((old) => old.token === src.token && old.status === 'struck'));
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
- next = { ...next, sources };
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