@undefineds.co/linx 0.3.18 → 0.3.20

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 (40) hide show
  1. package/dist/lib/auto-mode/pod-persistence.js +0 -2
  2. package/dist/lib/auto-mode/pod-persistence.js.map +1 -1
  3. package/dist/lib/capture/persistence.js +377 -0
  4. package/dist/lib/capture/persistence.js.map +1 -0
  5. package/dist/lib/capture/tool.js +242 -0
  6. package/dist/lib/capture/tool.js.map +1 -0
  7. package/dist/lib/linx-cloud-errors.js +5 -0
  8. package/dist/lib/linx-cloud-errors.js.map +1 -1
  9. package/dist/lib/linx-status-line.js +8 -1
  10. package/dist/lib/linx-status-line.js.map +1 -1
  11. package/dist/lib/models.js.map +1 -1
  12. package/dist/lib/pi-adapter/branding.js +103 -34
  13. package/dist/lib/pi-adapter/branding.js.map +1 -1
  14. package/dist/lib/pi-adapter/interactive.js +42 -29
  15. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  16. package/dist/lib/pi-adapter/pod-mirror.js +102 -4
  17. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  18. package/dist/lib/pi-adapter/pod-native.js +0 -2
  19. package/dist/lib/pi-adapter/pod-native.js.map +1 -1
  20. package/dist/lib/pi-adapter/runtime.js +12 -2
  21. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  22. package/dist/lib/status-line-command.js +2 -2
  23. package/dist/lib/status-line-command.js.map +1 -1
  24. package/dist/lib/symphony/pod-projection.js +104 -58
  25. package/dist/lib/symphony/pod-projection.js.map +1 -1
  26. package/dist/lib/symphony-command.js +1 -1
  27. package/dist/lib/symphony-command.js.map +1 -1
  28. package/dist/skills/basic/SKILL.md +46 -0
  29. package/dist/skills/capture/SKILL.md +165 -0
  30. package/dist/skills/symphony/SKILL.md +14 -4
  31. package/dist/skills/xpod-cli/SKILL.md +13 -2
  32. package/package.json +2 -2
  33. package/vendor/agent-runtime/dist/coordination.d.ts +93 -0
  34. package/vendor/agent-runtime/dist/coordination.js +145 -0
  35. package/vendor/agent-runtime/dist/index.d.ts +1 -0
  36. package/vendor/agent-runtime/dist/index.js +1 -0
  37. package/vendor/agent-runtime/dist/reconciler.d.ts +11 -0
  38. package/vendor/agent-runtime/dist/reconciler.js +41 -3
  39. package/vendor/agent-runtime/dist/symphony.d.ts +11 -9
  40. package/vendor/agent-runtime/dist/symphony.js +11 -7
