@undefineds.co/linx 0.3.20 → 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 (97) 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 +53 -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/linx-cloud-errors.js +0 -5
  15. package/dist/lib/linx-cloud-errors.js.map +1 -1
  16. package/dist/lib/linx-status-line.js +1 -8
  17. package/dist/lib/linx-status-line.js.map +1 -1
  18. package/dist/lib/models.js +3 -2
  19. package/dist/lib/models.js.map +1 -1
  20. package/dist/lib/pi-adapter/auth.js +68 -0
  21. package/dist/lib/pi-adapter/auth.js.map +1 -0
  22. package/dist/lib/pi-adapter/branding.js +34 -103
  23. package/dist/lib/pi-adapter/branding.js.map +1 -1
  24. package/dist/lib/pi-adapter/interactive.js +35 -49
  25. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  26. package/dist/lib/pi-adapter/pod-mirror.js +38 -107
  27. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  28. package/dist/lib/pi-adapter/pod-native.js +2 -0
  29. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  30. package/dist/lib/pi-adapter/pod-tools.js +140 -0
  31. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  32. package/dist/lib/pi-adapter/runtime.js +2 -12
  33. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  34. package/dist/lib/pi-adapter/session.js +13 -17
  35. package/dist/lib/pi-adapter/session.js.map +1 -1
  36. package/dist/lib/pi-adapter/stream.js +2 -20
  37. package/dist/lib/pi-adapter/stream.js.map +1 -1
  38. package/dist/lib/pod-chat-store.js +53 -4
  39. package/dist/lib/pod-chat-store.js.map +1 -1
  40. package/dist/lib/resource-identity.js +2 -0
  41. package/dist/lib/resource-identity.js.map +1 -0
  42. package/dist/lib/status-line-command.js +2 -2
  43. package/dist/lib/status-line-command.js.map +1 -1
  44. package/dist/lib/symphony/archive.js +15 -37
  45. package/dist/lib/symphony/archive.js.map +1 -1
  46. package/dist/lib/symphony/pod-projection.js +189 -1346
  47. package/dist/lib/symphony/pod-projection.js.map +1 -1
  48. package/dist/lib/symphony-command.js +209 -109
  49. package/dist/lib/symphony-command.js.map +1 -1
  50. package/dist/plugins/linx-symphony-codex/.codex-plugin/plugin.json +38 -0
  51. package/dist/plugins/linx-symphony-codex/.mcp.json +10 -0
  52. package/dist/plugins/linx-symphony-codex/README.md +9 -0
  53. package/dist/plugins/linx-symphony-codex/hooks.json +60 -0
  54. package/dist/plugins/linx-symphony-codex/scripts/symphony-hook-events.mjs +119 -0
  55. package/dist/plugins/linx-symphony-codex/scripts/symphony-mcp.mjs +335 -0
  56. package/dist/plugins/linx-symphony-codex/skills/symphony/SKILL.md +791 -0
  57. package/dist/skills/symphony/SKILL.md +7 -0
  58. package/dist/skills/xpod-cli/SKILL.md +2 -13
  59. package/package.json +4 -4
  60. package/vendor/agent-runtime/dist/chat-reconciler.d.ts +33 -0
  61. package/vendor/agent-runtime/dist/chat-reconciler.js +108 -0
  62. package/vendor/agent-runtime/dist/index.d.ts +4 -1
  63. package/vendor/agent-runtime/dist/index.js +4 -1
  64. package/vendor/agent-runtime/dist/matrix-client.d.ts +149 -0
  65. package/vendor/agent-runtime/dist/matrix-client.js +220 -0
  66. package/vendor/agent-runtime/dist/pod-resource-identity.d.ts +17 -0
  67. package/vendor/agent-runtime/dist/pod-resource-identity.js +54 -0
  68. package/vendor/agent-runtime/dist/reconciler.d.ts +0 -11
  69. package/vendor/agent-runtime/dist/reconciler.js +5 -43
  70. package/vendor/agent-runtime/dist/symphony.d.ts +272 -27
  71. package/vendor/agent-runtime/dist/symphony.js +1268 -21
  72. package/vendor/agent-runtime/dist/workspace.d.ts +61 -0
  73. package/vendor/agent-runtime/dist/workspace.js +81 -0
  74. package/vendor/agent-runtime/package.json +5 -1
  75. package/vendor/stores/dist/current-pod-base.d.ts +2 -0
  76. package/vendor/stores/dist/current-pod-base.js +14 -0
  77. package/vendor/stores/dist/exact-records.d.ts +7 -0
  78. package/vendor/stores/dist/exact-records.js +87 -0
  79. package/vendor/stores/dist/index.d.ts +1 -0
  80. package/vendor/stores/dist/index.js +1 -0
  81. package/vendor/stores/dist/login.d.ts +51 -0
  82. package/vendor/stores/dist/login.js +195 -0
  83. package/vendor/stores/dist/pod-collection.d.ts +28 -0
  84. package/vendor/stores/dist/pod-collection.js +194 -0
  85. package/vendor/stores/dist/pod-write-guard.d.ts +5 -0
  86. package/vendor/stores/dist/pod-write-guard.js +133 -0
  87. package/vendor/stores/dist/symphony-control.d.ts +245 -0
  88. package/vendor/stores/dist/symphony-control.js +2175 -0
  89. package/vendor/stores/package.json +14 -0
  90. package/dist/lib/capture/persistence.js +0 -377
  91. package/dist/lib/capture/persistence.js.map +0 -1
  92. package/dist/lib/capture/tool.js +0 -242
  93. package/dist/lib/capture/tool.js.map +0 -1
  94. package/dist/skills/basic/SKILL.md +0 -46
  95. package/dist/skills/capture/SKILL.md +0 -165
  96. package/vendor/agent-runtime/dist/coordination.d.ts +0 -93
  97. package/vendor/agent-runtime/dist/coordination.js +0 -145
