@astrosheep/square 0.3.94 → 0.3.95

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 (85) hide show
  1. package/dist/artifact.js +1 -1
  2. package/dist/attention.d.ts +19 -0
  3. package/dist/attention.js +30 -0
  4. package/dist/boundary-presentation.js +33 -40
  5. package/dist/catch-decisions.d.ts +1 -2
  6. package/dist/catch-decisions.js +41 -21
  7. package/dist/cli/context.js +0 -2
  8. package/dist/cli/express-presentation.js +3 -3
  9. package/dist/cli/json.js +1 -1
  10. package/dist/cli/program.js +1 -1
  11. package/dist/cli/registry.js +1 -3
  12. package/dist/cli/square-commands.d.ts +0 -2
  13. package/dist/cli/square-commands.js +21 -27
  14. package/dist/decisions.d.ts +0 -5
  15. package/dist/decisions.js +0 -13
  16. package/dist/delivery-attempts.d.ts +52 -0
  17. package/dist/delivery-attempts.js +73 -0
  18. package/dist/delivery-effects.d.ts +139 -0
  19. package/dist/delivery-effects.js +163 -0
  20. package/dist/delivery-operations.js +43 -68
  21. package/dist/delivery-primitives.d.ts +5 -0
  22. package/dist/delivery-primitives.js +3 -0
  23. package/dist/delivery.d.ts +1 -1
  24. package/dist/delivery.js +6 -5
  25. package/dist/help.js +4 -6
  26. package/dist/http-delivery.d.ts +16 -0
  27. package/dist/http-delivery.js +101 -0
  28. package/dist/model.d.ts +4 -6
  29. package/dist/model.js +1 -1
  30. package/dist/notifications.js +2 -0
  31. package/dist/operation-presentation.d.ts +1 -2
  32. package/dist/operation-presentation.js +12 -20
  33. package/dist/ports.d.ts +5 -14
  34. package/dist/presentation-operations.d.ts +14 -19
  35. package/dist/presentation-operations.js +47 -57
  36. package/dist/presentation.d.ts +2 -9
  37. package/dist/presentation.js +13 -48
  38. package/dist/receive.d.ts +66 -0
  39. package/dist/receive.js +54 -0
  40. package/dist/runtime.d.ts +0 -1
  41. package/dist/runtime.js +2 -3
  42. package/dist/sqlite-square.d.ts +0 -1
  43. package/dist/sqlite-square.js +29 -55
  44. package/dist/square-api.d.ts +1 -1
  45. package/dist/square-api.js +1 -1
  46. package/dist/square-core.d.ts +0 -9
  47. package/dist/square-domain.js +9 -31
  48. package/dist/square-facade.d.ts +5 -21
  49. package/dist/square-legacy-framing.d.ts +2 -0
  50. package/dist/square-legacy-framing.js +8 -0
  51. package/dist/square-migration.d.ts +3 -1
  52. package/dist/square-migration.js +46 -13
  53. package/dist/square-receive.d.ts +6 -0
  54. package/dist/square-receive.js +18 -0
  55. package/dist/square-sqlite.d.ts +5 -3
  56. package/dist/square-sqlite.js +89 -90
  57. package/dist/square-tools.js +3 -11
  58. package/dist/views.d.ts +2 -3
  59. package/dist/views.js +13 -19
  60. package/dist/wakes.d.ts +0 -3
  61. package/dist/wakes.js +3 -5
  62. package/examples/symmetric-domains.mjs +250 -0
  63. package/guides/application-facade-delivery-evidence.md +5 -5
  64. package/guides/attention-primitive.md +32 -0
  65. package/guides/change-waits.md +2 -2
  66. package/guides/codex-delivery.md +1 -1
  67. package/guides/delivery-primitives.md +180 -0
  68. package/guides/mcp.md +1 -1
  69. package/guides/migration.md +12 -6
  70. package/guides/participant-ownership.md +1 -1
  71. package/guides/pi-tools.md +2 -2
  72. package/guides/primitive-catch-consolidation.md +5 -5
  73. package/guides/primitive-history-consolidation.md +2 -2
  74. package/guides/primitive-status-consolidation.md +3 -3
  75. package/guides/primitive-storage-consolidation.md +1 -1
  76. package/guides/receive-primitive.md +90 -0
  77. package/guides/row-runtime-delivery-evidence.md +1 -1
  78. package/guides/row-storage-design.md +17 -23
  79. package/guides/square-api.md +77 -10
  80. package/guides/symmetric-domains.md +92 -0
  81. package/package.json +14 -1
  82. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  83. package/plugins/claude/skills/square/SKILL.md +2 -6
  84. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  85. 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',
