@undefineds.co/linx 0.3.8 → 0.3.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +17 -0
  2. package/dist/generated/version.js +2 -2
  3. package/dist/generated/version.js.map +1 -1
  4. package/dist/index.js +13 -3
  5. package/dist/index.js.map +1 -1
  6. package/dist/lib/auto-mode/pod-approval.js +216 -2
  7. package/dist/lib/auto-mode/pod-approval.js.map +1 -1
  8. package/dist/lib/linx-status-line.js +335 -0
  9. package/dist/lib/linx-status-line.js.map +1 -0
  10. package/dist/lib/linx-tui-contract.js +3 -3
  11. package/dist/lib/linx-tui-contract.js.map +1 -1
  12. package/dist/lib/models.js +2 -2
  13. package/dist/lib/models.js.map +1 -1
  14. package/dist/lib/pi-adapter/auth.js +68 -0
  15. package/dist/lib/pi-adapter/auth.js.map +1 -0
  16. package/dist/lib/pi-adapter/interactive.js +326 -231
  17. package/dist/lib/pi-adapter/interactive.js.map +1 -1
  18. package/dist/lib/pi-adapter/pod-mirror.js +52 -2
  19. package/dist/lib/pi-adapter/pod-mirror.js.map +1 -1
  20. package/dist/lib/pi-adapter/pod-tools.js +140 -0
  21. package/dist/lib/pi-adapter/pod-tools.js.map +1 -0
  22. package/dist/lib/pi-adapter/runtime.js +14 -4
  23. package/dist/lib/pi-adapter/runtime.js.map +1 -1
  24. package/dist/lib/pi-adapter/stream.js +24 -1
  25. package/dist/lib/pi-adapter/stream.js.map +1 -1
  26. package/dist/lib/status-line-command.js +108 -0
  27. package/dist/lib/status-line-command.js.map +1 -0
  28. package/dist/lib/symphony/pod-projection.js +15 -7
  29. package/dist/lib/symphony/pod-projection.js.map +1 -1
  30. package/dist/lib/symphony-command.js +20 -21
  31. package/dist/lib/symphony-command.js.map +1 -1
  32. package/dist/skills/symphony/SKILL.md +69 -10
  33. package/dist/skills/xpod-cli/SKILL.md +70 -0
  34. package/package.json +9 -3
  35. package/vendor/agent-runtime/dist/client-inbox-subscription.d.ts +56 -0
  36. package/vendor/agent-runtime/dist/client-inbox-subscription.js +93 -0
  37. package/vendor/agent-runtime/dist/index.d.ts +1 -0
  38. package/vendor/agent-runtime/dist/index.js +1 -0
  39. package/vendor/agent-runtime/dist/reconciler.d.ts +60 -1
  40. package/vendor/agent-runtime/dist/reconciler.js +148 -4
  41. package/vendor/agent-runtime/dist/thread-reconciler-controller.d.ts +2 -1
  42. package/vendor/agent-runtime/dist/thread-reconciler-controller.js +4 -0
@@ -2,6 +2,7 @@ export * from './acp.js';
2
2
  export * from './agent-runtime.js';
3
3
  export * from './auto-mode.js';
4
4
  export * from './companion-model.js';
5
+ export * from './client-inbox-subscription.js';
5
6
  export * from './control-plane.js';
6
7
  export * from './file-sync.js';
7
8
  export * from './reconciler.js';
@@ -2,6 +2,7 @@ export * from './acp.js';
2
2
  export * from './agent-runtime.js';
3
3
  export * from './auto-mode.js';
4
4
  export * from './companion-model.js';
5
+ export * from './client-inbox-subscription.js';
5
6
  export * from './control-plane.js';
6
7
  export * from './file-sync.js';
7
8
  export * from './reconciler.js';
@@ -1,11 +1,43 @@
1
1
  import type { AgentParticipantRole } from './turn-controller.js';
2
2
  export type ThreadPolicyKind = 'direct' | 'auto' | 'symphony' | 'open_group' | 'review';
3
3
  export type ThreadKind = 'main' | 'control' | 'worker' | 'review' | 'schedule' | 'schedule_run';
