@astrosheep/square 0.3.94 → 0.3.96

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 (99) hide show
  1. package/dist/artifact.js +1 -1
  2. package/dist/attention-presentation.d.ts +5 -4
  3. package/dist/attention-presentation.js +18 -8
  4. package/dist/attention.d.ts +19 -0
  5. package/dist/attention.js +30 -0
  6. package/dist/boundary-presentation.js +50 -48
  7. package/dist/catch-decisions.d.ts +1 -2
  8. package/dist/catch-decisions.js +41 -21
  9. package/dist/channel.d.ts +72 -0
  10. package/dist/channel.js +86 -0
  11. package/dist/claude-delivery.js +2 -1
  12. package/dist/cli/context.js +0 -2
  13. package/dist/cli/express-presentation.js +3 -3
  14. package/dist/cli/json.js +1 -1
  15. package/dist/cli/program.js +1 -1
  16. package/dist/cli/registry.js +1 -3
  17. package/dist/cli/square-commands.d.ts +0 -2
  18. package/dist/cli/square-commands.js +21 -27
  19. package/dist/decisions.d.ts +0 -5
  20. package/dist/decisions.js +0 -13
  21. package/dist/delivery-attempts.d.ts +52 -0
  22. package/dist/delivery-attempts.js +73 -0
  23. package/dist/delivery-effects.d.ts +139 -0
  24. package/dist/delivery-effects.js +163 -0
  25. package/dist/delivery-operations.js +43 -68
  26. package/dist/delivery-primitives.d.ts +5 -0
  27. package/dist/delivery-primitives.js +3 -0
  28. package/dist/delivery.d.ts +1 -1
  29. package/dist/delivery.js +8 -7
  30. package/dist/help.js +4 -6
  31. package/dist/http-delivery.d.ts +16 -0
  32. package/dist/http-delivery.js +101 -0
  33. package/dist/inbox.js +3 -1
  34. package/dist/mcp.js +65 -63
  35. package/dist/model.d.ts +10 -6
  36. package/dist/model.js +20 -1
  37. package/dist/notifications.js +2 -0
  38. package/dist/operation-presentation.d.ts +1 -2
  39. package/dist/operation-presentation.js +12 -20
  40. package/dist/ports.d.ts +8 -14
  41. package/dist/presentation-operations.d.ts +15 -19
  42. package/dist/presentation-operations.js +47 -57
  43. package/dist/presentation.d.ts +2 -9
  44. package/dist/presentation.js +13 -48
  45. package/dist/receive.d.ts +66 -0
  46. package/dist/receive.js +54 -0
  47. package/dist/runtime.d.ts +0 -1
  48. package/dist/runtime.js +2 -3
  49. package/dist/sqlite-square.d.ts +3 -2
  50. package/dist/sqlite-square.js +83 -57
  51. package/dist/square-api.d.ts +1 -1
  52. package/dist/square-api.js +1 -1
  53. package/dist/square-core.d.ts +1 -9
  54. package/dist/square-domain.js +29 -36
  55. package/dist/square-facade.d.ts +25 -23
  56. package/dist/square-legacy-framing.d.ts +2 -0
  57. package/dist/square-legacy-framing.js +8 -0
  58. package/dist/square-migration.d.ts +3 -1
  59. package/dist/square-migration.js +46 -13
  60. package/dist/square-receive.d.ts +6 -0
  61. package/dist/square-receive.js +18 -0
  62. package/dist/square-sqlite.d.ts +5 -3
  63. package/dist/square-sqlite.js +100 -92
  64. package/dist/square-tools.d.ts +1 -0
  65. package/dist/square-tools.js +12 -12
  66. package/dist/views.d.ts +2 -3
  67. package/dist/views.js +14 -19
  68. package/dist/wakes.d.ts +0 -3
  69. package/dist/wakes.js +3 -5
  70. package/examples/symmetric-domains.mjs +250 -0
  71. package/guides/application-facade-delivery-evidence.md +5 -5
  72. package/guides/attention-primitive.md +32 -0
  73. package/guides/change-waits.md +2 -2
  74. package/guides/codex-delivery.md +1 -1
  75. package/guides/delivery-primitives.md +180 -0
  76. package/guides/mcp.md +1 -1
  77. package/guides/migration.md +12 -6
  78. package/guides/participant-ownership.md +1 -1
  79. package/guides/pi-tools.md +2 -2
  80. package/guides/primitive-catch-consolidation.md +5 -5
  81. package/guides/primitive-history-consolidation.md +2 -2
  82. package/guides/primitive-status-consolidation.md +3 -3
  83. package/guides/primitive-storage-consolidation.md +1 -1
  84. package/guides/receive-primitive.md +90 -0
  85. package/guides/row-runtime-delivery-evidence.md +1 -1
  86. package/guides/row-storage-design.md +17 -23
  87. package/guides/square-api.md +77 -10
  88. package/guides/symmetric-domains.md +92 -0
  89. package/package.json +18 -1
  90. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  91. package/plugins/claude/skills/square/SKILL.md +2 -6
  92. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  93. package/plugins/paseo/client/marble.ts +7 -0
  94. package/plugins/paseo/client/markdown.tsx +65 -9
  95. package/plugins/paseo/client/palette.ts +43 -20
  96. package/plugins/paseo/client/square-message.tsx +57 -48
  97. package/plugins/paseo/shared/message.ts +1 -2
  98. package/plugins/pi/square-pi.js +6 -4
  99. package/skills/square/SKILL.md +2 -6
