@undefineds.co/linx 0.3.19 → 0.3.22

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 (75) hide show
  1. package/dist/generated/version.js +1 -1
  2. package/dist/index.js +6 -1
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/auto-mode/pod-persistence.js +51 -3
  5. package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
  6. package/dist/lib/auto-mode/secretary.js +2 -2
  7. package/dist/lib/auto-mode/secretary.js.map +1 -1
  8. package/dist/lib/chat-api.js +23 -61
  9. package/dist/lib/chat-api.js.map +1 -1
  10. package/dist/lib/codex-plugin/index.js +1 -0
  11. package/dist/lib/codex-plugin/index.js.map +1 -1
  12. package/dist/lib/codex-plugin/symphony-mcp.js +335 -0
  13. package/dist/lib/codex-plugin/symphony-mcp.js.map +1 -0
  14. package/dist/lib/models.js +3 -2
  15. package/dist/lib/models.js.map +1 -1
  16. package/dist/lib/pi-adapter/auth.js +68 -0
  17. package/dist/lib/pi-adapter/auth.js.map +1 -0
  18. package/dist/lib/pi-adapter/interactive.js +12 -17
  19. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  20. package/dist/lib/pi-adapter/pod-mirror.js +34 -5
  21. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  22. package/dist/lib/pi-adapter/pod-tools.js +140 -0
  23. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  24. package/dist/lib/pi-adapter/session.js +13 -17
  25. package/dist/lib/pi-adapter/session.js.map +1 -1
  26. package/dist/lib/pi-adapter/stream.js +2 -20
  27. package/dist/lib/pi-adapter/stream.js.map +1 -1
  28. package/dist/lib/pod-chat-store.js +53 -4
  29. package/dist/lib/pod-chat-store.js.map +1 -1
  30. package/dist/lib/resource-identity.js +2 -0
  31. package/dist/lib/resource-identity.js.map +1 -0
  32. package/dist/lib/symphony/archive.js +15 -37
  33. package/dist/lib/symphony/archive.js.map +1 -1
  34. package/dist/lib/symphony/pod-projection.js +188 -1347
  35. package/dist/lib/symphony/pod-projection.js.map +1 -1
  36. package/dist/lib/symphony-command.js +208 -108
  37. package/dist/lib/symphony-command.js.map +1 -1
  38. package/dist/plugins/linx-symphony-codex/.codex-plugin/plugin.json +38 -0
  39. package/dist/plugins/linx-symphony-codex/.mcp.json +10 -0
  40. package/dist/plugins/linx-symphony-codex/README.md +9 -0
  41. package/dist/plugins/linx-symphony-codex/hooks.json +60 -0
  42. package/dist/plugins/linx-symphony-codex/scripts/symphony-hook-events.mjs +119 -0
  43. package/dist/plugins/linx-symphony-codex/scripts/symphony-mcp.mjs +335 -0
  44. package/dist/plugins/linx-symphony-codex/skills/symphony/SKILL.md +791 -0
  45. package/dist/skills/symphony/SKILL.md +15 -4
  46. package/package.json +4 -4
  47. package/vendor/agent-runtime/dist/chat-reconciler.d.ts +33 -0
  48. package/vendor/agent-runtime/dist/chat-reconciler.js +108 -0
  49. package/vendor/agent-runtime/dist/index.d.ts +4 -0
  50. package/vendor/agent-runtime/dist/index.js +4 -0
  51. package/vendor/agent-runtime/dist/matrix-client.d.ts +149 -0
  52. package/vendor/agent-runtime/dist/matrix-client.js +220 -0
  53. package/vendor/agent-runtime/dist/pod-resource-identity.d.ts +17 -0
  54. package/vendor/agent-runtime/dist/pod-resource-identity.js +54 -0
  55. package/vendor/agent-runtime/dist/reconciler.js +2 -2
  56. package/vendor/agent-runtime/dist/symphony.d.ts +273 -28
  57. package/vendor/agent-runtime/dist/symphony.js +1267 -20
  58. package/vendor/agent-runtime/dist/workspace.d.ts +61 -0
  59. package/vendor/agent-runtime/dist/workspace.js +81 -0
  60. package/vendor/agent-runtime/package.json +5 -1
  61. package/vendor/stores/dist/current-pod-base.d.ts +2 -0
  62. package/vendor/stores/dist/current-pod-base.js +14 -0
  63. package/vendor/stores/dist/exact-records.d.ts +7 -0
  64. package/vendor/stores/dist/exact-records.js +87 -0
  65. package/vendor/stores/dist/index.d.ts +1 -0
  66. package/vendor/stores/dist/index.js +1 -0
  67. package/vendor/stores/dist/login.d.ts +51 -0
  68. package/vendor/stores/dist/login.js +195 -0
  69. package/vendor/stores/dist/pod-collection.d.ts +28 -0
  70. package/vendor/stores/dist/pod-collection.js +194 -0
  71. package/vendor/stores/dist/pod-write-guard.d.ts +5 -0
  72. package/vendor/stores/dist/pod-write-guard.js +133 -0
  73. package/vendor/stores/dist/symphony-control.d.ts +245 -0
  74. package/vendor/stores/dist/symphony-control.js +2175 -0
  75. package/vendor/stores/package.json +14 -0
@@ -5,10 +5,10 @@ import { getSymphonyArchiveKey } from '../../../vendor/agent-runtime/dist/sympho
5
5
  import { DEFAULT_AGENT_RUNTIME_COMPANION_MODEL_ID } from '../../../vendor/agent-runtime/dist/companion-model.js';
6
6
  import { decideThreadControlEvent } from '../../../vendor/agent-runtime/dist/thread-reconciler-controller.js';
7
7
  import { createLinxPodSyncScope } from '../../../vendor/agent-runtime/dist/sync.js';
8
- import { insertExactRecordOnce, resolvePodResourceTemplateValue, upsertExactRecord, } from '@undefineds.co/drizzle-solid';
8
+ import { buildSymphonyChatRow as buildSharedSymphonyChatRow, buildSymphonyContactRows as buildSharedSymphonyContactRows, buildSymphonyControlRows, buildSymphonyDeliveryRow as buildSharedSymphonyDeliveryRow, buildSymphonyIssueRow as buildSharedSymphonyIssueRow, buildSymphonyMessageIri as buildSharedSymphonyMessageIri, buildSymphonyRunRow as buildSharedSymphonyRunRow, buildSymphonyRunStepRow as buildSharedSymphonyRunStepRow, buildSymphonyRuntimeRunStepRow as buildSharedSymphonyRuntimeRunStepRow, buildSymphonySessionRow as buildSharedSymphonySessionRow, buildSymphonyStatusMessageRow as buildSharedSymphonyStatusMessageRow, buildSymphonyTaskRow as buildSharedSymphonyTaskRow, buildSymphonyThreadRow as buildSharedSymphonyThreadRow, listOpenSymphonyIssuesFromControlState, listRecentSymphonyReportsFromControlState, listRunningSymphonyWorkersFromControlState, } from '../../../vendor/stores/dist/symphony-control.js';
9
+ import { insertExactRecordOnce, upsertExactRecord, } from '@undefineds.co/drizzle-solid';
9
10
  import { getDefaultPodDataSession } from '../pod-data-session.js';
10
- import { ContactClass, ContactType, EvidenceKind, chatRepository, agentResource, contactResource, deliveryResource, evidenceResource, ideaResource, issueResource, messageResource, reportResource, runResource, runStepResource, sessionResource, taskResource, threadRepository, } from '../models.js';
11
- import { pathToWorkspaceUri } from '../pi-adapter/pod-mirror-mapping.js';
11
+ import { chatRepository, agentResource, contactResource, deliveryResource, ideaResource, issueResource, messageResource, runResource, runStepResource, sessionResource, taskResource, threadRepository, } from '../models.js';
12
12
  import { getSymphonyHome } from './archive.js';
13
13
  const SYMPHONY_CHAT_ID = 'symphony';
14
14
  const SYMPHONY_SECRETARY_AGENT_ID = '__secretary__';
@@ -16,240 +16,6 @@ const SYMPHONY_CONTACT_ID = 'symphony';
16
16
  const SYMPHONY_POLICY_VERSION = 'linx-symphony-session/v1';
17
17
  const SYMPHONY_WORKER_POD_ACCESS_POLICY_VERSION = 'linx-symphony-worker-pod-access/v1';
18
18
  const SYMPHONY_ARCHIVE_PROVENANCE_VERSION = 'linx-symphony-archive/v1';