4
- export type ReconcilerEventType = 'message.appended' | 'input.required' | 'approval.required' | 'delivery.submitted' | 'delivery.completed' | 'delivery.failed' | 'schedule.tick' | 'worker.blocked' | 'change.requested' | 'issue.updated' | 'task.updated' | 'run.updated' | (string & {});
4
+ 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 & {});
5
5
  export type ReconcilerActorRole = AgentParticipantRole | 'worker' | 'reviewer' | 'runtime' | 'scheduler' | 'tool' | 'assistant';
6
6
  export type WakeJobTargetRole = AgentParticipantRole | 'worker' | 'reviewer';
7
7
  export type WakeJobPriority = 'low' | 'normal' | 'high';
8
8
  export type WakeJobStatus = 'queued';
9
+ export type ReconcilerNotificationAudience = 'user';
10
+ export type ReconcilerNotificationChannel = 'inbox';
11
+ export type ReconcilerClientFocusState = 'focused' | 'background' | 'closed';
12
+ export type ReconcilerControlResourceClaimStatus = 'claimed' | 'lost' | 'display_only' | 'none';
13
+ export interface ReconcilerControlResourceClaimState {
14
+ status: ReconcilerControlResourceClaimStatus;
15
+ controlResource?: string;
16
+ leaseOwner?: string;
17
+ leaseExpiresAt?: string;
18
+ reason?: string;
19
+ }
20
+ export interface ReconcilerClientContext {
21
+ id: string;
22
+ agentCapable?: boolean;
23
+ secretaryRuntimeAvailable?: boolean;
24
+ focusState?: ReconcilerClientFocusState;
25
+ activeThread?: string;
26
+ activeChat?: string;
27
+ generationLocked?: boolean;
28
+ controlResourceClaim?: ReconcilerControlResourceClaimState;
29
+ }
30
+ export interface SecretaryInboxWakeContext {
31
+ eventId: string;
32
+ eventType: ReconcilerEventType;
33
+ controlResource?: string;
34
+ sourceThread?: string;
35
+ sourceRun?: string;
36
+ sourceTask?: string;
37
+ requestKind?: string;
38
+ priority: WakeJobPriority;
39
+ shortSummary?: string;
40
+ }
9
41
  export interface ReconcilerActorRef {
10
42
  id?: string;
11
43
  role?: ReconcilerActorRole;
@@ -60,12 +92,32 @@ export interface WakeJob {
60
92
  sourceEventType: ReconcilerEventType;
61
93
  createdAt: string;
62
94
  }
95
+ export interface ReconcilerNotificationEvent {
96
+ id: string;
97
+ thread: string;
98
+ chat?: string;
99
+ audience: ReconcilerNotificationAudience;
100
+ channel: ReconcilerNotificationChannel;
101
+ priority: WakeJobPriority;
102
+ reason: string;
103
+ sourceEventId?: string;
104
+ sourceEventType: ReconcilerEventType;
105
+ sourceResource?: string;
106
+ inboxNotification?: string;
107
+ sourceThread?: string;
108
+ sourceRun?: string;
109
+ sourceTask?: string;
110
+ requestKind?: string;
111
+ shortSummary?: string;
112
+ createdAt: string;
113
+ }
63
114
  export interface ReconcileDecision {
64
115
  id: string;
65
116
  policyKind: ThreadPolicyKind;
66
117
  event: ThreadControlEvent;
67
118
  placement: ThreadPlacement;
68
119
  wakeJobs: WakeJob[];
120
+ notificationEvents?: ReconcilerNotificationEvent[];
69
121
  skippedReason?: string;
70
122
  createdAt: string;
71
123
  }
@@ -82,6 +134,7 @@ export interface WakeJobSummary {
82
134
  sourceEventId?: string;
83
135
  sourceEventType: ReconcilerEventType;
84
136
  sourceResource?: string;
137
+ inboxNotification?: string;
85
138
  controlGate?: string;
86
139
  }
87
140
  export interface ReconcileDecisionSummary {
@@ -92,6 +145,7 @@ export interface ReconcileDecisionSummary {
92
145
  chat?: string;
93
146
  skippedReason?: string;
94
147
  wakeJobs: WakeJobSummary[];
148
+ notificationEvents?: ReconcilerNotificationEvent[];
95
149
  createdAt: string;
96
150
  }
97
151
  export interface ReconcileThreadEventInput {
@@ -101,6 +155,7 @@ export interface ReconcileThreadEventInput {
101
155
  thread?: string;
102
156
  now?: Date;
103
157
  randomId?: string;
158
+ client?: ReconcilerClientContext;
104
159
  }
105
160
  export interface ThreadReconciler {
106
161
  readonly policy: ThreadPolicy;
@@ -115,3 +170,7 @@ export declare function resolveThreadPlacement(input: {
115
170
  thread?: string;
116
171
  randomId?: string;
117
172
  }): ThreadPlacement;
173
+ export declare function buildSecretaryInboxWakeContext(event: ThreadControlEvent, options?: {
174
+ priority?: WakeJobPriority;
175
+ }): SecretaryInboxWakeContext;
176
+ export declare function canClientScheduleInboxWake(client: ReconcilerClientContext | undefined, event: ThreadControlEvent, now?: Date | string | number): boolean;
@@ -24,14 +24,16 @@ export function reconcileThreadEvent(input) {
24
24
  thread: input.thread,
25
25
  randomId: input.randomId,
26
26
  });
27
- const jobs = selectWakeJobs(policy, event, placement, createdAt, input.randomId);
28
- const skippedReason = jobs.length === 0 ? skipReasonFor(policy, event) : undefined;
27
+ const jobs = selectWakeJobs(policy, event, placement, createdAt, input.randomId, input.client);
28
+ const notificationEvents = selectNotificationEvents(event, placement, createdAt, input.randomId);
29
+ const skippedReason = jobs.length === 0 ? skipReasonFor(policy, event, input.client) : undefined;
29
30
  return {
30
31
  id: createReconcilerId('decision', input.randomId),
31
32
  policyKind: policy.kind,
32
33
  event,
33
34
  placement,
34
35
  wakeJobs: jobs,
36
+ ...(notificationEvents.length > 0 ? { notificationEvents } : {}),
35
37
  ...(skippedReason ? { skippedReason } : {}),
36
38
  createdAt,
37
39
  };
@@ -57,8 +59,12 @@ export function summarizeReconcileDecision(decision) {
57
59
  ...(job.sourceEventId ? { sourceEventId: job.sourceEventId } : {}),
58
60
  sourceEventType: job.sourceEventType,
59
61
  ...(decision.event.resource ? { sourceResource: decision.event.resource } : {}),
62
+ ...(normalizeText(decision.event.data?.inboxNotification) ? { inboxNotification: normalizeText(decision.event.data?.inboxNotification) } : {}),
60
63
  ...(resolveControlGate(decision.event) ? { controlGate: resolveControlGate(decision.event) } : {}),
61
64
  })),
65
+ ...(decision.notificationEvents && decision.notificationEvents.length > 0
66
+ ? { notificationEvents: decision.notificationEvents.map((event) => ({ ...event })) }
67
+ : {}),
62
68
  createdAt: decision.createdAt,
63
69
  };