@@ -0,0 +1,145 @@
1
+ export const CLIENT_RECONCILER_LEASE_TTL_MS = 30_000;
2
+ export const CLIENT_CAPABILITY_HEARTBEAT_TTL_MS = 45_000;
3
+ const CLIENT_KIND_PRIORITY = {
4
+ cli: 40,
5
+ desktop: 30,
6
+ mobile: 20,
7
+ web: 10,
8
+ };
9
+ export function defaultReconcilerOwnerForPolicyKind(policyKind) {
10
+ return policyKind === 'open_group' ? 'server' : 'client';
11
+ }
12
+ export function resolveReconcilerOwnership(input = {}) {
13
+ const humanAuthorityCount = resolveHumanAuthorityCount(input);
14
+ const owner = input.reconcilerOwner === 'client' || input.reconcilerOwner === 'server'
15
+ ? input.reconcilerOwner
16
+ : humanAuthorityCount !== undefined && humanAuthorityCount > 1
17
+ ? 'server'
18
+ : defaultReconcilerOwnerForPolicyKind(input.policyKind);
19
+ return {
20
+ reconcilerOwner: owner,
21
+ ...(humanAuthorityCount !== undefined ? { humanAuthorityCount } : {}),
22
+ };
23
+ }
24
+ export function isSingleHumanAuthority(input) {
25
+ return resolveHumanAuthorityCount(input) === 1;
26
+ }
27
+ export function hasMultipleHumanAuthorities(input) {
28
+ const count = resolveHumanAuthorityCount(input);
29
+ return count !== undefined && count > 1;
30
+ }
31
+ export function defaultSharedWakeAgentJobDedupeKey(input) {
32
+ return [input.thread, input.triggerMessage, input.agent].join('|');
33
+ }
34
+ export function createSharedWakeAgentJob(input) {
35
+ const job = {
36
+ ...input,
37
+ id: input.id ?? sharedWakeAgentJobId(input),
38
+ };
39
+ return job;
40
+ }
41
+ export function sharedWakeAgentJobId(input) {
42
+ return `wake_${hashString(defaultSharedWakeAgentJobDedupeKey(input))}`;
43
+ }
44
+ export function isClientReconcilerLeaseActive(lease, now = Date.now()) {
45
+ if (!lease?.fencingToken || !lease.thread || !lease.ownerClientId || !lease.ownerUser || !lease.expiresAt) {
46
+ return false;
47
+ }
48
+ const expiresAt = Date.parse(lease.expiresAt);
49
+ const nowMs = toEpochMs(now);
50
+ return Number.isFinite(expiresAt) && Number.isFinite(nowMs) && expiresAt > nowMs;
51
+ }
52
+ export function canClientCoordinateThread(input) {
53
+ const clientId = normalizeText(input.clientId);
54
+ if (!clientId || !input.thread || !isClientReconcilerLeaseActive(input.lease, input.now)) {
55
+ return false;
56
+ }
57
+ return input.lease.thread === input.thread && input.lease.ownerClientId === clientId;
58
+ }
59
+ export function isClientCapabilityAlive(client, now = Date.now(), heartbeatTtlMs = CLIENT_CAPABILITY_HEARTBEAT_TTL_MS) {
60
+ const heartbeatAt = Date.parse(client.heartbeatAt);
61
+ const nowMs = toEpochMs(now);
62
+ return Number.isFinite(heartbeatAt) && Number.isFinite(nowMs) && nowMs - heartbeatAt <= heartbeatTtlMs;
63
+ }
64
+ export function selectClientReconciler(clients, options = {}) {
65
+ const now = options.now ?? Date.now();
66
+ const eligible = clients
67
+ .filter((client) => client.canCoordinateClientOwned)
68
+ .filter((client) => !options.ownerUser || client.user === options.ownerUser)
69
+ .filter((client) => isClientCapabilityAlive(client, now, options.heartbeatTtlMs))
70
+ .sort(compareClientCapabilityForCoordination);
71
+ return eligible[0] ?? null;
72
+ }
73
+ export function grantClientReconcilerLease(options) {
74
+ const now = options.now ?? Date.now();
75
+ const ttl = options.leaseTtlMs ?? CLIENT_RECONCILER_LEASE_TTL_MS;
76
+ const previousOwner = options.previousLease && isClientReconcilerLeaseActive(options.previousLease, now)
77
+ ? options.clients.find((client) => (client.clientId === options.previousLease?.ownerClientId
78
+ && client.user === options.ownerUser
79
+ && client.canCoordinateClientOwned
80
+ && isClientCapabilityAlive(client, now, options.heartbeatTtlMs)))
81
+ : undefined;
82
+ const selected = previousOwner ?? selectClientReconciler(options.clients, {
83
+ ownerUser: options.ownerUser,
84
+ now,
85
+ heartbeatTtlMs: options.heartbeatTtlMs,
86
+ });
87
+ if (!selected) {
88
+ return null;
89
+ }
90
+ const nowMs = toEpochMs(now);
91
+ const expiresAt = new Date(nowMs + ttl).toISOString();
92
+ return {
93
+ thread: options.thread,
94
+ ownerClientId: selected.clientId,
95
+ ownerUser: selected.user,
96
+ fencingToken: options.fencingToken ?? createClientReconcilerFencingToken(options.thread, selected.clientId, nowMs, options.randomId),
97
+ expiresAt,
98
+ };
99
+ }
100
+ export function createClientReconcilerFencingToken(thread, clientId, now = Date.now(), randomId) {
101
+ const epoch = toEpochMs(now);
102
+ const entropy = normalizeText(randomId) ?? Math.random().toString(36).slice(2, 10);
103
+ return `client_${hashString(`${thread}|${clientId}|${epoch}|${entropy}`)}`;
104
+ }
105
+ function compareClientCapabilityForCoordination(a, b) {
106
+ const priority = CLIENT_KIND_PRIORITY[b.kind] - CLIENT_KIND_PRIORITY[a.kind];
107
+ if (priority !== 0)
108
+ return priority;
109
+ const heartbeat = Date.parse(b.heartbeatAt) - Date.parse(a.heartbeatAt);
110
+ if (heartbeat !== 0)
111
+ return heartbeat;
112
+ return a.clientId.localeCompare(b.clientId);
113
+ }
114
+ function resolveHumanAuthorityCount(input) {
115
+ if (typeof input.humanAuthorityCount === 'number' && Number.isFinite(input.humanAuthorityCount)) {
116
+ return Math.max(0, Math.floor(input.humanAuthorityCount));
117
+ }
118
+ if (!input.humanAuthorities)
119
+ return undefined;
120
+ const unique = new Set();
121
+ for (const value of input.humanAuthorities) {
122
+ const normalized = normalizeText(value);
123
+ if (normalized)
124
+ unique.add(normalized);
125
+ }
126
+ return unique.size;
127
+ }
128
+ function toEpochMs(value) {
129
+ if (typeof value === 'number')
130
+ return value;
131
+ if (value instanceof Date)
132
+ return value.getTime();
133
+ return Date.parse(value);
134
+ }
135
+ function normalizeText(value) {
136
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
137
+ }
138
+ function hashString(value) {
139
+ let hash = 0x811c9dc5;
140
+ for (let index = 0; index < value.length; index += 1) {
141
+ hash ^= value.charCodeAt(index);
142
+ hash = Math.imul(hash, 0x01000193);
143
+ }
144
+ return (hash >>> 0).toString(36);
145
+ }
@@ -4,6 +4,7 @@ export * from './auto-mode.js';
4
4
  export * from './companion-model.js';