package/dist/artifact.js CHANGED
@@ -117,7 +117,7 @@ function synchronous(value) {
117
117
  }
118
118
  }
119
119
  const readMethods = [
120
- 'header', 'participant', 'roster', 'attention', 'incomingAttention', 'hold',
120
+ 'header', 'participant', 'roster', 'attention', 'incomingAttention',
121
121
  'activity', 'activityHeads', 'activities', 'activityCount', 'contextIndexes', 'sayOrdinals', 'recentSayTimes',
122
122
  'audience', 'directed', 'directedAt', 'directedCount', 'pendingAudience',
123
123
  'receipt', 'receiptFacts', 'receiptAssignments', 'lease', 'participantCounts', 'seat', 'seatsForOwner',
@@ -1,4 +1,4 @@
1
- import type { DirectedNotificationRoute } from './model.js';
1
+ import type { ActivityDelivery, DirectedNotificationRoute } from './model.js';
2
2
  export declare const ATTENTION_BODY_MAX = 200;
3
3
  export interface AttentionPreview {
4
4
  squarePath: string;
@@ -7,13 +7,14 @@ export interface AttentionPreview {
7
7
  actor: string;
8
8
  route: DirectedNotificationRoute;
9
9
  body: string;
10
+ delivery?: ActivityDelivery;
10
11
  }
11
- export declare function previewAttentionBody(body: string): string;
12
- export declare function attentionBodyIsClipped(body: string): boolean;
12
+ export declare function previewAttentionBody(body: string, delivery?: ActivityDelivery): string;
13
+ export declare function attentionBodyIsClipped(body: string, delivery?: ActivityDelivery): boolean;
13
14
  export declare function displayAttentionPath(squarePath: string): string;
14
15
  export declare function renderAttentionDescription({ actor, route }: Pick<AttentionPreview, 'actor' | 'route'>): string;
15
16
  /** Full notification text for human-facing expansion, never the bounded model preview. */
16
17
  export declare function renderAttentionFull(attention: AttentionPreview): string;
17
18
  /** Shared model-facing envelope; transport correlation and human cards stay outside it. */
18
- export declare function renderSquareReminder(previews: readonly string[]): string;
19
+ export declare function renderSquareReminder(previews: readonly string[], deliveries?: readonly (ActivityDelivery | undefined)[]): string;
19
20
  export declare function renderAttentionPreview(attention: AttentionPreview): string;
@@ -1,14 +1,16 @@
1
1
  import { homedir } from 'node:os';
2
2
  import { formatActivityId } from './square-core.js';
3
3
  export const ATTENTION_BODY_MAX = 200;
4
- export function previewAttentionBody(body) {
5
- const compact = body.replace(/\r\n/g, '\n');
6
- if (compact.length <= ATTENTION_BODY_MAX)
4
+ export function previewAttentionBody(body, delivery) {
5
+ const compact = (delivery?.mode === 'direct' ? delivery.preview ?? body : body).replace(/\r\n/g, '\n');
6
+ if (delivery?.mode === 'direct' || compact.length <= ATTENTION_BODY_MAX)
7
7
  return compact;
8
8
  return `${compact.slice(0, ATTENTION_BODY_MAX).trimEnd()}…`;
9
9
  }
10
- export function attentionBodyIsClipped(body) {
11
- return body.replace(/\r\n/g, '\n').length > ATTENTION_BODY_MAX;
10
+ export function attentionBodyIsClipped(body, delivery) {
11
+ return delivery?.mode === 'direct'
12
+ ? delivery.preview !== undefined
13
+ : body.replace(/\r\n/g, '\n').length > ATTENTION_BODY_MAX;
12
14
  }
13
15
  export function displayAttentionPath(squarePath) {
14
16
  return squarePath.startsWith(homedir())
@@ -25,7 +27,7 @@ export function renderAttentionDescription({ actor, route }) {
25
27
  }
26
28
  /** Full notification text for human-facing expansion, never the bounded model preview. */
27
29
  export function renderAttentionFull(attention) {
28
- return renderAttentionBlock(attention, attention.body);
30
+ return attention.delivery?.mode === 'direct' ? renderDirectBlock(attention) : renderAttentionBlock(attention, attention.body);
29
31
  }
30
32
  /** Attribute values keep their quotes; the stored body and the human card stay unescaped. */
31
33
  function escapeAttribute(value) {
@@ -49,14 +51,20 @@ function renderAttentionBlock(attention, body) {
49
51
  '</square-activity>',
50
52
  ].join('\n');
51
53
  }
54
+ function renderDirectBlock(attention) {
55
+ return escapeSquareTags(previewAttentionBody(attention.body, attention.delivery));
56
+ }
52
57
  /** Shared model-facing envelope; transport correlation and human cards stay outside it. */
53
- export function renderSquareReminder(previews) {
58
+ export function renderSquareReminder(previews, deliveries = []) {
54
59
  if (previews.length === 0)
55
60
  return '';
61
+ const header = deliveries.some(item => item?.mode === 'direct')
62
+ ? 'This is an external notification, not a direct message from the user.'
63
+ : 'This is an external reminder from Square, not a direct message from the user.';
56
64
  return [
57
65
  '',
58
66
  '<system-reminder>',
59
- 'This is an external reminder from Square, not a direct message from the user.',
67
+ header,
60
68
  '',
61
69
  previews.join('\n\n'),
62
70
  '</system-reminder>',
@@ -64,6 +72,8 @@ export function renderSquareReminder(previews) {
64
72
  ].join('\n');
65
73
  }
66
74
  export function renderAttentionPreview(attention) {
75
+ if (attention.delivery?.mode === 'direct')
76
+ return renderDirectBlock(attention);
67
77
  const remaining = attention.body.replace(/\r\n/g, '\n').length - ATTENTION_BODY_MAX;
68
78
  const attributes = [
69
79
  `square="${escapeAttribute(displayAttentionPath(attention.squarePath))}"`,
@@ -0,0 +1,19 @@
1
+ export type AudienceRoute = 'mention' | 'reply' | 'attention' | 'bell';
2
+ export interface AudienceRecipient {
3
+ readonly recipient: string;
4
+ readonly route: AudienceRoute;
5
+ }
6
+ export interface AudienceInput {
7
+ readonly actor: string;
8
+ readonly recipients: readonly string[];
9
+ readonly mentions?: readonly string[];
10
+ readonly replyAuthor?: string;
11
+ readonly reach?: 'bell';
12
+ readonly incoming?: readonly {
13
+ readonly listener: string;
14
+ readonly kind: 'listen' | 'ignore';
15
+ }[];
16
+ readonly sameIdentity?: (a: string, b: string) => boolean;
17
+ }
18
+ /** Resolve attention against the caller's eligible recipient snapshot. */
19
+ export declare function resolveAudience(input: AudienceInput): readonly AudienceRecipient[];
@@ -0,0 +1,30 @@
1
+ /** Resolve attention against the caller's eligible recipient snapshot. */
2
+ export function resolveAudience(input) {
3
+ const same = input.sameIdentity ?? ((a, b) => a === b);
4
+ const incoming = input.incoming ?? [];
5
+ const audience = [];
6
+ const add = (requested, route) => {
7
+ const recipient = input.recipients.find((item) => same(item, requested));
8
+ if (recipient === undefined || same(recipient, input.actor)
9
+ || audience.some((item) => same(item.recipient, recipient)))
10
+ return;
11
+ if (input.reach !== 'bell' && incoming.some((item) => item.kind === 'ignore' && same(item.listener, recipient)))
12
+ return;
13
+ audience.push({ recipient, route });
14
+ };
15
+ if (input.reach === 'bell') {
16
+ for (const recipient of input.recipients)
17
+ add(recipient, 'bell');
18
+ }
19
+ else {
20
+ for (const mention of input.mentions ?? [])
21
+ add(mention, 'mention');
22
+ if (input.replyAuthor !== undefined)
23
+ add(input.replyAuthor, 'reply');
24
+ for (const recipient of input.recipients) {
25
+ if (incoming.some((item) => item.kind === 'listen' && same(item.listener, recipient)))
26
+ add(recipient, 'attention');
27
+ }
28
+ }
29
+ return audience;
30
+ }
@@ -3,6 +3,7 @@ import { createHash } from 'node:crypto';
3
3
  import { withFileLock } from './file-lock.js';
4
4
  import { createArtifactPort } from './artifact.js';
5
5
  import { preparePendingPresentation } from './presentation-operations.js';
6
+ import { presentPreparedBatch } from './delivery-effects.js';
6
7
  import { sessionInbox, projectBoundaryEligibility, pendingAtBoundary, pendingKey } from './inbox.js';
7
8
  import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
8
9
  export { pendingAtBoundary };
@@ -13,10 +14,13 @@ export function renderPendingAtBoundary(inbox) {
13
14
  function renderBoundary(inbox) {
14
15
  const queued = inbox.flatMap((membership) => membership.notifications.map((notification) => ({ membership, notification })));
15
16
  const blocks = [];
17
+ const agoraBlocks = [];
18
+ const deliveries = [];
19
+ let remainingAgora = queued.filter(({ notification }) => notification.delivery?.mode !== 'direct').length;
16
20
  const complete = [];
17
21
  let omitted = 0;
18
22
  const omission = (count) => `${count} more ${count === 1 ? 'activity' : 'activities'} not shown.`;
19
- for (const [index, entry] of queued.entries()) {
23
+ for (const entry of queued) {
20
24
  const { membership, notification } = entry;
21
25
  const block = [
22
26
  renderAttentionPreview({
@@ -26,23 +30,29 @@ function renderBoundary(inbox) {
26
30
  actor: notification.actor,
27
31
  route: notification.route,
28
32
  body: notification.body,
33
+ delivery: notification.delivery,
29
34
  }),
30
35
  ].join('\n');
31
- const omittedAfter = omitted + queued.length - index - 1;
32
- const prospective = renderSquareReminder([...blocks, block, ...(omittedAfter > 0 ? [omission(omittedAfter)] : [])]);
33
- if (prospective.length > CONTEXT_MAX) {
34
- omitted += 1;
35
- continue;
36
+ if (notification.delivery?.mode !== 'direct') {
37
+ remainingAgora -= 1;
38
+ const omittedAfter = omitted + remainingAgora;
39
+ const prospective = renderSquareReminder([...agoraBlocks, block, ...(omittedAfter > 0 ? [omission(omittedAfter)] : [])]);
40
+ if (prospective.length > CONTEXT_MAX) {
41
+ omitted += 1;
42
+ continue;
43
+ }
44
+ agoraBlocks.push(block);
36
45
  }
37
46
  blocks.push(block);
47
+ deliveries.push(notification.delivery);
38
48
  complete.push({
39
49
  membership,
40
50
  actIndexes: [notification.actIndex],
41
- markSeen: !attentionBodyIsClipped(notification.body),
51
+ markSeen: !attentionBodyIsClipped(notification.body, notification.delivery),
42
52
  });
43
53
  }
44
54
  return {
45
- context: renderSquareReminder([...blocks, ...(omitted > 0 ? [omission(omitted)] : [])]),
55
+ context: renderSquareReminder([...blocks, ...(omitted > 0 ? [omission(omitted)] : [])], deliveries),
46
56
  complete,
47
57
  };
48
58
  }
@@ -56,53 +66,45 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
56
66
  const artifacts = new Map();
57
67
  const claims = [];
58
68
  try {
59
- // Claim the entire candidate batch before its one external effect. No later item needs
60
- // a new claim (and new eligibility decision) after its bytes have already been rendered.
61
- for (const entry of renderBoundary(pending).complete) {
62
- const { location, seat } = entry.membership;
63
- let artifact = artifacts.get(location);
64
- if (artifact === undefined) {
65
- // This boundary owns the connection until all effect evidence/cleanup has settled.
66
- // Request cancellation travels with pre-effect operations, not that lifetime.
67
- artifact = createArtifactPort(location);
68
- artifacts.set(location, artifact);
69
- }
70
- for (const index of entry.actIndexes) {
71
- const claim = await preparePendingPresentation({ artifact, location, participant: seat.participant,
72
- activity: index, seat, markSeen: entry.markSeen, signal });
73
- if (claim !== undefined)
74
- claims.push({ key: pendingKey(entry.membership, index), claim });
75
- }
76
- }
77
- const eligible = new Set();
78
- for (const { key, claim } of claims)
79
- if (await claim.eligible())
80
- eligible.add(key);
81
- const fresh = renderBoundary(pending.map(membership => ({ ...membership,
82
- notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
83
- })));
84
- const included = new Set(fresh.complete.flatMap(entry => entry.actIndexes.map(index => pendingKey(entry.membership, index))));
85
- if (included.size === 0 || signal?.aborted)
86
- return undefined;
87
- const rendered = claims.filter(({ key }) => included.has(key));
88
- let result;
89
69
  try {
90
- result = await present(fresh.context);
70
+ // Enumeration owns discovery/connections only; returned handles belong to the common runner.
71
+ for (const entry of renderBoundary(pending).complete) {
72
+ const { location, seat } = entry.membership;
73
+ let artifact = artifacts.get(location);
74
+ if (artifact === undefined) {
75
+ // Keep connections alive through all signal-independent settlement/cleanup.
76
+ artifact = createArtifactPort(location);
77
+ artifacts.set(location, artifact);
78
+ }
79
+ for (const index of entry.actIndexes) {
80
+ const claim = await preparePendingPresentation({ artifact, location, participant: seat.participant,
81
+ activity: index, seat, markSeen: entry.markSeen, signal });
82
+ if (claim !== undefined)
83
+ claims.push({ key: pendingKey(entry.membership, index), claim });
84
+ }
85
+ }
91
86
  }
92
87
  catch (error) {
93
- await Promise.all(rendered.map(({ claim }) => claim.release('failed', error instanceof Error ? error.message : String(error))));
88
+ // Transfer has not happened: enumeration must abandon any custody already acquired.
89
+ await Promise.all(claims.map(({ claim }) => claim.release()));
94
90
  throw error;
95
91
  }
96
- // These are successful sink effects, not fresh delivery attempts. Settle the entire
97
- // rendered set before closing any connection, even if one member's settlement fails.
98
- const settlements = await Promise.allSettled(rendered.map(({ claim }) => claim.recordPresented()));
99
- for (const settlement of settlements)
100
- if (settlement.status === 'rejected')
101
- throw settlement.reason;
102
- return result;
92
+ const report = await presentPreparedBatch(claims.map(({ claim }) => claim), eligibleClaims => {
93
+ const eligible = new Set(claims.filter(({ claim }) => eligibleClaims.includes(claim)).map(({ key }) => key));
94
+ const fresh = renderBoundary(pending.map(membership => ({ ...membership,
95
+ notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
96
+ })));
97
+ const included = new Set(fresh.complete.flatMap(entry => entry.actIndexes.map(index => pendingKey(entry.membership, index))));
98
+ return { payload: fresh.context, included: claims.filter(({ key }) => included.has(key)).map(({ claim }) => claim) };
99
+ }, present, { signal, classifyError: () => 'failed' });
100
+ if (report.observation.kind === 'threw')
101
+ throw report.observation.error;
102
+ for (const { result } of report.settlements)
103
+ if (result.persistence.status === 'error')
104
+ throw result.persistence.error;
105
+ return report.observation.kind === 'returned' ? report.observation.value : undefined;
103
106
  }
104
107
  finally {
105
- await Promise.all(claims.map(({ claim }) => claim.release()));
106
108
  await Promise.all([...artifacts.values()].map(artifact => artifact.close()));
107
109
  }
108
110
  }
@@ -6,7 +6,6 @@ export interface CatchSelection {
6
6
  readonly activities: readonly PerceivedActivity[];
7
7
  readonly remaining: number;
8
8
  readonly standingCount: number;
9
- readonly held: boolean;
10
9
  readonly deliveryAvailable: boolean;
11
10
  readonly now: number;
12
11
  readonly ownActivityCount: number;
@@ -15,7 +14,7 @@ export interface CatchSelection {
15
14
  readonly hint?: string;
16
15
  }
17
16
  /** Capture a selected page (or an empty completion) without opening another resource. */
18
- export declare function observeCatchSelection(facts: SquareRead, participant: string, activities: readonly StoredAct[], filter: DirectedFilter, at: number): CatchSelection;
17
+ export declare function observeCatchSelection(facts: SquareRead, participant: string, activities: readonly StoredAct[], filter: DirectedFilter, at: number, remaining?: number): CatchSelection;
19
18
  export declare const CATCH_DEFAULT_LIMIT = 10;
20
19
  export declare const CATCH_MAX_LIMIT = 100;
21
20
  export declare function catchLimit(value: number | undefined): number;
@@ -4,15 +4,17 @@ import { SquareError } from './model.js';
4
4
  import { resolveKnownName } from './decisions.js';
5
5
  import { toPerceivedActivity } from './views.js';
6
6
  import { catchHint } from './hints.js';
7
+ import { consumeReceived, ReceiveError, selectReceivedHeads } from './receive.js';
8
+ import { squareReceiveRead } from './square-receive.js';
7
9
  /** Capture a selected page (or an empty completion) without opening another resource. */
8
- export function observeCatchSelection(facts, participant, activities, filter, at) {
10
+ export function observeCatchSelection(facts, participant, activities, filter, at, remaining) {
9
11
  if (typeof at !== 'number' || !Number.isFinite(at))
10
12
  throw new SquareError('invalid_args', 'Catch time must be a finite number.');
11
13
  const counts = facts.participantCounts(), header = facts.header(), current = facts.participant(participant);
12
14
  return {
13
15
  participant, activities: activities.map(activity => toPerceivedActivity(facts, activity, participant)),
14
- remaining: facts.directedCount(participant, filter),
15
- standingCount: counts.joined, held: facts.hold().active, now: at,
16
+ remaining: remaining ?? facts.directedCount(participant, filter),
17
+ standingCount: counts.joined, now: at,
16
18
  deliveryAvailable: facts.seat(participant)?.wakeAddress !== undefined,
17
19
  ownActivityCount: current.sayCount, hardCap: header.hardCap,
18
20
  terminalStatus: terminalStatusFor(current, header.hardCap, counts),
@@ -27,19 +29,23 @@ export function catchLimit(value) {
27
29
  }
28
30
  return limit;
29
31
  }
30
- /** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
31
- export function selectCatchPage(facts, requestedName, options) {
32
+ function catchPageSelection(facts, requestedName, options) {
32
33
  const participant = resolveKnownName(facts, requestedName);
33
34
  const limit = catchLimit(options.limit);
34
- const senders = options.from?.map((name) => resolveKnownName(facts, name));
35
+ const senders = options.from?.map(name => resolveKnownName(facts, name));
35
36
  const filter = { unseenOnly: true, ...(senders === undefined ? {} : { senders }), ...(options.mention === true ? { attentionOnly: true } : {}) };
36
- return { participant, indexes: facts.directed(participant, filter, { edge: 'oldest', limit }).items.map((item) => item.index), filter };
37
+ return { participant, filter, selection: { kind: 'page', query: filter, limit } };
38
+ }
39
+ /** Select non-ID catch coordinates without hydrating bodies or recording receipts. */
40
+ export function selectCatchPage(facts, requestedName, options) {
41
+ const { participant, filter, selection } = catchPageSelection(facts, requestedName, options);
42
+ return { participant, indexes: selectReceivedHeads(squareReceiveRead(facts), participant, selection).map(item => item.key), filter };
37
43
  }
38
44
  /** Selection, receipts and resulting cursor belong to one artifact write transaction. */
39
45
  export function decideCatch(facts, requestedName, options, at) {
40
46
  let viewer;
41
- let delivered;
42
47
  let filter = { unseenOnly: true };
48
+ let selection;
43
49
  if (options.id !== undefined) {
44
50
  viewer = resolveKnownName(facts, requestedName);
45
51
  catchLimit(options.limit);
@@ -48,25 +54,39 @@ export function decideCatch(facts, requestedName, options, at) {
48
54
  const index = parseActivityId(options.id);
49
55
  if (index === undefined)
50
56
  throw new SquareError('invalid_args', 'Invalid catch id: expected an activity id like act/12.');
51
- const activity = facts.activity(index);
52
- if (activity === undefined || activity.kind !== 'say' || facts.directedAt(viewer, index) === undefined) {
53
- throw new SquareError('invalid_args', `✕ ${formatActivityId(index)} isn't addressed to you`, {
54
- reason: 'not_addressed', activityId: formatActivityId(index), activityExists: activity !== undefined,
55
- });
56
- }
57
- delivered = [activity];
57
+ selection = { kind: 'exact', key: index, remainingQuery: filter };
58
58
  }
59
59
  else {
60
- const page = selectCatchPage(facts, requestedName, options);
60
+ const page = catchPageSelection(facts, requestedName, options);
61
61
  viewer = page.participant;
62
62
  filter = page.filter;
63
- delivered = facts.activities(page.indexes);
63
+ selection = page.selection;
64
+ }
65
+ let consumed;
66
+ try {
67
+ consumed = consumeReceived({
68
+ ...squareReceiveRead(facts),
69
+ insertReceipt: (ref, time) => facts.markSeen(ref.recipient, [ref.item], time) > 0,
70
+ }, viewer, selection, at);
71
+ }
72
+ catch (error) {
73
+ if ((error instanceof TypeError || error instanceof RangeError) && (typeof at !== 'number' || !Number.isFinite(at))) {
74
+ throw new SquareError('invalid_args', 'Invalid square artifact: row is malformed.');
75
+ }
76
+ if (!(error instanceof ReceiveError))
77
+ throw error;
78
+ if (error.code === 'missing_item')
79
+ throw new SquareError('invalid_args', 'Invalid square artifact: addressed activity is missing.');
80
+ const index = error.key;
81
+ throw new SquareError('invalid_args', `✕ ${formatActivityId(index)} isn't addressed to you`, {
82
+ reason: 'not_addressed', activityId: formatActivityId(index), activityExists: facts.activity(index) !== undefined,
83
+ });
64
84
  }
65
- const marked = facts.markSeen(viewer, delivered.map((item) => item.index), at);
66
- const selection = observeCatchSelection(facts, viewer, delivered, filter, at);
85
+ const delivered = consumed.items.map(item => item.data);
86
+ const observed = observeCatchSelection(facts, viewer, delivered, filter, at, consumed.remaining);
67
87
  // Receipts are the durable catch count: everything directed here minus what is still unseen.
68
88
  const caught = facts.directedCount(viewer, {}) - facts.directedCount(viewer, { unseenOnly: true });
69
89
  // An exact id catch is a targeted fetch and stays repeatable, so it carries no nudge.
70
- const hint = options.id === undefined ? catchHint(caught - marked, caught) : undefined;
71
- return hint === undefined ? selection : { ...selection, hint };
90
+ const hint = options.id === undefined ? catchHint(caught - consumed.newlyConsumed, caught) : undefined;
91
+ return hint === undefined ? observed : { ...observed, hint };
72
92
  }
@@ -0,0 +1,72 @@
1
+ import { type HarnessSessionChannel } from './participant-identity.js';
2
+ import type { SquareResourceOwner, WakeTransportPort } from './ports.js';
3
+ import type { ActivityId, ChannelSendInput, ChannelSendResult, DeliveryDeferral, HeldSeat, MetadataMatch, OperationControl, Owner, Participant, WakeAddress } from './square-facade.js';
4
+ export { harnessOwner, decodeHarnessOwner, localUserOwner } from './owner.js';
5
+ export type { ActivityDelivery, ActivityMetadata, ChannelTruncate } from './square-facade.js';
6
+ export type { ActivityId, ChannelSendInput, ChannelSendResult, DeliveryDeferral, HeldSeat, MetadataMatch, OperationControl, Owner, WakeAddress, SquareResourceOwner, WakeTransportPort, HarnessSessionChannel };
7
+ export interface ResolvedHarnessSession {
8
+ readonly channel: HarnessSessionChannel;
9
+ readonly sessionId: string;
10
+ readonly agentId?: string;
11
+ readonly owner: Owner;
12
+ readonly wakeAddress?: WakeAddress;
13
+ }
14
+ export declare function resolveHarnessSession(env?: NodeJS.ProcessEnv): ResolvedHarnessSession | undefined;
15
+ export interface ChannelOpenInput {
16
+ readonly path: string;
17
+ readonly cwd?: string;
18
+ readonly env?: NodeJS.ProcessEnv;
19
+ readonly clock?: () => number;
20
+ readonly wakeTransport?: WakeTransportPort | false;
21
+ readonly resources?: SquareResourceOwner;
22
+ }
23
+ export interface ChannelCreateInput extends ChannelOpenInput {
24
+ readonly force?: boolean;
25
+ readonly description?: string;
26
+ }
27
+ type EndpointOptions = {
28
+ readonly owner: Owner;
29
+ readonly wakeAddress?: WakeAddress;
30
+ };
31
+ type EndpointIdentity = {
32
+ readonly owner: Owner;
33
+ readonly generation: ActivityId;
34
+ };
35
+ /** A scoped direct-messaging vocabulary over the shared Square resource. */
36
+ export declare class Channel {
37
+ private readonly square;
38
+ private constructor();
39
+ get location(): string;
40
+ static open(input: ChannelOpenInput, control?: OperationControl): Promise<Channel>;
41
+ static create(input: ChannelCreateInput, control?: OperationControl): Promise<Channel>;
42
+ static inMemory(input?: Omit<ChannelCreateInput, 'path' | 'resources' | 'force'>): Channel;
43
+ attach(name: string, options: EndpointOptions & {
44
+ readonly takeover?: boolean;
45
+ }, control?: OperationControl): Promise<ChannelEndpoint>;
46
+ reattach(name: string, options: EndpointOptions, control?: OperationControl): Promise<ChannelEndpoint | null>;
47
+ endpoint(name: string, options: {
48
+ readonly owner: Owner;
49
+ }): Promise<ChannelEndpoint>;
50
+ endpointsOf(owner: Owner): Promise<readonly HeldSeat[]>;
51
+ detach(name: string, identity: EndpointIdentity): Promise<true | null>;
52
+ remove(name: string, identity: EndpointIdentity): Promise<boolean>;
53
+ cancelDeliveries(name: string, identity: EndpointIdentity): Promise<boolean>;
54
+ send(input: ChannelSendInput, control?: OperationControl): Promise<ChannelSendResult>;
55
+ close(): Promise<void>;
56
+ }
57
+ /** Captures one recipient generation through its existing Participant handle. */
58
+ export declare class ChannelEndpoint {
59
+ private readonly channel;
60
+ private readonly participant;
61
+ constructor(channel: Channel, participant: Participant);
62
+ get name(): string;
63
+ get generation(): ActivityId;
64
+ leave(control?: OperationControl): Promise<void>;
65
+ deferDelivery(input: Readonly<{
66
+ match: MetadataMatch;
67
+ durationMs: number;
68
+ }>, control?: OperationControl): Promise<DeliveryDeferral>;
69
+ renewDeliveryDeferral(id: string, durationMs: number, control?: OperationControl): Promise<DeliveryDeferral | null>;
70
+ releaseDeliveryDeferral(id: string, control?: OperationControl): Promise<boolean>;
71
+ send(input: Omit<ChannelSendInput, 'from'>, control?: OperationControl): Promise<ChannelSendResult>;
72
+ }
@@ -0,0 +1,86 @@
1
+ import { codexWakeAddress } from './codex-address.js';
2
+ import { SquareError } from './model.js';
3
+ import { harnessOwner } from './owner.js';
4
+ import { harnessSessionSources } from './participant-identity.js';
5
+ import { Square } from './square-api.js';
6
+ import { squareLocation } from './square-location.js';
7
+ export { harnessOwner, decodeHarnessOwner, localUserOwner } from './owner.js';
8
+ export function resolveHarnessSession(env = process.env) {
9
+ for (const { variable, channel } of harnessSessionSources) {
10
+ const sessionId = env[variable]?.trim();
11
+ if (!sessionId)
12
+ continue;
13
+ const agentId = channel === 'claude-code' && env.CLAUDE_CODE_CHILD_SESSION?.trim() === '1'
14
+ ? env.CLAUDE_CODE_AGENT_ID?.trim() || undefined : undefined;
15
+ const wakeAddress = channel === 'codex' ? codexWakeAddress(sessionId, env)
16
+ : channel === 'paseo' ? { kind: 'paseo', address: { agentId: sessionId } } : undefined;
17
+ return { channel, sessionId, ...(agentId === undefined ? {} : { agentId }),
18
+ owner: harnessOwner({ channel, session: sessionId, ...(agentId === undefined ? {} : { agent: agentId }) }),
19
+ ...(wakeAddress === undefined ? {} : { wakeAddress }) };
20
+ }
21
+ return undefined;
22
+ }
23
+ function explicitLocation(input) {
24
+ if (typeof input?.path !== 'string' || input.path.trim() === '')
25
+ throw new SquareError('invalid_args', 'Channel path is required.');
26
+ return squareLocation(input);
27
+ }
28
+ /** A scoped direct-messaging vocabulary over the shared Square resource. */
29
+ export class Channel {
30
+ square;
31
+ constructor(square) {
32
+ this.square = square;
33
+ }
34
+ get location() { return this.square.location; }
35
+ static async open(input, control) {
36
+ const path = explicitLocation(input);
37
+ return new Channel(await Square.at({ ...input, path }, control));
38
+ }
39
+ static async create(input, control) {
40
+ const path = explicitLocation(input);
41
+ return new Channel(await Square.build({ ...input, path, hardCap: null, markdown: input.description ?? '# Channel\n' }, control));
42
+ }
43
+ static inMemory(input = {}) {
44
+ return new Channel(Square.inMemory({ ...input, hardCap: null, markdown: input.description ?? '# Channel\n' }));
45
+ }
46
+ async attach(name, options, control) {
47
+ return new ChannelEndpoint(this, (await this.square.join(name, options, control)).participant);
48
+ }
49
+ async reattach(name, options, control) {
50
+ const participant = await this.square.reattach(name, options, control);
51
+ return participant === null ? null : new ChannelEndpoint(this, participant);
52
+ }
53
+ async endpoint(name, options) {
54
+ return new ChannelEndpoint(this, await this.square.participant(name, options));
55
+ }
56
+ endpointsOf(owner) { return this.square.seatsOf(owner); }
57
+ detach(name, identity) { return this.square.detach(name, identity); }
58
+ async remove(name, identity) { return await this.square.endHeld(name, identity) !== null; }
59
+ cancelDeliveries(name, identity) { return this.square.cancelDeliveries(name, identity); }
60
+ send(input, control) { return this.square.send(input, control); }
61
+ close() { return this.square.close(); }
62
+ }
63
+ /** Captures one recipient generation through its existing Participant handle. */
64
+ export class ChannelEndpoint {
65
+ channel;
66
+ participant;
67
+ constructor(channel, participant) {
68
+ this.channel = channel;
69
+ this.participant = participant;
70
+ }
71
+ get name() { return this.participant.name; }
72
+ get generation() { return this.participant.generation; }
73
+ async leave(control) { await this.participant.done(undefined, {}, control); }
74
+ deferDelivery(input, control) {
75
+ return this.participant.deferDelivery(input, control);
76
+ }
77
+ renewDeliveryDeferral(id, durationMs, control) {
78
+ return this.participant.renewDeliveryDeferral(id, durationMs, control);
79
+ }
80
+ releaseDeliveryDeferral(id, control) {
81
+ return this.participant.releaseDeliveryDeferral(id, control);
82
+ }
83
+ send(input, control) {
84
+ return this.channel.send({ ...input, from: this.name }, control);
85
+ }
86
+ }
@@ -150,7 +150,8 @@ export async function observeClaudeDelivery(sessionId, text, operation, env = pr
150
150
  return { recognized, current: false };
151
151
  if (operation === 'stored')
152
152
  await presentPending({ artifact: row.artifact, location: request.location,
153
- participant: request.participant, activity: request.activity, seat: request.seat, sink: { present() { } },
153
+ // Claude's stored event consumes even Agora previews; retain that contract for both modes.
154
+ participant: request.participant, activity: request.activity, seat: request.seat, markSeen: true, sink: { present() { } },
154
155
  current: facts => currentPayload(facts, request) === payload });
155
156
  }
156
157
  return { recognized, current: true };
@@ -99,8 +99,6 @@ const LOCATION_REQUIRED_COMMANDS = new Set([
99
99
  'unlisten',
100
100
  'listening',
101
101
  'done',
102
- 'hold',
103
- 'resume',
104
102
  'history',
105
103
  'status',
106
104
  'participants',
@@ -88,7 +88,7 @@ export async function executeExpression(open, squarePath, requestedName, rawInpu
88
88
  const selected = square === undefined ? undefined : await selectOwnSeat(square, owner, name).catch(() => undefined);
89
89
  const fresh = await square?.recovery(selected?.name ?? name).catch(() => undefined);
90
90
  name = fresh?.participant ?? name;
91
- const output = { squarePath, name, owner: opts.recoveryOwner, draftPath, participantCount: fresh?.standingCount, held: fresh?.held };
91
+ const output = { squarePath, name, owner: opts.recoveryOwner, draftPath, participantCount: fresh?.standingCount };
92
92
  const confirmedUnsent = landedId === undefined && detail?.landedId === undefined
93
93
  && detail?.expressEffect !== 'landed' && detail?.expressEffect !== 'unknown'
94
94
  && (!attempted || detail?.expressEffect === 'unsent' || (isSquareError(error) && error.facts?.activityUnsent === true));
@@ -116,9 +116,9 @@ export async function executeExpression(open, squarePath, requestedName, rawInpu
116
116
  exitCode(1);
117
117
  return;
118
118
  }
119
- if (error.code === 'held' || error.code === 'throttled') {
119
+ if (error.code === 'throttled') {
120
120
  process.stdout.write(renderExpressNoWait({ ...output, reason: error.code, retryCommand: retryInput,
121
- delayMs: error.facts?.retryAfterMs ?? 1, holder: error.facts?.holder, holdReason: error.facts?.holdReason,
121
+ delayMs: error.facts?.retryAfterMs ?? 1,
122
122
  }));
123
123
  exitCode(1);
124
124
  return;
package/dist/cli/json.js CHANGED
@@ -1,6 +1,6 @@
1
1
  export const JSON_COMMANDS = new Set([
2
2
  'join', 'express', 'catch', 'history', 'listen', 'ignore', 'unlisten', 'listening',
3
- 'hold', 'resume', 'done', 'status', 'participants', 'inbox',
3
+ 'done', 'status', 'participants', 'inbox',
4
4
  ]);
5
5
  /** Serialize the SDK receipt, not its optional presentation or live participant handle. */
6
6
  export function jsonResult(result) {
@@ -17,7 +17,7 @@ async function handleSquareError(error, context) {
17
17
  process.stderr.write(`${lines.join('\n')}\n`);
18
18
  else
19
19
  process.stderr.write(formatRefusal(squarePath, lines, {
20
- participantCount: recovery?.standingCount, held: recovery?.held,
20
+ participantCount: recovery?.standingCount,
21
21
  }));
22
22
  process.exit(error.code === 'not_found' ? 1 : 2);
23
23
  }