@@ -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 };
@@ -56,53 +57,45 @@ async function presentPendingAtBoundaryUnlocked(identity, present, lookup = sess
56
57
  const artifacts = new Map();
57
58
  const claims = [];
58
59
  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
60
  try {
90
- result = await present(fresh.context);
61
+ // Enumeration owns discovery/connections only; returned handles belong to the common runner.
62
+ for (const entry of renderBoundary(pending).complete) {
63
+ const { location, seat } = entry.membership;
64
+ let artifact = artifacts.get(location);
65
+ if (artifact === undefined) {
66
+ // Keep connections alive through all signal-independent settlement/cleanup.
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
+ }
91
77
  }
92
78
  catch (error) {
93
- await Promise.all(rendered.map(({ claim }) => claim.release('failed', error instanceof Error ? error.message : String(error))));
79
+ // Transfer has not happened: enumeration must abandon any custody already acquired.
80
+ await Promise.all(claims.map(({ claim }) => claim.release()));
94
81
  throw error;
95
82
  }
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;
83
+ const report = await presentPreparedBatch(claims.map(({ claim }) => claim), eligibleClaims => {
84
+ const eligible = new Set(claims.filter(({ claim }) => eligibleClaims.includes(claim)).map(({ key }) => key));
85
+ const fresh = renderBoundary(pending.map(membership => ({ ...membership,
86
+ notifications: membership.notifications.filter(note => eligible.has(pendingKey(membership, note.actIndex))),
87
+ })));
88
+ const included = new Set(fresh.complete.flatMap(entry => entry.actIndexes.map(index => pendingKey(entry.membership, index))));
89
+ return { payload: fresh.context, included: claims.filter(({ key }) => included.has(key)).map(({ claim }) => claim) };
90
+ }, present, { signal, classifyError: () => 'failed' });
91
+ if (report.observation.kind === 'threw')
92
+ throw report.observation.error;
93
+ for (const { result } of report.settlements)
94
+ if (result.persistence.status === 'error')
95
+ throw result.persistence.error;
96
+ return report.observation.kind === 'returned' ? report.observation.value : undefined;
103
97
  }
104
98
  finally {
105
- await Promise.all(claims.map(({ claim }) => claim.release()));
106
99
  await Promise.all([...artifacts.values()].map(artifact => artifact.close()));
107
100
  }
108
101
  }
@@ -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
  }
@@ -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
  }
@@ -1,4 +1,4 @@
1
- import { buildCommand, doneCommand, expressCommand, holdCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, resumeCommand, unlistenCommand } from './square-commands.js';
1
+ import { buildCommand, doneCommand, expressCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, unlistenCommand } from './square-commands.js';
2
2
  import { doctorCommand, migrateCommand } from './maintenance-commands.js';
3
3
  import { harnessCommand, installCommand, uninstallCommand } from './harness-command.js';
4
4
  import { helpCommand, versionCommand } from './meta-commands.js';
@@ -21,8 +21,6 @@ export const commandRegistry = [
21
21
  { names: ['unlisten'], spec: unlistenCommand },
22
22
  { names: ['listening'], spec: listeningCommand },
23
23
  { names: ['done'], spec: doneCommand },
24
- { names: ['hold'], spec: holdCommand },
25
- { names: ['resume'], spec: resumeCommand },
26
24
  { names: ['install'], spec: installCommand },
27
25
  { names: ['uninstall'], spec: uninstallCommand },
28
26
  { names: ['harness'], spec: harnessCommand },
@@ -35,6 +35,4 @@ export declare const ignoreCommand: CommandSpec<ListenerIntent, string>;
35
35
  export declare const unlistenCommand: CommandSpec<ListenerIntent, string>;
36
36
  export declare const listeningCommand: CommandSpec<ListenerIntent, string>;
37
37
  export declare const doneCommand: CommandSpec<BodyIntent, string>;
38
- export declare const holdCommand: CommandSpec<BodyIntent, string>;
39
- export declare const resumeCommand: CommandSpec<BodyIntent, string>;
40
38
  export {};
@@ -54,7 +54,7 @@ export const buildCommand = {
54
54
  const cap = data.hardCap === null ? 'unlimited' : formatHardCap(data.hardCap);
55
55
  const throttle = data.throttlePerMinute === undefined ? [] : [` · throttle ${data.throttlePerMinute}/min`];
56
56
  return withPathOutput(square.location, ['· the square stands ready', ` · cap ${cap}`, ...throttle].join('\n'), {
57
- participantCount: 0, held: false,
57
+ participantCount: 0,
58
58
  });
59
59
  },
60
60
  present: (result) => process.stdout.write(result),
@@ -182,29 +182,23 @@ export const listeningCommand = {
182
182
  },
183
183
  present: (result) => process.stdout.write(result),
184
184
  };