5
5
  export * from './client-inbox-subscription.js';
6
6
  export * from './control-plane.js';
7
+ export * from './coordination.js';
7
8
  export * from './file-sync.js';
8
9
  export * from './reconciler.js';
9
10
  export * from './runtime.js';
@@ -4,6 +4,7 @@ export * from './auto-mode.js';
4
4
  export * from './companion-model.js';
5
5
  export * from './client-inbox-subscription.js';
6
6
  export * from './control-plane.js';
7
+ export * from './coordination.js';
7
8
  export * from './file-sync.js';
8
9
  export * from './reconciler.js';
9
10
  export * from './runtime.js';
@@ -1,4 +1,5 @@
1
1
  import type { AgentParticipantRole } from './turn-controller.js';
2
+ import { type ClientReconcilerLease, type ReconcilerOwner } from './coordination.js';
2
3
  export type ThreadPolicyKind = 'direct' | 'auto' | 'symphony' | 'open_group' | 'review';
3
4
  export type ThreadKind = 'main' | 'control' | 'worker' | 'review' | 'schedule' | 'schedule_run';
4
5
  export type ReconcilerEventType = 'message.appended' | 'input.required' | 'approval.required' | 'inbox.notification.created' | 'inbox.notification.updated' | 'delivery.submitted' | 'delivery.completed' | 'delivery.failed' | 'schedule.tick' | 'worker.blocked' | 'change.requested' | 'issue.updated' | 'task.updated' | 'run.updated' | (string & {});
@@ -62,6 +63,9 @@ export interface ThreadControlEvent<TData extends Record<string, unknown> = Reco
62
63
  }