64
70
  }
@@ -114,7 +120,7 @@ export function resolveThreadPlacement(input) {
114
120
  kind: 'control',
115
121
  };
116
122
  }
117
- function selectWakeJobs(policy, event, placement, createdAt, randomId) {
123
+ function selectWakeJobs(policy, event, placement, createdAt, randomId, client) {
118
124
  if (policy.kind === 'direct') {
119
125
  return isUserMessage(event)
120
126
  ? [createWakeJob(policy, event, placement, {
@@ -128,6 +134,11 @@ function selectWakeJobs(policy, event, placement, createdAt, randomId) {
128
134
  : [];
129
135
  }
130
136
  if (policy.kind === 'auto') {
137
+ if (isActionableInboxEvent(event)) {
138
+ return canClientScheduleInboxWake(client, event, createdAt)
139
+ ? [createSecretaryWakeJob(policy, event, placement, 'Auto mode claimed an actionable control resource on this client and schedules the same-Thread Secretary to check Inbox.', 'high', createdAt, randomId)]
140
+ : [];
141
+ }
131
142
  if (isInputApprovalOrBlocker(event)) {
132
143
  return [createSecretaryWakeJob(policy, event, placement, 'Auto mode routes input, approval, and blocker events to the same-Thread Secretary.', 'high', createdAt, randomId)];
133
144
  }
@@ -140,6 +151,11 @@ function selectWakeJobs(policy, event, placement, createdAt, randomId) {
140
151
  return [];
141
152
  }
142
153
  if (policy.kind === 'symphony') {
154
+ if (isActionableInboxEvent(event)) {
155
+ return canClientScheduleInboxWake(client, event, createdAt)
156
+ ? [createSecretaryWakeJob(policy, event, placement, 'Symphony claimed an actionable control resource on this client and schedules Secretary to check Inbox; the payload is runtime control context, not a human user message.', 'high', createdAt, randomId)]
157
+ : [];
158
+ }
143
159
  if (isInputApprovalOrBlocker(event) || event.type === 'change.requested') {
144
160
  return [createSecretaryWakeJob(policy, event, placement, 'Symphony routes blocker, input, approval, and change requests to Secretary for semantic judgment.', 'high', createdAt, randomId)];
145
161
  }
@@ -197,6 +213,92 @@ function selectWakeJobs(policy, event, placement, createdAt, randomId) {
197
213
  }
198
214
  return [];
199
215
  }
216
+ function selectNotificationEvents(event, placement, createdAt, randomId) {
217
+ if (!isActionableInboxEvent(event)) {
218
+ return [];
219
+ }
220
+ const wakeContext = buildSecretaryInboxWakeContext(event, { priority: 'high' });
221
+ return [{
222
+ id: createReconcilerId('notice', randomId ?? event.id),
223
+ thread: placement.thread,
224
+ ...(placement.chat ? { chat: placement.chat } : {}),
225
+ audience: 'user',
226
+ channel: 'inbox',
227
+ priority: 'high',
228
+ reason: 'Inbox envelope changed; notify subscribed clients to refresh Inbox and read the linked control resource without converting it into chat.',
229
+ ...(event.id ? { sourceEventId: event.id } : {}),
230
+ sourceEventType: event.type,
231
+ ...(event.resource ? { sourceResource: event.resource } : {}),
232
+ ...(normalizeText(event.data?.inboxNotification) ? { inboxNotification: normalizeText(event.data?.inboxNotification) } : {}),
233
+ ...(wakeContext.sourceThread ? { sourceThread: wakeContext.sourceThread } : {}),
234
+ ...(wakeContext.sourceRun ? { sourceRun: wakeContext.sourceRun } : {}),
235
+ ...(wakeContext.sourceTask ? { sourceTask: wakeContext.sourceTask } : {}),
236
+ ...(wakeContext.requestKind ? { requestKind: wakeContext.requestKind } : {}),
237
+ ...(wakeContext.shortSummary ? { shortSummary: wakeContext.shortSummary } : {}),
238
+ createdAt,
239
+ }];
240
+ }
241
+ export function buildSecretaryInboxWakeContext(event, options = {}) {
242
+ const controlResource = normalizeText(event.resource)
243
+ ?? normalizeText(event.data?.controlResource)
244
+ ?? normalizeText(event.data?.controlResourceId)
245
+ ?? normalizeText(event.data?.approval)
246
+ ?? normalizeText(event.data?.inputRequest);
247
+ const requestKind = normalizeText(event.data?.requestKind)
248
+ ?? normalizeText(event.data?.kind)
249
+ ?? normalizeText(event.data?.type);
250
+ const sourceThread = normalizeText(event.data?.sourceThread);
251
+ const sourceRun = normalizeText(event.data?.sourceRun);
252
+ const sourceTask = normalizeText(event.data?.sourceTask);
253
+ const shortSummary = normalizeText(event.data?.shortSummary)
254
+ ?? normalizeText(event.data?.summary)
255
+ ?? normalizeText(event.content);
256
+ return {
257
+ eventId: event.id ?? createReconcilerId('event', controlResource ?? requestKind),
258
+ eventType: event.type,
259
+ ...(controlResource ? { controlResource } : {}),
260
+ ...(sourceThread ? { sourceThread } : {}),
261
+ ...(sourceRun ? { sourceRun } : {}),
262
+ ...(sourceTask ? { sourceTask } : {}),
263
+ ...(requestKind ? { requestKind } : {}),
264
+ priority: options.priority ?? 'normal',
265
+ ...(shortSummary ? { shortSummary } : {}),
266
+ };
267
+ }
268
+ export function canClientScheduleInboxWake(client, event, now = Date.now()) {
269
+ if (!client || !isActionableInboxEvent(event)) {
270
+ return false;
271
+ }
272
+ if (client.agentCapable !== true) {
273
+ return false;
274
+ }
275
+ if (client.secretaryRuntimeAvailable === false) {
276
+ return false;
277
+ }
278
+ const claim = client.controlResourceClaim;
279
+ if (!claim || claim.status !== 'claimed') {
280
+ return false;
281
+ }
282
+ const eventControlResource = normalizeText(event.resource)
283
+ ?? normalizeText(event.data?.controlResource)
284
+ ?? normalizeText(event.data?.controlResourceId);
285
+ const claimControlResource = normalizeText(claim.controlResource);
286
+ if (eventControlResource && claimControlResource && eventControlResource !== claimControlResource) {
287
+ return false;
288
+ }
289
+ const leaseOwner = normalizeText(claim.leaseOwner);
290
+ if (leaseOwner && leaseOwner !== client.id) {
291
+ return false;
292
+ }
293
+ if (claim.leaseExpiresAt) {
294
+ const expiresAt = Date.parse(claim.leaseExpiresAt);
295
+ const nowMs = typeof now === 'number' ? now : Date.parse(now instanceof Date ? now.toISOString() : now);
296
+ if (!Number.isNaN(expiresAt) && !Number.isNaN(nowMs) && expiresAt <= nowMs) {
297
+ return false;
298
+ }
299
+ }
300
+ return true;
301
+ }
200
302
  function createSecretaryWakeJob(policy, event, placement, reason, priority, createdAt, randomId) {
201
303
  return createWakeJob(policy, event, placement, {
202
304
  targetAgent: policy.secretaryAgent ?? DEFAULT_SECRETARY_AGENT,
@@ -245,12 +347,34 @@ function threadKindFromEvent(event) {
245
347
  if (event.type === 'issue.updated' || event.type === 'task.updated' || event.type === 'run.updated') {
246
348
  return 'control';
247
349
  }
350
+ if (isInboxEvent(event)) {
351
+ return 'control';
352
+ }
248
353
  return 'main';
249
354
  }
250
355
  function isInputApprovalOrBlocker(event) {
251
356
  return event.type === 'input.required' || event.type === 'approval.required' || event.type === 'worker.blocked';
252
357
  }
253
358
  function resolveControlGate(event) {
359
+ if (isInboxEvent(event)) {
360
+ const explicitGate = normalizeText(event.data?.controlGate);
361
+ if (explicitGate) {
362
+ return explicitGate;
363
+ }
364
+ const requestKind = normalizeText(event.data?.requestKind)
365
+ ?? normalizeText(event.data?.kind)
366
+ ?? normalizeText(event.data?.type);
367
+ if (requestKind && /approval|auth|credential|grant|permission|destructive/iu.test(requestKind)) {
368
+ return 'authority';
369
+ }
370
+ if (requestKind && /block|fail|feasibil/iu.test(requestKind)) {
371
+ return 'feasibility';
372
+ }
373
+ if (requestKind && /change|scope|rebase|steer/iu.test(requestKind)) {
374
+ return 'change';
375
+ }
376
+ return 'binding';
377
+ }
254
378
  if (event.type === 'input.required' || event.type === 'approval.required') {
255
379
  return 'authority';
256
380
  }
@@ -290,6 +414,19 @@ function isPrimaryAgentMessage(event) {
290
414
  || event.data?.role === 'assistant'
291
415
  || event.data?.source === 'primary-agent');
292
416
  }
417
+ function isInboxEvent(event) {
418
+ return event.type === 'inbox.notification.created' || event.type === 'inbox.notification.updated';
419
+ }
420
+ function isActionableInboxEvent(event) {
421
+ if (!isInboxEvent(event)) {
422
+ return false;
423
+ }
424
+ const status = normalizeText(event.data?.status);
425
+ if (!status) {
426
+ return event.type === 'inbox.notification.created';
427
+ }
428
+ return status === 'pending' || status === 'handling';
429
+ }
293
430
  function isTaskDispatchDelivery(event) {
294
431
  return event.data?.deliveryType === 'task_dispatch'
295
432
  || event.data?.type === 'task_dispatch'
@@ -328,10 +465,17 @@ function isMentioned(content, agent) {
328
465
  .filter(Boolean)
329
466
  .some((name) => normalized.includes(`@${name.toLowerCase()}`));
330
467
  }
331
- function skipReasonFor(policy, event) {
468
+ function skipReasonFor(policy, event, client) {
332
469
  if (policy.kind === 'open_group') {
333
470
  return 'No mentioned or subscribed agents matched the event.';
334
471
  }
472
+ if (isActionableInboxEvent(event) && (policy.kind === 'auto' || policy.kind === 'symphony')) {
473
+ if (!client) {
474
+ return `Policy ${policy.kind} keeps ${event.type} display-only until a subscribed client claims the control resource.`;
475
+ }
476
+ const claimStatus = client.controlResourceClaim?.status ?? 'none';
477
+ return `Policy ${policy.kind} keeps ${event.type} display-only for client ${client.id} because control resource claim status is ${claimStatus}.`;
478
+ }
335
479
  return `Policy ${policy.kind} does not wake an agent for ${event.type}.`;
336
480
  }
337
481
  function createSyntheticThreadUri(kind, value) {
@@ -1,4 +1,4 @@
1
- import { type ReconcileDecision, type ReconcileDecisionSummary, type ReconcileThreadEventInput, type ThreadControlEvent, type ThreadPolicy, type ThreadPolicyKind, type WakeJob } from './reconciler.js';
1
+ import { type ReconcileDecision, type ReconcileDecisionSummary, type ReconcileThreadEventInput, type ReconcilerNotificationEvent, type ThreadControlEvent, type ThreadPolicy, type ThreadPolicyKind, type WakeJob } from './reconciler.js';
2
2
  import { type WakeJobExecutionRecord, type WakeJobExecutionRecordSummary, type WakeJobSchedulerSnapshot, type WakeJobSchedulerSnapshotSummary } from './wake-scheduler.js';
3
3
  type MaybePromise<T> = T | Promise<T>;
4
4
  export interface ThreadWakeJobContext {
@@ -19,6 +19,7 @@ export interface ThreadReconcilerControllerOptions {
19
19
  onWakeJobStarted?: (record: WakeJobExecutionRecord, decision: ReconcileDecisionSummary) => void;
20
20
  onWakeJobCompleted?: (record: WakeJobExecutionRecord, decision: ReconcileDecisionSummary) => void;
21
21
  onWakeJobFailed?: (record: WakeJobExecutionRecord, decision: ReconcileDecisionSummary) => void;
22
+ onNotificationEvent?: (event: ReconcilerNotificationEvent, decision: ReconcileDecisionSummary) => void;
22
23
  }
23
24
  export interface ThreadReconcilerDispatchOptions extends Omit<ReconcileThreadEventInput, 'policy' | 'event'> {
24
25
  }
@@ -58,10 +58,14 @@ class ConfiguredThreadReconcilerController {
58
58
  dispatch(event, options = {}) {
59
59
  const { decision, summary } = decideThreadControlEvent({
60
60
  ...options,
61
+ now: options.now ?? this.options.now?.(),
61
62
  policy: this.options.policy,
62
63
  event,
63
64
  });
64
65
  this.options.onDecision?.(summary);
66
+ for (const notificationEvent of decision.notificationEvents ?? []) {
67
+ this.options.onNotificationEvent?.(notificationEvent, summary);
68
+ }
65
69
  for (const job of decision.wakeJobs) {
66
70
  const key = defaultWakeJobDedupeKey(job);
67
71
  if (!this.contexts.has(key)) {