185
- function lifecycleCommand(verb) {
186
- return {
187
- parse(argv, context) {
188
- const args = argv.filter((arg) => arg !== '--json');
189
- if (args.length > (verb === 'resume' ? 0 : 1))
190
- usage(context.command);
191
- return { body: args[0] };
192
- },
193
- async execute(intent, context) {
194
- const body = verb === 'hold' ? (await resolveBody(intent.body ?? '')).replace(/\r\n/g, '\n').trim()
195
- : verb === 'done' && (intent.body === '-' || ((intent.body === undefined || intent.body === '') && !process.stdin.isTTY))
196
- ? await readStdin() : intent.body;
197
- const squarePath = requireSquarePath(context);
198
- const result = await withSquare(context, async (square) => {
199
- const participant = await selectOwnSeat(square, commandIdentity(context.env, context.owner).owner, context.name);
200
- const options = context.json ? {} : { observe: true };
201
- return verb === 'resume' ? participant.resume(options) : participant[verb](body, options);
202
- });
203
- return context.json ? jsonResult(result) : renderLifecycleResult(squarePath, verb, result);
204
- },
205
- present: (result) => process.stdout.write(result),
206
- };
207
- }
208
- export const doneCommand = lifecycleCommand('done');
209
- export const holdCommand = lifecycleCommand('hold');
210
- export const resumeCommand = lifecycleCommand('resume');
185
+ export const doneCommand = {
186
+ parse(argv, context) {
187
+ const args = argv.filter((arg) => arg !== '--json');
188
+ if (args.length > 1)
189
+ usage(context.command);
190
+ return { body: args[0] };
191
+ },
192
+ async execute(intent, context) {
193
+ const body = intent.body === '-' || ((intent.body === undefined || intent.body === '') && !process.stdin.isTTY)
194
+ ? await readStdin() : intent.body;
195
+ const squarePath = requireSquarePath(context);
196
+ const result = await withSquare(context, async (square) => {
197
+ const participant = await selectOwnSeat(square, commandIdentity(context.env, context.owner).owner, context.name);
198
+ const options = context.json ? {} : { observe: true };
199
+ return participant.done(body, options);
200
+ });
201
+ return context.json ? jsonResult(result) : renderLifecycleResult(squarePath, result);
202
+ },
203
+ present: (result) => process.stdout.write(result),
204
+ };
@@ -26,9 +26,6 @@ export type ActDecision = ({
26
26
  } | {
27
27
  type: 'throttled';
28
28
  delayMs: number;
29
- } | {
30
- type: 'held';
31
- reason: string | undefined;
32
29
  } | {
33
30
  type: 'bell_quota';
34
31
  nextAt: number;
@@ -52,8 +49,6 @@ export declare function coreListen(facts: SquareRead, actor: string, target: str
52
49
  export declare function coreIgnore(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
53
50
  export declare function coreUnlisten(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
54
51
  export declare function coreListening(facts: SquareRead, actor: string): readonly string[];
55
- export declare function coreHold(facts: SquareRead, actor: string, body: string, now: number): TimedAct<'hold'>;
56
- export declare function coreResume(facts: SquareRead, actor: string, now: number): TimedAct<'resume'>;
57
52
  export declare function compareParticipantActivity(a: Pick<ParticipantSummary, 'name' | 'lastActiveAt'>, b: Pick<ParticipantSummary, 'name' | 'lastActiveAt'>): number;
58
53
  export declare function coreParticipants(facts: SquareRead, now: number): ParticipantSummary[];
59
54
  export {};
package/dist/decisions.js CHANGED
@@ -130,9 +130,6 @@ export function decideAct(facts, input) {
130
130
  throw activityRefusal('already_done', `${name} is already done`);
131
131
  if (!current.joined)
132
132
  throw activityRefusal('not_joined', `${name} has not joined this square`);
133
- const hold = facts.hold();
134
- if (hold.active)
135
- return { type: 'held', reason: hold.reason };
136
133
  if (header.hardCap !== null && current.sayCount >= header.hardCap)
137
134
  return { type: 'capped', count: current.sayCount, hardCap: header.hardCap };
138
135
  if (reach === 'bell') {
@@ -185,16 +182,6 @@ export function coreListening(facts, actor) {
185
182
  const current = resolveStanding(facts, actor);
186
183
  return facts.attention(current.name).filter((item) => item.kind === 'listen').map((item) => item.target);
187
184
  }
188
- export function coreHold(facts, actor, body, now) {
189
- const current = resolveStanding(facts, actor);
190
- requireStanding(current);
191
- return { kind: 'hold', actor: current.name, at: now, body: body.replace(/\r\n/g, '\n').trim() };
192
- }
193
- export function coreResume(facts, actor, now) {
194
- const current = resolveStanding(facts, actor);
195
- requireStanding(current);
196
- return { kind: 'resume', actor: current.name, at: now };
197
- }
198
185
  export function compareParticipantActivity(a, b) {
199
186
  return (b.lastActiveAt ?? -Infinity) - (a.lastActiveAt ?? -Infinity) || a.name.localeCompare(b.name);
200
187
  }
@@ -0,0 +1,52 @@
1
+ export type DeliveryPurpose = 'dispatch' | 'presentation';
2
+ export type AttemptOutcome = 'dispatching' | 'accepted' | 'failed' | 'unknown' | 'released' | 'presented' | 'clipped';
3
+ export interface Attempt<D> {
4
+ readonly purpose: DeliveryPurpose;
5
+ readonly token: string;
6
+ readonly ordinal: number;
7
+ readonly outcome: AttemptOutcome;
8
+ readonly at: number;
9
+ readonly expiresAt?: number;
10
+ readonly data: D;
11
+ }
12
+ /** One item/recipient/authority/purpose series, bound inside the caller's transaction.
13
+ * The caller owns token uniqueness, isolation and rollback; replacements retain coordinates.
14
+ */
15
+ export interface AttemptRows<D> {
16
+ list(): readonly Attempt<D>[];
17
+ byToken(token: string): Attempt<D> | undefined;
18
+ insert(attempt: Attempt<D>): void;
19
+ replace(attempt: Attempt<D>): void;
20
+ }
21
+ export type AttemptClaim<D> = {
22
+ readonly status: 'acquired';
23
+ readonly token: string;
24
+ readonly ordinal: number;
25
+ } | {
26
+ readonly status: 'busy' | 'terminal';
27
+ readonly evidence: Attempt<D>;
28
+ readonly interrupted?: true;
29
+ };
30
+ export declare function claimAttempt<D>(rows: AttemptRows<D>, input: {
31
+ purpose: DeliveryPurpose;
32
+ token: string;
33
+ at: number;
34
+ leaseMs: number;
35
+ data: D;
36
+ }): AttemptClaim<D>;
37
+ export declare function prepareAttempt<D>(rows: AttemptRows<D>, input: {
38
+ token: string;
39
+ data: D;
40
+ }): boolean;
41
+ export declare function settleAttempt<D>(rows: AttemptRows<D>, input: {
42
+ token: string;
43
+ outcome: Exclude<AttemptOutcome, 'dispatching'>;
44
+ at: number;
45
+ data: D;
46
+ }): boolean;
47
+ type Custody = Pick<Attempt<unknown>, 'purpose' | 'outcome' | 'expiresAt'>;
48
+ /** Presentation custody suppresses dispatch independently of dispatch acceptance. */
49
+ export declare function presentationBlocksDispatch(attempts: readonly Custody[], now: number): boolean;
50
+ /** An expired dispatch is uncertain, not permission for automatic replay. */
51
+ export declare function automaticDispatchEligible(attempts: readonly Custody[], now: number): boolean;
52
+ export {};
@@ -0,0 +1,73 @@
1
+ function finite(value) {
2
+ if (typeof value !== 'number' || !Number.isFinite(value))
3
+ throw new RangeError('time must be finite');
4
+ }
5
+ function token(value) {
6
+ if (typeof value !== 'string' || value.length === 0)
7
+ throw new TypeError('token must be nonempty');
8
+ }
9
+ export function claimAttempt(rows, input) {
10
+ if (input.purpose !== 'dispatch' && input.purpose !== 'presentation')
11
+ throw new TypeError('unsupported purpose');
12
+ token(input.token);
13
+ finite(input.at);
14
+ if (!Number.isSafeInteger(input.leaseMs) || input.leaseMs <= 0)
15
+ throw new RangeError('leaseMs must be a positive safe integer');
16
+ const expiresAt = input.at + input.leaseMs;
17
+ finite(expiresAt);
18
+ const attempts = rows.list();
19
+ const terminal = attempts.find(row => row.outcome === 'unknown'
20
+ || row.outcome === (input.purpose === 'dispatch' ? 'accepted' : 'presented'));
21
+ if (terminal !== undefined)
22
+ return { status: 'terminal', evidence: terminal };
23
+ const dispatching = attempts.find(row => row.outcome === 'dispatching');
24
+ if (dispatching !== undefined) {
25
+ if (dispatching.expiresAt === undefined || dispatching.expiresAt > input.at)
26
+ return { status: 'busy', evidence: dispatching };
27
+ const { expiresAt: _expiry, ...previous } = dispatching;
28
+ const evidence = { ...previous, outcome: 'unknown', at: input.at };
29
+ rows.replace(evidence);
30
+ return { status: 'terminal', evidence, interrupted: true };
31
+ }
32
+ const ordinal = (attempts[0]?.ordinal ?? -1) + 1;
33
+ if (!Number.isSafeInteger(ordinal) || ordinal < 0)
34
+ throw new RangeError('attempt ordinal exhausted');
35
+ rows.insert({ purpose: input.purpose, token: input.token, ordinal, outcome: 'dispatching', at: input.at, expiresAt, data: input.data });
36
+ return { status: 'acquired', token: input.token, ordinal };
37
+ }
38
+ export function prepareAttempt(rows, input) {
39
+ token(input.token);
40
+ const previous = rows.byToken(input.token);
41
+ if (previous?.outcome !== 'dispatching')
42
+ return false;
43
+ rows.replace({ ...previous, data: input.data });
44
+ return true;
45
+ }
46
+ export function settleAttempt(rows, input) {
47
+ token(input.token);
48
+ finite(input.at);
49
+ if (!['accepted', 'failed', 'unknown', 'released', 'presented', 'clipped'].includes(input.outcome))
50
+ throw new TypeError('unsupported completion');
51
+ const previous = rows.byToken(input.token);
52
+ if (previous === undefined)
53
+ return false;
54
+ if (previous.outcome === 'accepted' && input.outcome === 'accepted')
55
+ return true;
56
+ if (previous.outcome !== 'dispatching' && !(previous.outcome === 'unknown' && input.outcome === 'accepted'))
57
+ return false;
58
+ const { expiresAt: _expiry, ...retained } = previous;
59
+ rows.replace({ ...retained, outcome: input.outcome, at: input.at, data: input.data });
60
+ return true;
61
+ }
62
+ /** Presentation custody suppresses dispatch independently of dispatch acceptance. */
63
+ export function presentationBlocksDispatch(attempts, now) {
64
+ finite(now);
65
+ return attempts.some(row => row.purpose === 'presentation'
66
+ && (row.outcome === 'presented' || row.outcome === 'clipped' || row.outcome === 'unknown'
67
+ || row.outcome === 'dispatching' && (row.expiresAt === undefined || row.expiresAt > now)));
68
+ }
69
+ /** An expired dispatch is uncertain, not permission for automatic replay. */
70
+ export function automaticDispatchEligible(attempts, now) {
71
+ return !presentationBlocksDispatch(attempts, now) && !attempts.some(row => row.purpose === 'dispatch'
72
+ && (row.outcome === 'accepted' || row.outcome === 'unknown' || row.outcome === 'dispatching'));
73
+ }