19
- function ensureTrailingSlash(value) {
20
- return value.endsWith('/') ? value : `${value}/`;
21
- }
22
- function podBaseUrlFromWebId(webId) {
23
- const marker = '/profile/card#me';
24
- if (webId.includes(marker)) {
25
- return `${webId.slice(0, webId.indexOf(marker) + 1)}`;
26
- }
27
- const url = new URL(webId);
28
- return `${url.origin}/`;
29
- }
30
- function podFileUrlFromWebId(webId, path) {
31
- return new URL(path.replace(/^\/+/, ''), podBaseUrlFromWebId(webId)).toString();
32
- }
33
- function podFileUrl(podSession, path) {
34
- return new URL(path.replace(/^\/+/, ''), ensureTrailingSlash(podSession.podUrl)).toString();
35
- }
36
- async function writePodFileToSession(session, file) {
37
- const url = podFileUrl(session, file.path);
38
- await ensurePodResourceContainers(session.fetch, url);
39
- const response = await session.fetch(url, {
40
- method: 'PUT',
41
- headers: { 'Content-Type': file.contentType },
42
- body: file.content.endsWith('\n') ? file.content : `${file.content}\n`,
43
- });
44
- if (!response.ok) {
45
- const details = await response.text().catch(() => '');
46
- const suffix = details.trim() ? ` - ${details.trim().slice(0, 500)}` : '';
47
- throw new Error(`Failed to write Symphony Pod file ${url}: ${response.status} ${response.statusText}${suffix}`);
48
- }
49
- }
50
- async function ensurePodResourceContainers(fetcher, resourceUrl) {
51
- for (const containerUrl of containerUrlsForResource(resourceUrl)) {
52
- const existing = await fetcher(containerUrl, { method: 'HEAD' }).catch(() => null);
53
- if (existing?.ok)
54
- continue;
55
- if (existing && existing.status !== 404 && existing.status !== 405)
56
- continue;
57
- const response = await fetcher(containerUrl, {
58
- method: 'PUT',
59
- headers: {
60
- Link: '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
61
- 'Content-Type': 'text/turtle; charset=utf-8',
62
- },
63
- body: '',
64
- });
65
- if (!response.ok && response.status !== 409) {
66
- throw new Error(`Failed to ensure Symphony Pod container ${containerUrl}: ${response.status} ${response.statusText}`);
67
- }
68
- }
69
- }
70
- function containerUrlsForResource(resourceUrl) {
71
- const url = new URL(resourceUrl);
72
- const parts = url.pathname.split('/').filter(Boolean);
73
- const containers = [];
74
- let path = '/';
75
- for (let index = 0; index < parts.length - 1; index += 1) {
76
- path += `${parts[index]}/`;
77
- containers.push(new URL(path, url.origin).toString());
78
- }
79
- return containers;
80
- }
81
- function datePathParts(input) {
82
- const date = safeDate(input);
83
- return {
84
- yyyy: String(date.getUTCFullYear()),
85
- MM: String(date.getUTCMonth() + 1).padStart(2, '0'),
86
- dd: String(date.getUTCDate()).padStart(2, '0'),
87
- };
88
- }
89
- function buildSymphonyIssueDocumentPath(issue) {
90
- return `/.data/issues/${buildSymphonyIssueId(issue)}/issue.md`;
91
- }
92
- function buildSymphonyIdeaDocumentPath(idea) {
93
- const { yyyy, MM, dd } = datePathParts(idea.createdAt);
94
- return `/.data/ideas/${yyyy}/${MM}/${dd}/${getSymphonyArchiveKey(idea.uri)}/idea.md`;
95
- }
96
- function buildSymphonyReportDocumentPath(worker) {
97
- const { yyyy, MM, dd } = datePathParts(worker.session.completedAt ?? worker.session.updatedAt);
98
- return `/.data/reports/${yyyy}/${MM}/${dd}/${getSymphonyArchiveKey(worker.session.uri)}-report.md`;
99
- }
100
- function buildSymphonyEvidenceDocumentPath(worker, stage) {
101
- const { yyyy, MM, dd } = datePathParts(worker.session.completedAt ?? worker.session.updatedAt);
102
- return `/.data/evidence/${yyyy}/${MM}/${dd}/${getSymphonyArchiveKey(worker.session.uri)}-${stage}-evidence.md`;
103
- }
104
- function renderMarkdownList(items) {
105
- const values = (items ?? []).map((item) => item.trim()).filter(Boolean);
106
- return values.length > 0 ? values.map((item) => `- ${item}`).join('\n') : '- None recorded.';
107
- }
108
- function renderSymphonyIssueMarkdown(plan) {
109
- const issue = plan.issue;
110
- const sections = [
111
- `# ${issue.title || buildSymphonyIssueId(issue)}`,
112
- '',
113
- '## Summary',
114
- issue.description?.trim() || issue.title || 'No summary recorded.',
115
- '',
116
- '## Status',
117
- `- Status: ${issue.status}`,
118
- `- Priority: ${issue.priority}`,
119
- `- Source: ${issue.source}`,
120
- '',
121
- '## Acceptance Criteria',
122
- renderMarkdownList(plan.workers.flatMap((worker) => worker.taskRecord.acceptanceCriteria ?? [])),
123
- '',
124
- '## Tasks',
125
- renderMarkdownList(plan.workers.map((worker) => `${worker.taskRecord.title}: ${worker.taskRecord.objective}`)),
126
- '',
127
- '## Source Context',
128
- `- Chat: ${issue.chat ?? plan.session.chat ?? 'not recorded'}`,
129
- `- Thread: ${issue.thread ?? plan.session.thread ?? 'not recorded'}`,
130
- `- Messages: ${(issue.messages ?? []).join(', ') || 'not recorded'}`,
131
- '',
132
- '## Control Records',
133
- `- Issue: ${issue.uri}`,
134
- ...plan.workers.flatMap((worker) => [
135
- `- Task: ${worker.task}`,
136
- `- Delivery: ${worker.delivery.uri}`,
137
- `- Session: ${worker.session.uri}`,
138
- ]),
139
- ];
140
- return sections.join('\n');
141
- }
142
- function renderSymphonyIdeaMarkdown(idea) {
143
- return [
144
- `# ${idea.summary || getSymphonyArchiveKey(idea.uri)}`,
145
- '',
146
- '## Input',
147
- idea.input?.trim() || idea.summary || 'No input recorded.',
148
- '',
149
- '## Current Understanding',
150
- idea.currentUnderstanding?.trim() || 'No current understanding recorded.',
151
- '',
152
- '## Open Questions',
153
- renderMarkdownList(idea.openQuestions),
154
- '',
155
- '## Conflicts',
156
- renderMarkdownList(idea.conflicts),
157
- '',
158
- '## Next Step',
159
- idea.nextStep?.trim() || 'No next step recorded.',
160
- '',
161
- '## Source Context',
162
- `- Status: ${idea.status}`,
163
- `- Commitment: ${idea.commitment}`,
164
- `- Chat: ${idea.chat ?? 'not recorded'}`,
165
- `- Thread: ${idea.thread ?? 'not recorded'}`,
166
- `- Messages: ${(idea.messages ?? []).join(', ') || 'not recorded'}`,
167
- `- Idea: ${idea.uri}`,
168
- ].join('\n');
169
- }
170
- function renderSymphonyReportMarkdown(plan, worker, stage) {
171
- const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
172
- const summary = status === 'completed'
173
- ? `${worker.taskRecord.title} completed.`
174
- : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
175
- return [
176
- `# ${worker.taskRecord.title} — ${status}`,
177
- '',
178
- '## Summary',
179
- summary,
180
- '',
181
- '## Outcome',
182
- `- Status: ${status}`,
183
- `- Backend: ${worker.session.backend}`,
184
- `- Agent: ${worker.session.target.agent ?? worker.delivery.targetAgent}`,
185
- `- Auto mode session: ${worker.session.autoModeSessionId ?? 'not recorded'}`,
186
- `- Exit code: ${worker.session.exitCode ?? 'not recorded'}`,
187
- '',
188
- '## Task',
189
- worker.taskRecord.objective,
190
- '',
191
- '## Acceptance Criteria',
192
- renderMarkdownList(worker.taskRecord.acceptanceCriteria),
193
- '',
194
- '## Linked Control Records',
195
- `- Issue: ${buildSymphonyIssueId(plan.issue)}`,
196
- `- Task: ${worker.task}`,
197
- `- Delivery: ${worker.delivery.uri}`,
198
- `- Session: ${worker.session.uri}`,
199
- `- Run status: ${worker.session.status}`,
200
- '',
201
- '## Post-Run Reconciliation',
202
- '- Secretary must review this report and linked Evidence before closing the task.',
203
- '- Classify any follow-up as same_issue_task, new_issue, idea, evidence_only, or ask_user.',
204
- '',
205
- ...(worker.session.error || worker.delivery.error || worker.taskRecord.error ? [
206
- '## Error',
207
- worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? '',
208
- '',
209
- ] : []),
210
- ].join('\n');
211
- }
212
- function renderSymphonyEvidenceMarkdown(plan, webId, worker, stage) {
213
- const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
214
- const summary = status === 'completed'
215
- ? `${worker.taskRecord.title} completed with runtime status ${worker.session.status}.`
216
- : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? 'worker did not complete successfully.'}`;
217
- const run = buildSymphonyRunIri(webId, worker);
218
- const runStep = buildSymphonyRunStepIri(webId, worker, stage);
219
- return [
220
- `# ${worker.taskRecord.title} — ${status} evidence`,
221
- '',
222
- '## Summary',
223
- summary,
224
- '',
225
- '## Runtime Facts',
226
- `- Backend: ${worker.session.backend}`,
227
- `- Agent: ${worker.session.target.agent ?? worker.delivery.targetAgent}`,
228
- `- Model: ${worker.session.model ?? 'not recorded'}`,
229
- `- Auto mode session: ${worker.session.autoModeSessionId ?? 'not recorded'}`,
230
- `- Exit code: ${worker.session.exitCode ?? 'not recorded'}`,
231
- `- Run status: ${worker.session.status}`,
232
- '',
233
- '## Acceptance Criteria',
234
- renderMarkdownList(worker.taskRecord.acceptanceCriteria),
235
- '',
236
- '## Linked Control Records',
237
- `- Issue: ${buildSymphonyIssueIri(webId, plan.issue)}`,
238
- `- Task: ${buildSymphonyTaskIri(webId, worker.task)}`,
239
- `- Delivery: ${buildSymphonyDeliveryIri(webId, worker)}`,
240
- `- Run: ${run}`,
241
- `- Source RunStep: ${runStep}`,
242
- `- Worker Thread: ${selectWorkerThreadIri(plan, webId, worker)}`,
243
- '',
244
- ...(worker.session.error || worker.delivery.error || worker.taskRecord.error ? [
245
- '## Error',
246
- worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? '',
247
- '',
248
- ] : []),
249
- '## Secretary Follow-Up Review',
250
- 'This Evidence is append-only proof/finding material. Secretary must use it with the Report and RunSteps to decide acceptance and whether follow-up work should be captured.',
251
- ].join('\n');
252
- }
253
19
  function normalizeSymphonyRunPlan(plan) {
254
20
  const workers = Array.isArray(plan.workers) && plan.workers.length > 0
255
21
  ? plan.workers
@@ -326,6 +92,7 @@ function normalizeSymphonyRunPlan(plan) {
326
92
  delivery: primary.delivery,
327
93
  session: primary.session,
328
94
  workers: normalizedWorkers,
95
+ ...(plan.followUpIssues?.length ? { followUpIssues: plan.followUpIssues } : {}),
329
96
  };
330
97
  }
331
98
  async function dynamicImport(specifier) {
@@ -353,15 +120,14 @@ async function createDefaultRuntime() {
353
120
  issueResource: models.issueResource,
354
121
  taskResource: models.taskResource,
355
122
  deliveryResource: models.deliveryResource,
356
- evidenceResource: models.evidenceResource,
357
- reportResource: models.reportResource,
358
123
  runResource: models.runResource,
359
124
  runStepResource: models.runStepResource,
125
+ evidenceResource: models.evidenceResource,
126
+ reportResource: models.reportResource,
360
127
  agentResource: models.agentResource,
361
128
  contactResource: models.contactResource,
362
129
  auditResource: models.auditResource,
363
130
  inboxNotificationResource: models.inboxNotificationResource,
364
- writePodFile: writePodFileToSession,
365
131
  };
366
132
  }
367
133
  function selectTargetChatIri(value, webId, plan) {
@@ -422,6 +188,24 @@ function buildSymphonyIssueId(issue) {
422
188
  function buildSymphonyIssueIri(webId, issue) {
423
189
  return issueResource.buildIri(webId, { id: buildSymphonyIssueId(issue) });
424
190
  }
191
+ function normalizeSymphonyIssueIri(webId, issue) {
192
+ if (/^https?:\/\//u.test(issue)) {
193
+ return issue;
194
+ }
195
+ return buildSymphonyIssueIri(webId, {
196
+ uri: issue,
197
+ title: '',
198
+ status: 'open',
199
+ priority: 'medium',
200
+ source: 'cli',
201
+ issuer: { source: 'system' },
202
+ tasks: [],
203
+ deliveries: [],
204
+ sessions: [],
205
+ createdAt: new Date().toISOString(),
206
+ updatedAt: new Date().toISOString(),
207
+ });
208
+ }
425
209
  function buildSymphonyTaskKey(task) {
426
210
  return getSymphonyArchiveKey(task);
427
211
  }
@@ -448,19 +232,6 @@ function buildSymphonyReportDeliveryIri(webId, worker) {
448
232
  createdAt: safeDate(worker.session.completedAt ?? worker.session.updatedAt),
449
233
  });
450
234
  }