@@ -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 = normalizeWorkerWorkspaceRef(worker.session.workspaceRef ?? plan.session.workspaceRef, 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.workspace ? { uri: workspace.workspace } : {}),
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 normalizeWorkerWorkspaceRef(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?.workspace ? { workspace: workspace.workspace } : {}),
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,147 +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
- status,
1030
- tool: `symphony:${worker.session.backend}`,
1031
- tokenUsage: 0,
1032
- messages: worker.session.messages,
1033
- policyVersion: SYMPHONY_POLICY_VERSION,
1034
- metadata: {
1035
- kind: 'symphony-run',
1036
- surface: 'symphony',
1037
- issue: plan.issue.uri,
1038
- task: worker.task,
1039
- delivery: worker.delivery.uri,
1040
- session: worker.session.uri,
1041
- issuer: plan.issue.issuer,
1042
- worker: workerSummary,
1043
- workers: [workerSummary],
1044
- backend: worker.session.backend,
1045
- mode: worker.session.mode,
1046
- model: worker.session.model,
1047
- workspacePath: worker.session.cwd,
1048
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1049
- reconciler: buildSymphonyReconcilerMetadata(worker),
1050
- autoModeSessionId: worker.session.autoModeSessionId,
1051
- exitCode: worker.session.exitCode,
1052
- dryRun: worker.session.dryRun,
1053
- error: worker.session.error ?? worker.delivery.error ?? plan.issue.error,
1054
- target: worker.session.target,
1055
- },
1056
- createdAt,
1057
- updatedAt,
1058
- ...(status === 'completed' || status === 'error' ? { archivedAt: updatedAt } : {}),
1059
- };
1060
- }
1061
- function buildSymphonyIssueRow(plan, webId) {
1062
- const createdAt = safeDate(plan.issue.createdAt);
1063
- const updatedAt = safeDate(plan.issue.updatedAt);
1064
- const secretaryContact = buildSecretaryContactIri(webId);
1065
- return {
1066
- id: buildSymphonyIssueId(plan.issue),
1067
- // File-primary: title remains a compact index label for existing Issue schemas.
1068
- // The full problem statement and acceptance narrative live in issue.md.
1069
- title: plan.issue.title,
1070
- document: podFileUrlFromWebId(webId, buildSymphonyIssueDocumentPath(plan.issue)),
1071
- description: undefined,
1072
- status: plan.issue.status,
1073
- priority: plan.issue.priority,
1074
- labels: ['symphony'],
1075
- chat: selectTargetChatIri(plan.session.target?.chat, webId, plan),
1076
- thread: selectTargetThreadIri(plan.session.target?.thread, webId, plan),
1077
- tasks: Array.from(new Set((plan.issue.tasks?.length ? plan.issue.tasks : plan.workers.map((worker) => worker.task))
1078
- .map((task) => normalizeSymphonyTaskIri(webId, task)))),
1079
- createdBy: plan.issue.issuer.webId ?? webId,
1080
- assignedTo: secretaryContact,
1081
- createdAt,
1082
- updatedAt,
1083
- ...(plan.issue.closedAt ? { closedAt: safeDate(plan.issue.closedAt) } : {}),
1084
- };
1085
- }
1086
680
  function buildSymphonyIdeaRow(idea, webId) {
1087
681
  const createdAt = safeDate(idea.createdAt);
1088
682
  const updatedAt = safeDate(idea.updatedAt);
1089
683
  return {
1090
684
  id: getSymphonyArchiveKey(idea.uri),
1091
685
  summary: idea.summary,
1092
- document: podFileUrlFromWebId(webId, buildSymphonyIdeaDocumentPath(idea)),
1093
- // File-primary: the source text lives in idea.md; TTL keeps only routing/index facts.
1094
- input: undefined,
686
+ input: idea.input,
1095
687
  status: idea.status,
1096
688
  commitment: idea.commitment,
1097
689
  affectedArea: idea.affectedArea,
1098
- currentUnderstanding: undefined,
1099
- openQuestions: undefined,
690
+ currentUnderstanding: idea.currentUnderstanding,
691
+ openQuestions: idea.openQuestions,
1100
692
  related: idea.relatedRecords,
1101
- conflicts: undefined,
1102
- nextStep: undefined,
693
+ conflicts: idea.conflicts,
694
+ nextStep: idea.nextStep,
1103
695
  promotedTo: idea.promotedTo,
1104
696
  chat: idea.chat,
1105
697
  thread: idea.thread,
@@ -1107,277 +699,25 @@ function buildSymphonyIdeaRow(idea, webId) {
1107
699
  createdBy: webId,
1108
700
  metadata: {
1109
701
  surface: 'symphony',
1110
- filePrimary: true,
1111
- documentPath: buildSymphonyIdeaDocumentPath(idea),
1112
702
  ...buildSymphonyArchiveMetadata({ idea: idea.uri }),
1113
703
  },
1114
704
  createdAt,
1115
705
  updatedAt,
1116
706
  };
1117
707
  }
1118
- function mapSymphonyTaskStatus(status) {
1119
- if (status === 'running')
1120
- return 'active';
1121
- if (status === 'pending')
1122
- return 'open';
1123
- return status;
1124
- }
1125
- function mapSymphonyRunStatus(status) {
1126
- if (status === 'planned')
1127
- return 'queued';
1128
- if (status === 'running')
1129
- return 'running';
1130
- if (status === 'completed')
1131
- return 'completed';
1132
- if (status === 'failed')
1133
- return 'failed';
1134
- return 'queued';
1135
- }
1136
- function buildSymphonyTaskRow(plan, webId, worker) {
1137
- const createdAt = safeDate(worker.taskRecord.createdAt);
1138
- const updatedAt = safeDate(worker.taskRecord.updatedAt);
1139
- const workerContact = buildWorkerContactIri(webId, worker);
1140
- const workerAgent = buildWorkerAgentIri(webId, worker);
1141
- return {
1142
- id: taskResource.buildId({ id: buildSymphonyTaskKey(worker.task) }),
1143
- title: worker.taskRecord.title,
1144
- instruction: worker.taskRecord.objective,
1145
- prompt: worker.delivery.projection.prompt,
1146
- issue: buildSymphonyIssueIri(webId, plan.issue),
1147
- message: plan.issue.messages?.at(-1),
1148
- thread: selectWorkerThreadIri(plan, webId, worker),
1149
- workspace: pathToWorkspaceUri(worker.session.cwd) ?? pathToWorkspaceUri(plan.session.cwd) ?? 'file:///',
1150
- status: mapSymphonyTaskStatus(worker.taskRecord.status),
1151
- priority: plan.issue.priority,
1152
- assignedTo: workerContact,
1153
- source: buildSymphonyIssueIri(webId, plan.issue),
1154
- metadata: {
1155
- surface: 'symphony',
1156
- ...buildSymphonyArchiveMetadata({ task: worker.taskRecord.uri }),
1157
- acceptanceCriteria: worker.taskRecord.acceptanceCriteria,
1158
- backend: worker.session.backend,
1159
- target: worker.session.target,
1160
- assignedContact: workerContact,
1161
- assignedAgent: workerAgent,
1162
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1163
- spaceContract: buildSymphonySpaceContract(plan, webId, worker),
1164
- podAccessPolicy: buildSymphonyWorkerPodAccessPolicy(plan, webId, worker),
1165
- reconciler: buildSymphonyReconcilerMetadata(worker),
1166
- },
1167
- createdAt,
1168
- updatedAt,
1169
- };
1170
- }
1171
- function buildSymphonyDeliveryRow(plan, webId, worker) {
1172
- const createdAt = safeDate(worker.delivery.createdAt);
1173
- const updatedAt = safeDate(worker.delivery.updatedAt);
1174
- const secretaryAgent = agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID });
1175
- const secretaryContact = buildSecretaryContactIri(webId);
1176
- const workerAgent = buildWorkerAgentIri(webId, worker);
1177
- const workerContact = buildWorkerContactIri(webId, worker);
1178
- return {
1179
- id: deliveryResource.buildId({
1180
- id: getSymphonyArchiveKey(worker.delivery.uri),
1181
- task: buildSymphonyTaskIri(webId, worker.task),
1182
- createdAt,
1183
- }),
1184
- kind: worker.delivery.type,
1185
- status: worker.delivery.status,
1186
- task: buildSymphonyTaskIri(webId, worker.task),
1187
- source: secretaryContact,
1188
- target: workerContact,
1189
- chat: selectWorkerChatIri(plan, webId, worker),
1190
- thread: selectWorkerThreadIri(plan, webId, worker),
1191
- targetThread: selectWorkerThreadIri(plan, webId, worker),
1192
- targetSession: worker.session.uri,
1193
- actor: secretaryAgent,
1194
- object: buildSymphonyTaskIri(webId, worker.task),
1195
- objective: worker.taskRecord.objective,
1196
- payload: {
1197
- issue: buildSymphonyIssueIri(webId, plan.issue),
1198
- acceptanceCriteria: worker.taskRecord.acceptanceCriteria,
1199
- backend: worker.session.backend,
1200
- mode: worker.session.mode,
1201
- target: worker.session.target,
1202
- targetContact: workerContact,
1203
- targetAgent: workerAgent,
1204
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1205
- spaceContract: buildSymphonySpaceContract(plan, webId, worker),
1206
- podAccessPolicy: buildSymphonyWorkerPodAccessPolicy(plan, webId, worker),
1207
- reconciler: buildSymphonyReconcilerMetadata(worker),
1208
- },
1209
- projection: worker.delivery.projection,
1210
- projectedRole: worker.delivery.projection.runtimeRole,
1211
- metadata: {
1212
- surface: 'symphony',
1213
- ...buildSymphonyArchiveMetadata({
1214
- issue: plan.issue.uri,
1215
- task: worker.task,
1216
- delivery: worker.delivery.uri,
1217
- session: worker.session.uri,
1218
- }),
1219
- autoModeSessionId: worker.delivery.autoModeSessionId,
1220
- sourceAgent: secretaryAgent,
1221
- targetContact: workerContact,
1222
- targetAgent: workerAgent,
1223
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1224
- spaceContract: buildSymphonySpaceContract(plan, webId, worker),
1225
- podAccessPolicy: buildSymphonyWorkerPodAccessPolicy(plan, webId, worker),
1226
- reconciler: buildSymphonyReconcilerMetadata(worker),
1227
- },
1228
- error: worker.delivery.error,
1229
- createdAt,
1230
- dispatchedAt: worker.delivery.status === 'dispatched' || worker.delivery.status === 'completed'
1231
- ? updatedAt
1232
- : undefined,
1233
- completedAt: worker.delivery.completedAt ? safeDate(worker.delivery.completedAt) : undefined,
1234
- updatedAt,
1235
- };
1236
- }
1237
- function buildSymphonyReportRow(plan, webId, worker, stage) {
1238
- const completedAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1239
- const workerAgent = buildWorkerAgentIri(webId, worker);
1240
- const run = buildSymphonyRunIri(webId, worker);
1241
- const task = buildSymphonyTaskIri(webId, worker.task);
1242
- const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
1243
- const summary = status === 'completed'
1244
- ? `${worker.taskRecord.title} completed.`
1245
- : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
1246
- const evidence = buildSymphonyEvidenceIri(webId, worker, stage);
1247
- return {
1248
- id: reportResource.buildId({
1249
- id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
1250
- task,
1251
- createdAt: completedAt,
1252
- }),
1253
- reportKind: 'handoff',
1254
- status: 'published',
1255
- outcome: status === 'completed' ? 'accepted' : 'blocked',
1256
- about: run,
1257
- issue: buildSymphonyIssueIri(webId, plan.issue),
1258
- task,
1259
- delivery: buildSymphonyDeliveryIri(webId, worker),
1260
- run,
1261
- thread: selectWorkerThreadIri(plan, webId, worker),
1262
- evidence: [
1263
- evidence,
1264
- ],
1265
- summary,
1266
- actor: workerAgent,
1267
- source: podFileUrlFromWebId(webId, buildSymphonyReportDocumentPath(worker)),
1268
- metricFacts: {
1269
- backend: worker.session.backend,
1270
- agent: worker.session.target.agent,
1271
- autoModeSessionId: worker.session.autoModeSessionId,
1272
- exitCode: worker.session.exitCode,
1273
- },
1274
- metadata: {
1275
- surface: 'symphony',
1276
- filePrimary: true,
1277
- reportFile: buildSymphonyReportDocumentPath(worker),
1278
- reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1279
- postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
1280
- ...buildSymphonyArchiveMetadata({
1281
- issue: plan.issue.uri,
1282
- task: worker.task,
1283
- delivery: worker.delivery.uri,
1284
- session: worker.session.uri,
1285
- }),
1286
- },
1287
- createdAt: completedAt,
1288
- publishedAt: completedAt,
1289
- updatedAt: completedAt,
1290
- };
1291
- }
1292
- function buildSymphonyEvidenceRow(plan, webId, worker, stage) {
1293
- const createdAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1294
- const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
1295
- const run = buildSymphonyRunIri(webId, worker);
1296
- const task = buildSymphonyTaskIri(webId, worker.task);
1297
- const delivery = buildSymphonyDeliveryIri(webId, worker);
1298
- const runStep = buildSymphonyRunStepIri(webId, worker, stage);
1299
- const workerAgent = buildWorkerAgentIri(webId, worker);
1300
- return {
1301
- id: evidenceResource.buildId({
1302
- id: `${getSymphonyArchiveKey(worker.session.uri)}-${stage}`,
1303
- createdAt,
1304
- }),
1305
- evidenceKind: EvidenceKind.RUNTIME_LOG,
1306
- about: run,
1307
- issue: buildSymphonyIssueIri(webId, plan.issue),
1308
- task,
1309
- delivery,
1310
- run,
1311
- thread: selectWorkerThreadIri(plan, webId, worker),
1312
- summary: status === 'completed'
1313
- ? `${worker.taskRecord.title} completed with runtime status ${worker.session.status}.`
1314
- : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error ?? 'worker did not complete successfully.'}`,
1315
- source: podFileUrlFromWebId(webId, buildSymphonyEvidenceDocumentPath(worker, stage)),
1316
- actor: workerAgent,
1317
- outcome: status,
1318
- metadata: {
1319
- surface: 'symphony',
1320
- filePrimary: true,
1321
- evidenceFile: buildSymphonyEvidenceDocumentPath(worker, stage),
1322
- sourceRunStep: runStep,
1323
- report: buildSymphonyReportIri(webId, worker),
1324
- reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1325
- postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
1326
- runtime: {
1327
- backend: worker.session.backend,
1328
- model: worker.session.model,
1329
- autoModeSessionId: worker.session.autoModeSessionId,
1330
- exitCode: worker.session.exitCode,
1331
- status: worker.session.status,
1332
- },
1333
- ...buildSymphonyArchiveMetadata({
1334
- issue: plan.issue.uri,
1335
- task: worker.task,
1336
- delivery: worker.delivery.uri,
1337
- session: worker.session.uri,
1338
- }),
1339
- },
1340
- createdAt,
1341
- };
1342
- }
1343
- function buildPostRunReconciliationMetadata(plan, webId, worker, stage) {
1344
- return {
1345
- required: true,
1346
- status: 'pending_secretary_review',
1347
- owner: buildSecretaryContactIri(webId),
1348
- sourceIssue: buildSymphonyIssueIri(webId, plan.issue),
1349
- sourceTask: buildSymphonyTaskIri(webId, worker.task),
1350
- sourceDelivery: buildSymphonyDeliveryIri(webId, worker),
1351
- sourceRun: buildSymphonyRunIri(webId, worker),
1352
- sourceRunStep: buildSymphonyRunStepIri(webId, worker, stage),
1353
- sourceEvidence: buildSymphonyEvidenceIri(webId, worker, stage),
1354
- sourceReport: buildSymphonyReportIri(webId, worker),
1355
- classifications: [
1356
- 'same_issue_task',
1357
- 'new_issue',
1358
- 'idea',
1359
- 'evidence_only',
1360
- 'ask_user',
1361
- ],
1362
- rule: 'Secretary must reconcile acceptance and follow-up before task closure.',
1363
- };
1364
- }
1365
708
  function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1366
709
  const completedAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1367
- const workerAgent = buildWorkerAgentIri(webId, worker);
1368
- const workerContact = buildWorkerContactIri(webId, worker);
710
+ const workerAgent = agentResource.buildIri(webId, {
711
+ id: buildWorkerAgentId(worker.session.backend, worker.session.target.agent),
712
+ });
1369
713
  const secretaryAgent = agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID });
1370
- const secretaryContact = buildSecretaryContactIri(webId);
1371
714
  const run = buildSymphonyRunIri(webId, worker);
1372
- const report = buildSymphonyReportIri(webId, worker);
1373
715
  const task = buildSymphonyTaskIri(webId, worker.task);
1374
716
  const originalDelivery = buildSymphonyDeliveryIri(webId, worker);
1375
717
  const status = worker.session.status === 'failed' || stage === 'failed' ? 'failed' : 'completed';
1376
718
  const summary = status === 'completed'
1377
719
  ? `${worker.taskRecord.title} completed.`
1378
720
  : `${worker.taskRecord.title} failed: ${worker.session.error ?? worker.delivery.error ?? 'worker did not complete successfully.'}`;
1379
- const evidence = buildSymphonyEvidenceIri(webId, worker, stage);
1380
- const sourceRunStep = buildSymphonyRunStepIri(webId, worker, stage);
1381
721
  return {
1382
722
  id: deliveryResource.buildId({
1383
723
  id: `${getSymphonyArchiveKey(worker.session.uri)}-report`,
@@ -1387,14 +727,14 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1387
727
  kind: 'report',
1388
728
  status: 'completed',
1389
729
  task,
1390
- source: workerContact,
1391
- target: secretaryContact,
730
+ source: workerAgent,
731
+ target: secretaryAgent,
1392
732
  chat: selectWorkerChatIri(plan, webId, worker),
1393
733
  thread: selectWorkerThreadIri(plan, webId, worker),
1394
734
  targetThread: selectTargetThreadIri(plan.issue.thread ?? worker.session.target?.thread, webId, plan),
1395
735
  targetSession: buildSymphonyControlSessionUri(webId, plan),
1396
736
  actor: workerAgent,
1397
- object: report,
737
+ object: run,
1398
738
  objective: summary,
1399
739
  payload: {
1400
740
  kind: 'symphony_report',
@@ -1403,25 +743,18 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1403
743
  issue: buildSymphonyIssueIri(webId, plan.issue),
1404
744
  task,
1405
745
  delivery: originalDelivery,
1406
- report,
1407
746
  reportDelivery: buildSymphonyReportDeliveryIri(webId, worker),
1408
747
  session: buildSymphonyControlSessionUri(webId, plan),
1409
748
  run,
1410
749
  backend: worker.session.backend,
1411
750
  agent: worker.session.target.agent,
1412
- contact: worker.session.target.contact ?? buildWorkerContactId(worker),
1413
- sourceAgent: workerAgent,
1414
- sourceContact: workerContact,
1415
751
  autoModeSessionId: worker.session.autoModeSessionId,
1416
752
  exitCode: worker.session.exitCode,
1417
753
  error: worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error,
1418
- reportFile: buildSymphonyReportDocumentPath(worker),
1419
- evidenceFile: buildSymphonyEvidenceDocumentPath(worker, stage),
1420
- postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
754
+ acceptanceReview: worker.delivery.acceptanceReview ?? worker.taskRecord.acceptanceReview ?? worker.session.acceptanceReview,
1421
755
  evidence: {
1422
- statusMessage: buildSymphonyMessageUri(webId, plan, buildStatusMessageRow(plan, webId, stage)),
1423
- sourceRunStep,
1424
- evidence,
756
+ statusMessage: buildSharedSymphonyMessageIri(webId, plan, buildSharedSymphonyStatusMessageRow(plan, webId, stage)),
757
+ runStep: buildSymphonyRunStepIri(webId, worker, stage),
1425
758
  },
1426
759
  },
1427
760
  projection: {
@@ -1438,11 +771,7 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1438
771
  session: worker.session.uri,
1439
772
  }),
1440
773
  reportKind: 'worker-completion',
1441
- filePrimary: true,
1442
- reportFile: buildSymphonyReportDocumentPath(worker),
1443
- evidence,
1444
- evidenceFile: buildSymphonyEvidenceDocumentPath(worker, stage),
1445
- postRunReconciliation: buildPostRunReconciliationMetadata(plan, webId, worker, stage),
774
+ acceptanceReview: worker.delivery.acceptanceReview ?? worker.taskRecord.acceptanceReview ?? worker.session.acceptanceReview,
1446
775
  },
1447
776
  error: status === 'failed' ? worker.session.error ?? worker.delivery.error ?? worker.taskRecord.error : undefined,
1448
777
  createdAt: completedAt,
@@ -1452,101 +781,13 @@ function buildSymphonyReportDeliveryRow(plan, webId, worker, stage) {
1452
781
  updatedAt: completedAt,
1453
782
  };
1454
783
  }
1455
- function buildSymphonyRunRow(plan, webId, worker) {
1456
- const createdAt = safeDate(worker.session.createdAt);
1457
- const updatedAt = safeDate(worker.session.updatedAt);
1458
- return {
1459
- id: runResource.buildId({
1460
- id: getSymphonyArchiveKey(worker.session.uri),
1461
- task: buildSymphonyTaskIri(webId, worker.task),
1462
- createdAt,
1463
- }),
1464
- task: buildSymphonyTaskIri(webId, worker.task),
1465
- delivery: buildSymphonyDeliveryIri(webId, worker),
1466
- trigger: plan.issue.messages?.at(-1) ?? buildSymphonyIssueIri(webId, plan.issue),
1467
- input: buildSymphonyDeliveryIri(webId, worker),
1468
- thread: selectWorkerThreadIri(plan, webId, worker),
1469
- workspace: pathToWorkspaceUri(worker.session.cwd) ?? pathToWorkspaceUri(plan.session.cwd) ?? 'file:///',
1470
- status: mapSymphonyRunStatus(worker.session.status),
1471
- runner: worker.session.backend,
1472
- prompt: worker.delivery.projection.prompt,
1473
- externalRunId: worker.session.autoModeSessionId,
1474
- error: worker.session.error,
1475
- metadata: {
1476
- surface: 'symphony',
1477
- ...buildSymphonyArchiveMetadata({ session: worker.session.uri }),
1478
- mode: worker.session.mode,
1479
- model: worker.session.model,
1480
- target: worker.session.target,
1481
- workspace: buildSymphonyWorkspaceMetadata(plan, worker),
1482
- spaceContract: buildSymphonySpaceContract(plan, webId, worker),
1483
- podAccessPolicy: buildSymphonyWorkerPodAccessPolicy(plan, webId, worker),
1484
- reconciler: buildSymphonyReconcilerMetadata(worker),
1485
- exitCode: worker.session.exitCode,
1486
- dryRun: worker.session.dryRun,
1487
- },
1488
- createdAt,
1489
- startedAt: worker.session.status === 'running' || worker.session.status === 'completed' || worker.session.status === 'failed'
1490
- ? updatedAt
1491
- : undefined,
1492
- completedAt: worker.session.completedAt ? safeDate(worker.session.completedAt) : undefined,
1493
- updatedAt,
1494
- };
1495
- }
1496
- function buildSymphonyRunStepRow(plan, webId, worker, stage) {
1497
- const run = buildSymphonyRunIri(webId, worker);
1498
- const createdAt = stage === 'planned' ? safeDate(worker.session.createdAt) : safeDate(worker.session.updatedAt);
1499
- const stepType = stage === 'planned'
1500
- ? 'run.created'
1501
- : stage === 'running'
1502
- ? 'run.started'
1503
- : stage === 'completed'
1504
- ? 'run.completed'
1505
- : 'run.failed';
1506
- return {
1507
- id: runStepResource.buildId({
1508
- id: `${getSymphonyArchiveKey(worker.session.uri)}-${stage}`,
1509
- run,
1510
- }),
1511
- run,
1512
- stepType,
1513
- message: buildStatusContent(plan, stage),
1514
- data: {
1515
- surface: 'symphony',
1516
- stage,
1517
- issue: buildSymphonyIssueIri(webId, plan.issue),
1518
- task: buildSymphonyTaskIri(webId, worker.task),
1519
- delivery: buildSymphonyDeliveryIri(webId, worker),
1520
- archive: buildSymphonyArchiveRefs({ session: worker.session.uri }),
1521
- autoModeSessionId: worker.session.autoModeSessionId,
1522
- },
1523
- createdAt,
1524
- };
1525
- }
1526
- function normalizeSymphonyActorKey(value, fallback) {
1527
- return (value ?? fallback)
784
+ function buildWorkerAgentId(backend, agent) {
785
+ const suffix = (agent ?? `${backend}-worker`)
1528
786
  .trim()
1529
787
  .replace(/[^a-zA-Z0-9._-]/gu, '-')
1530
788
  .replace(/-+/gu, '-')
1531
- .replace(/^-|-$/gu, '')
1532
- || fallback;
1533
- }
1534
- function buildWorkerContactId(worker) {
1535
- return normalizeSymphonyActorKey(worker.session.target.contact ?? worker.session.target.agent ?? worker.delivery.targetAgent, worker.session.backend);
1536
- }
1537
- function buildWorkerAgentId(backend, agent, contact) {
1538
- return normalizeSymphonyActorKey(agent ?? contact, backend);
1539
- }
1540
- function buildSecretaryContactIri(webId) {
1541
- return contactResource.buildIri(webId, { id: SYMPHONY_CONTACT_ID });
1542
- }
1543
- function buildWorkerAgentIri(webId, worker) {
1544
- return agentResource.buildIri(webId, {
1545
- id: buildWorkerAgentId(worker.session.backend, worker.session.target.agent, worker.session.target.contact),
1546
- });
1547
- }
1548
- function buildWorkerContactIri(webId, worker) {
1549
- return contactResource.buildIri(webId, { id: buildWorkerContactId(worker) });
789
+ .replace(/^-|-$/gu, '');
790
+ return `symphony-${suffix || `${backend}-worker`}`;
1550
791
  }
1551
792
  function buildSymphonyAgents(plan) {
1552
793
  const now = safeDate(plan.session.updatedAt);
@@ -1563,7 +804,7 @@ function buildSymphonyAgents(plan) {
1563
804
  ];
1564
805
  const seen = new Set(agents.map((agent) => agent.id));
1565
806
  for (const worker of plan.workers) {
1566
- 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);
1567
808
  if (seen.has(id)) {
1568
809
  continue;
1569
810
  }
@@ -1580,146 +821,6 @@ function buildSymphonyAgents(plan) {
1580
821
  }
1581
822
  return agents;
1582
823
  }
1583
- function buildSymphonyContacts(plan, webId) {
1584
- const now = safeDate(plan.session.updatedAt);
1585
- const contacts = [
1586
- {
1587
- id: SYMPHONY_CONTACT_ID,
1588
- name: 'AI Secretary',
1589
- entity: agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID }),
1590
- rdfType: ContactClass.AGENT,
1591
- contactType: ContactType.AGENT,
1592
- createdAt: now,
1593
- updatedAt: now,
1594
- },
1595
- ];
1596
- const seen = new Set(contacts.map((contact) => contact.id));
1597
- for (const worker of plan.workers) {
1598
- const id = buildWorkerContactId(worker);
1599
- if (seen.has(id)) {
1600
- continue;
1601
- }
1602
- seen.add(id);
1603
- contacts.push({
1604
- id,
1605
- name: worker.session.target.label ?? worker.session.target.contact ?? worker.session.target.agent ?? backendDisplayName(worker.session.backend),
1606
- entity: buildWorkerAgentIri(webId, worker),
1607
- rdfType: ContactClass.AGENT,
1608
- contactType: ContactType.AGENT,
1609
- createdAt: now,
1610
- updatedAt: now,
1611
- });
1612
- }
1613
- return contacts;
1614
- }
1615
- function buildProgressBlock(plan, stage) {
1616
- const statusByStage = {
1617
- planned: 'pending',
1618
- running: 'running',
1619
- completed: 'done',
1620
- failed: 'error',
1621
- };
1622
- const workerSteps = plan.workers.map((worker, index) => ({
1623
- id: `${buildSymphonyThreadId(plan)}-worker-${index + 1}`,
1624
- label: `${worker.session.target.label ?? worker.session.target.agent ?? backendDisplayName(worker.session.backend)} worker`,
1625
- status: worker.session.status === 'completed'
1626
- ? 'done'
1627
- : worker.session.status === 'failed'
1628
- ? 'error'
1629
- : worker.session.status === 'running'
1630
- ? 'running'
1631
- : statusByStage[stage],
1632
- detail: worker.session.autoModeSessionId ?? worker.session.uri,
1633
- }));
1634
- return {
1635
- type: 'task_progress',
1636
- task: plan.task,
1637
- title: plan.issue.title,
1638
- steps: [
1639
- {
1640
- id: `${buildSymphonyThreadId(plan)}-plan`,
1641
- label: 'Secretary created task projection',
1642
- status: stage === 'planned' ? 'running' : 'done',
1643
- detail: plan.issue.uri,
1644
- },
1645
- ...workerSteps,
1646
- {
1647
- id: `${buildSymphonyThreadId(plan)}-finish`,
1648
- label: 'Archive Symphony result',
1649
- status: stage === 'completed' ? 'done' : stage === 'failed' ? 'error' : 'pending',
1650
- detail: plan.issue.error ?? plan.session.error ?? `${plan.workers.length} worker${plan.workers.length === 1 ? '' : 's'}`,
1651
- },
1652
- ],
1653
- currentStep: stage === 'planned' ? 1 : stage === 'running' ? 2 : workerSteps.length + 2,
1654
- totalSteps: workerSteps.length + 2,
1655
- };
1656
- }
1657
- function buildStatusContent(plan, stage) {
1658
- if (stage === 'planned') {
1659
- return `I created a Symphony issue with ${plan.workers.length} worker${plan.workers.length === 1 ? '' : 's'}.\n\n${plan.issue.description ?? plan.issue.title}`;
1660
- }
1661
- if (stage === 'running') {
1662
- const running = plan.workers
1663
- .filter((worker) => worker.session.status === 'running')
1664
- .map((worker) => worker.session.target.label ?? worker.session.target.agent ?? backendDisplayName(worker.session.backend));
1665
- return `Symphony workers are active: ${running.length > 0 ? running.join(', ') : plan.workers.length}.\n\nIssue: ${plan.issue.uri}`;
1666
- }
1667
- if (stage === 'completed') {
1668
- return `Symphony issue completed.\n\nWorkers: ${plan.workers.length}`;
1669
- }
1670
- return `Symphony issue failed.\n\n${plan.issue.error ?? plan.session.error ?? plan.delivery.error ?? 'Backend did not complete successfully.'}`;
1671
- }
1672
- function buildStatusMessageRow(plan, webId, stage) {
1673
- const createdAt = stage === 'planned'
1674
- ? safeDate(plan.issue.createdAt)
1675
- : safeDate(plan.session.updatedAt);
1676
- const content = buildStatusContent(plan, stage);
1677
- const secretaryAgent = agentResource.buildIri(webId, { id: SYMPHONY_SECRETARY_AGENT_ID });
1678
- const routeTargetAgent = agentResource.buildIri(webId, {
1679
- id: buildWorkerAgentId(plan.session.backend, plan.session.target?.agent, plan.session.target?.contact),
1680
- });
1681
- return {
1682
- id: `${buildSymphonyThreadId(plan)}-${stage}`,
1683
- chat: selectTargetChatIri(plan.session.target?.chat, webId, plan),
1684
- thread: selectTargetThreadIri(plan.session.target?.thread, webId, plan),
1685
- maker: secretaryAgent,
1686
- role: 'assistant',
1687
- content,
1688
- richContent: JSON.stringify({
1689
- blocks: [buildProgressBlock(plan, stage)],
1690
- symphony: {
1691
- stage,
1692
- issue: plan.issue.uri,
1693
- task: plan.task,
1694
- delivery: plan.delivery.uri,
1695
- session: plan.session.uri,
1696
- issuer: plan.issue.issuer,
1697
- workers: plan.workers.map((worker) => ({
1698
- task: worker.task,
1699
- title: worker.taskRecord.title,
1700
- objective: worker.taskRecord.objective,
1701
- acceptanceCriteria: worker.taskRecord.acceptanceCriteria,
1702
- taskStatus: worker.taskRecord.status,
1703
- delivery: worker.delivery.uri,
1704
- session: worker.session.uri,
1705
- backend: worker.session.backend,
1706
- agent: worker.session.target.agent,
1707
- contact: worker.session.target.contact ?? buildWorkerContactId(worker),
1708
- status: worker.session.status,
1709
- autoModeSessionId: worker.session.autoModeSessionId,
1710
- })),
1711
- autoModeSessionId: plan.session.autoModeSessionId,
1712
- },
1713
- }),
1714
- status: stage === 'failed' ? 'error' : 'sent',
1715
- senderName: 'AI Secretary',
1716
- routedBy: secretaryAgent,
1717
- routeTargetAgent,
1718
- coordinationId: plan.session.uri,
1719
- createdAt,
1720
- updatedAt: createdAt,
1721
- };
1722
- }
1723
824
  function auditActionForStage(stage) {
1724
825
  if (stage === 'running')
1725
826
  return 'symphony.dispatched';
@@ -1742,13 +843,15 @@ function buildSymphonyReportInboxNotificationRow(webId, worker) {
1742
843
  const createdAt = safeDate(worker.session.completedAt ?? worker.session.updatedAt);
1743
844
  return {
1744
845
  id: stableReportInboxNotificationId(worker),
1745
- actor: buildWorkerContactIri(webId, worker),
846
+ actor: agentResource.buildIri(webId, {
847
+ id: buildWorkerAgentId(worker.session.backend, worker.session.target.agent),
848
+ }),
1746
849
  object: buildSymphonyReportDeliveryIri(webId, worker),
1747
850
  createdAt,
1748
851
  };
1749
852
  }
1750
853
  function buildSymphonyAuditRow(plan, webId, stage) {
1751
- const message = buildStatusMessageRow(plan, webId, stage);
854
+ const message = buildSharedSymphonyStatusMessageRow(plan, webId, stage);
1752
855
  const createdAt = safeDate(message.createdAt);
1753
856
  return {
1754
857
  id: stableAuditId(plan, stage),
@@ -1757,7 +860,7 @@ function buildSymphonyAuditRow(plan, webId, stage) {
1757
860
  actorRole: 'secretary',
1758
861
  onBehalfOf: webId,
1759
862
  session: buildSymphonyControlSessionUri(webId, plan),
1760
- entry: buildSymphonyMessageUri(webId, plan, message),
863
+ entry: buildSharedSymphonyMessageIri(webId, plan, message),
1761
864
  policyVersion: SYMPHONY_POLICY_VERSION,
1762
865
  createdAt,
1763
866
  };
@@ -1794,6 +897,7 @@ async function upsertMessage(db, runtime, row) {
1794
897
  routedBy: row.routedBy,
1795
898
  routeTargetAgent: row.routeTargetAgent,
1796
899
  coordinationId: row.coordinationId,
900
+ metadata: row.metadata,
1797
901
  updatedAt: row.updatedAt,
1798
902
  });
1799
903
  }
@@ -1805,6 +909,7 @@ async function upsertSession(db, runtime, row) {
1805
909
  owner: row.owner,
1806
910
  chat: row.chat,
1807
911
  thread: row.thread,
912
+ sessionType: row.sessionType,
1808
913
  status: row.status,
1809
914
  tool: row.tool,
1810
915
  tokenUsage: row.tokenUsage,
@@ -1819,12 +924,12 @@ async function upsertIssue(db, runtime, row) {
1819
924
  await upsertExactRecord(db, runtime.issueResource, { id: row.id }, row, {
1820
925
  title: row.title,
1821
926
  description: row.description,
1822
- document: row.document,
1823
927
  status: row.status,
1824
928
  priority: row.priority,
1825
929
  labels: row.labels,
1826
930
  chat: row.chat,
1827
931
  thread: row.thread,
932
+ parentIssue: row.parentIssue,
1828
933
  tasks: row.tasks,
1829
934
  assignedTo: row.assignedTo,
1830
935
  updatedAt: row.updatedAt,
@@ -1834,7 +939,6 @@ async function upsertIssue(db, runtime, row) {
1834
939
  async function upsertIdea(db, runtime, row) {
1835
940
  await upsertExactRecord(db, runtime.ideaResource, { id: row.id }, row, {
1836
941
  summary: row.summary,
1837
- document: row.document,
1838
942
  input: row.input,
1839
943
  status: row.status,
1840
944
  commitment: row.commitment,
@@ -1869,51 +973,6 @@ async function upsertTask(db, runtime, row) {
1869
973
  updatedAt: row.updatedAt,
1870
974
  });
1871
975
  }
1872
- async function upsertReport(db, runtime, row) {
1873
- await upsertExactRecord(db, runtime.reportResource, {
1874
- id: row.id,
1875
- task: row.task,
1876
- createdAt: row.createdAt,
1877
- }, row, {
1878
- reportKind: row.reportKind,
1879
- status: row.status,
1880
- outcome: row.outcome,
1881
- about: row.about,
1882
- issue: row.issue,
1883
- task: row.task,
1884
- delivery: row.delivery,
1885
- run: row.run,
1886
- thread: row.thread,
1887
- evidence: row.evidence,
1888
- summary: row.summary,
1889
- reviewer: row.reviewer,
1890
- actor: row.actor,
1891
- source: row.source,
1892
- metricFacts: row.metricFacts,
1893
- metadata: row.metadata,
1894
- publishedAt: row.publishedAt,
1895
- updatedAt: row.updatedAt,
1896
- });
1897
- }
1898
- async function upsertEvidence(db, runtime, row) {
1899
- await upsertExactRecord(db, runtime.evidenceResource, {
1900
- id: row.id,
1901
- createdAt: row.createdAt,
1902
- }, row, {
1903
- evidenceKind: row.evidenceKind,
1904
- about: row.about,
1905
- issue: row.issue,
1906
- task: row.task,
1907
- delivery: row.delivery,
1908
- run: row.run,
1909
- thread: row.thread,
1910
- summary: row.summary,
1911
- source: row.source,
1912
- actor: row.actor,
1913
- outcome: row.outcome,
1914
- metadata: row.metadata,
1915
- });
1916
- }
1917
976
  async function upsertDelivery(db, runtime, row) {
1918
977
  await upsertExactRecord(db, runtime.deliveryResource, { id: row.id }, row, {
1919
978
  kind: row.kind,
@@ -1960,6 +1019,31 @@ async function upsertRun(db, runtime, row) {
1960
1019
  async function insertRunStepOnce(db, runtime, row) {
1961
1020
  await insertExactRecordOnce(db, runtime.runStepResource, String(row.id), row);
1962
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
+ }
1963
1047
  async function upsertAgent(db, runtime, row) {
1964
1048
  const target = { id: row.id };
1965
1049
  const agentResourceWithId = runtime.agentResource;
@@ -1979,7 +1063,7 @@ async function upsertAgent(db, runtime, row) {
1979
1063
  async function upsertContact(db, runtime, row) {
1980
1064
  await upsertExactRecord(db, runtime.contactResource, { id: row.id }, row, {
1981
1065
  name: row.name,
1982
- entity: row.entity,
1066
+ about: row.about,
1983
1067
  rdfType: row.rdfType,
1984
1068
  contactType: row.contactType,
1985
1069
  updatedAt: row.updatedAt,
@@ -1998,7 +1082,7 @@ async function insertInboxNotificationOnce(db, runtime, row) {
1998
1082
  await insertExactRecordOnce(db, runtime.inboxNotificationResource, String(row.id), row);
1999
1083
  }
2000
1084
  function collectMessageUris(webId, plan, stages) {
2001
- 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)))));
2002
1086
  }
2003
1087
  function collectSymphonyProjectionResources(webId, plan, stages) {
2004
1088
  const resources = [];
@@ -2013,13 +1097,16 @@ function collectSymphonyProjectionResources(webId, plan, stages) {
2013
1097
  };
2014
1098
  add('chat', selectTargetChatIri(plan.session.target?.chat, webId, plan));
2015
1099
  add('issue', buildSymphonyIssueIri(webId, plan.issue));
1100
+ for (const issue of plan.followUpIssues ?? []) {
1101
+ add('issue', buildSymphonyIssueIri(webId, issue));
1102
+ }
2016
1103
  for (const message of collectMessageUris(webId, plan, stages)) {
2017
1104
  add('message', message);
2018
1105
  }
2019
1106
  for (const agent of buildSymphonyAgents(plan)) {
2020
1107
  add('agent', agentResource.buildIri(webId, { id: agent.id }));
2021
1108
  }
2022
- for (const contact of buildSymphonyContacts(plan, webId)) {
1109
+ for (const contact of buildSharedSymphonyContactRows(plan, webId)) {
2023
1110
  add('contact', contactResource.buildIri(webId, { id: contact.id }));
2024
1111
  }
2025
1112
  for (const worker of plan.workers) {
@@ -2032,10 +1119,10 @@ function collectSymphonyProjectionResources(webId, plan, stages) {
2032
1119
  for (const stage of stages) {
2033
1120
  add('runStep', buildSymphonyRunStepIri(webId, worker, stage));
2034
1121
  }
1122
+ for (const step of worker.runSteps ?? []) {
1123
+ add('runStep', buildSymphonyRuntimeRunStepIri(webId, worker, step));
1124
+ }
2035
1125
  if (worker.session.status === 'completed' || worker.session.status === 'failed') {
2036
- const terminalStage = worker.session.status === 'failed' ? 'failed' : 'completed';
2037
- add('evidence', buildSymphonyEvidenceIri(webId, worker, terminalStage));
2038
- add('report', buildSymphonyReportIri(webId, worker));
2039
1126
  add('delivery', buildSymphonyReportDeliveryIri(webId, worker));
2040
1127
  }
2041
1128
  }
@@ -2072,7 +1159,6 @@ export async function persistSymphonyControlStateToPod(plan, options = {}) {
2072
1159
  };
2073
1160
  const projected = withTargetRefs(normalizedPlan, refs, podSession.webId);
2074
1161
  const resources = collectSymphonyProjectionResources(podSession.webId, projected, stages);
2075
- const latestMessage = buildStatusMessageRow(projected, podSession.webId, stage);
2076
1162
  const controlWrite = createLinxPodSyncScope({
2077
1163
  source: 'symphony-control-state',
2078
1164
  plane: 'control-plane',
@@ -2108,9 +1194,7 @@ export async function persistSymphonyControlStateToPod(plan, options = {}) {
2108
1194
  webId: podSession.webId,
2109
1195
  stage,
2110
1196
  stages,
2111
- latestMessage,
2112
1197
  shouldUpsertChat: !normalizedPlan.session.target?.chat,
2113
- podSession,
2114
1198
  }),
2115
1199
  });
2116
1200
  return {
@@ -2184,10 +1268,6 @@ function resolveProjectionResourceModel(runtime, kind) {
2184
1268
  return runtime.taskResource;
2185
1269
  if (kind === 'delivery')
2186
1270
  return runtime.deliveryResource;
2187
- if (kind === 'evidence')
2188
- return runtime.evidenceResource;
2189
- if (kind === 'report')
2190
- return runtime.reportResource;
2191
1271
  if (kind === 'run')
2192
1272
  return runtime.runResource;
2193
1273
  if (kind === 'runStep')
@@ -2202,6 +1282,14 @@ function resolveProjectionResourceModel(runtime, kind) {
2202
1282
  return runtime.inboxNotificationResource ?? null;
2203
1283
  return null;
2204
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
+ }
2205
1293
  function serializeOrmRowAsJsonLd(model, iri, row) {
2206
1294
  const context = {};
2207
1295
  const document = {
@@ -2325,18 +1413,7 @@ export async function persistSymphonyIdeaToPod(idea, options = {}) {
2325
1413
  },
2326
1414
  },
2327
1415
  {
2328
- id: 'symphony.idea.write-file-primary-document',
2329
- kind: 'upsert',
2330
- apply: async () => {
2331
- await runtime.writePodFile?.(podSession, {
2332
- path: buildSymphonyIdeaDocumentPath(idea),
2333
- content: renderSymphonyIdeaMarkdown(idea),
2334
- contentType: 'text/markdown; charset=utf-8',
2335
- });
2336
- },
2337
- },
2338
- {
2339
- id: 'symphony.idea.upsert-meta',
1416
+ id: 'symphony.idea.upsert',
2340
1417
  kind: 'upsert',
2341
1418
  apply: () => upsertIdea(db, runtime, buildSymphonyIdeaRow(idea, podSession.webId)),
2342
1419
  },
@@ -2352,13 +1429,10 @@ export async function listOpenSymphonyIssuesFromPod(options = {}) {
2352
1429
  }
2353
1430
  const db = runtime.createDb(podSession);
2354
1431
  try {
2355
- await db.init([runtime.issueResource]).catch(() => undefined);
2356
- const rows = await db.select().from(runtime.issueResource).execute();
2357
- return rows
2358
- .map((row) => issueRowToSymphonyIssueRecord(row, podSession.webId))
2359
- .filter((issue) => issue !== null)
2360
- .filter((issue) => !isClosedIssueStatus(issue.status))
2361
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
1432
+ return await listOpenSymphonyIssuesFromControlState({
1433
+ db,
1434
+ webId: podSession.webId,
1435
+ });
2362
1436
  }
2363
1437
  catch {
2364
1438
  return null;
@@ -2372,13 +1446,7 @@ export async function listRunningSymphonyWorkersFromPod(options = {}) {
2372
1446
  }
2373
1447
  const db = runtime.createDb(podSession);
2374
1448
  try {
2375
- await db.init([runtime.sessionResource]).catch(() => undefined);
2376
- const rows = await db.select().from(runtime.sessionResource).execute();
2377
- return rows
2378
- .filter(isSymphonySessionRow)
2379
- .flatMap((row) => extractRunningSymphonyWorkersFromSession(row))
2380
- .sort(compareWorkerStatusUpdatedAt)
2381
- .map(({ updatedAt: _updatedAt, ...worker }) => worker);
1449
+ return await listRunningSymphonyWorkersFromControlState({ db });
2382
1450
  }
2383
1451
  catch {
2384
1452
  return null;
@@ -2392,217 +1460,22 @@ export async function listRecentSymphonyReportsFromPod(options = {}) {
2392
1460
  }
2393
1461
  const db = runtime.createDb(podSession);
2394
1462
  try {
2395
- await db.init([runtime.deliveryResource]).catch(() => undefined);
2396
- const rows = await db.select().from(runtime.deliveryResource).execute();
2397
- return rows
2398
- .map(deliveryRowToSymphonyReportStatus)
2399
- .filter((report) => report !== null)
2400
- .sort((left, right) => right.sortAt - left.sortAt)
2401
- .slice(0, options.limit ?? 5)
2402
- .map(({ sortAt: _sortAt, ...report }) => report);
1463
+ return await listRecentSymphonyReportsFromControlState({
1464
+ db,
1465
+ limit: options.limit ?? 5,
1466
+ });
2403
1467
  }
2404
1468
  catch {
2405
1469
  return null;
2406
1470
  }
2407
1471
  }
2408
- function issueRowToSymphonyIssueRecord(row, webId) {
2409
- const record = asRecord(row);
2410
- const id = normalizeString(record?.id);
2411
- const title = normalizeString(record?.title);
2412
- if (!record || !id || !title) {
2413
- return null;
2414
- }
2415
- const status = normalizeIssueStatus(record.status);
2416
- const priority = normalizeIssuePriority(record.priority);
2417
- const tasks = Array.isArray(record.tasks)
2418
- ? record.tasks.map((item) => normalizeString(item)).filter((item) => Boolean(item))
2419
- : [];
2420
- const createdAt = toIsoDate(record.createdAt);
2421
- const updatedAt = toIsoDate(record.updatedAt) ?? createdAt;
2422
- return {
2423
- uri: symphonyIssueUriFromResourceId(id),
2424
- title,
2425
- description: normalizeString(record.description),
2426
- status,
2427
- priority,
2428
- source: 'cli',
2429
- issuer: {
2430
- source: 'user',
2431
- webId: normalizeString(record.createdBy) ?? webId,
2432
- ...(normalizeString(record.chat) ? { chat: normalizeString(record.chat) } : {}),
2433
- ...(normalizeString(record.thread) ? { thread: normalizeString(record.thread) } : {}),
2434
- },
2435
- tasks,
2436
- deliveries: [],
2437
- sessions: [],
2438
- ...(normalizeString(record.chat) ? { chat: normalizeString(record.chat) } : {}),
2439
- ...(normalizeString(record.thread) ? { thread: normalizeString(record.thread) } : {}),
2440
- createdAt,
2441
- updatedAt,
2442
- ...(record.closedAt ? { closedAt: toIsoDate(record.closedAt) ?? updatedAt } : {}),
2443
- };
2444
- }
2445
- function symphonyIssueUriFromResourceId(id) {
2446
- const normalized = resolvePodResourceTemplateValue(issueResource, id) ?? id;
2447
- return `urn:undefineds:linx:issue:${normalized}`;
2448
- }
2449
- function normalizeIssueStatus(value) {
2450
- const normalized = normalizeString(value);
2451
- if (normalized === 'open'
2452
- || normalized === 'triaging'
2453
- || normalized === 'in_progress'
2454
- || normalized === 'blocked'
2455
- || normalized === 'resolved'
2456
- || normalized === 'closed') {
2457
- return normalized;
2458
- }
2459
- return 'open';
2460
- }
2461
- function normalizeIssuePriority(value) {
2462
- const normalized = normalizeString(value);
2463
- if (normalized === 'low' || normalized === 'medium' || normalized === 'high' || normalized === 'urgent') {
2464
- return normalized;
2465
- }
2466
- return 'medium';
2467
- }
2468
- function isClosedIssueStatus(status) {
2469
- return status === 'closed' || status === 'resolved';
2470
- }
2471
- function isSymphonySessionRow(row) {
2472
- if (!row || typeof row !== 'object') {
2473
- return false;
2474
- }
2475
- const record = row;
2476
- const metadata = asRecord(record.metadata);
2477
- return metadata?.kind === 'symphony-run'
2478
- || record.policyVersion === SYMPHONY_POLICY_VERSION
2479
- || (typeof record.tool === 'string' && record.tool.startsWith('symphony:'));
2480
- }
2481
- function extractRunningSymphonyWorkersFromSession(row) {
2482
- const metadata = asRecord(row.metadata) ?? {};
2483
- const sessionStatus = normalizePodSymphonySessionStatus(metadata.status ?? row.status);
2484
- const workers = Array.isArray(metadata.workers) ? metadata.workers : [];
2485
- const updatedAt = safeOptionalDate(row.updatedAt);
2486
- if (workers.length === 0) {
2487
- if (sessionStatus !== 'running') {
2488
- return [];
2489
- }
2490
- return [{
2491
- status: sessionStatus,
2492
- backend: normalizeString(metadata.backend) ?? parseBackendFromTool(row.tool) ?? 'unknown',
2493
- mode: normalizeString(metadata.mode) ?? 'auto',
2494
- cwd: normalizeString(metadata.workspacePath),
2495
- autoModeSessionId: normalizeString(metadata.autoModeSessionId),
2496
- target: normalizeSymphonyWorkerTarget(asRecord(metadata.target)),
2497
- updatedAt,
2498
- }];
2499
- }
2500
- return workers
2501
- .map((item) => asRecord(item))
2502
- .filter((item) => item !== null)
2503
- .map((worker) => ({
2504
- status: normalizePodSymphonySessionStatus(worker.status ?? worker.taskStatus ?? sessionStatus),
2505
- backend: normalizeString(worker.backend) ?? normalizeString(metadata.backend) ?? parseBackendFromTool(row.tool) ?? 'unknown',
2506
- mode: normalizeString(worker.mode) ?? normalizeString(metadata.mode) ?? 'auto',
2507
- cwd: normalizeString(worker.workspacePath) ?? normalizeString(metadata.workspacePath),
2508
- autoModeSessionId: normalizeString(worker.autoModeSessionId) ?? normalizeString(metadata.autoModeSessionId),
2509
- target: normalizeSymphonyWorkerTarget(asRecord(worker.target), worker, asRecord(metadata.target)),
2510
- updatedAt,
2511
- }))
2512
- .filter((worker) => worker.status === 'running');
2513
- }
2514
- function deliveryRowToSymphonyReportStatus(row) {
2515
- const record = asRecord(row);
2516
- if (!record) {
2517
- return null;
2518
- }
2519
- const metadata = asRecord(record.metadata);
2520
- const payload = asRecord(record.payload);
2521
- if (record.kind !== 'report' && metadata?.reportKind !== 'worker-completion' && payload?.kind !== 'symphony_report') {
2522
- return null;
2523
- }
2524
- const completedAt = safeOptionalDate(record.completedAt);
2525
- const updatedAt = safeOptionalDate(record.updatedAt);
2526
- const createdAt = safeOptionalDate(record.createdAt);
2527
- const sortAt = completedAt?.getTime() ?? updatedAt?.getTime() ?? createdAt?.getTime() ?? 0;
2528
- const agent = normalizeString(payload?.agent);
2529
- const title = normalizeString(record.objective);
2530
- const summary = normalizeString(payload?.summary);
2531
- const task = normalizeString(record.task);
2532
- const archive = asRecord(metadata?.archive);
2533
- const delivery = normalizeString(payload?.delivery) ?? normalizeString(archive?.delivery);
2534
- const reportDelivery = normalizeString(payload?.reportDelivery) ?? normalizeString(record.id);
2535
- const run = normalizeString(payload?.run) ?? normalizeString(record.object);
2536
- const chat = normalizeString(record.chat);
2537
- const thread = normalizeString(record.thread);
2538
- const autoModeSessionId = normalizeString(payload?.autoModeSessionId);
2539
- const error = normalizeString(payload?.error) ?? normalizeString(record.error);
2540
- return {
2541
- status: normalizeString(payload?.outcome) ?? normalizeString(record.status) ?? 'completed',
2542
- backend: normalizeString(payload?.backend) ?? 'unknown',
2543
- ...(agent ? { agent } : {}),
2544
- ...(title ? { title } : {}),
2545
- ...(summary ? { summary } : {}),
2546
- ...(task ? { task } : {}),
2547
- ...(delivery ? { delivery } : {}),
2548
- ...(reportDelivery ? { reportDelivery } : {}),
2549
- ...(run ? { run } : {}),
2550
- ...(chat ? { chat } : {}),
2551
- ...(thread ? { thread } : {}),
2552
- ...(autoModeSessionId ? { autoModeSessionId } : {}),
2553
- ...(error ? { error } : {}),
2554
- ...(completedAt ? { completedAt: completedAt.toISOString() } : {}),
2555
- ...(updatedAt ? { updatedAt: updatedAt.toISOString() } : {}),
2556
- sortAt,
2557
- };
2558
- }
2559
- function normalizeSymphonyWorkerTarget(target, worker = {}, fallback = null) {
2560
- const normalized = {
2561
- label: normalizeString(target?.label) ?? normalizeString(worker.title) ?? normalizeString(fallback?.label),
2562
- agent: normalizeString(target?.agent) ?? normalizeString(worker.agent) ?? normalizeString(fallback?.agent),
2563
- chat: normalizeString(target?.chat) ?? normalizeString(worker.chat) ?? normalizeString(fallback?.chat),
2564
- };
2565
- return Object.values(normalized).some(Boolean) ? normalized : undefined;
2566
- }
2567
- function compareWorkerStatusUpdatedAt(left, right) {
2568
- return (right.updatedAt?.getTime() ?? 0) - (left.updatedAt?.getTime() ?? 0);
2569
- }
2570
- function normalizePodSymphonySessionStatus(value) {
2571
- const normalized = normalizeString(value);
2572
- if (normalized === 'active')
2573
- return 'running';
2574
- if (normalized === 'error')
2575
- return 'failed';
2576
- if (normalized === 'queued')
2577
- return 'planned';
2578
- return normalized ?? 'planned';
2579
- }
2580
- function parseBackendFromTool(value) {
2581
- const tool = normalizeString(value);
2582
- if (!tool?.startsWith('symphony:')) {
2583
- return undefined;
2584
- }
2585
- return tool.slice('symphony:'.length) || undefined;
2586
- }
2587
- function asRecord(value) {
2588
- return value && typeof value === 'object' && !Array.isArray(value)
2589
- ? value
2590
- : null;
2591
- }
2592
- function normalizeString(value) {
2593
- return typeof value === 'string' && value.trim() ? value.trim() : undefined;
2594
- }
2595
- function safeOptionalDate(value) {
2596
- if (!value) {
2597
- return undefined;
2598
- }
2599
- const date = value instanceof Date ? value : new Date(String(value));
2600
- return Number.isFinite(date.getTime()) ? date : undefined;
2601
- }
2602
- function toIsoDate(value) {
2603
- return (safeOptionalDate(value) ?? new Date()).toISOString();
2604
- }
2605
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
+ });
2606
1479
  return [
2607
1480
  {
2608
1481
  id: 'symphony.prepare-resources',
@@ -2616,10 +1489,10 @@ function buildSymphonyProjectionOperations(input) {
2616
1489
  input.runtime.issueResource,
2617
1490
  input.runtime.taskResource,
2618
1491
  input.runtime.deliveryResource,
2619
- input.runtime.evidenceResource,
2620
- input.runtime.reportResource,
2621
1492
  input.runtime.runResource,
2622
1493
  input.runtime.runStepResource,
1494
+ input.runtime.evidenceResource,
1495
+ input.runtime.reportResource,
2623
1496
  input.runtime.agentResource,
2624
1497
  input.runtime.contactResource,
2625
1498
  input.runtime.auditResource,
@@ -2628,82 +1501,84 @@ function buildSymphonyProjectionOperations(input) {
2628
1501
  },
2629
1502
  },
2630
1503
  {
2631
- id: 'symphony.write-file-primary-documents',
1504
+ id: 'symphony.upsert-issue',
2632
1505
  kind: 'upsert',
2633
- apply: async () => {
2634
- await input.runtime.writePodFile?.(input.podSession, {
2635
- path: buildSymphonyIssueDocumentPath(input.plan.issue),
2636
- content: renderSymphonyIssueMarkdown(input.plan),
2637
- contentType: 'text/markdown; charset=utf-8',
2638
- });
2639
- },
1506
+ apply: () => upsertIssue(input.db, input.runtime, controlRows.issue),
2640
1507
  },
2641
- {
2642
- id: 'symphony.upsert-issue-meta',
1508
+ ...controlRows.issues.slice(1).map((row, index) => ({
1509
+ id: `symphony.upsert-follow-up-issue:${index + 1}`,
2643
1510
  kind: 'upsert',
2644
- apply: () => upsertIssue(input.db, input.runtime, buildSymphonyIssueRow(input.plan, input.webId)),
2645
- },
2646
- ...input.plan.workers.flatMap((worker, index) => [
2647
- {
2648
- id: `symphony.upsert-task:${index + 1}`,
2649
- kind: 'upsert',
2650
- apply: () => upsertTask(input.db, input.runtime, buildSymphonyTaskRow(input.plan, input.webId, worker)),
2651
- },
2652
- {
2653
- id: `symphony.upsert-delivery:${index + 1}`,
2654
- kind: 'upsert',
2655
- apply: () => upsertDelivery(input.db, input.runtime, buildSymphonyDeliveryRow(input.plan, input.webId, worker)),
2656
- },
2657
- {
2658
- id: `symphony.upsert-run:${index + 1}`,
2659
- kind: 'upsert',
2660
- apply: () => upsertRun(input.db, input.runtime, buildSymphonyRunRow(input.plan, input.webId, worker)),
2661
- },
2662
- ...input.stages.map((stage) => ({
2663
- id: `symphony.insert-run-step:${index + 1}:${stage}`,
2664
- kind: 'insert',
2665
- apply: () => insertRunStepOnce(input.db, input.runtime, buildSymphonyRunStepRow(input.plan, input.webId, worker, stage)),
2666
- })),
2667
- ]),
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
+ })),
2668
1543
  ...buildSymphonyReportOperations(input),
2669
1544
  ...buildSymphonyAgents(input.plan).map((agent) => ({
2670
1545
  id: `symphony.upsert-agent:${agent.id}`,
2671
1546
  kind: 'upsert',
2672
1547
  apply: () => upsertAgent(input.db, input.runtime, agent),
2673
1548
  })),
2674
- ...buildSymphonyContacts(input.plan, input.webId).map((contact) => ({
1549
+ ...controlRows.contacts.map((contact) => ({
2675
1550
  id: `symphony.upsert-contact:${contact.id}`,
2676
1551
  kind: 'upsert',
2677
1552
  apply: () => upsertContact(input.db, input.runtime, contact),
2678
1553
  })),
2679
- {
1554
+ ...controlRows.chats.map((row, index) => ({
2680
1555
  id: 'symphony.upsert-chat',
2681
1556
  kind: 'upsert',
2682
1557
  shouldRun: () => input.shouldUpsertChat,
2683
- apply: () => upsertChat(input.db, input.runtime, buildSymphonyChatRow(input.plan, input.webId, input.stage, input.latestMessage.content)),
2684
- },
2685
- ...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) => ({
2686
1561
  id: `symphony.upsert-thread:${index + 1}`,
2687
1562
  kind: 'upsert',
2688
1563
  apply: () => upsertThread(input.db, input.runtime, row),
2689
1564
  })),
2690
- ...input.plan.workers.map((worker, index) => ({
1565
+ ...controlRows.sessions.map((row, index) => ({
2691
1566
  id: `symphony.upsert-session:${index + 1}`,
2692
1567
  kind: 'upsert',
2693
- apply: () => upsertSession(input.db, input.runtime, buildSymphonySessionRow(input.plan, input.webId, worker)),
1568
+ apply: () => upsertSession(input.db, input.runtime, row),
2694
1569
  })),
2695
- ...input.stages.flatMap((stage) => [
1570
+ ...controlRows.messages.flatMap((row, index) => [
2696
1571
  {
2697
- id: `symphony.upsert-message:${stage}`,
1572
+ id: `symphony.upsert-message:${input.stages[index] ?? index + 1}`,
2698
1573
  kind: 'upsert',
2699
- apply: () => upsertMessage(input.db, input.runtime, buildStatusMessageRow(input.plan, input.webId, stage)),
2700
- },
2701
- {
2702
- id: `symphony.insert-audit:${stage}`,
2703
- kind: 'insert',
2704
- apply: () => insertAuditOnce(input.db, input.runtime, buildSymphonyAuditRow(input.plan, input.webId, stage)),
1574
+ apply: () => upsertMessage(input.db, input.runtime, row),
2705
1575
  },
2706
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
+ })),
2707
1582
  ];
2708
1583
  }
2709
1584
  function buildSymphonyReportOperations(input) {
@@ -2712,38 +1587,6 @@ function buildSymphonyReportOperations(input) {
2712
1587
  }
2713
1588
  const terminalStage = input.stage;
2714
1589
  return input.plan.workers.flatMap((worker, index) => [
2715
- {
2716
- id: `symphony.write-evidence-file:${index + 1}`,
2717
- kind: 'upsert',
2718
- apply: async () => {
2719
- await input.runtime.writePodFile?.(input.podSession, {
2720
- path: buildSymphonyEvidenceDocumentPath(worker, terminalStage),
2721
- content: renderSymphonyEvidenceMarkdown(input.plan, input.webId, worker, terminalStage),
2722
- contentType: 'text/markdown; charset=utf-8',
2723
- });
2724
- },
2725
- },
2726
- {
2727
- id: `symphony.upsert-evidence:${index + 1}`,
2728
- kind: 'upsert',
2729
- apply: () => upsertEvidence(input.db, input.runtime, buildSymphonyEvidenceRow(input.plan, input.webId, worker, terminalStage)),
2730
- },
2731
- {
2732
- id: `symphony.write-report-file:${index + 1}`,
2733
- kind: 'upsert',
2734
- apply: async () => {
2735
- await input.runtime.writePodFile?.(input.podSession, {
2736
- path: buildSymphonyReportDocumentPath(worker),
2737
- content: renderSymphonyReportMarkdown(input.plan, worker, terminalStage),
2738
- contentType: 'text/markdown; charset=utf-8',
2739
- });
2740
- },
2741
- },
2742
- {
2743
- id: `symphony.upsert-report:${index + 1}`,
2744
- kind: 'upsert',
2745
- apply: () => upsertReport(input.db, input.runtime, buildSymphonyReportRow(input.plan, input.webId, worker, terminalStage)),
2746
- },
2747
1590
  {
2748
1591
  id: `symphony.upsert-report-delivery:${index + 1}`,
2749
1592
  kind: 'upsert',
@@ -2769,21 +1612,21 @@ export const __symphonyPodProjectionInternal = {
2769
1612
  buildSymphonyMessageUri,
2770
1613
  auditActionForStage,
2771
1614
  buildSymphonyAuditRow,
2772
- buildStatusMessageRow,
2773
- buildSymphonyChatRow,
2774
- buildSymphonyThreadRow,
2775
- buildSymphonySessionRow,
2776
- buildSymphonyIssueRow,
1615
+ buildStatusMessageRow: buildSharedSymphonyStatusMessageRow,
1616
+ buildSymphonyChatRow: buildSharedSymphonyChatRow,
1617
+ buildSymphonyThreadRow: buildSharedSymphonyThreadRow,
1618
+ buildSymphonySessionRow: buildSharedSymphonySessionRow,
1619
+ buildSymphonyIssueRow: buildSharedSymphonyIssueRow,
2777
1620
  buildSymphonyIdeaRow,
2778
- buildSymphonyTaskRow,
2779
- buildSymphonyDeliveryRow,
2780
- buildSymphonyEvidenceRow,
1621
+ buildSymphonyTaskRow: buildSharedSymphonyTaskRow,
1622
+ buildSymphonyDeliveryRow: buildSharedSymphonyDeliveryRow,
2781
1623
  buildSymphonyReportDeliveryRow,
2782
1624
  buildSymphonyReportInboxNotificationRow,
2783
- buildSymphonyRunRow,
2784
- buildSymphonyRunStepRow,
1625
+ buildSymphonyRunRow: buildSharedSymphonyRunRow,
1626
+ buildSymphonyRunStepRow: buildSharedSymphonyRunStepRow,
1627
+ buildSymphonyRuntimeRunStepRow: buildSharedSymphonyRuntimeRunStepRow,
2785
1628
  buildSymphonyAgents,
2786
- buildSymphonyContacts,
1629
+ buildSymphonyContacts: buildSharedSymphonyContactRows,
2787
1630
  withChatThreadRefs,
2788
1631
  normalizeSymphonyRunPlan,
2789
1632
  };