63
64
  export interface ThreadPolicy {
64
65
  kind: ThreadPolicyKind;
66
+ reconcilerOwner?: ReconcilerOwner;
67
+ humanAuthorities?: string[];
68
+ humanAuthorityCount?: number;
65
69
  secretaryAgent?: string;
66
70
  defaultAssistantAgent?: string;
67
71
  assignedWorkerAgent?: string;
@@ -114,6 +118,7 @@ export interface ReconcilerNotificationEvent {
114
118
  export interface ReconcileDecision {
115
119
  id: string;
116
120
  policyKind: ThreadPolicyKind;
121
+ reconcilerOwner: ReconcilerOwner;
117
122
  event: ThreadControlEvent;
118
123
  placement: ThreadPlacement;
119
124
  wakeJobs: WakeJob[];
@@ -140,6 +145,7 @@ export interface WakeJobSummary {
140
145
  export interface ReconcileDecisionSummary {
141
146
  id: string;
142
147
  policyKind: ThreadPolicyKind;
148
+ reconcilerOwner: ReconcilerOwner;
143
149
  eventType: ReconcilerEventType;
144
150
  thread: string;
145
151
  chat?: string;
@@ -151,6 +157,11 @@ export interface ReconcileDecisionSummary {
151
157
  export interface ReconcileThreadEventInput {
152
158
  policy: ThreadPolicyKind | ThreadPolicy;
153
159
  event: ThreadControlEvent;
160
+ reconcilerOwner?: ReconcilerOwner;
161
+ humanAuthorities?: string[];
162
+ humanAuthorityCount?: number;
163
+ clientReconcilerLease?: ClientReconcilerLease | null;
164
+ requireClientReconcilerLease?: boolean;
154
165
  chat?: string;
155
166
  thread?: string;
156
167
  now?: Date;
@@ -1,3 +1,4 @@
1
+ import { canClientCoordinateThread, resolveReconcilerOwnership, } from './coordination.js';
1
2
  const DEFAULT_SECRETARY_AGENT = '__secretary__';
2
3
  const DEFAULT_ASSISTANT_AGENT = 'primary-agent';
3
4
  const DEFAULT_REVIEWER_AGENT = 'ai-reviewer';
@@ -24,12 +25,27 @@ export function reconcileThreadEvent(input) {
24
25
  thread: input.thread,
25
26
  randomId: input.randomId,
26
27
  });
27
- const jobs = selectWakeJobs(policy, event, placement, createdAt, input.randomId, input.client);
28
+ const ownership = resolveReconcilerOwnership({
29
+ policyKind: policy.kind,
30
+ humanAuthorities: input.humanAuthorities ?? policy.humanAuthorities,
31
+ humanAuthorityCount: input.humanAuthorityCount ?? policy.humanAuthorityCount,
32
+ reconcilerOwner: input.reconcilerOwner ?? policy.reconcilerOwner,
33
+ });
34
+ const clientCoordinationSkip = resolveClientCoordinationSkip({
35
+ ownership,
36
+ input,
37
+ placement,
38
+ createdAt,
39
+ });
40
+ const jobs = clientCoordinationSkip
41
+ ? []
42
+ : selectWakeJobs(policy, event, placement, createdAt, input.randomId, input.client);
28
43
  const notificationEvents = selectNotificationEvents(event, placement, createdAt, input.randomId);
29
- const skippedReason = jobs.length === 0 ? skipReasonFor(policy, event, input.client) : undefined;
44
+ const skippedReason = clientCoordinationSkip ?? (jobs.length === 0 ? skipReasonFor(policy, event, input.client) : undefined);
30
45
  return {
31
46
  id: createReconcilerId('decision', input.randomId),
32
47
  policyKind: policy.kind,
48
+ reconcilerOwner: ownership.reconcilerOwner,
33
49
  event,
34
50
  placement,
35
51
  wakeJobs: jobs,
@@ -42,6 +58,7 @@ export function summarizeReconcileDecision(decision) {
42
58
  return {
43
59
  id: decision.id,
44
60
  policyKind: decision.policyKind,
61
+ reconcilerOwner: decision.reconcilerOwner,
45
62
  eventType: decision.event.type,
46
63
  thread: decision.placement.thread,
47
64
  ...(decision.placement.chat ? { chat: decision.placement.chat } : {}),
@@ -120,6 +137,27 @@ export function resolveThreadPlacement(input) {
120
137
  kind: 'control',
121
138
  };
122
139
  }
140
+ function resolveClientCoordinationSkip(input) {
141
+ if (input.ownership.reconcilerOwner !== 'client') {
142
+ return undefined;
143
+ }
144
+ if (!input.input.requireClientReconcilerLease && !input.input.clientReconcilerLease) {
145
+ return undefined;
146
+ }
147
+ const clientId = input.input.client?.id;
148
+ if (canClientCoordinateThread({
149
+ clientId,
150
+ thread: input.placement.thread,
151
+ lease: input.input.clientReconcilerLease,
152
+ now: input.createdAt,
153
+ })) {
154
+ return undefined;
155
+ }
156
+ const owner = input.input.clientReconcilerLease?.ownerClientId;
157
+ return owner
158
+ ? `Client-owned Reconciler is leased by ${owner}; client ${clientId ?? 'unknown'} must not reconcile ${input.placement.thread}.`
159
+ : `Client-owned Reconciler requires an active client coordinator lease for ${input.placement.thread}.`;
160
+ }
123
161
  function selectWakeJobs(policy, event, placement, createdAt, randomId, client) {
124
162
  if (policy.kind === 'direct') {
125
163
  return isUserMessage(event)
@@ -127,7 +165,7 @@ function selectWakeJobs(policy, event, placement, createdAt, randomId, client) {
127
165
  targetAgent: policy.defaultAssistantAgent ?? DEFAULT_ASSISTANT_AGENT,
128
166
  targetRole: 'primary-agent',
129
167
  priority: 'normal',
130
- reason: 'Direct thread routes user messages to the default assistant.',
168
+ reason: 'Single-human assistant surfaces route user messages to the default assistant.',
131
169
  createdAt,
132
170
  randomId,
133
171
  })]
@@ -11,7 +11,7 @@ export declare const SYMPHONY_ISSUE_FILE_NAME = "issue.json";
11
11
  export declare const SYMPHONY_TASK_FILE_NAME = "task.json";
12
12
  export declare const SYMPHONY_DELIVERY_FILE_NAME = "delivery.json";
13
13
  export declare const SYMPHONY_SESSION_FILE_NAME = "session.json";
14
- export type SymphonyWorkspaceKind = 'git' | 'folder';
14
+ export type WorkerWorkspaceKind = 'git' | 'folder';
15
15
  export type SymphonyIdeaStatus = 'captured' | 'exploring' | 'candidate' | 'promoted' | 'deferred' | 'rejected' | 'superseded';
16
16
  export type SymphonyIdeaCommitment = 'thought' | 'direction' | 'tentative_decision' | 'committed';
17
17
  export type SymphonyIssueStatus = 'open' | 'triaging' | 'in_progress' | 'blocked' | 'resolved' | 'closed';
@@ -23,13 +23,13 @@ export type SymphonyResourceKind = 'idea' | 'issue' | 'task' | 'delivery' | 'ses
23
23
  export interface SymphonyReconcilerState {
24
24
  decisions: ReconcileDecisionSummary[];
25
25
  }
26
- export interface SymphonyWorkspaceRef {
26
+ export interface WorkerWorkspaceRef {
27
27
  path: string;
28
- kind: SymphonyWorkspaceKind;
28
+ kind: WorkerWorkspaceKind;
29
29
  repository?: string;
30
30
  branch?: string;
31
31
  worktree?: string;
32
- workspaceUri?: string;
32
+ workspace?: string;
33
33
  baseRevision?: string;
34
34
  environment?: SymphonyWorkerEnvironmentRef;
35
35
  }
@@ -59,6 +59,7 @@ export type SymphonyDelegationTargetSource = 'active-session' | 'group-chat' | '
59
59
  export interface SymphonyDelegationTarget extends SymphonyChatThreadRef {
60
60
  source: SymphonyDelegationTargetSource;
61
61
  backend: AutoModeWorkerBackend;
62
+ contact?: string;
62
63
  agent?: string;
63
64
  label?: string;
64
65
  }
@@ -104,6 +105,7 @@ export interface SymphonyTaskRecord extends SymphonyChatThreadRef {
104
105
  status: SymphonyTaskStatus;
105
106
  target: SymphonyDelegationTarget;
106
107
  backend: AutoModeWorkerBackend;
108
+ contact?: string;
107
109
  agent?: string;
108
110
  delivery: string;
109
111
  session: string;
@@ -145,7 +147,7 @@ export interface SymphonySessionRecord extends SymphonyChatThreadRef {
145
147
  secretaryAutoEnabled?: boolean;
146
148
  status: SymphonySessionStatus;
147
149
  cwd: string;
148
- workspace?: SymphonyWorkspaceRef;
150
+ workspaceRef?: WorkerWorkspaceRef;
149
151
  target: SymphonyDelegationTarget;
150
152
  model?: string;
151
153
  supervisor?: SymphonySupervisorPolicy;
@@ -178,18 +180,18 @@ export interface SymphonyWorkerSpec extends Partial<SymphonyDelegationTarget> {
178
180
  acceptanceCriteria?: string[];
179
181
  model?: string;
180
182
  supervisorIntervalMs?: number;
181
- workspace?: Partial<SymphonyWorkspaceRef>;
183
+ workspace?: Partial<WorkerWorkspaceRef>;
182
184
  }
183
185
  export interface CreateSymphonyRunPlanInput {
184
186
  objective: string;
185
187
  title?: string;
186
188
  acceptanceCriteria?: string[];
187
189
  workspacePath: string;
188
- workspaceKind?: SymphonyWorkspaceKind;
190
+ workspaceKind?: WorkerWorkspaceKind;
189
191
  repository?: string;
190
192
  branch?: string;
191
193
  worktree?: string;
192
- workspaceUri?: string;
194
+ workspace?: string;
193
195
  baseRevision?: string;
194
196
  environment?: Partial<SymphonyWorkerEnvironmentRef>;
195
197
  backend: AutoModeWorkerBackend;
@@ -240,7 +242,7 @@ export declare function renderSymphonyRuntimePrompt(input: {
240
242
  task: string;
241
243
  objective: string;
242
244
  acceptanceCriteria?: string[];
243
- workspace: SymphonyWorkspaceRef;
245
+ workspace: WorkerWorkspaceRef;
244
246
  backend: AutoModeWorkerBackend;
245
247
  mode: AutoModeMode;
246
248
  secretaryAutoEnabled?: boolean;
@@ -72,7 +72,7 @@ export function createRunPlan(input) {
72
72
  ...(normalizeOptionalText(input.repository) ? { repository: normalizeOptionalText(input.repository) } : {}),
73
73
  ...(normalizeOptionalText(input.branch) ? { branch: normalizeOptionalText(input.branch) } : {}),
74
74
  ...(normalizeOptionalText(input.worktree) ? { worktree: normalizeOptionalText(input.worktree) } : {}),
75
- ...(normalizeOptionalText(input.workspaceUri) ? { workspaceUri: normalizeOptionalText(input.workspaceUri) } : {}),
75
+ ...(normalizeOptionalText(input.workspace) ? { workspace: normalizeOptionalText(input.workspace) } : {}),
76
76
  ...(normalizeOptionalText(input.baseRevision) ? { baseRevision: normalizeOptionalText(input.baseRevision) } : {}),
77
77
  environment: normalizeSymphonyWorkerEnvironment(input.environment, input.backend),
78
78
  };
@@ -107,7 +107,7 @@ export function createRunPlan(input) {
107
107
  thread: target.thread ?? chatThread.thread,
108
108
  messages: target.messages ?? chatThread.messages,
109
109
  });
110
- const targetAgent = target.agent ?? `${target.backend}-worker`;
110
+ const targetAgent = target.agent ?? target.contact ?? target.backend;
111
111
  const dispatchReconciler = createSymphonyDispatchReconcilerState({
112
112
  issue: issueUri,
113
113
  task: uris.task,
@@ -146,7 +146,7 @@ export function createRunPlan(input) {
146
146
  ...(input.secretaryAutoEnabled !== undefined ? { secretaryAutoEnabled: input.secretaryAutoEnabled } : {}),
147
147
  status: 'planned',
148
148
  cwd: workerWorkspace.path,
149
- workspace: workerWorkspace,
149
+ workspaceRef: workerWorkspace,
150
150
  target,
151
151
  ...(spec.model ? { model: spec.model } : {}),
152
152
  ...(spec.supervisor ? { supervisor: spec.supervisor } : {}),
@@ -233,12 +233,13 @@ export function renderSymphonyRuntimePrompt(input) {
233
233
  ...(input.issuer?.thread ? [`Issuer thread: ${input.issuer.thread}`] : []),
234
234
  ...(input.target?.chat ? [`Target chat: ${input.target.chat}`] : []),
235
235
  ...(input.target?.thread ? [`Target thread: ${input.target.thread}`] : []),
236
+ ...(input.target?.contact ? [`Target contact: ${input.target.contact}`] : []),
236
237
  ...(input.target?.agent ? [`Target agent: ${input.target.agent}`] : []),
237
238
  ...(input.workerIndex && input.workerCount ? [`Worker: ${input.workerIndex}/${input.workerCount}`] : []),
238
239
  ...(workThread ? [`Work thread: ${workThread}`] : []),
239
240
  `Workspace: ${input.workspace.path}`,
240
241
  `Workspace kind: ${input.workspace.kind}`,
241
- ...(input.workspace.workspaceUri ? [`Workspace URI: ${input.workspace.workspaceUri}`] : []),
242
+ ...(input.workspace.workspace ? [`Workspace resource: ${input.workspace.workspace}`] : []),
242
243
  ...(input.workspace.repository ? [`Workspace repository: ${input.workspace.repository}`] : []),
243
244
  ...(input.workspace.branch ? [`Workspace branch: ${input.workspace.branch}`] : []),
244
245
  ...(input.workspace.baseRevision ? [`Workspace base revision: ${input.workspace.baseRevision}`] : []),
@@ -405,7 +406,7 @@ function normalizeSymphonyWorkerWorkspace(root, override, backend) {
405
406
  ...(normalizeOptionalText(override?.repository ?? root.repository) ? { repository: normalizeOptionalText(override?.repository ?? root.repository) } : {}),
406
407
  ...(normalizeOptionalText(override?.branch ?? root.branch) ? { branch: normalizeOptionalText(override?.branch ?? root.branch) } : {}),
407
408
  ...(normalizeOptionalText(override?.worktree ?? root.worktree) ? { worktree: normalizeOptionalText(override?.worktree ?? root.worktree) } : {}),
408
- ...(normalizeOptionalText(override?.workspaceUri ?? root.workspaceUri) ? { workspaceUri: normalizeOptionalText(override?.workspaceUri ?? root.workspaceUri) } : {}),
409
+ ...(normalizeOptionalText(override?.workspace ?? root.workspace) ? { workspace: normalizeOptionalText(override?.workspace ?? root.workspace) } : {}),
409
410
  ...(normalizeOptionalText(override?.baseRevision ?? root.baseRevision) ? { baseRevision: normalizeOptionalText(override?.baseRevision ?? root.baseRevision) } : {}),
410
411
  environment: normalizeSymphonyWorkerEnvironment(override?.environment ?? root.environment, backend),
411
412
  };
@@ -488,12 +489,14 @@ function createSymphonySupervisorPolicy(intervalMs) {
488
489
  }
489
490
  function normalizeSymphonyDelegationTarget(input) {
490
491
  const explicit = input.target ?? {};
492
+ const backend = explicit.backend ?? input.backend;
491
493
  const chatThread = normalizeSymphonyChatThreadRef({
492
494
  chat: explicit.chat ?? input.chat,
493
495
  thread: explicit.thread ?? input.thread,
494
496
  messages: explicit.messages ?? input.messages,
495
497
  });
496
- const agent = normalizeOptionalText(explicit.agent) ?? `${input.backend}-worker`;
498
+ const contact = normalizeOptionalText(explicit.contact) ?? normalizeOptionalText(explicit.agent) ?? backend;
499
+ const agent = normalizeOptionalText(explicit.agent) ?? contact;
497
500
  const label = normalizeOptionalText(explicit.label);
498
501
  const source = explicit.source
499
502
  ?? (chatThread.chat || chatThread.thread
@@ -503,7 +506,8 @@ function normalizeSymphonyDelegationTarget(input) {
503
506
  : 'default');
504
507
  return {
505
508
  source,
506
- backend: explicit.backend ?? input.backend,
509
+ backend,
510
+ contact,
507
511
  agent,
508
512
  ...(label ? { label } : {}),
509
513
  ...chatThread,