@unson/brainbase-mcp 0.43.0 → 0.44.1

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 (74) hide show
  1. package/contracts/canonical-task/operation-schema.sql +58 -0
  2. package/contracts/canonical-task/search-indexes.sql +10 -0
  3. package/contracts/canonical-task/store-schema.sql +77 -0
  4. package/dist/automation-run.d.ts +213 -0
  5. package/dist/automation-run.js +479 -0
  6. package/dist/canonical-task-operations.d.ts +205 -0
  7. package/dist/canonical-task-operations.js +700 -0
  8. package/dist/canonical-task-postgres.d.ts +139 -0
  9. package/dist/canonical-task-postgres.js +554 -0
  10. package/dist/canonical-task-service.d.ts +13 -0
  11. package/dist/canonical-task-service.js +55 -3
  12. package/dist/canonical-task-store-config.d.ts +31 -0
  13. package/dist/canonical-task-store-config.js +53 -0
  14. package/dist/config-parser.d.ts +298 -0
  15. package/dist/config-parser.js +593 -0
  16. package/dist/connected-onboarding.d.ts +57 -0
  17. package/dist/connected-onboarding.js +37 -0
  18. package/dist/durable-wait-http.d.ts +3 -1
  19. package/dist/durable-wait-http.js +73 -6
  20. package/dist/durable-waits.d.ts +93 -3
  21. package/dist/durable-waits.js +320 -8
  22. package/dist/honcho.d.ts +25 -0
  23. package/dist/honcho.js +81 -0
  24. package/dist/judgment-event-adapter.d.ts +72 -0
  25. package/dist/judgment-event-adapter.js +114 -0
  26. package/dist/judgment-event-outbox.d.ts +182 -0
  27. package/dist/judgment-event-outbox.js +568 -0
  28. package/dist/judgment-execution-outcome.d.ts +46 -0
  29. package/dist/judgment-execution-outcome.js +149 -0
  30. package/dist/judgment-host.d.ts +18 -7
  31. package/dist/judgment-host.js +82 -34
  32. package/dist/judgment-lifecycle-core.d.ts +198 -0
  33. package/dist/judgment-lifecycle-core.js +464 -0
  34. package/dist/judgment-node-evidence.d.ts +57 -0
  35. package/dist/judgment-node-evidence.js +297 -0
  36. package/dist/judgment-runtime-resolution.d.ts +264 -0
  37. package/dist/judgment-runtime-resolution.js +1248 -0
  38. package/dist/judgment-runtime-resolution.public.d.ts +255 -0
  39. package/dist/knowledge-event.d.ts +28 -0
  40. package/dist/knowledge-event.js +114 -0
  41. package/dist/mcp-runtime.d.ts +75 -0
  42. package/dist/mcp-runtime.js +188 -0
  43. package/dist/meeting-automation-runtime.d.ts +102 -0
  44. package/dist/meeting-automation-runtime.js +249 -0
  45. package/dist/meeting-automation.d.ts +155 -0
  46. package/dist/meeting-automation.js +341 -0
  47. package/dist/meeting-minutes-context-receipt.d.ts +91 -0
  48. package/dist/meeting-minutes-context-receipt.js +280 -0
  49. package/dist/meeting-source-sync.d.ts +89 -0
  50. package/dist/meeting-source-sync.js +230 -0
  51. package/dist/mesh.d.ts +58 -0
  52. package/dist/mesh.js +134 -0
  53. package/dist/onboarding-runtime.d.ts +47 -0
  54. package/dist/onboarding-runtime.js +4719 -0
  55. package/dist/routine-cycle.d.ts +51 -0
  56. package/dist/routine-cycle.js +153 -0
  57. package/dist/routine-liveness.d.ts +49 -0
  58. package/dist/routine-liveness.js +227 -0
  59. package/dist/routine-runtime.d.ts +101 -0
  60. package/dist/routine-runtime.js +237 -0
  61. package/dist/routines.d.ts +3 -0
  62. package/dist/routines.js +3 -0
  63. package/dist/run-receipt.d.ts +40 -0
  64. package/dist/run-receipt.js +635 -0
  65. package/dist/runtime-host.d.ts +64 -0
  66. package/dist/runtime-host.js +70 -0
  67. package/dist/world-work.d.ts +16 -0
  68. package/package.json +115 -3
  69. package/ui/meeting-minutes.css +122 -0
  70. package/ui/meeting-minutes.js +316 -0
  71. package/ui/objective-editor.js +47 -11
  72. package/ui/world/world-district.js +18 -3
  73. package/ui/world/world-placement.js +13 -0
  74. package/ui/world/world-work-rail.js +7 -1
@@ -208,6 +208,52 @@ function normalizeConnections(value) {
208
208
  ]));
209
209
  }
210
210
 