451
- function buildSymphonyReportIri(webId, worker) {
452
- return reportResource.buildIri(webId, {
453
- id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
454
- task: buildSymphonyTaskIri(webId, worker.task),
455
- createdAt: safeDate(worker.session.completedAt ?? worker.session.updatedAt),
456
- });
457
- }
458
- function buildSymphonyEvidenceIri(webId, worker, stage) {
459
- return evidenceResource.buildIri(webId, {
460
- id: `${getSymphonyArchiveKey(worker.session.uri)}-${stage}`,
461
- createdAt: safeDate(worker.session.completedAt ?? worker.session.updatedAt),
462
- });
463
- }
464
235
  function buildSymphonyRunIri(webId, worker) {
465
236
  return runResource.buildIri(webId, {
466
237
  id: getSymphonyArchiveKey(worker.session.uri),
@@ -474,6 +245,12 @@ function buildSymphonyRunStepIri(webId, worker, stage) {
474
245
  run: buildSymphonyRunIri(webId, worker),
475
246
  });
476
247
  }
248
+ function buildSymphonyRuntimeRunStepIri(webId, worker, step) {
249
+ return runStepResource.buildIri(webId, {
250
+ id: getSymphonyArchiveKey(step.uri),
251
+ run: buildSymphonyRunIri(webId, worker),
252
+ });
253
+ }
477
254
  function buildSymphonyWorkerPodAccessPolicy(plan, webId, worker) {
478
255
  return {
479
256
  version: SYMPHONY_WORKER_POD_ACCESS_POLICY_VERSION,
@@ -592,11 +369,11 @@ function resolveSymphonyRuntimeSessionRelation(plan, webId, worker) {
592
369
  return 'runtime-projected-worker-session';
593
370
  }
594
371
  function buildSymphonyWorkspaceMetadata(plan, worker) {
595
- const workspace = normalizeSymphonyWorkspaceRef(worker.session.workspace ?? plan.session.workspace, worker.session.cwd ?? plan.session.cwd);
372
+ const workspace = normalizeWorkerWorkspace(worker.session.workspace ?? plan.session.workspace, worker.session.cwd ?? plan.session.cwd);
596
373
  return {
597
374
  path: workspace.path,
598
375
  kind: workspace.kind,
599
- ...(workspace.workspaceUri ? { uri: workspace.workspaceUri } : {}),
376
+ ...(workspace.container ? { container: workspace.container } : {}),
600
377
  ...(workspace.repository ? { repository: workspace.repository } : {}),
601
378
  ...(workspace.branch ? { branch: workspace.branch } : {}),
602
379
  ...(workspace.worktree ? { worktree: workspace.worktree } : {}),
@@ -609,14 +386,14 @@ function buildSymphonyWorkspaceMetadata(plan, worker) {
609
386
  equivalenceRequires: ['baseRevision', 'checksum-or-etag-or-artifact-uri'],
610
387
  };
611
388
  }
612
- function normalizeSymphonyWorkspaceRef(workspace, fallbackPath) {
389
+ function normalizeWorkerWorkspace(workspace, fallbackPath) {
613
390
  return {
614
391
  path: workspace?.path ?? fallbackPath,
615
392
  kind: workspace?.kind ?? 'folder',
616
393
  ...(workspace?.repository ? { repository: workspace.repository } : {}),
617
394
  ...(workspace?.branch ? { branch: workspace.branch } : {}),
618
395
  ...(workspace?.worktree ? { worktree: workspace.worktree } : {}),
619
- ...(workspace?.workspaceUri ? { workspaceUri: workspace.workspaceUri } : {}),
396
+ ...(workspace?.container ? { container: workspace.container } : {}),
620
397
  ...(workspace?.baseRevision ? { baseRevision: workspace.baseRevision } : {}),
621
398
  ...(workspace?.environment ? { environment: workspace.environment } : {}),
622
399
  };
@@ -719,6 +496,7 @@ function withChatThreadRefs(plan, refs) {
719
496
  thread: refs.thread,
720
497
  messages: refs.messages,
721
498
  },
499
+ ...(worker.runSteps?.length ? { runSteps: worker.runSteps } : {}),
722
500
  }));
723
501
  const primary = workers[0] ?? {
724
502
  task: plan.task,
@@ -753,6 +531,7 @@ function withChatThreadRefs(plan, refs) {
753
531
  delivery: primary.delivery,
754
532
  session: primary.session,
755
533
  workers,
534
+ ...(plan.followUpIssues?.length ? { followUpIssues: plan.followUpIssues } : {}),
756
535
  };
757
536
  }
758
537
  function withTargetRefs(plan, refs, webId) {
@@ -795,6 +574,7 @@ function withTargetRefs(plan, refs, webId) {
795
574
  messages: workerRefs.messages,
796
575
  target,
797
576
  },
577
+ ...(worker.runSteps?.length ? { runSteps: worker.runSteps } : {}),
798
578
  };
799
579
  });
800
580
  const primary = workers[0] ?? {
@@ -830,69 +610,9 @@ function withTargetRefs(plan, refs, webId) {
830
610
  delivery: primary.delivery,
831
611
  session: primary.session,
832
612
  workers,
613
+ ...(plan.followUpIssues?.length ? { followUpIssues: plan.followUpIssues } : {}),
833
614
  };
834
615
  }
835
- function buildSymphonyChatRow(plan, webId, stage, lastPreview) {
836
- const createdAt = safeDate(plan.issue.createdAt);
837
- const updatedAt = safeDate(plan.session.updatedAt);
838
- const secretaryAgent = agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID });
839
- const secretaryContact = buildSecretaryContactIri(webId);
840
- const workerMembers = plan.workers.map((worker) => ({
841
- contact: buildWorkerContactIri(webId, worker),
842
- agent: buildWorkerAgentIri(webId, worker),
843
- label: worker.session.target.label ?? worker.session.target.contact ?? worker.session.target.agent ?? backendDisplayName(worker.session.backend),
844
- }));
845
- const participants = Array.from(new Set([webId, secretaryContact, ...workerMembers.map((member) => member.contact)]));
846
- const targetChat = selectTargetChatIri(plan.session.target?.chat, webId, plan);
847
- return {
848
- id: buildTargetChatId(plan, webId),
849
- title: plan.session.target?.label ?? (targetChat === buildSymphonyChatUri(webId) ? 'AI Secretary · Symphony' : 'Symphony Delegation'),
850
- participants,
851
- metadata: {
852
- kind: targetChat === buildSymphonyChatUri(webId) ? 'symphony-control-room' : 'symphony-target-room',
853
- surface: 'symphony',
854
- secretaryAgent,
855
- secretaryContact,
856
- currentBackend: plan.session.backend,
857
- target: plan.session.target,
858
- currentStage: stage,
859
- memberRoles: Object.fromEntries([
860
- [webId, 'owner'],
861
- [secretaryContact, 'admin'],
862
- ...workerMembers.map((member) => [member.contact, 'member']),
863
- ]),
864
- members: [
865
- { uri: webId, role: 'user', label: 'User' },
866
- { uri: secretaryContact, agent: secretaryAgent, role: 'secretary', label: 'AI Secretary' },
867
- ...workerMembers.map((member) => ({
868
- uri: member.contact,
869
- agent: member.agent,
870
- role: 'worker',
871
- label: member.label,
872
- })),
873
- ],
874
- },
875
- lastActiveAt: updatedAt,
876
- lastMessagePreview: lastPreview ? normalizeTitle(lastPreview, 100) : undefined,
877
- createdAt,
878
- updatedAt,
879
- };
880
- }
881
- function collectSymphonyThreadProjectionGroups(plan, webId) {
882
- const groups = new Map();
883
- for (const worker of plan.workers) {
884
- const chat = selectWorkerChatIri(plan, webId, worker);
885
- const thread = selectWorkerThreadIri(plan, webId, worker);
886
- const key = `${chat}\0${thread}`;
887
- const existing = groups.get(key);
888
- if (existing) {
889
- existing.workers.push(worker);
890
- continue;
891
- }
892
- groups.set(key, { chat, thread, workers: [worker] });
893
- }
894
- return Array.from(groups.values());
895
- }
896
616
  function buildSymphonyWorkerSummary(plan, webId, worker) {
897
617
  return {
898
618
  task: worker.task,
@@ -905,9 +625,6 @@ function buildSymphonyWorkerSummary(plan, webId, worker) {
905
625
  sessionResource: buildSymphonyWorkerSessionUri(webId, worker),
906
626
  backend: worker.session.backend,
907
627
  agent: worker.session.target.agent,
908
- contact: worker.session.target.contact ?? buildWorkerContactId(worker),
909
- contactResource: buildWorkerContactIri(webId, worker),
910
- agentResource: buildWorkerAgentIri(webId, worker),
911
628
  status: worker.session.status,
912
629
  autoModeSessionId: worker.session.autoModeSessionId,
913
630
  target: worker.session.target,
@@ -915,6 +632,7 @@ function buildSymphonyWorkerSummary(plan, webId, worker) {
915
632
  workspace: buildSymphonyWorkspaceMetadata(plan, worker),
916
633
  podAccessPolicy: buildSymphonyWorkerPodAccessPolicy(plan, webId, worker),
917
634
  reconciler: buildSymphonyReconcilerMetadata(worker),
635
+ acceptanceReview: worker.taskRecord.acceptanceReview ?? worker.delivery.acceptanceReview ?? worker.session.acceptanceReview,
918
636
  };
919
637
  }
920
638
  function buildSymphonyReconcilerMetadata(worker) {
@@ -959,148 +677,21 @@ function createFallbackSymphonyDispatchDecision(worker) {
959
677
  randomId: `${worker.delivery.uri}-dispatch`,
960
678
  }).summary;
961
679
  }
962
- function buildSymphonyThreadRows(plan, webId, stage) {
963
- return collectSymphonyThreadProjectionGroups(plan, webId)
964
- .map((group) => buildSymphonyThreadRow(plan, webId, stage, group));
965
- }
966
- function buildSymphonyThreadRow(plan, webId, stage, group) {
967
- const workers = group?.workers ?? plan.workers;
968
- const primaryWorker = workers[0] ?? {
969
- task: plan.task,
970
- taskRecord: plan.taskRecord,
971
- delivery: plan.delivery,
972
- session: plan.session,
973
- };
974
- const createdAt = safeDate(primaryWorker.session.createdAt);
975
- const updatedAt = safeDate(primaryWorker.session.updatedAt);
976
- const parent = group?.chat ?? selectTargetChatIri(plan.session.target?.chat, webId, plan);
977
- const thread = group?.thread ?? selectTargetThreadIri(plan.session.target?.thread, webId, plan);
978
- const workspace = pathToWorkspaceUri(primaryWorker.session.cwd) ?? pathToWorkspaceUri(plan.session.cwd);
979
- return {
980
- id: threadRepository.idForChat(parent, thread),
981
- parent,
982
- title: normalizeTitle(plan.issue.title || plan.issue.description || 'Symphony Task'),
983
- ...(workspace ? { workspace } : {}),
984
- metadata: {
985
- kind: 'symphony-run',
986
- surface: 'symphony',
987
- stage,
988
- status: plan.session.status,
989
- issue: plan.issue.uri,
990
- task: primaryWorker.task,
991
- delivery: primaryWorker.delivery.uri,
992
- session: primaryWorker.session.uri,
993
- issuer: plan.issue.issuer,
994
- workers: workers.map((worker) => buildSymphonyWorkerSummary(plan, webId, worker)),
995
- backend: primaryWorker.session.backend,
996
- mode: primaryWorker.session.mode,
997
- model: primaryWorker.session.model,
998
- workspacePath: primaryWorker.session.cwd,
999
- workspace: buildSymphonyWorkspaceMetadata(plan, primaryWorker),
1000
- reconciler: buildSymphonyReconcilerMetadata(primaryWorker),
1001
- autoModeSessionId: primaryWorker.session.autoModeSessionId,
1002
- exitCode: primaryWorker.session.exitCode,
1003
- error: primaryWorker.session.error ?? primaryWorker.delivery.error ?? plan.issue.error,
1004
- target: primaryWorker.session.target,
1005
- },
1006
- createdAt,
1007
- updatedAt,
1008
- };
1009
- }
1010
- function buildSymphonySessionRow(plan, webId, worker = plan.workers[0] ?? {
1011
- task: plan.task,
1012
- taskRecord: plan.taskRecord,
1013
- delivery: plan.delivery,
1014
- session: plan.session,
1015
- }) {
1016
- const createdAt = safeDate(worker.session.createdAt);
1017
- const updatedAt = safeDate(worker.session.updatedAt);
1018
- const status = worker.session.status === 'completed'
1019
- ? 'completed'
1020
- : worker.session.status === 'failed'
1021
- ? 'error'
1022
- : 'active';
1023
- const workerSummary = buildSymphonyWorkerSummary(plan, webId, worker);
1024
- return {
1025
- id: buildSymphonySessionRecordId(worker.session),
1026
- owner: webId,
1027
- chat: selectWorkerChatIri(plan, webId, worker),
1028
- thread: selectWorkerThreadIri(plan, webId, worker),
1029
- sessionType: 'group',
1030
- status,
1031
- tool: `symphony:${worker.session.backend}`,
1032
- tokenUsage: 0,
1033
- messages: worker.session.messages,
1034
- policyVersion: SYMPHONY_POLICY_VERSION,
1035
- metadata: {
1036
- kind: 'symphony-run',
1037
- surface: 'symphony',
1038
- issue: plan.issue.uri,
1039
- task: worker.task,
1040
- delivery: worker.delivery.uri,
1041
- session: worker.session.uri,
1042
- issuer: plan.issue.issuer,
1043
- worker: workerSummary,
1044
- workers: [workerSummary],
1045
- backend: worker.session.backend,
1046
- mode: worker.session.mode,
1047
- model: worker.session.model,
1048
- workspacePath: worker.session.cwd,
1049
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1050
- reconciler: buildSymphonyReconcilerMetadata(worker),
1051
- autoModeSessionId: worker.session.autoModeSessionId,
1052
- exitCode: worker.session.exitCode,
1053
- dryRun: worker.session.dryRun,
1054
- error: worker.session.error ?? worker.delivery.error ?? plan.issue.error,
1055
- target: worker.session.target,
1056
- },
1057
- createdAt,
1058
- updatedAt,
1059
- ...(status === 'completed' || status === 'error' ? { archivedAt: updatedAt } : {}),
1060
- };
1061
- }
1062
- function buildSymphonyIssueRow(plan, webId) {
1063
- const createdAt = safeDate(plan.issue.createdAt);
1064
- const updatedAt = safeDate(plan.issue.updatedAt);
1065
- const secretaryContact = buildSecretaryContactIri(webId);
1066
- return {
1067
- id: buildSymphonyIssueId(plan.issue),
1068
- // File-primary: title remains a compact index label for existing Issue schemas.
1069
- // The full problem statement and acceptance narrative live in issue.md.
1070
- title: plan.issue.title,
1071
- document: podFileUrlFromWebId(webId, buildSymphonyIssueDocumentPath(plan.issue)),
1072
- description: undefined,
1073
- status: plan.issue.status,
1074
- priority: plan.issue.priority,
1075
- labels: ['symphony'],
1076
- chat: selectTargetChatIri(plan.session.target?.chat, webId, plan),
1077
- thread: selectTargetThreadIri(plan.session.target?.thread, webId, plan),
1078
- tasks: Array.from(new Set((plan.issue.tasks?.length ? plan.issue.tasks : plan.workers.map((worker) => worker.task))
1079
- .map((task) => normalizeSymphonyTaskIri(webId, task)))),
1080
- createdBy: plan.issue.issuer.webId ?? webId,
1081
- assignedTo: secretaryContact,
1082
- createdAt,
1083
- updatedAt,
1084
- ...(plan.issue.closedAt ? { closedAt: safeDate(plan.issue.closedAt) } : {}),
1085
- };
1086
- }
1087
680
  function buildSymphonyIdeaRow(idea, webId) {
1088
681
  const createdAt = safeDate(idea.createdAt);
1089
682
  const updatedAt = safeDate(idea.updatedAt);
1090
683
  return {
1091
684
  id: getSymphonyArchiveKey(idea.uri),
1092
685
  summary: idea.summary,
1093
- document: podFileUrlFromWebId(webId, buildSymphonyIdeaDocumentPath(idea)),
1094
- // File-primary: the source text lives in idea.md; TTL keeps only routing/index facts.
1095
- input: undefined,
686
+ input: idea.input,
1096
687
  status: idea.status,
1097
688
  commitment: idea.commitment,
1098
689
  affectedArea: idea.affectedArea,
1099
- currentUnderstanding: undefined,
1100
- openQuestions: undefined,
690
+ currentUnderstanding: idea.currentUnderstanding,
691
+ openQuestions: idea.openQuestions,
1101
692
  related: idea.relatedRecords,
1102
- conflicts: undefined,
1103
- nextStep: undefined,
693
+ conflicts: idea.conflicts,
694
+ nextStep: idea.nextStep,
1104
695
  promotedTo: idea.promotedTo,
1105
696
  chat: idea.chat,
1106
697
  thread: idea.thread,
@@ -1108,277 +699,25 @@ function buildSymphonyIdeaRow(idea, webId) {
1108
699
  createdBy: webId,
1109
700
  metadata: {
1110
701
  surface: 'symphony',
1111
- filePrimary: true,
1112
- documentPath: buildSymphonyIdeaDocumentPath(idea),
1113
702
  ...buildSymphonyArchiveMetadata({ idea: idea.uri }),
1114
703
  },
1115
704
  createdAt,
1116
705
  updatedAt,
1117
706
  };
1118
707
  }
1119
- function mapSymphonyTaskStatus(status) {
1120
- if (status === 'running')
1121
- return 'active';
1122
- if (status === 'pending')
1123
- return 'open';
1124
- return status;
1125
- }
1126
- function mapSymphonyRunStatus(status) {
1127
- if (status === 'planned')
1128
- return 'queued';
1129
- if (status === 'running')
1130
- return 'running';
1131
- if (status === 'completed')
1132
- return 'completed';
1133
- if (status === 'failed')
1134
- return 'failed';
1135
- return 'queued';
1136
- }
1137
- function buildSymphonyTaskRow(plan, webId, worker) {
1138
- const createdAt = safeDate(worker.taskRecord.createdAt);
1139
- const updatedAt = safeDate(worker.taskRecord.updatedAt);
1140
- const workerContact = buildWorkerContactIri(webId, worker);
1141
- const workerAgent = buildWorkerAgentIri(webId, worker);
1142
- return {
1143
- id: taskResource.buildId({ id: buildSymphonyTaskKey(worker.task) }),
1144
- title: worker.taskRecord.title,
1145
- instruction: worker.taskRecord.objective,
1146
- prompt: worker.delivery.projection.prompt,
1147
- issue: buildSymphonyIssueIri(webId, plan.issue),
1148
- message: plan.issue.messages?.at(-1),
1149
- thread: selectWorkerThreadIri(plan, webId, worker),
1150
- workspace: pathToWorkspaceUri(worker.session.cwd) ?? pathToWorkspaceUri(plan.session.cwd) ?? 'file:///',
1151
- status: mapSymphonyTaskStatus(worker.taskRecord.status),
1152
- priority: plan.issue.priority,
1153
- assignedTo: workerContact,
1154
- source: buildSymphonyIssueIri(webId, plan.issue),
1155
- metadata: {
1156
- surface: 'symphony',
1157
- ...buildSymphonyArchiveMetadata({ task: worker.taskRecord.uri }),
1158
- acceptanceCriteria: worker.taskRecord.acceptanceCriteria,
1159
- backend: worker.session.backend,
1160
- target: worker.session.target,
1161
- assignedContact: workerContact,
1162
- assignedAgent: workerAgent,
1163
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1164
- spaceContract: buildSymphonySpaceContract(plan, webId, worker),
1165
- podAccessPolicy: buildSymphonyWorkerPodAccessPolicy(plan, webId, worker),
1166
- reconciler: buildSymphonyReconcilerMetadata(worker),
1167
- },
1168
- createdAt,
1169
- updatedAt,
1170
- };
1171
- }
1172
- function buildSymphonyDeliveryRow(plan, webId, worker) {
1173
- const createdAt = safeDate(worker.delivery.createdAt);
1174
- const updatedAt = safeDate(worker.delivery.updatedAt);
1175
- const secretaryAgent = agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID });
1176
- const secretaryContact = buildSecretaryContactIri(webId);
1177
- const workerAgent = buildWorkerAgentIri(webId, worker);
1178
- const workerContact = buildWorkerContactIri(webId, worker);
1179
- return {
1180
- id: deliveryResource.buildId({
1181
- id: getSymphonyArchiveKey(worker.delivery.uri),
1182
- task: buildSymphonyTaskIri(webId, worker.task),
1183
- createdAt,
1184
- }),
1185
- kind: worker.delivery.type,
1186
- status: worker.delivery.status,
1187
- task: buildSymphonyTaskIri(webId, worker.task),
1188
- source: secretaryContact,
1189
- target: workerContact,
1190
- chat: selectWorkerChatIri(plan, webId, worker),
1191
- thread: selectWorkerThreadIri(plan, webId, worker),
1192
- targetThread: selectWorkerThreadIri(plan, webId, worker),
1193
- targetSession: worker.session.uri,
1194
- actor: secretaryAgent,
1195
- object: buildSymphonyTaskIri(webId, worker.task),
1196
- objective: worker.taskRecord.objective,
1197
- payload: {
1198
- issue: buildSymphonyIssueIri(webId, plan.issue),
1199
- acceptanceCriteria: worker.taskRecord.acceptanceCriteria,
1200
- backend: worker.session.backend,
1201
- mode: worker.session.mode,
1202
- target: worker.session.target,
1203
- targetContact: workerContact,
1204
- targetAgent: workerAgent,
1205
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1206
- spaceContract: buildSymphonySpaceContract(plan, webId, worker),
1207
- podAccessPolicy: buildSymphonyWorkerPodAccessPolicy(plan, webId, worker),
1208
- reconciler: buildSymphonyReconcilerMetadata(worker),
1209
- },
1210
- projection: worker.delivery.projection,
1211
- projectedRole: worker.delivery.projection.runtimeRole,
1212
- metadata: {
1213
- surface: 'symphony',
1214
- ...buildSymphonyArchiveMetadata({
1215
- issue: plan.issue.uri,
1216
- task: worker.task,
1217
- delivery: worker.delivery.uri,
1218
- session: worker.session.uri,
1219
- }),
1220
- autoModeSessionId: worker.delivery.autoModeSessionId,
1221
- sourceAgent: secretaryAgent,
1222
- targetContact: workerContact,
1223
- targetAgent: workerAgent,
1224
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1225
- spaceContract: buildSymphonySpaceContract(plan, webId, worker),
1226
- podAccessPolicy: buildSymphonyWorkerPodAccessPolicy(plan, webId, worker),
1227
- reconciler: buildSymphonyReconcilerMetadata(worker),
1228
- },
1229
- error: worker.delivery.error,
1230
- createdAt,
1231
- dispatchedAt: worker.delivery.status === 'dispatched' || worker.delivery.status === 'completed'
1232
- ? updatedAt
1233
- : undefined,
1234
- completedAt: worker.delivery.completedAt ? safeDate(worker.delivery.completedAt) : undefined,
1235
- updatedAt,
1236
- };
1237
- }
1238
- function buildSymphonyReportRow(plan, webId, worker, stage) {
1239
- const completedAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1240
- const workerAgent = buildWorkerAgentIri(webId, worker);
1241
- const run = buildSymphonyRunIri(webId, worker);
1242
- const task = buildSymphonyTaskIri(webId, worker.task);
1243
- const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
1244
- const summary = status === 'completed'
1245
- ? `${worker.taskRecord.title} completed.`
1246
- : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
1247
- const evidence = buildSymphonyEvidenceIri(webId, worker, stage);
1248
- return {
1249
- id: reportResource.buildId({
1250
- id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
1251
- task,
1252
- createdAt: completedAt,
1253
- }),
1254
- reportKind: 'handoff',
1255
- status: 'published',
1256
- outcome: status === 'completed' ? 'accepted' : 'blocked',
1257
- about: run,
1258
- issue: buildSymphonyIssueIri(webId, plan.issue),
1259
- task,
1260
- delivery: buildSymphonyDeliveryIri(webId, worker),
1261
- run,
1262
- thread: selectWorkerThreadIri(plan, webId, worker),
1263
- evidence: [
1264
- evidence,
1265
- ],
1266
- summary,
1267
- actor: workerAgent,
1268
- source: podFileUrlFromWebId(webId, buildSymphonyReportDocumentPath(worker)),
1269
- metricFacts: {
1270
- backend: worker.session.backend,
1271
- agent: worker.session.target.agent,
1272
- autoModeSessionId: worker.session.autoModeSessionId,
1273
- exitCode: worker.session.exitCode,
1274
- },
1275
- metadata: {
1276
- surface: 'symphony',
1277
- filePrimary: true,
1278
- reportFile: buildSymphonyReportDocumentPath(worker),
1279
- reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1280
- postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
1281
- ...buildSymphonyArchiveMetadata({
1282
- issue: plan.issue.uri,
1283
- task: worker.task,
1284
- delivery: worker.delivery.uri,
1285
- session: worker.session.uri,
1286
- }),
1287
- },
1288
- createdAt: completedAt,
1289
- publishedAt: completedAt,
1290
- updatedAt: completedAt,
1291
- };
1292
- }
1293
- function buildSymphonyEvidenceRow(plan, webId, worker, stage) {
1294
- const createdAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1295
- const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
1296
- const run = buildSymphonyRunIri(webId, worker);
1297
- const task = buildSymphonyTaskIri(webId, worker.task);
1298
- const delivery = buildSymphonyDeliveryIri(webId, worker);
1299
- const runStep = buildSymphonyRunStepIri(webId, worker, stage);
1300
- const workerAgent = buildWorkerAgentIri(webId, worker);
1301
- return {
1302
- id: evidenceResource.buildId({
1303
- id: `${getSymphonyArchiveKey(worker.session.uri)}-${stage}`,
1304
- createdAt,
1305
- }),
1306
- evidenceKind: EvidenceKind.RUNTIME_LOG,
1307
- about: run,
1308
- issue: buildSymphonyIssueIri(webId, plan.issue),
1309
- task,
1310
- delivery,
1311
- run,
1312
- thread: selectWorkerThreadIri(plan, webId, worker),
1313
- summary: status === 'completed'
1314
- ? `${worker.taskRecord.title} completed with runtime status ${worker.session.status}.`
1315
- : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? 'worker did not complete successfully.'}`,
1316
- source: podFileUrlFromWebId(webId, buildSymphonyEvidenceDocumentPath(worker, stage)),
1317
- actor: workerAgent,
1318
- outcome: status,
1319
- metadata: {
1320
- surface: 'symphony',
1321
- filePrimary: true,
1322
- evidenceFile: buildSymphonyEvidenceDocumentPath(worker, stage),
1323
- sourceRunStep: runStep,
1324
- report: buildSymphonyReportIri(webId, worker),
1325
- reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1326
- postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
1327
- runtime: {
1328
- backend: worker.session.backend,
1329
- model: worker.session.model,
1330
- autoModeSessionId: worker.session.autoModeSessionId,
1331
- exitCode: worker.session.exitCode,
1332
- status: worker.session.status,
1333
- },
1334
- ...buildSymphonyArchiveMetadata({
1335
- issue: plan.issue.uri,
1336
- task: worker.task,
1337
- delivery: worker.delivery.uri,
1338
- session: worker.session.uri,
1339
- }),
1340
- },
1341
- createdAt,
1342
- };
1343
- }
1344
- function buildPostRunReconciliationMetadata(plan, webId, worker, stage) {
1345
- return {
1346
- required: true,
1347
- status: 'pending_secretary_review',
1348
- owner: buildSecretaryContactIri(webId),
1349
- sourceIssue: buildSymphonyIssueIri(webId, plan.issue),
1350
- sourceTask: buildSymphonyTaskIri(webId, worker.task),
1351
- sourceDelivery: buildSymphonyDeliveryIri(webId, worker),
1352
- sourceRun: buildSymphonyRunIri(webId, worker),
1353
- sourceRunStep: buildSymphonyRunStepIri(webId, worker, stage),
1354
- sourceEvidence: buildSymphonyEvidenceIri(webId, worker, stage),
1355
- sourceReport: buildSymphonyReportIri(webId, worker),
1356
- classifications: [
1357
- 'same_issue_task',
1358
- 'new_issue',
1359
- 'idea',
1360
- 'evidence_only',
1361
- 'ask_user',
1362
- ],
1363
- rule: 'Secretary must reconcile acceptance and follow-up before task closure.',
1364
- };
1365
- }
1366
708
  function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1367
709
  const completedAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1368
- const workerAgent = buildWorkerAgentIri(webId, worker);
1369
- const workerContact = buildWorkerContactIri(webId, worker);
710
+ const workerAgent = agentResource.buildIri(webId, {
711
+ id: buildWorkerAgentId(worker.session.backend, worker.session.target.agent),
712
+ });
1370
713
  const secretaryAgent = agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID });
1371
- const secretaryContact = buildSecretaryContactIri(webId);
1372
714
  const run = buildSymphonyRunIri(webId, worker);
1373
- const report = buildSymphonyReportIri(webId, worker);
1374
715
  const task = buildSymphonyTaskIri(webId, worker.task);
1375
716
  const originalDelivery = buildSymphonyDeliveryIri(webId, worker);
1376
717
  const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
1377
718
  const summary = status === 'completed'
1378
719
  ? `${worker.taskRecord.title} completed.`
1379
720
  : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
1380
- const evidence = buildSymphonyEvidenceIri(webId, worker, stage);
1381
- const sourceRunStep = buildSymphonyRunStepIri(webId, worker, stage);
1382
721
  return {
1383
722
  id: deliveryResource.buildId({
1384
723
  id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
@@ -1388,14 +727,14 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1388
727
  kind: 'report',
1389
728
  status: 'completed',
1390
729
  task,
1391
- source: workerContact,
1392
- target: secretaryContact,
730
+ source: workerAgent,
731
+ target: secretaryAgent,
1393
732
  chat: selectWorkerChatIri(plan, webId, worker),
1394
733
  thread: selectWorkerThreadIri(plan, webId, worker),
1395
734
  targetThread: selectTargetThreadIri(plan.issue.thread ?? worker.session.target?.thread, webId, plan),
1396
735
  targetSession: buildSymphonyControlSessionUri(webId, plan),
1397
736
  actor: workerAgent,
1398
- object: report,
737
+ object: run,
1399
738
  objective: summary,
1400
739
  payload: {
1401
740
  kind: 'symphony_report',
@@ -1404,25 +743,18 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1404
743
  issue: buildSymphonyIssueIri(webId, plan.issue),
1405
744
  task,
1406
745
  delivery: originalDelivery,
1407
- report,
1408
746
  reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1409
747
  session: buildSymphonyControlSessionUri(webId, plan),
1410
748
  run,
1411
749
  backend: worker.session.backend,
1412
750
  agent: worker.session.target.agent,
1413
- contact: worker.session.target.contact ?? buildWorkerContactId(worker),
1414
- sourceAgent: workerAgent,
1415
- sourceContact: workerContact,
1416
751
  autoModeSessionId: worker.session.autoModeSessionId,
1417
752
  exitCode: worker.session.exitCode,
1418
753
  error: worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error,
1419
- reportFile: buildSymphonyReportDocumentPath(worker),
1420
- evidenceFile: buildSymphonyEvidenceDocumentPath(worker, stage),
1421
- postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
754
+ acceptanceReview: worker.delivery.acceptanceReview ?? worker.taskRecord.acceptanceReview ?? worker.session.acceptanceReview,
1422
755
  evidence: {
1423
- statusMessage: buildSymphonyMessageUri(webId, plan, buildStatusMessageRow(plan, webId, stage)),
1424
- sourceRunStep,
1425
- evidence,
756
+ statusMessage: buildSharedSymphonyMessageIri(webId, plan, buildSharedSymphonyStatusMessageRow(plan, webId, stage)),
757
+ runStep: buildSymphonyRunStepIri(webId, worker, stage),
1426
758
  },
1427
759
  },
1428
760
  projection: {
@@ -1439,11 +771,7 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1439
771
  session: worker.session.uri,
1440
772
  }),
1441
773
  reportKind: 'worker-completion',
1442
- filePrimary: true,
1443
- reportFile: buildSymphonyReportDocumentPath(worker),
1444
- evidence,
1445
- evidenceFile: buildSymphonyEvidenceDocumentPath(worker, stage),
1446
- postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
774
+ acceptanceReview: worker.delivery.acceptanceReview ?? worker.taskRecord.acceptanceReview ?? worker.session.acceptanceReview,
1447
775
  },
1448
776
  error: status === 'failed' ? worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error : undefined,
1449
777
  createdAt: completedAt,
@@ -1453,101 +781,13 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1453
781
  updatedAt: completedAt,
1454
782
  };
1455
783
  }
1456
- function buildSymphonyRunRow(plan, webId, worker) {
1457
- const createdAt = safeDate(worker.session.createdAt);
1458
- const updatedAt = safeDate(worker.session.updatedAt);
1459
- return {
1460
- id: runResource.buildId({
1461
- id: getSymphonyArchiveKey(worker.session.uri),
1462
- task: buildSymphonyTaskIri(webId, worker.task),
1463
- createdAt,
1464
- }),
1465
- task: buildSymphonyTaskIri(webId, worker.task),
1466
- delivery: buildSymphonyDeliveryIri(webId, worker),
1467
- trigger: plan.issue.messages?.at(-1) ?? buildSymphonyIssueIri(webId, plan.issue),
1468
- input: buildSymphonyDeliveryIri(webId, worker),
1469
- thread: selectWorkerThreadIri(plan, webId, worker),
1470
- workspace: pathToWorkspaceUri(worker.session.cwd) ?? pathToWorkspaceUri(plan.session.cwd) ?? 'file:///',
1471
- status: mapSymphonyRunStatus(worker.session.status),
1472
- runner: worker.session.backend,
1473
- prompt: worker.delivery.projection.prompt,
1474
- externalRunId: worker.session.autoModeSessionId,
1475
- error: worker.session.error,
1476
- metadata: {
1477
- surface: 'symphony',
1478
- ...buildSymphonyArchiveMetadata({ session: worker.session.uri }),
1479
- mode: worker.session.mode,
1480
- model: worker.session.model,
1481
- target: worker.session.target,
1482
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1483
- spaceContract: buildSymphonySpaceContract(plan, webId, worker),
1484
- podAccessPolicy: buildSymphonyWorkerPodAccessPolicy(plan, webId, worker),
1485
- reconciler: buildSymphonyReconcilerMetadata(worker),
1486
- exitCode: worker.session.exitCode,
1487
- dryRun: worker.session.dryRun,
1488
- },
1489
- createdAt,
1490
- startedAt: worker.session.status === 'running' || worker.session.status === 'completed' || worker.session.status === 'failed'
1491
- ? updatedAt
1492
- : undefined,
1493
- completedAt: worker.session.completedAt ? safeDate(worker.session.completedAt) : undefined,
1494
- updatedAt,
1495
- };
1496
- }
1497
- function buildSymphonyRunStepRow(plan, webId, worker, stage) {
1498
- const run = buildSymphonyRunIri(webId, worker);
1499
- const createdAt = stage === 'planned' ? safeDate(worker.session.createdAt) : safeDate(worker.session.updatedAt);
1500
- const stepType = stage === 'planned'
1501
- ? 'run.created'
1502
- : stage === 'running'
1503
- ? 'run.started'
1504
- : stage === 'completed'
1505
- ? 'run.completed'
1506
- : 'run.failed';
1507
- return {
1508
- id: runStepResource.buildId({
1509
- id: `${getSymphonyArchiveKey(worker.session.uri)}-${stage}`,
1510
- run,
1511
- }),
1512
- run,
1513
- stepType,
1514
- message: buildStatusContent(plan, stage),
1515
- data: {
1516
- surface: 'symphony',
1517
- stage,
1518
- issue: buildSymphonyIssueIri(webId, plan.issue),
1519
- task: buildSymphonyTaskIri(webId, worker.task),
1520
- delivery: buildSymphonyDeliveryIri(webId, worker),
1521
- archive: buildSymphonyArchiveRefs({ session: worker.session.uri }),
1522
- autoModeSessionId: worker.session.autoModeSessionId,
1523
- },
1524
- createdAt,
1525
- };
1526
- }
1527
- function normalizeSymphonyActorKey(value, fallback) {
1528
- return (value ?? fallback)
784
+ function buildWorkerAgentId(backend, agent) {
785
+ const suffix = (agent ?? `${backend}-worker`)
1529
786
  .trim()
1530
787
  .replace(/[^a-zA-Z0-9._-]/gu, '-')
1531
788
  .replace(/-+/gu, '-')
1532
- .replace(/^-|-$/gu, '')
1533
- || fallback;
1534
- }
1535
- function buildWorkerContactId(worker) {
1536
- return normalizeSymphonyActorKey(worker.session.target.contact ?? worker.session.target.agent ?? worker.delivery.targetAgent, worker.session.backend);
1537
- }
1538
- function buildWorkerAgentId(backend, agent, contact) {
1539
- return normalizeSymphonyActorKey(agent ?? contact, backend);
1540
- }
1541
- function buildSecretaryContactIri(webId) {
1542
- return contactResource.buildIri(webId, { id: SYMPHONY_CONTACT_ID });
1543
- }
1544
- function buildWorkerAgentIri(webId, worker) {
1545
- return agentResource.buildIri(webId, {
1546
- id: buildWorkerAgentId(worker.session.backend, worker.session.target.agent, worker.session.target.contact),
1547
- });
1548
- }
1549
- function buildWorkerContactIri(webId, worker) {
1550
- return contactResource.buildIri(webId, { id: buildWorkerContactId(worker) });
789
+ .replace(/^-|-$/gu, '');
790
+ return `symphony-${suffix || `${backend}-worker`}`;
1551
791
  }
1552
792
  function buildSymphonyAgents(plan) {
1553
793
  const now = safeDate(plan.session.updatedAt);
@@ -1564,7 +804,7 @@ function buildSymphonyAgents(plan) {
1564
804
  ];
1565
805
  const seen = new Set(agents.map((agent) => agent.id));
1566
806
  for (const worker of plan.workers) {
1567
- const id = buildWorkerAgentId(worker.session.backend, worker.session.target.agent, worker.session.target.contact);
807
+ const id = buildWorkerAgentId(worker.session.backend, worker.session.target.agent);
1568
808
  if (seen.has(id)) {
1569
809
  continue;
1570
810
  }
@@ -1581,146 +821,6 @@ function buildSymphonyAgents(plan) {
1581
821
  }
1582
822
  return agents;
1583
823
  }
1584
- function buildSymphonyContacts(plan, webId) {
1585
- const now = safeDate(plan.session.updatedAt);
1586
- const contacts = [
1587
- {
1588
- id: SYMPHONY_CONTACT_ID,
1589
- name: 'AI Secretary',
1590
- entity: agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID }),
1591
- rdfType: ContactClass.AGENT,
1592
- contactType: ContactType.AGENT,
1593
- createdAt: now,
1594
- updatedAt: now,
1595
- },
1596
- ];
1597
- const seen = new Set(contacts.map((contact) => contact.id));
1598
- for (const worker of plan.workers) {
1599
- const id = buildWorkerContactId(worker);
1600
- if (seen.has(id)) {
1601
- continue;
1602
- }
1603
- seen.add(id);
1604
- contacts.push({
1605
- id,
1606
- name: worker.session.target.label ?? worker.session.target.contact ?? worker.session.target.agent ?? backendDisplayName(worker.session.backend),
1607
- entity: buildWorkerAgentIri(webId, worker),
1608
- rdfType: ContactClass.AGENT,
1609
- contactType: ContactType.AGENT,
1610
- createdAt: now,
1611
- updatedAt: now,
1612
- });
1613
- }
1614
- return contacts;
1615
- }
1616
- function buildProgressBlock(plan, stage) {
1617
- const statusByStage = {
1618
- planned: 'pending',
1619
- running: 'running',
1620
- completed: 'done',
1621
- failed: 'error',
1622
- };
1623
- const workerSteps = plan.workers.map((worker, index) => ({
1624
- id: `${buildSymphonyThreadId(plan)}-worker-${index + 1}`,
1625
- label: `${worker.session.target.label ?? worker.session.target.agent ?? backendDisplayName(worker.session.backend)} worker`,
1626
- status: worker.session.status === 'completed'
1627
- ? 'done'
1628
- : worker.session.status === 'failed'
1629
- ? 'error'
1630
- : worker.session.status === 'running'
1631
- ? 'running'
1632
- : statusByStage[stage],
1633
- detail: worker.session.autoModeSessionId ?? worker.session.uri,
1634
- }));
1635
- return {
1636
- type: 'task_progress',
1637
- task: plan.task,
1638
- title: plan.issue.title,
1639
- steps: [
1640
- {
1641
- id: `${buildSymphonyThreadId(plan)}-plan`,
1642
- label: 'Secretary created task projection',
1643
- status: stage === 'planned' ? 'running' : 'done',
1644
- detail: plan.issue.uri,
1645
- },
1646
- ...workerSteps,
1647
- {
1648
- id: `${buildSymphonyThreadId(plan)}-finish`,
1649
- label: 'Archive Symphony result',
1650
- status: stage === 'completed' ? 'done' : stage === 'failed' ? 'error' : 'pending',
1651
- detail: plan.issue.error ?? plan.session.error ?? `${plan.workers.length} worker${plan.workers.length === 1 ? '' : 's'}`,
1652
- },
1653
- ],
1654
- currentStep: stage === 'planned' ? 1 : stage === 'running' ? 2 : workerSteps.length + 2,
1655
- totalSteps: workerSteps.length + 2,
1656
- };
1657
- }
1658
- function buildStatusContent(plan, stage) {
1659
- if (stage === 'planned') {
1660
- return `I created a Symphony issue with ${plan.workers.length} worker${plan.workers.length === 1 ? '' : 's'}.\n\n${plan.issue.description ?? plan.issue.title}`;
1661
- }
1662
- if (stage === 'running') {
1663
- const running = plan.workers
1664
- .filter((worker) => worker.session.status === 'running')
1665
- .map((worker) => worker.session.target.label ?? worker.session.target.agent ?? backendDisplayName(worker.session.backend));
1666
- return `Symphony workers are active: ${running.length > 0 ? running.join(', ') : plan.workers.length}.\n\nIssue: ${plan.issue.uri}`;
1667
- }
1668
- if (stage === 'completed') {
1669
- return `Symphony issue completed.\n\nWorkers: ${plan.workers.length}`;
1670
- }
1671
- return `Symphony issue failed.\n\n${plan.issue.error ?? plan.session.error ?? plan.delivery.error ?? 'Backend did not complete successfully.'}`;
1672
- }
1673
- function buildStatusMessageRow(plan, webId, stage) {
1674
- const createdAt = stage === 'planned'
1675
- ? safeDate(plan.issue.createdAt)
1676
- : safeDate(plan.session.updatedAt);
1677
- const content = buildStatusContent(plan, stage);
1678
- const secretaryAgent = agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID });
1679
- const routeTargetAgent = agentResource.buildIri(webId, {
1680
- id: buildWorkerAgentId(plan.session.backend, plan.session.target?.agent, plan.session.target?.contact),
1681
- });
1682
- return {
1683
- id: `${buildSymphonyThreadId(plan)}-${stage}`,
1684
- chat: selectTargetChatIri(plan.session.target?.chat, webId, plan),
1685
- thread: selectTargetThreadIri(plan.session.target?.thread, webId, plan),
1686
- maker: secretaryAgent,
1687
- role: 'assistant',
1688
- content,
1689
- richContent: JSON.stringify({
1690
- blocks: [buildProgressBlock(plan, stage)],
1691
- symphony: {
1692
- stage,
1693
- issue: plan.issue.uri,
1694
- task: plan.task,
1695
- delivery: plan.delivery.uri,
1696
- session: plan.session.uri,
1697
- issuer: plan.issue.issuer,
1698
- workers: plan.workers.map((worker) => ({
1699
- task: worker.task,
1700
- title: worker.taskRecord.title,
1701
- objective: worker.taskRecord.objective,
1702
- acceptanceCriteria: worker.taskRecord.acceptanceCriteria,
1703
- taskStatus: worker.taskRecord.status,
1704
- delivery: worker.delivery.uri,
1705
- session: worker.session.uri,
1706
- backend: worker.session.backend,
1707
- agent: worker.session.target.agent,
1708
- contact: worker.session.target.contact ?? buildWorkerContactId(worker),
1709
- status: worker.session.status,
1710
- autoModeSessionId: worker.session.autoModeSessionId,
1711
- })),
1712
- autoModeSessionId: plan.session.autoModeSessionId,
1713
- },
1714
- }),
1715
- status: stage === 'failed' ? 'error' : 'sent',
1716
- senderName: 'AI Secretary',
1717
- routedBy: secretaryAgent,
1718
- routeTargetAgent,
1719
- coordinationId: plan.session.uri,
1720
- createdAt,
1721
- updatedAt: createdAt,
1722
- };
1723
- }
1724
824
  function auditActionForStage(stage) {
1725
825
  if (stage === 'running')
1726
826
  return 'symphony.dispatched';
@@ -1743,13 +843,15 @@ function buildSymphonyReportInboxNotificationRow(webId, worker) {
1743
843
  const createdAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1744
844
  return {
1745
845
  id: stableReportInboxNotificationId(worker),
1746
- actor: buildWorkerContactIri(webId, worker),
846
+ actor: agentResource.buildIri(webId, {
847
+ id: buildWorkerAgentId(worker.session.backend, worker.session.target.agent),
848
+ }),
1747
849
  object: buildSymphonyReportDeliveryIri(webId, worker),
1748
850
  createdAt,
1749
851
  };
1750
852
  }
1751
853
  function buildSymphonyAuditRow(plan, webId, stage) {
1752
- const message = buildStatusMessageRow(plan, webId, stage);
854
+ const message = buildSharedSymphonyStatusMessageRow(plan, webId, stage);
1753
855
  const createdAt = safeDate(message.createdAt);
1754
856
  return {
1755
857
  id: stableAuditId(plan, stage),
@@ -1758,7 +860,7 @@ function buildSymphonyAuditRow(plan, webId, stage) {
1758
860
  actorRole: 'secretary',
1759
861
  onBehalfOf: webId,
1760
862
  session: buildSymphonyControlSessionUri(webId, plan),
1761
- entry: buildSymphonyMessageUri(webId, plan, message),
863
+ entry: buildSharedSymphonyMessageIri(webId, plan, message),
1762
864
  policyVersion: SYMPHONY_POLICY_VERSION,
1763
865
  createdAt,
1764
866
  };
@@ -1795,6 +897,7 @@ async function upsertMessage(db, runtime, row) {
1795
897
  routedBy: row.routedBy,
1796
898
  routeTargetAgent: row.routeTargetAgent,
1797
899
  coordinationId: row.coordinationId,
900
+ metadata: row.metadata,
1798
901
  updatedAt: row.updatedAt,
1799
902
  });
1800
903
  }
@@ -1821,12 +924,12 @@ async function upsertIssue(db, runtime, row) {
1821
924
  await upsertExactRecord(db, runtime.issueResource, { id: row.id }, row, {
1822
925
  title: row.title,
1823
926
  description: row.description,
1824
- document: row.document,
1825
927
  status: row.status,
1826
928
  priority: row.priority,
1827
929
  labels: row.labels,
1828
930
  chat: row.chat,
1829
931
  thread: row.thread,
932
+ parentIssue: row.parentIssue,
1830
933
  tasks: row.tasks,
1831
934
  assignedTo: row.assignedTo,
1832
935
  updatedAt: row.updatedAt,
@@ -1836,7 +939,6 @@ async function upsertIssue(db, runtime, row) {
1836
939
  async function upsertIdea(db, runtime, row) {
1837
940
  await upsertExactRecord(db, runtime.ideaResource, { id: row.id }, row, {
1838
941
  summary: row.summary,
1839
- document: row.document,
1840
942
  input: row.input,
1841
943
  status: row.status,
1842
944
  commitment: row.commitment,
@@ -1871,51 +973,6 @@ async function upsertTask(db, runtime, row) {
1871
973
  updatedAt: row.updatedAt,
1872
974
  });
1873
975
  }
1874
- async function upsertReport(db, runtime, row) {
1875
- await upsertExactRecord(db, runtime.reportResource, {
1876
- id: row.id,
1877
- task: row.task,
1878
- createdAt: row.createdAt,
1879
- }, row, {
1880
- reportKind: row.reportKind,
1881
- status: row.status,
1882
- outcome: row.outcome,
1883
- about: row.about,
1884
- issue: row.issue,
1885
- task: row.task,
1886
- delivery: row.delivery,
1887
- run: row.run,
1888
- thread: row.thread,
1889
- evidence: row.evidence,
1890
- summary: row.summary,
1891
- reviewer: row.reviewer,
1892
- actor: row.actor,
1893
- source: row.source,
1894
- metricFacts: row.metricFacts,
1895
- metadata: row.metadata,
1896
- publishedAt: row.publishedAt,
1897
- updatedAt: row.updatedAt,
1898
- });
1899
- }
1900
- async function upsertEvidence(db, runtime, row) {
1901
- await upsertExactRecord(db, runtime.evidenceResource, {
1902
- id: row.id,
1903
- createdAt: row.createdAt,
1904
- }, row, {
1905
- evidenceKind: row.evidenceKind,
1906
- about: row.about,
1907
- issue: row.issue,
1908
- task: row.task,
1909
- delivery: row.delivery,
1910
- run: row.run,
1911
- thread: row.thread,
1912
- summary: row.summary,
1913
- source: row.source,
1914
- actor: row.actor,
1915
- outcome: row.outcome,
1916
- metadata: row.metadata,
1917
- });
1918
- }
1919
976
  async function upsertDelivery(db, runtime, row) {
1920
977
  await upsertExactRecord(db, runtime.deliveryResource, { id: row.id }, row, {
1921
978
  kind: row.kind,
@@ -1962,6 +1019,31 @@ async function upsertRun(db, runtime, row) {
1962
1019
  async function insertRunStepOnce(db, runtime, row) {
1963
1020
  await insertExactRecordOnce(db, runtime.runStepResource, String(row.id), row);
1964
1021
  }
1022
+ async function insertEvidenceOnce(db, runtime, row) {
1023
+ await insertExactRecordOnce(db, runtime.evidenceResource, String(row.id), row);
1024
+ }
1025
+ async function upsertReport(db, runtime, row) {
1026
+ await upsertExactRecord(db, runtime.reportResource, { id: row.id }, row, {
1027
+ reportKind: row.reportKind,
1028
+ status: row.status,
1029
+ outcome: row.outcome,
1030
+ about: row.about,
1031
+ issue: row.issue,
1032
+ task: row.task,
1033
+ delivery: row.delivery,
1034
+ run: row.run,
1035
+ thread: row.thread,
1036
+ evidence: row.evidence,
1037
+ summary: row.summary,
1038
+ reviewer: row.reviewer,
1039
+ actor: row.actor,
1040
+ source: row.source,
1041
+ metricFacts: row.metricFacts,
1042
+ metadata: row.metadata,
1043
+ publishedAt: row.publishedAt,
1044
+ updatedAt: row.updatedAt,
1045
+ });
1046
+ }
1965
1047
  async function upsertAgent(db, runtime, row) {
1966
1048
  const target = { id: row.id };
1967
1049
  const agentResourceWithId = runtime.agentResource;
@@ -1981,7 +1063,7 @@ async function upsertAgent(db, runtime, row) {
1981
1063
  async function upsertContact(db, runtime, row) {
1982
1064
  await upsertExactRecord(db, runtime.contactResource, { id: row.id }, row, {
1983
1065
  name: row.name,
1984
- entity: row.entity,
1066
+ about: row.about,
1985
1067
  rdfType: row.rdfType,
1986
1068
  contactType: row.contactType,
1987
1069
  updatedAt: row.updatedAt,
@@ -2000,7 +1082,7 @@ async function insertInboxNotificationOnce(db, runtime, row) {
2000
1082
  await insertExactRecordOnce(db, runtime.inboxNotificationResource, String(row.id), row);
2001
1083
  }
2002
1084
  function collectMessageUris(webId, plan, stages) {
2003
- return Array.from(new Set(stages.map((stage) => buildSymphonyMessageUri(webId, plan, buildStatusMessageRow(plan, webId, stage)))));
1085
+ return Array.from(new Set(stages.map((stage) => buildSharedSymphonyMessageIri(webId, plan, buildSharedSymphonyStatusMessageRow(plan, webId, stage)))));
2004
1086
  }
2005
1087
  function collectSymphonyProjectionResources(webId, plan, stages) {
2006
1088
  const resources = [];
@@ -2015,13 +1097,16 @@ function collectSymphonyProjectionResources(webId, plan, stages) {
2015
1097
  };
2016
1098
  add('chat', selectTargetChatIri(plan.session.target?.chat, webId, plan));
2017
1099
  add('issue', buildSymphonyIssueIri(webId, plan.issue));
1100
+ for (const issue of plan.followUpIssues ?? []) {
1101
+ add('issue', buildSymphonyIssueIri(webId, issue));
1102
+ }
2018
1103
  for (const message of collectMessageUris(webId, plan, stages)) {
2019
1104
  add('message', message);
2020
1105
  }
2021
1106
  for (const agent of buildSymphonyAgents(plan)) {
2022
1107
  add('agent', agentResource.buildIri(webId, { id: agent.id }));
2023
1108
  }
2024
- for (const contact of buildSymphonyContacts(plan, webId)) {
1109
+ for (const contact of buildSharedSymphonyContactRows(plan, webId)) {
2025
1110
  add('contact', contactResource.buildIri(webId, { id: contact.id }));
2026
1111
  }
2027
1112
  for (const worker of plan.workers) {
@@ -2034,10 +1119,10 @@ function collectSymphonyProjectionResources(webId, plan, stages) {
2034
1119
  for (const stage of stages) {
2035
1120
  add('runStep', buildSymphonyRunStepIri(webId, worker, stage));
2036
1121
  }
1122
+ for (const step of worker.runSteps ?? []) {
1123
+ add('runStep', buildSymphonyRuntimeRunStepIri(webId, worker, step));
1124
+ }
2037
1125
  if (worker.session.status === 'completed' || worker.session.status === 'failed') {
2038
- const terminalStage = worker.session.status === 'failed' ? 'failed' : 'completed';
2039
- add('evidence', buildSymphonyEvidenceIri(webId, worker, terminalStage));
2040
- add('report', buildSymphonyReportIri(webId, worker));
2041
1126
  add('delivery', buildSymphonyReportDeliveryIri(webId, worker));
2042
1127
  }
2043
1128
  }
@@ -2074,7 +1159,6 @@ export async function persistSymphonyControlStateToPod(plan, options = {}) {
2074
1159
  };
2075
1160
  const projected = withTargetRefs(normalizedPlan, refs, podSession.webId);
2076
1161
  const resources = collectSymphonyProjectionResources(podSession.webId, projected, stages);
2077
- const latestMessage = buildStatusMessageRow(projected, podSession.webId, stage);
2078
1162
  const controlWrite = createLinxPodSyncScope({
2079
1163
  source: 'symphony-control-state',
2080
1164
  plane: 'control-plane',
@@ -2110,9 +1194,7 @@ export async function persistSymphonyControlStateToPod(plan, options = {}) {
2110
1194
  webId: podSession.webId,
2111
1195
  stage,
2112
1196
  stages,
2113
- latestMessage,
2114
1197
  shouldUpsertChat: !normalizedPlan.session.target?.chat,
2115
- podSession,
2116
1198
  }),
2117
1199
  });
2118
1200
  return {
@@ -2186,10 +1268,6 @@ function resolveProjectionResourceModel(runtime, kind) {
2186
1268
  return runtime.taskResource;
2187
1269
  if (kind === 'delivery')
2188
1270
  return runtime.deliveryResource;
2189
- if (kind === 'evidence')
2190
- return runtime.evidenceResource;
2191
- if (kind === 'report')
2192
- return runtime.reportResource;
2193
1271
  if (kind === 'run')
2194
1272
  return runtime.runResource;
2195
1273
  if (kind === 'runStep')
@@ -2204,6 +1282,14 @@ function resolveProjectionResourceModel(runtime, kind) {
2204
1282
  return runtime.inboxNotificationResource ?? null;
2205
1283
  return null;
2206
1284
  }
1285
+ function asRecord(value) {
1286
+ return value && typeof value === 'object' && !Array.isArray(value)
1287
+ ? value
1288
+ : null;
1289
+ }
1290
+ function normalizeString(value) {
1291
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
1292
+ }
2207
1293
  function serializeOrmRowAsJsonLd(model, iri, row) {
2208
1294
  const context = {};
2209
1295
  const document = {
@@ -2327,18 +1413,7 @@ export async function persistSymphonyIdeaToPod(idea, options = {}) {
2327
1413
  },
2328
1414
  },
2329
1415
  {
2330
- id: 'symphony.idea.write-file-primary-document',
2331
- kind: 'upsert',
2332
- apply: async () => {
2333
- await runtime.writePodFile?.(podSession, {
2334
- path: buildSymphonyIdeaDocumentPath(idea),
2335
- content: renderSymphonyIdeaMarkdown(idea),
2336
- contentType: 'text/markdown; charset=utf-8',
2337
- });
2338
- },
2339
- },
2340
- {
2341
- id: 'symphony.idea.upsert-meta',
1416
+ id: 'symphony.idea.upsert',
2342
1417
  kind: 'upsert',
2343
1418
  apply: () => upsertIdea(db, runtime, buildSymphonyIdeaRow(idea, podSession.webId)),
2344
1419
  },
@@ -2354,13 +1429,10 @@ export async function listOpenSymphonyIssuesFromPod(options = {}) {
2354
1429
  }
2355
1430
  const db = runtime.createDb(podSession);
2356
1431
  try {
2357
- await db.init([runtime.issueResource]).catch(() => undefined);
2358
- const rows = await db.select().from(runtime.issueResource).execute();
2359
- return rows
2360
- .map((row) => issueRowToSymphonyIssueRecord(row, podSession.webId))
2361
- .filter((issue) => issue !== null)
2362
- .filter((issue) => !isClosedIssueStatus(issue.status))
2363
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1432
+ return await listOpenSymphonyIssuesFromControlState({
1433
+ db,
1434
+ webId: podSession.webId,
1435
+ });
2364
1436
  }
2365
1437
  catch {
2366
1438
  return null;
@@ -2374,13 +1446,7 @@ export async function listRunningSymphonyWorkersFromPod(options = {}) {
2374
1446
  }
2375
1447
  const db = runtime.createDb(podSession);
2376
1448
  try {
2377
- await db.init([runtime.sessionResource]).catch(() => undefined);
2378
- const rows = await db.select().from(runtime.sessionResource).execute();
2379
- return rows
2380
- .filter(isSymphonySessionRow)
2381
- .flatMap((row) => extractRunningSymphonyWorkersFromSession(row))
2382
- .sort(compareWorkerStatusUpdatedAt)
2383
- .map(({ updatedAt: _updatedAt, ...worker }) => worker);
1449
+ return await listRunningSymphonyWorkersFromControlState({ db });
2384
1450
  }
2385
1451
  catch {
2386
1452
  return null;
@@ -2394,217 +1460,22 @@ export async function listRecentSymphonyReportsFromPod(options = {}) {
2394
1460
  }
2395
1461
  const db = runtime.createDb(podSession);
2396
1462
  try {
2397
- await db.init([runtime.deliveryResource]).catch(() => undefined);
2398
- const rows = await db.select().from(runtime.deliveryResource).execute();
2399
- return rows
2400
- .map(deliveryRowToSymphonyReportStatus)
2401
- .filter((report) => report !== null)
2402
- .sort((left, right) => right.sortAt - left.sortAt)
2403
- .slice(0, options.limit ?? 5)
2404
- .map(({ sortAt: _sortAt, ...report }) => report);
1463
+ return await listRecentSymphonyReportsFromControlState({
1464
+ db,
1465
+ limit: options.limit ?? 5,
1466
+ });
2405
1467
  }
2406
1468
  catch {
2407
1469
  return null;
2408
1470
  }
2409
1471
  }
2410
- function issueRowToSymphonyIssueRecord(row, webId) {
2411
- const record = asRecord(row);
2412
- const id = normalizeString(record?.id);
2413
- const title = normalizeString(record?.title);
2414
- if (!record || !id || !title) {
2415
- return null;
2416
- }
2417
- const status = normalizeIssueStatus(record.status);
2418
- const priority = normalizeIssuePriority(record.priority);
2419
- const tasks = Array.isArray(record.tasks)
2420
- ? record.tasks.map((item) => normalizeString(item)).filter((item) => Boolean(item))
2421
- : [];
2422
- const createdAt = toIsoDate(record.createdAt);
2423
- const updatedAt = toIsoDate(record.updatedAt) ?? createdAt;
2424
- return {
2425
- uri: symphonyIssueUriFromResourceId(id),
2426
- title,
2427
- description: normalizeString(record.description),
2428
- status,
2429
- priority,
2430
- source: 'cli',
2431
- issuer: {
2432
- source: 'user',
2433
- webId: normalizeString(record.createdBy) ?? webId,
2434
- ...(normalizeString(record.chat) ? { chat: normalizeString(record.chat) } : {}),
2435
- ...(normalizeString(record.thread) ? { thread: normalizeString(record.thread) } : {}),
2436
- },
2437
- tasks,
2438
- deliveries: [],
2439
- sessions: [],
2440
- ...(normalizeString(record.chat) ? { chat: normalizeString(record.chat) } : {}),
2441
- ...(normalizeString(record.thread) ? { thread: normalizeString(record.thread) } : {}),
2442
- createdAt,
2443
- updatedAt,
2444
- ...(record.closedAt ? { closedAt: toIsoDate(record.closedAt) ?? updatedAt } : {}),
2445
- };
2446
- }
2447
- function symphonyIssueUriFromResourceId(id) {
2448
- const normalized = resolvePodResourceTemplateValue(issueResource, id) ?? id;
2449
- return `urn:undefineds:linx:issue:${normalized}`;
2450
- }
2451
- function normalizeIssueStatus(value) {
2452
- const normalized = normalizeString(value);
2453
- if (normalized === 'open'
2454
- || normalized === 'triaging'
2455
- || normalized === 'in_progress'
2456
- || normalized === 'blocked'
2457
- || normalized === 'resolved'
2458
- || normalized === 'closed') {
2459
- return normalized;
2460
- }
2461
- return 'open';
2462
- }
2463
- function normalizeIssuePriority(value) {
2464
- const normalized = normalizeString(value);
2465
- if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'urgent') {
2466
- return normalized;
2467
- }
2468
- return 'medium';
2469
- }
2470
- function isClosedIssueStatus(status) {
2471
- return status === 'closed' || status === 'resolved';
2472
- }
2473
- function isSymphonySessionRow(row) {
2474
- if (!row || typeof row !== 'object') {
2475
- return false;
2476
- }
2477
- const record = row;
2478
- const metadata = asRecord(record.metadata);
2479
- return metadata?.kind === 'symphony-run'
2480
- || record.policyVersion === SYMPHONY_POLICY_VERSION
2481
- || (typeof record.tool === 'string' && record.tool.startsWith('symphony:'));
2482
- }
2483
- function extractRunningSymphonyWorkersFromSession(row) {
2484
- const metadata = asRecord(row.metadata) ?? {};
2485
- const sessionStatus = normalizePodSymphonySessionStatus(metadata.status ?? row.status);
2486
- const workers = Array.isArray(metadata.workers) ? metadata.workers : [];
2487
- const updatedAt = safeOptionalDate(row.updatedAt);
2488
- if (workers.length === 0) {
2489
- if (sessionStatus !== 'running') {
2490
- return [];
2491
- }
2492
- return [{
2493
- status: sessionStatus,
2494
- backend: normalizeString(metadata.backend) ?? parseBackendFromTool(row.tool) ?? 'unknown',
2495
- mode: normalizeString(metadata.mode) ?? 'auto',
2496
- cwd: normalizeString(metadata.workspacePath),
2497
- autoModeSessionId: normalizeString(metadata.autoModeSessionId),
2498
- target: normalizeSymphonyWorkerTarget(asRecord(metadata.target)),
2499
- updatedAt,
2500
- }];
2501
- }
2502
- return workers
2503
- .map((item) => asRecord(item))
2504
- .filter((item) => item !== null)
2505
- .map((worker) => ({
2506
- status: normalizePodSymphonySessionStatus(worker.status ?? worker.taskStatus ?? sessionStatus),
2507
- backend: normalizeString(worker.backend) ?? normalizeString(metadata.backend) ?? parseBackendFromTool(row.tool) ?? 'unknown',
2508
- mode: normalizeString(worker.mode) ?? normalizeString(metadata.mode) ?? 'auto',
2509
- cwd: normalizeString(worker.workspacePath) ?? normalizeString(metadata.workspacePath),
2510
- autoModeSessionId: normalizeString(worker.autoModeSessionId) ?? normalizeString(metadata.autoModeSessionId),
2511
- target: normalizeSymphonyWorkerTarget(asRecord(worker.target), worker, asRecord(metadata.target)),
2512
- updatedAt,
2513
- }))
2514
- .filter((worker) => worker.status === 'running');
2515
- }
2516
- function deliveryRowToSymphonyReportStatus(row) {
2517
- const record = asRecord(row);
2518
- if (!record) {
2519
- return null;
2520
- }
2521
- const metadata = asRecord(record.metadata);
2522
- const payload = asRecord(record.payload);
2523
- if (record.kind !== 'report' && metadata?.reportKind !== 'worker-completion' && payload?.kind !== 'symphony_report') {
2524
- return null;
2525
- }
2526
- const completedAt = safeOptionalDate(record.completedAt);
2527
- const updatedAt = safeOptionalDate(record.updatedAt);
2528
- const createdAt = safeOptionalDate(record.createdAt);
2529
- const sortAt = completedAt?.getTime() ?? updatedAt?.getTime() ?? createdAt?.getTime() ?? 0;
2530
- const agent = normalizeString(payload?.agent);
2531
- const title = normalizeString(record.objective);
2532
- const summary = normalizeString(payload?.summary);
2533
- const task = normalizeString(record.task);
2534
- const archive = asRecord(metadata?.archive);
2535
- const delivery = normalizeString(payload?.delivery) ?? normalizeString(archive?.delivery);
2536
- const reportDelivery = normalizeString(payload?.reportDelivery) ?? normalizeString(record.id);
2537
- const run = normalizeString(payload?.run) ?? normalizeString(record.object);
2538
- const chat = normalizeString(record.chat);
2539
- const thread = normalizeString(record.thread);
2540
- const autoModeSessionId = normalizeString(payload?.autoModeSessionId);
2541
- const error = normalizeString(payload?.error) ?? normalizeString(record.error);
2542
- return {
2543
- status: normalizeString(payload?.outcome) ?? normalizeString(record.status) ?? 'completed',
2544
- backend: normalizeString(payload?.backend) ?? 'unknown',
2545
- ...(agent ? { agent } : {}),
2546
- ...(title ? { title } : {}),
2547
- ...(summary ? { summary } : {}),
2548
- ...(task ? { task } : {}),
2549
- ...(delivery ? { delivery } : {}),
2550
- ...(reportDelivery ? { reportDelivery } : {}),
2551
- ...(run ? { run } : {}),
2552
- ...(chat ? { chat } : {}),
2553
- ...(thread ? { thread } : {}),
2554
- ...(autoModeSessionId ? { autoModeSessionId } : {}),
2555
- ...(error ? { error } : {}),
2556
- ...(completedAt ? { completedAt: completedAt.toISOString() } : {}),
2557
- ...(updatedAt ? { updatedAt: updatedAt.toISOString() } : {}),
2558
- sortAt,
2559
- };
2560
- }
2561
- function normalizeSymphonyWorkerTarget(target, worker = {}, fallback = null) {
2562
- const normalized = {
2563
- label: normalizeString(target?.label) ?? normalizeString(worker.title) ?? normalizeString(fallback?.label),
2564
- agent: normalizeString(target?.agent) ?? normalizeString(worker.agent) ?? normalizeString(fallback?.agent),
2565
- chat: normalizeString(target?.chat) ?? normalizeString(worker.chat) ?? normalizeString(fallback?.chat),
2566
- };
2567
- return Object.values(normalized).some(Boolean) ? normalized : undefined;
2568
- }
2569
- function compareWorkerStatusUpdatedAt(left, right) {
2570
- return (right.updatedAt?.getTime() ?? 0) - (left.updatedAt?.getTime() ?? 0);
2571
- }
2572
- function normalizePodSymphonySessionStatus(value) {
2573
- const normalized = normalizeString(value);
2574
- if (normalized === 'active')
2575
- return 'running';
2576
- if (normalized === 'error')
2577
- return 'failed';
2578
- if (normalized === 'queued')
2579
- return 'planned';
2580
- return normalized ?? 'planned';
2581
- }
2582
- function parseBackendFromTool(value) {
2583
- const tool = normalizeString(value);
2584
- if (!tool?.startsWith('symphony:')) {
2585
- return undefined;
2586
- }
2587
- return tool.slice('symphony:'.length) || undefined;
2588
- }
2589
- function asRecord(value) {
2590
- return value && typeof value === 'object' && !Array.isArray(value)
2591
- ? value
2592
- : null;
2593
- }
2594
- function normalizeString(value) {
2595
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
2596
- }
2597
- function safeOptionalDate(value) {
2598
- if (!value) {
2599
- return undefined;
2600
- }
2601
- const date = value instanceof Date ? value : new Date(String(value));
2602
- return Number.isFinite(date.getTime()) ? date : undefined;
2603
- }
2604
- function toIsoDate(value) {
2605
- return (safeOptionalDate(value) ?? new Date()).toISOString();
2606
- }
2607
1472
  function buildSymphonyProjectionOperations(input) {
1473
+ const controlRows = buildSymphonyControlRows({
1474
+ plan: input.plan,
1475
+ webId: input.webId,
1476
+ stage: input.stage,
1477
+ stages: input.stages,
1478
+ });
2608
1479
  return [
2609
1480
  {
2610
1481
  id: 'symphony.prepare-resources',
@@ -2618,10 +1489,10 @@ function buildSymphonyProjectionOperations(input) {
2618
1489
  input.runtime.issueResource,
2619
1490
  input.runtime.taskResource,
2620
1491
  input.runtime.deliveryResource,
2621
- input.runtime.evidenceResource,
2622
- input.runtime.reportResource,
2623
1492
  input.runtime.runResource,
2624
1493
  input.runtime.runStepResource,
1494
+ input.runtime.evidenceResource,
1495
+ input.runtime.reportResource,
2625
1496
  input.runtime.agentResource,
2626
1497
  input.runtime.contactResource,
2627
1498
  input.runtime.auditResource,
@@ -2630,82 +1501,84 @@ function buildSymphonyProjectionOperations(input) {
2630
1501
  },
2631
1502
  },
2632
1503
  {
2633
- id: 'symphony.write-file-primary-documents',
1504
+ id: 'symphony.upsert-issue',
2634
1505
  kind: 'upsert',
2635
- apply: async () => {
2636
- await input.runtime.writePodFile?.(input.podSession, {
2637
- path: buildSymphonyIssueDocumentPath(input.plan.issue),
2638
- content: renderSymphonyIssueMarkdown(input.plan),
2639
- contentType: 'text/markdown; charset=utf-8',
2640
- });
2641
- },
1506
+ apply: () => upsertIssue(input.db, input.runtime, controlRows.issue),
2642
1507
  },
2643
- {
2644
- id: 'symphony.upsert-issue-meta',
1508
+ ...controlRows.issues.slice(1).map((row, index) => ({
1509
+ id: `symphony.upsert-follow-up-issue:${index + 1}`,
2645
1510
  kind: 'upsert',
2646
- apply: () => upsertIssue(input.db, input.runtime, buildSymphonyIssueRow(input.plan, input.webId)),
2647
- },
2648
- ...input.plan.workers.flatMap((worker, index) => [
2649
- {
2650
- id: `symphony.upsert-task:${index + 1}`,
2651
- kind: 'upsert',
2652
- apply: () => upsertTask(input.db, input.runtime, buildSymphonyTaskRow(input.plan, input.webId, worker)),
2653
- },
2654
- {
2655
- id: `symphony.upsert-delivery:${index + 1}`,
2656
- kind: 'upsert',
2657
- apply: () => upsertDelivery(input.db, input.runtime, buildSymphonyDeliveryRow(input.plan, input.webId, worker)),
2658
- },
2659
- {
2660
- id: `symphony.upsert-run:${index + 1}`,
2661
- kind: 'upsert',
2662
- apply: () => upsertRun(input.db, input.runtime, buildSymphonyRunRow(input.plan, input.webId, worker)),
2663
- },
2664
- ...input.stages.map((stage) => ({
2665
- id: `symphony.insert-run-step:${index + 1}:${stage}`,
2666
- kind: 'insert',
2667
- apply: () => insertRunStepOnce(input.db, input.runtime, buildSymphonyRunStepRow(input.plan, input.webId, worker, stage)),
2668
- })),
2669
- ]),
1511
+ apply: () => upsertIssue(input.db, input.runtime, row),
1512
+ })),
1513
+ ...controlRows.tasks.map((row, index) => ({
1514
+ id: `symphony.upsert-task:${index + 1}`,
1515
+ kind: 'upsert',
1516
+ apply: () => upsertTask(input.db, input.runtime, row),
1517
+ })),
1518
+ ...controlRows.deliveries.map((row, index) => ({
1519
+ id: `symphony.upsert-delivery:${index + 1}`,
1520
+ kind: 'upsert',
1521
+ apply: () => upsertDelivery(input.db, input.runtime, row),
1522
+ })),
1523
+ ...controlRows.runs.map((row, index) => ({
1524
+ id: `symphony.upsert-run:${index + 1}`,
1525
+ kind: 'upsert',
1526
+ apply: () => upsertRun(input.db, input.runtime, row),
1527
+ })),
1528
+ ...controlRows.runSteps.map((row, index) => ({
1529
+ id: `symphony.insert-run-step:${index + 1}`,
1530
+ kind: 'insert',
1531
+ apply: () => insertRunStepOnce(input.db, input.runtime, row),
1532
+ })),
1533
+ ...controlRows.evidence.map((row, index) => ({
1534
+ id: `symphony.insert-evidence:${index + 1}`,
1535
+ kind: 'insert',
1536
+ apply: () => insertEvidenceOnce(input.db, input.runtime, row),
1537
+ })),
1538
+ ...controlRows.reports.map((row, index) => ({
1539
+ id: `symphony.upsert-report:${index + 1}`,
1540
+ kind: 'upsert',
1541
+ apply: () => upsertReport(input.db, input.runtime, row),
1542
+ })),
2670
1543
  ...buildSymphonyReportOperations(input),
2671
1544
  ...buildSymphonyAgents(input.plan).map((agent) => ({
2672
1545
  id: `symphony.upsert-agent:${agent.id}`,
2673
1546
  kind: 'upsert',
2674
1547
  apply: () => upsertAgent(input.db, input.runtime, agent),
2675
1548
  })),
2676
- ...buildSymphonyContacts(input.plan, input.webId).map((contact) => ({
1549
+ ...controlRows.contacts.map((contact) => ({
2677
1550
  id: `symphony.upsert-contact:${contact.id}`,
2678
1551
  kind: 'upsert',
2679
1552
  apply: () => upsertContact(input.db, input.runtime, contact),
2680
1553
  })),
2681
- {
1554
+ ...controlRows.chats.map((row, index) => ({
2682
1555
  id: 'symphony.upsert-chat',
2683
1556
  kind: 'upsert',
2684
1557
  shouldRun: () => input.shouldUpsertChat,
2685
- apply: () => upsertChat(input.db, input.runtime, buildSymphonyChatRow(input.plan, input.webId, input.stage, input.latestMessage.content)),
2686
- },
2687
- ...buildSymphonyThreadRows(input.plan, input.webId, input.stage).map((row, index) => ({
1558
+ apply: () => upsertChat(input.db, input.runtime, row),
1559
+ })),
1560
+ ...controlRows.threads.map((row, index) => ({
2688
1561
  id: `symphony.upsert-thread:${index + 1}`,
2689
1562
  kind: 'upsert',
2690
1563
  apply: () => upsertThread(input.db, input.runtime, row),
2691
1564
  })),
2692
- ...input.plan.workers.map((worker, index) => ({
1565
+ ...controlRows.sessions.map((row, index) => ({
2693
1566
  id: `symphony.upsert-session:${index + 1}`,
2694
1567
  kind: 'upsert',
2695
- apply: () => upsertSession(input.db, input.runtime, buildSymphonySessionRow(input.plan, input.webId, worker)),
1568
+ apply: () => upsertSession(input.db, input.runtime, row),
2696
1569
  })),
2697
- ...input.stages.flatMap((stage) => [
1570
+ ...controlRows.messages.flatMap((row, index) => [
2698
1571
  {
2699
- id: `symphony.upsert-message:${stage}`,
1572
+ id: `symphony.upsert-message:${input.stages[index] ?? index + 1}`,
2700
1573
  kind: 'upsert',
2701
- apply: () => upsertMessage(input.db, input.runtime, buildStatusMessageRow(input.plan, input.webId, stage)),
2702
- },
2703
- {
2704
- id: `symphony.insert-audit:${stage}`,
2705
- kind: 'insert',
2706
- apply: () => insertAuditOnce(input.db, input.runtime, buildSymphonyAuditRow(input.plan, input.webId, stage)),
1574
+ apply: () => upsertMessage(input.db, input.runtime, row),
2707
1575
  },
2708
1576
  ]),
1577
+ ...input.stages.map((stage) => ({
1578
+ id: `symphony.insert-audit:${stage}`,
1579
+ kind: 'insert',
1580
+ apply: () => insertAuditOnce(input.db, input.runtime, buildSymphonyAuditRow(input.plan, input.webId, stage)),
1581
+ })),
2709
1582
  ];
2710
1583
  }
2711
1584
  function buildSymphonyReportOperations(input) {
@@ -2714,38 +1587,6 @@ function buildSymphonyReportOperations(input) {
2714
1587
  }
2715
1588
  const terminalStage = input.stage;
2716
1589
  return input.plan.workers.flatMap((worker, index) => [
2717
- {
2718
- id: `symphony.write-evidence-file:${index + 1}`,
2719
- kind: 'upsert',
2720
- apply: async () => {
2721
- await input.runtime.writePodFile?.(input.podSession, {
2722
- path: buildSymphonyEvidenceDocumentPath(worker, terminalStage),
2723
- content: renderSymphonyEvidenceMarkdown(input.plan, input.webId, worker, terminalStage),
2724
- contentType: 'text/markdown; charset=utf-8',
2725
- });
2726
- },
2727
- },
2728
- {
2729
- id: `symphony.upsert-evidence:${index + 1}`,
2730
- kind: 'upsert',
2731
- apply: () => upsertEvidence(input.db, input.runtime, buildSymphonyEvidenceRow(input.plan, input.webId, worker, terminalStage)),
2732
- },
2733
- {
2734
- id: `symphony.write-report-file:${index + 1}`,
2735
- kind: 'upsert',
2736
- apply: async () => {
2737
- await input.runtime.writePodFile?.(input.podSession, {
2738
- path: buildSymphonyReportDocumentPath(worker),
2739
- content: renderSymphonyReportMarkdown(input.plan, worker, terminalStage),
2740
- contentType: 'text/markdown; charset=utf-8',
2741
- });
2742
- },
2743
- },
2744
- {
2745
- id: `symphony.upsert-report:${index + 1}`,
2746
- kind: 'upsert',
2747
- apply: () => upsertReport(input.db, input.runtime, buildSymphonyReportRow(input.plan, input.webId, worker, terminalStage)),
2748
- },
2749
1590
  {
2750
1591
  id: `symphony.upsert-report-delivery:${index + 1}`,
2751
1592
  kind: 'upsert',
@@ -2771,21 +1612,21 @@ export const __symphonyPodProjectionInternal = {
2771
1612
  buildSymphonyMessageUri,
2772
1613
  auditActionForStage,
2773
1614
  buildSymphonyAuditRow,
2774
- buildStatusMessageRow,
2775
- buildSymphonyChatRow,
2776
- buildSymphonyThreadRow,
2777
- buildSymphonySessionRow,
2778
- buildSymphonyIssueRow,
1615
+ buildStatusMessageRow: buildSharedSymphonyStatusMessageRow,
1616
+ buildSymphonyChatRow: buildSharedSymphonyChatRow,
1617
+ buildSymphonyThreadRow: buildSharedSymphonyThreadRow,
1618
+ buildSymphonySessionRow: buildSharedSymphonySessionRow,
1619
+ buildSymphonyIssueRow: buildSharedSymphonyIssueRow,
2779
1620
  buildSymphonyIdeaRow,
2780
- buildSymphonyTaskRow,
2781
- buildSymphonyDeliveryRow,
2782
- buildSymphonyEvidenceRow,
1621
+ buildSymphonyTaskRow: buildSharedSymphonyTaskRow,
1622
+ buildSymphonyDeliveryRow: buildSharedSymphonyDeliveryRow,
2783
1623
  buildSymphonyReportDeliveryRow,
2784
1624
  buildSymphonyReportInboxNotificationRow,
2785
- buildSymphonyRunRow,
2786
- buildSymphonyRunStepRow,
1625
+ buildSymphonyRunRow: buildSharedSymphonyRunRow,
1626
+ buildSymphonyRunStepRow: buildSharedSymphonyRunStepRow,
1627
+ buildSymphonyRuntimeRunStepRow: buildSharedSymphonyRuntimeRunStepRow,
2787
1628
  buildSymphonyAgents,
2788
- buildSymphonyContacts,
1629
+ buildSymphonyContacts: buildSharedSymphonyContactRows,
2789
1630
  withChatThreadRefs,
2790
1631
  normalizeSymphonyRunPlan,
2791
1632
  };