211
+ // 参加者の行 (story-meeting-participants-row-v1). The host decides who is linked,
212
+ // unconfirmed or not a person; anything malformed is unreadable, never a person (P9).
213
+ const PARTICIPANT_STATES = new Set(['linked', 'unconfirmed', 'not_person']);
214
+
215
+ function normalizeParticipant(value) {
216
+ if (!isRecord(value) || !PARTICIPANT_STATES.has(value.state)) return null;
217
+ const displayName = text(value.display_name);
218
+ if (!displayName) return null;
219
+ const others = Number(value.other_meetings);
220
+ const participant = {
221
+ displayName,
222
+ state: value.state,
223
+ removable: value.removable === true,
224
+ otherMeetings: Number.isInteger(others) && others > 0 ? others : 0,
225
+ };
226
+ if (value.state === 'linked') {
227
+ const name = text(value.name);
228
+ const entityId = text(value.entity_id);
229
+ if (!name || !entityId) return null;
230
+ participant.name = name;
231
+ participant.entityId = entityId;
232
+ participant.href = connectionHref(value.href);
233
+ }
234
+ return participant;
235
+ }
236
+
237
+ function normalizeParticipants(value) {
238
+ if (!isRecord(value) || !['recorded', 'none', 'unavailable'].includes(value.status)) return { status: 'unavailable' };
239
+ if (value.status !== 'recorded') return { status: value.status };
240
+ const participants = Array.isArray(value.participants) ? value.participants.map(normalizeParticipant) : [];
241
+ if (participants.some((participant) => participant === null)) return { status: 'unavailable' };
242
+ if (!participants.length) return { status: 'none' };
243
+ return { status: 'recorded', participants, redrafting: value.redrafting === true };
244
+ }
245
+
246
+ function normalizeCandidateList(values, idField) {
247
+ if (!Array.isArray(values)) return [];
248
+ return values.flatMap((value) => {
249
+ const id = text(value?.[idField]);
250
+ const name = text(value?.name);
251
+ if (!id || !name) return [];
252
+ const evidence = Array.isArray(value.evidence) ? value.evidence.map((item) => text(item)).filter(Boolean) : [];
253
+ return [{ id, name, company: text(value.company), evidence }];
254
+ });
255
+ }
256
+
211
257
  function firstDate(values) {
212
258
  for (const value of values) {
213
259
  const date = parseDate(value);
@@ -636,6 +682,7 @@ export function createMeetingMinutesUI({
636
682
  versionActionExtensions = [],
637
683
  meetingLabel,
638
684
  meetingConnections,
685
+ meetingParticipants,
639
686
  canCreateMeeting,
640
687
  createUnavailableReason,
641
688
  canEditMinutes,
@@ -666,11 +713,14 @@ export function createMeetingMinutesUI({
666
713
  withdrawingMinutesId: null,
667
714
  // { meetingId, rows } where rows is null while the host is answering.
668
715
  connections: null,
716
+ // { meetingId, value, openName } for the 参加者 row (story-meeting-participants-row-v1).
717
+ participants: null,
669
718
  };
670
719
  let listRequestGeneration = 0;
671
720
  let detailRequestGeneration = 0;
672
721
  let createReasonSequence = 0;
673
722
  let connectionsSlot = null;
723
+ let participantsSlot = null;
674
724
  let railProjectSlot = null;
675
725
  let railProjectMeeting = null;
676
726
 
@@ -1052,6 +1102,260 @@ export function createMeetingMinutesUI({
1052
1102
  if (railProjectSlot && railProjectMeeting?.meeting_id === meetingId) fillRailProject(railProjectSlot, railProjectMeeting);
1053
1103
  }
1054
1104
 
1105
+ function hasParticipants() {
1106
+ return isRecord(meetingParticipants) && typeof meetingParticipants.read === 'function';
1107
+ }
1108
+
1109
+ function currentParticipants(meeting) {
1110
+ const value = state.participants;
1111
+ return value && value.meetingId === meeting?.meeting_id ? value : null;
1112
+ }
1113
+
1114
+ function participantError(error, fallback) {
1115
+ const reason = text(error?.message);
1116
+ return reason && !/^[a-z_]+$/u.test(reason) ? reason : fallback;
1117
+ }
1118
+
1119
+ function fillParticipants(slot, meeting) {
1120
+ const current = currentParticipants(meeting);
1121
+ const value = current?.value ?? null;
1122
+ slot.hidden = value?.status === 'none';
1123
+ if (value?.status === 'none') {
1124
+ slot.replaceChildren();
1125
+ return;
1126
+ }
1127
+ const row = makeElement(doc, 'div', { className: 'bb-minutes-participants-row' });
1128
+ row.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-label', textContent: '参加者' }));
1129
+ if (!value) {
1130
+ row.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note', textContent: '読み込んでいます' }));
1131
+ slot.replaceChildren(row);
1132
+ return;
1133
+ }
1134
+ if (value.status !== 'recorded') {
1135
+ row.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note is-unavailable', textContent: '参加者を読み込めませんでした' }));
1136
+ slot.replaceChildren(row);
1137
+ return;
1138
+ }
1139
+ const list = makeElement(doc, 'ul', { className: 'bb-minutes-participant-items' });
1140
+ for (const participant of value.participants) {
1141
+ const entry = makeElement(doc, 'li', { attrs: { 'data-participant-state': participant.state } });
1142
+ if (participant.state === 'linked') {
1143
+ entry.append(connectionLink({ label: participant.name, href: participant.href }, 'bb-minutes-connection-link'));
1144
+ if (participant.name !== participant.displayName) {
1145
+ entry.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-detail', textContent: `(${participant.displayName})` }));
1146
+ }
1147
+ } else if (participant.state === 'unconfirmed') {
1148
+ const open = current.openName === participant.displayName;
1149
+ const badge = button(doc, `${participant.displayName}(未確認)`, () => toggleParticipantPanel(meeting, participant.displayName), {
1150
+ className: 'bb-minutes-participant-badge',
1151
+ attrs: { 'aria-expanded': open ? 'true' : 'false' },
1152
+ });
1153
+ entry.append(badge);
1154
+ } else {
1155
+ entry.append(makeElement(doc, 'span', { textContent: participant.displayName }));
1156
+ entry.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-detail', textContent: '(人ではない)' }));
1157
+ }
1158
+ if (participant.removable && typeof meetingParticipants.unlink === 'function') {
1159
+ entry.append(button(doc, '取り消す', () => void unlinkParticipant(meeting, participant), { className: 'bb-minutes-participant-undo' }));
1160
+ }
1161
+ list.append(entry);
1162
+ }
1163
+ row.append(list);
1164
+ const children = [row];
1165
+ if (value.redrafting) {
1166
+ children.push(makeElement(doc, 'p', { className: 'bb-minutes-connection-note', textContent: '名前を結んだので、この議事録を作り直しています。' }));
1167
+ }
1168
+ if (current.notice) {
1169
+ children.push(makeElement(doc, 'p', { className: 'bb-minutes-participant-notice', textContent: current.notice, attrs: { role: 'status' } }));
1170
+ }
1171
+ if (current.openName && value.participants.some((participant) => participant.state === 'unconfirmed' && participant.displayName === current.openName)) {
1172
+ children.push(renderParticipantPanel(meeting, current));
1173
+ }
1174
+ slot.replaceChildren(...children);
1175
+ }
1176
+
1177
+ function refreshParticipants(meeting) {
1178
+ if (participantsSlot && state.detail?.meeting?.meeting_id === meeting.meeting_id) fillParticipants(participantsSlot, meeting);
1179
+ }
1180
+
1181
+ async function loadParticipants(detail, isCurrentRequest, { notice = '' } = {}) {
1182
+ const meetingId = detail.meeting.meeting_id;
1183
+ const previous = currentParticipants(detail.meeting);
1184
+ state.participants = { meetingId, value: previous?.value ?? null, openName: null, notice };
1185
+ let value;
1186
+ try {
1187
+ value = normalizeParticipants(await meetingParticipants.read({
1188
+ meeting: detail.meeting,
1189
+ minutes: detail.minutes,
1190
+ versions: detail.versions,
1191
+ }));
1192
+ } catch {
1193
+ value = { status: 'unavailable' };
1194
+ }
1195
+ if (!isCurrentRequest() || state.participants?.meetingId !== meetingId) return;
1196
+ state.participants = { ...state.participants, value };
1197
+ refreshParticipants(detail.meeting);
1198
+ }
1199
+
1200
+ function reloadParticipants(meeting, notice) {
1201
+ const detail = state.detail;
1202
+ if (!detail || detail.meeting?.meeting_id !== meeting.meeting_id) return Promise.resolve();
1203
+ return loadParticipants(detail, () => state.detail?.meeting?.meeting_id === meeting.meeting_id, { notice });
1204
+ }
1205
+
1206
+ function toggleParticipantPanel(meeting, displayName) {
1207
+ const current = currentParticipants(meeting);
1208
+ if (!current) return;
1209
+ const closing = current.openName === displayName;
1210
+ state.participants = { ...current, openName: closing ? null : displayName, panel: closing ? null : { status: 'loading' }, notice: '' };
1211
+ refreshParticipants(meeting);
1212
+ if (!closing) void loadCandidates(meeting, displayName);
1213
+ }
1214
+
1215
+ async function loadCandidates(meeting, displayName) {
1216
+ let panel;
1217
+ try {
1218
+ if (typeof meetingParticipants.candidates !== 'function') throw new Error('候補を読み込めませんでした');
1219
+ const value = await meetingParticipants.candidates({ meeting }, displayName);
1220
+ if (!isRecord(value)) throw new Error('候補を読み込めませんでした');
1221
+ const others = Number(value.other_meetings);
1222
+ panel = {
1223
+ status: 'ready',
1224
+ people: normalizeCandidateList(value.people, 'entity_id'),
1225
+ contacts: normalizeCandidateList(value.contacts, 'contact_id'),
1226
+ companies: normalizeCandidateList(value.companies, 'entity_id'),
1227
+ otherMeetings: Number.isInteger(others) && others > 0 ? others : 0,
1228
+ };
1229
+ } catch (error) {
1230
+ panel = { status: 'error', message: participantError(error, '候補を読み込めませんでした') };
1231
+ }
1232
+ const current = currentParticipants(meeting);
1233
+ if (!current || current.openName !== displayName) return;
1234
+ state.participants = { ...current, panel: { ...panel, name: current.panel?.name ?? displayName } };
1235
+ refreshParticipants(meeting);
1236
+ }
1237
+
1238
+ function candidateRow(name, evidence, actionLabel, onClick, busy) {
1239
+ const item = makeElement(doc, 'li', { className: 'bb-minutes-participant-candidate' });
1240
+ const body = makeElement(doc, 'div');
1241
+ body.append(makeElement(doc, 'strong', { textContent: name }));
1242
+ if (evidence.length) body.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-detail', textContent: evidence.join('・') }));
1243
+ item.append(body, button(doc, actionLabel, onClick, { disabled: busy }));
1244
+ return item;
1245
+ }
1246
+
1247
+ function renderParticipantPanel(meeting, current) {
1248
+ const displayName = current.openName;
1249
+ const panelState = current.panel ?? { status: 'loading' };
1250
+ const busy = panelState.busy === true;
1251
+ const panel = makeElement(doc, 'div', {
1252
+ className: 'bb-minutes-participant-panel',
1253
+ attrs: { role: 'group', 'aria-label': `${displayName}を人物に結ぶ` },
1254
+ });
1255
+ panel.append(makeElement(doc, 'p', { className: 'bb-minutes-participant-panel-title', textContent: `「${displayName}」は誰ですか` }));
1256
+ if (panelState.status === 'loading') {
1257
+ panel.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note', textContent: '候補を読み込んでいます' }));
1258
+ } else if (panelState.status === 'error' && !panelState.people) {
1259
+ panel.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note is-unavailable', textContent: panelState.message, attrs: { role: 'alert' } }));
1260
+ }
1261
+ if (panelState.people) {
1262
+ if (panelState.otherMeetings > 0) {
1263
+ panel.append(makeElement(doc, 'p', {
1264
+ className: 'bb-minutes-participant-others',
1265
+ textContent: `同じ表示名の議事録があと${panelState.otherMeetings}件あります。結ぶとそれらも作り直します。`,
1266
+ }));
1267
+ }
1268
+ const list = makeElement(doc, 'ul', { className: 'bb-minutes-participant-candidates' });
1269
+ for (const person of panelState.people) {
1270
+ list.append(candidateRow(person.name, person.evidence, 'この人', () => void linkParticipant(meeting, displayName, { action: 'person', entity_id: person.id }), busy));
1271
+ }
1272
+ for (const contact of panelState.contacts) {
1273
+ const evidence = contact.evidence.length ? contact.evidence : contact.company ? [`名刺:${contact.company}`] : ['名刺'];
1274
+ list.append(candidateRow(contact.name, evidence, 'この名刺の名前で登録', () => void linkParticipant(meeting, displayName, { action: 'new_person', name: contact.name, contact_id: contact.id }), busy));
1275
+ }
1276
+ if (!panelState.people.length && !panelState.contacts.length) {
1277
+ panel.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note', textContent: '人物と名刺に、それらしい候補はありませんでした。' }));
1278
+ } else {
1279
+ panel.append(list);
1280
+ }
1281
+ const form = makeElement(doc, 'div', { className: 'bb-minutes-participant-new' });
1282
+ const field = labelInput(doc, '新しい人として登録する名前', { value: panelState.name ?? displayName, name: 'participant_name' });
1283
+ field.input.addEventListener('input', () => {
1284
+ const latest = currentParticipants(meeting);
1285
+ if (latest?.panel) latest.panel.name = field.input.value;
1286
+ });
1287
+ form.append(field.wrapper, button(doc, '新しい人として登録', () => void linkParticipant(meeting, displayName, {
1288
+ action: 'new_person',
1289
+ name: text(field.input.value, displayName),
1290
+ }), { disabled: busy, className: 'bb-minutes-button is-primary' }));
1291
+ panel.append(form);
1292
+ const notPerson = makeElement(doc, 'div', { className: 'bb-minutes-participant-not-person' });
1293
+ notPerson.append(makeElement(doc, 'span', { className: 'bb-minutes-connection-label', textContent: '人ではない' }));
1294
+ for (const company of panelState.companies) {
1295
+ notPerson.append(button(doc, `${company.name}(会社)に結ぶ`, () => void linkParticipant(meeting, displayName, { action: 'company', entity_id: company.id }), { disabled: busy }));
1296
+ }
1297
+ notPerson.append(button(doc, '結ばない', () => void linkParticipant(meeting, displayName, { action: 'not_person' }), { disabled: busy }));
1298
+ panel.append(notPerson);
1299
+ if (panelState.message) {
1300
+ panel.append(makeElement(doc, 'p', { className: 'bb-minutes-connection-note is-unavailable', textContent: panelState.message, attrs: { role: 'alert' } }));
1301
+ }
1302
+ if (panelState.existing) {
1303
+ panel.append(candidateRow(panelState.existing.name, ['同じ名前の人がいます'], 'この人', () => void linkParticipant(meeting, displayName, { action: 'person', entity_id: panelState.existing.id }), busy));
1304
+ }
1305
+ }
1306
+ panel.append(button(doc, 'あとで', () => toggleParticipantPanel(meeting, displayName), { className: 'bb-minutes-button is-quiet' }));
1307
+ return panel;
1308
+ }
1309
+
1310
+ function setPanel(meeting, displayName, patch) {
1311
+ const current = currentParticipants(meeting);
1312
+ if (!current || current.openName !== displayName) return false;
1313
+ state.participants = { ...current, panel: { ...current.panel, ...patch } };
1314
+ refreshParticipants(meeting);
1315
+ return true;
1316
+ }
1317
+
1318
+ function redraftNotice(result, done) {
1319
+ const count = Number(result?.redraft?.meetings);
1320
+ return Number.isInteger(count) && count > 0
1321
+ ? `${done}議事録${count}件を作り直します(数分かかります)。`
1322
+ : `${done}`;
1323
+ }
1324
+
1325
+ async function linkParticipant(meeting, displayName, input) {
1326
+ if (typeof meetingParticipants.link !== 'function') return;
1327
+ if (!setPanel(meeting, displayName, { busy: true, message: '', existing: null })) return;
1328
+ try {
1329
+ const result = await meetingParticipants.link({ meeting }, { display_name: displayName, ...input });
1330
+ const linkedName = text(result?.linked?.name);
1331
+ await reloadParticipants(meeting, redraftNotice(result, linkedName ? `「${displayName}」を${linkedName}に結びました。` : `「${displayName}」を記録しました。`));
1332
+ } catch (error) {
1333
+ const existing = isRecord(error?.candidate) && text(error.candidate.entity_id) && text(error.candidate.name)
1334
+ ? { id: text(error.candidate.entity_id), name: text(error.candidate.name) }
1335
+ : null;
1336
+ setPanel(meeting, displayName, {
1337
+ busy: false,
1338
+ existing,
1339
+ message: existing ? '同じ名前の人がいます。この人で良ければ選んでください。' : participantError(error, '結べませんでした。もう一度試してください。'),
1340
+ });
1341
+ }
1342
+ }
1343
+
1344
+ async function unlinkParticipant(meeting, participant) {
1345
+ try {
1346
+ const result = await meetingParticipants.unlink({ meeting }, {
1347
+ display_name: participant.displayName,
1348
+ ...(participant.entityId ? { entity_id: participant.entityId } : {}),
1349
+ });
1350
+ await reloadParticipants(meeting, redraftNotice(result, `「${participant.displayName}」の結び付きを取り消しました。`));
1351
+ } catch (error) {
1352
+ const current = currentParticipants(meeting);
1353
+ if (!current) return;
1354
+ state.participants = { ...current, notice: participantError(error, '取り消せませんでした。') };
1355
+ refreshParticipants(meeting);
1356
+ }
1357
+ }
1358
+
1055
1359
  function appendLoadError(parent, message, retry) {
1056
1360
  const notice = makeElement(doc, 'div', {
1057
1361
  className: 'bb-minutes-load-error',
@@ -1448,6 +1752,7 @@ export function createMeetingMinutesUI({
1448
1752
  function renderDetail(detail, documentRecord = null, selectedVersion = null) {
1449
1753
  clear(detailPane);
1450
1754
  connectionsSlot = null;
1755
+ participantsSlot = null;
1451
1756
  if (state.detailError) {
1452
1757
  const retry = state.meetingId === null ? loadMeetings : () => loadDetail(state.meetingId);
1453
1758
  appendLoadError(detailPane, state.detailError.startsWith('会議詳細を読み込めませんでした。')
@@ -1480,6 +1785,15 @@ export function createMeetingMinutesUI({
1480
1785
  textContent: participants ? `${when} / ${participants}` : when,
1481
1786
  }));
1482
1787
  detailPane.append(header);
1788
+ participantsSlot = null;
1789
+ if (hasParticipants()) {
1790
+ participantsSlot = makeElement(doc, 'section', {
1791
+ className: 'bb-minutes-participants',
1792
+ attrs: { 'aria-label': '参加者' },
1793
+ });
1794
+ fillParticipants(participantsSlot, meeting);
1795
+ detailPane.append(participantsSlot);
1796
+ }
1483
1797
  if (hasConnections()) {
1484
1798
  connectionsSlot = makeElement(doc, 'section', {
1485
1799
  className: 'bb-minutes-connections',
@@ -1631,9 +1945,11 @@ export function createMeetingMinutesUI({
1631
1945
  state.detail = detail;
1632
1946
  state.detailError = null;
1633
1947
  if (hasConnections()) state.connections = { meetingId, rows: null };
1948
+ if (hasParticipants() && state.participants?.meetingId !== meetingId) state.participants = null;
1634
1949
  renderDetail(detail);
1635
1950
  setStatus('会議詳細を読み込みました。');
1636
1951
  if (hasConnections()) void loadConnections(detail, isCurrentRequest);
1952
+ if (hasParticipants()) void loadParticipants(detail, isCurrentRequest);
1637
1953
  } catch (error) {
1638
1954
  if (!isCurrentRequest()) return state.detail;
1639
1955
  const preserved = preserveLoadedStateOnFailure(error) && state.detail?.meeting?.meeting_id === meetingId
@@ -260,6 +260,18 @@ function definitionForSave(definition) {
260
260
 
261
261
  function normalizeReadiness(value) {
262
262
  const object = objectValue(value);
263
+ const explicitState = normalizeState(firstValue(object, 'state'));
264
+ if (['permission_denied', 'api_unavailable', 'missing', 'conflict', 'error_retryable'].includes(explicitState)) {
265
+ return {
266
+ state: explicitState,
267
+ ready: null,
268
+ issues: null,
269
+ code: nonEmptyText(firstValue(object, 'code')) ?? explicitState,
270
+ currentRevision: nonEmptyText(firstValue(object, 'currentRevision', 'current_revision')),
271
+ message: nonEmptyText(firstValue(object, 'message')) ?? STATUS_LABELS[explicitState],
272
+ error: firstValue(object, 'error'),
273
+ };
274
+ }
263
275
  if (!object || typeof object.ready !== 'boolean') return { state: 'unknown', ready: null, issues: null };
264
276
  const issues = asArray(object.issues);
265
277
  const messageValue = firstValue(object, 'message', 'reason');
@@ -708,11 +720,17 @@ function renderCorrectionGuidance(guidance) {
708
720
  return section;
709
721
  }
710
722
 
711
- function renderReadiness(root, readiness) {
723
+ function renderReadiness(root, readiness, callbacks = {}) {
712
724
  const section = makeElement('section', { className: 'objective-editor-readiness', attrs: { 'aria-label': '判断利用可能性' } });
713
725
  const normalized = normalizeReadiness(readiness);
714
726
  section.append(makeElement('div', { className: 'objective-editor-subheading', text: '判断への利用' }));
715
727
  if (normalized.state !== 'ready') {
728
+ if (normalized.state !== 'unknown') {
729
+ section.append(statusNotice(normalized, {
730
+ onRetry: ['error_retryable', 'api_unavailable'].includes(normalized.state) ? callbacks.onRetryReadiness : null,
731
+ }));
732
+ return section;
733
+ }
716
734
  section.append(statusBadge('judgment_unknown'), makeElement('p', { className: 'objective-editor-muted', text: '判断に使える状態かどうかを、保存先から確認できていません。' }));
717
735
  return section;
718
736
  }
@@ -897,7 +915,7 @@ function buildObjectiveForm(state, callbacks, { workspace = false } = {}) {
897
915
  onAddCriterion: () => { captureDraft(); callbacks.onAddCriterion?.(); },
898
916
  onRemoveCriterion: (index) => { captureDraft(); callbacks.onRemoveCriterion?.(index); },
899
917
  }));
900
- form.append(renderReadiness(form, state.readiness));
918
+ form.append(renderReadiness(form, state.readiness, callbacks));
901
919
  form.append(renderConstraintRefs(form, state, callbacks));
902
920
  const actions = makeElement('div', { className: 'objective-editor-actions' });
903
921
  actions.append(makeElement('span', { className: 'objective-editor-muted', text: callbacks.canEdit ? '保存後に同じID・新版を再取得して確認します。' : '読み取り専用です。書込み権限はホストから提供してください。' }));
@@ -1199,9 +1217,16 @@ function constraintContent(doc, state, callbacks) {
1199
1217
  return content;
1200
1218
  }
1201
1219
 
1202
- function readinessContent(doc, readiness) {
1220
+ function readinessContent(doc, readiness, callbacks = {}) {
1203
1221
  const normalized = normalizeReadiness(readiness);
1204
1222
  if (normalized.state !== 'ready') {
1223
+ if (normalized.state !== 'unknown') {
1224
+ return [workspaceStatusNotice(doc, normalized, {
1225
+ label: '判断',
1226
+ onRetry: ['error_retryable', 'api_unavailable'].includes(normalized.state) ? callbacks.onRetryReadiness : null,
1227
+ retryLabel: '再読込',
1228
+ })];
1229
+ }
1205
1230
  return [statusBadge('judgment_unknown'), makeElement('p', { text: '判断に使える状態かどうかを、保存先から確認できていません。' })];
1206
1231
  }
1207
1232
  if (normalized.ready) {
@@ -1242,7 +1267,7 @@ function renderObjectiveDetail(doc, state, callbacks) {
1242
1267
  }));
1243
1268
  parts.push(workspaceRailBlock(doc, { title: `評価基準${objective.criteria ? ` ${objective.criteria.length}件` : ''}`, className: 'bb-objective-criteria-block', content: criteriaContent(doc, objective.criteria) }));
1244
1269
  parts.push(workspaceRailBlock(doc, { title: '制約の参照', className: 'bb-objective-constraints-block', content: constraintContent(doc, state, callbacks) }));
1245
- parts.push(workspaceRailBlock(doc, { title: '判断に使えるか', className: 'bb-objective-readiness-block', content: readinessContent(doc, state.readiness) }));
1270
+ parts.push(workspaceRailBlock(doc, { title: '判断に使えるか', className: 'bb-objective-readiness-block', content: readinessContent(doc, state.readiness, callbacks) }));
1246
1271
  if (callbacks.canEdit) {
1247
1272
  parts.push(makeElement('p', { className: 'objective-editor-muted bb-objective-revision-note', text: '直して保存すると同じ目的の新しい版を作ります。保存結果を読み戻して確認し、確認できない場合はその状態を表示します。' }));
1248
1273
  parts.push(workspaceActions(doc, [{ text: '目的を直す', variant: 'primary', onClick: callbacks.onEdit }]));
@@ -1416,6 +1441,7 @@ export function createObjectiveEditorController(options = {}) {
1416
1441
  onAddConstraintRef: addConstraintRef,
1417
1442
  onRemoveConstraintRef: removeConstraintRef,
1418
1443
  onReloadConstraints: () => loadConstraintRefs(state.selected),
1444
+ onRetryReadiness: () => loadReadiness(state.selected),
1419
1445
  onLoadStoryLinks: loadStoryLinks,
1420
1446
  onRetryStory: () => loadStoryLinks(state.story.storyId),
1421
1447
  onEdit: beginEdit,
@@ -1513,24 +1539,34 @@ export function createObjectiveEditorController(options = {}) {
1513
1539
  const request = ++readinessRequest;
1514
1540
  const current = () => version === selectionVersion && request === readinessRequest
1515
1541
  && state.selected?.id === objective.id && state.selected?.revision === objective.revision;
1542
+ const apply = (readiness) => {
1543
+ if (!current()) return false;
1544
+ state.readiness = readiness;
1545
+ if (state.selected) state.selected.readiness = readiness;
1546
+ if (Array.isArray(state.objectives.records)) {
1547
+ state.objectives.records = state.objectives.records.map((record) => (
1548
+ record?.id === objective.id && record?.revision === objective.revision
1549
+ ? { ...record, readiness }
1550
+ : record
1551
+ ));
1552
+ }
1553
+ render();
1554
+ return true;
1555
+ };
1516
1556
  const method = portMethod(port, 'checkObjectiveReadiness');
1517
1557
  if (!method) {
1518
- state.readiness = { state: 'unknown', ready: null, issues: null, message: '判断に使える状態を確かめる経路がありません。' };
1519
- render();
1558
+ apply({ state: 'unknown', ready: null, issues: null, message: '判断に使える状態を確かめる経路がありません。' });
1520
1559
  return state.readiness;
1521
1560
  }
1522
1561
  try {
1523
1562
  const payload = await method(objective.id, context, objective.revision);
1524
- if (!current()) return state.readiness;
1525
- state.readiness = normalizeReadiness(payload);
1563
+ if (!apply(normalizeReadiness(payload))) return state.readiness;
1526
1564
  } catch (error) {
1527
1565
  if (!current()) return state.readiness;
1528
1566
  const normalized = normalizeObjectiveError(error);
1529
1567
  state.permissionDenied ||= normalized.state === 'permission_denied';
1530
- state.readiness = { state: normalized.state, ready: null, issues: null, ...normalized };
1568
+ if (!apply({ state: normalized.state, ready: null, issues: null, ...normalized })) return state.readiness;
1531
1569
  }
1532
- if (state.selected) state.selected.readiness = state.readiness;
1533
- render();
1534
1570
  return state.readiness;
1535
1571
  }
1536
1572
 
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import { THREE, MapControls } from './world-vendor.js';
23
- import { DISTRICT_STREETS, districtGroundPlan, districtStage, districtStreetLots, skyAt } from './world-placement.js';
23
+ import { DISTRICT_STREETS, districtTaskActors, districtGroundPlan, districtStage, districtStreetLots, skyAt } from './world-placement.js';
24
24
  import { canvasTexture, drawFacade, FACADE_SIZES, FACADE_UNITS, gableRoof, hashUnit, paintVertices, roundedPlate, valueNoise } from './world-scenery.js';
25
25
  import { makeWorkspaceElement as el } from '../../workspace-kit.js';
26
26
  import { attentionSignText } from './world-exits.js';
@@ -46,7 +46,7 @@ export const DISTRICT_LEGEND = Object.freeze([
46
46
  ['is-stakes', '杭と縄の空き地=未着手(記録上)'],
47
47
  ['is-house', '明かりのついた家=完了して、出典・成果の記録がある(本設の建物)'],
48
48
  ['is-prefab', 'プレハブ=完了したが、出典・成果の記録が無い'],
49
- ['is-worker', 'ヘルメットの人=担当欄の担当'],
49
+ ['is-worker', 'ヘルメットの人=記録された担当。緑はAI担当、動きは進行中Taskと現在の稼働証跡が一致したときだけ'],
50
50
  ['is-empty', '誰もいない現場=担当の記録なし'],
51
51
  ['is-outside', '柵の外の人(破線の輪)=本文にだけ名前がある'],
52
52
  ['is-nopath', '通りへの道が無い=出典リンクなし'],
@@ -397,6 +397,7 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
397
397
  }
398
398
 
399
399
  // --- people (records: worker, person named in the text; atmosphere: walkers) ---------------------
400
+ const taskActors = [];
400
401
  function person(clothes, { helmet = false } = {}) {
401
402
  const group = new THREE.Group();
402
403
  group.add(mesh(new THREE.CapsuleGeometry(0.2, 0.46, 4, 10), material(clothes), { y: 0.5 }));
@@ -527,6 +528,12 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
527
528
  worker.position.set(0.55, 0.06, 0.55);
528
529
  group.add(worker);
529
530
  }
531
+ districtTaskActors(site).forEach((actor, index) => {
532
+ const worker = person(0x2f9c8b, { helmet: true });
533
+ worker.position.set(-0.5 + index * 0.55, 0.06, 0.35);
534
+ group.add(worker);
535
+ taskActors.push({ worker, x: worker.position.x, moving: actor.moving, heartbeatAt: actor.activity?.heartbeat_at, phase: hash(actor.id + site.task_id) });
536
+ });
530
537
  if (named && status !== 'completed') {
531
538
  const outside = person(0x6b5b95);
532
539
  outside.position.set(-0.95, 0, half + 0.65);
@@ -682,6 +689,7 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
682
689
  scene.add(root);
683
690
  root.add(selectionRing);
684
691
  const readable = work?.status === 'ok' && (work.reads.tasks.state === 'complete' || work.reads.tasks.state === 'partial');
692
+ taskActors.length = 0;
685
693
  const shown = readable ? work.sites.filter((site) => site.work.status !== 'cancelled') : [];
686
694
  const layout = districtStreetLots(shown.map((site) => ({ task_id: site.task_id, created_at: site.work.created_at, purpose_label: site.purpose_label })));
687
695
  const L = layout.length;
@@ -850,7 +858,8 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
850
858
  root.add(group);
851
859
  lots.set(site.task_id, group);
852
860
  const title = site.title.length > 15 ? `${site.title.slice(0, 14)}…` : site.title;
853
- addLabel(title, new THREE.Vector3(lot.x, 2.6, lot.z), `is-site${site.gaps.length ? ' is-check' : ''}`, { owner: group, near: 15, priority: 6 });
861
+ const actorNames = districtTaskActors(site).map(actor => actor.name).join('・');
862
+ addLabel(actorNames ? `${title} / ${actorNames}` : title, new THREE.Vector3(lot.x, 2.6, lot.z), `is-site${site.gaps.length ? ' is-check' : ''}`, { owner: group, near: 15, priority: 6 });
854
863
  }
855
864
  // Atmosphere: walkers on the sidewalks and back streets, smoke from chimneys (no record behind them).
856
865
  const routes = [-1.55, 1.55, -DISTRICT_STREETS.backStreet + 0.6, DISTRICT_STREETS.backStreet - 0.6];
@@ -1074,6 +1083,12 @@ export function createDistrictView({ doc, stage, reducedMotion = false, focusSel
1074
1083
  }
1075
1084
  if (!reducedMotion) {
1076
1085
  const seconds = time / 1000;
1086
+ for (const actor of taskActors) {
1087
+ const age = Date.now() - Date.parse(actor.heartbeatAt ?? '');
1088
+ if (!actor.moving || !Number.isFinite(age) || age < 0 || age > 300000) continue;
1089
+ actor.worker.position.set(actor.x + Math.sin(seconds * 1.4 + actor.phase) * 0.12, 0.06 + Math.abs(Math.sin(seconds * 3 + actor.phase)) * 0.025, 0.35);
1090
+ actor.worker.rotation.y = Math.sin(seconds * 1.4 + actor.phase) * 0.3;
1091
+ }
1077
1092
  for (const walker of walkers) {
1078
1093
  const { x, from, to, speed, phase } = walker.userData;
1079
1094
  const span = to - from;
@@ -313,3 +313,16 @@ export function districtStage(sites) {
313
313
  next: following ? { label: following.label, needed: following.min - permanent } : null,
314
314
  };
315
315
  }
316
+
317
+ /** Optional host-supplied actors. Movement needs current host-confirmed runtime evidence and an in-progress Task. */
318
+ export function districtTaskActors(site, now = Date.now()) {
319
+ if (site?.actors_state !== 'confirmed' || !Array.isArray(site.actors)) return [];
320
+ const seen = new Set();
321
+ return site.actors.filter(actor => {
322
+ if (!actor || typeof actor.id !== 'string' || !actor.id || typeof actor.name !== 'string' || !actor.name || actor.task_id !== site.task_id || actor.state !== site.work?.status || seen.has(actor.id)) return false;
323
+ seen.add(actor.id); return true;
324
+ }).map(actor => {
325
+ const age = now - Date.parse(actor.activity?.heartbeat_at ?? '');
326
+ return { ...actor, moving: site.work.status === 'in_progress' && actor.activity?.moving === true && actor.activity?.state === 'running' && Number.isFinite(age) && age >= 0 && age <= 300000 };
327
+ });
328
+ }
@@ -8,6 +8,8 @@
8
8
  * different marks. Nothing here writes; corrections go to the host's own task screen.
9
9
  */
10
10
 
11
+ import { districtTaskActors } from './world-placement.js';
12
+
11
13
  import {
12
14
  makeWorkspaceElement as el,
13
15
  workspaceRailBlock,
@@ -417,10 +419,14 @@ export function workSiteBlocks(doc, { business, site, readAt, taskHref }) {
417
419
  const how = person.link === 'recorded' ? '担当欄' : `${person.stated_as_assignee ? '本文が担当として記載' : '本文に記載'}(${person.basis === 'text_person_id' ? '人物IDつき' : '氏名の一致'})・担当欄には未接続`;
418
420
  people.append(linkLine(doc, person.link, person.name, how));
419
421
  }
422
+ for (const actor of districtTaskActors(site)) {
423
+ people.append(linkLine(doc, 'recorded', actor.name, `${actor.kind === 'agent' ? 'AI担当' : '担当'}・Taskの記録上の状態: ${site.work.label ?? site.work.status}・実行: ${actor.moving ? '稼働証跡確認済み' : '未確認または停止'}`));
424
+ }
425
+ if (site.actors_state === 'unconfirmed') people.append(linkLine(doc, 'unreadable', 'AI担当未確認', '現在の登録とTask委任を確認できませんでした'));
420
426
  for (const mention of site.ambiguous_mentions ?? []) {
421
427
  people.append(linkLine(doc, 'inferred', `「${mention.text}」`, `組織のGraphの${mention.candidates.map((candidate) => candidate.name).join('・')}のどれか一意に決まりません`));
422
428
  }
423
- blocks.push(workspaceRailBlock(doc, { title: '担当・関係者', content: site.people.length || site.ambiguous_mentions?.length ? people : { text: '担当欄も本文も、登録された人物を挙げていません。' } }));
429
+ blocks.push(workspaceRailBlock(doc, { title: '担当・関係者', content: site.people.length || site.ambiguous_mentions?.length || districtTaskActors(site).length || site.actors_state === 'unconfirmed' ? people : { text: '担当欄も本文も、登録された人物を挙げていません。' } }));
424
430
  const refs = el(doc, 'ul', { className: 'bb-world-rail-list' });
425
431
  for (const ref of site.source_refs) {
426
432
  const li = el(doc, 'li');