@astrosheep/square 0.3.93 → 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 (146) hide show
  1. package/dist/activity-feed.d.ts +3 -5
  2. package/dist/activity-feed.js +16 -14
  3. package/dist/artifact.js +3 -3
  4. package/dist/attention.d.ts +19 -0
  5. package/dist/attention.js +30 -0
  6. package/dist/boundary-presentation.d.ts +2 -2
  7. package/dist/boundary-presentation.js +33 -40
  8. package/dist/catch-decisions.d.ts +5 -8
  9. package/dist/catch-decisions.js +45 -25
  10. package/dist/claude-delivery-status.js +10 -4
  11. package/dist/claude-delivery.js +19 -11
  12. package/dist/claude-mod.js +24 -53
  13. package/dist/cli/context.d.ts +9 -4
  14. package/dist/cli/context.js +30 -27
  15. package/dist/cli/express-presentation.d.ts +3 -1
  16. package/dist/cli/express-presentation.js +28 -16
  17. package/dist/cli/json.d.ts +3 -0
  18. package/dist/cli/json.js +14 -0
  19. package/dist/cli/mcp-server-command.js +2 -2
  20. package/dist/cli/observation-commands.d.ts +1 -3
  21. package/dist/cli/observation-commands.js +43 -56
  22. package/dist/cli/program.js +3 -3
  23. package/dist/cli/registry.js +7 -6
  24. package/dist/cli/square-commands.d.ts +0 -2
  25. package/dist/cli/square-commands.js +30 -44
  26. package/dist/codex-hook.d.ts +3 -2
  27. package/dist/codex-hook.js +13 -4
  28. package/dist/codex-queue.js +4 -4
  29. package/dist/decisions.d.ts +5 -16
  30. package/dist/decisions.js +9 -33
  31. package/dist/delivery-attempts.d.ts +52 -0
  32. package/dist/delivery-attempts.js +73 -0
  33. package/dist/delivery-deferral.js +2 -2
  34. package/dist/delivery-effects.d.ts +139 -0
  35. package/dist/delivery-effects.js +163 -0
  36. package/dist/delivery-health.d.ts +0 -1
  37. package/dist/delivery-health.js +1 -3
  38. package/dist/delivery-operations.d.ts +2 -2
  39. package/dist/delivery-operations.js +76 -101
  40. package/dist/delivery-primitives.d.ts +5 -0
  41. package/dist/delivery-primitives.js +3 -0
  42. package/dist/delivery.d.ts +1 -1
  43. package/dist/delivery.js +7 -6
  44. package/dist/help.js +14 -14
  45. package/dist/http-delivery.d.ts +16 -0
  46. package/dist/http-delivery.js +101 -0
  47. package/dist/inbox.d.ts +9 -22
  48. package/dist/inbox.js +23 -27
  49. package/dist/index.d.ts +1 -3
  50. package/dist/index.js +1 -1
  51. package/dist/mcp.d.ts +3 -0
  52. package/dist/mcp.js +23 -24
  53. package/dist/model.d.ts +12 -21
  54. package/dist/model.js +5 -8
  55. package/dist/notifications.d.ts +1 -1
  56. package/dist/notifications.js +8 -5
  57. package/dist/operation-presentation.d.ts +6 -6
  58. package/dist/operation-presentation.js +55 -58
  59. package/dist/owner.d.ts +11 -0
  60. package/dist/owner.js +32 -0
  61. package/dist/paseo-delivery.d.ts +4 -1
  62. package/dist/paseo-delivery.js +7 -12
  63. package/dist/paseo-state.d.ts +2 -2
  64. package/dist/paseo-state.js +6 -4
  65. package/dist/plugins/opencode/delivery.js +27 -15
  66. package/dist/ports.d.ts +18 -47
  67. package/dist/presentation-operations.d.ts +14 -19
  68. package/dist/presentation-operations.js +51 -61
  69. package/dist/presentation.d.ts +7 -17
  70. package/dist/presentation.js +46 -91
  71. package/dist/receive.d.ts +66 -0
  72. package/dist/receive.js +54 -0
  73. package/dist/runtime.d.ts +1 -8
  74. package/dist/runtime.js +2 -17
  75. package/dist/seats.d.ts +2 -3
  76. package/dist/seats.js +7 -15
  77. package/dist/session-lifecycle.d.ts +6 -2
  78. package/dist/session-lifecycle.js +39 -11
  79. package/dist/sqlite-square.d.ts +36 -23
  80. package/dist/sqlite-square.js +162 -211
  81. package/dist/square-api.d.ts +16 -11
  82. package/dist/square-api.js +27 -23
  83. package/dist/square-core.d.ts +0 -14
  84. package/dist/square-core.js +0 -9
  85. package/dist/square-domain.d.ts +4 -2
  86. package/dist/square-domain.js +65 -52
  87. package/dist/square-facade.d.ts +47 -105
  88. package/dist/square-index.d.ts +5 -13
  89. package/dist/square-index.js +13 -38
  90. package/dist/square-legacy-framing.d.ts +2 -0
  91. package/dist/square-legacy-framing.js +8 -0
  92. package/dist/square-location.d.ts +1 -14
  93. package/dist/square-location.js +4 -70
  94. package/dist/square-migration-host-ledger.d.ts +2 -2
  95. package/dist/square-migration-host-ledger.js +6 -5
  96. package/dist/square-migration.d.ts +4 -1
  97. package/dist/square-migration.js +93 -38
  98. package/dist/square-receive.d.ts +6 -0
  99. package/dist/square-receive.js +18 -0
  100. package/dist/square-sqlite.d.ts +9 -3
  101. package/dist/square-sqlite.js +139 -114
  102. package/dist/square-tools.d.ts +7 -10
  103. package/dist/square-tools.js +21 -61
  104. package/dist/stream.js +4 -5
  105. package/dist/views.d.ts +23 -13
  106. package/dist/views.js +47 -53
  107. package/dist/wakes.d.ts +1 -5
  108. package/dist/wakes.js +4 -6
  109. package/examples/symmetric-domains.mjs +250 -0
  110. package/guides/application-facade-delivery-evidence.md +6 -6
  111. package/guides/attention-primitive.md +32 -0
  112. package/guides/change-waits.md +2 -2
  113. package/guides/claude-native.md +18 -9
  114. package/guides/codex-delivery.md +17 -6
  115. package/guides/delivery-primitives.md +180 -0
  116. package/guides/mcp-connection-lifetime.md +1 -1
  117. package/guides/mcp.md +3 -3
  118. package/guides/migration.md +148 -151
  119. package/guides/operation-cancellation.md +1 -1
  120. package/guides/participant-ownership.md +78 -47
  121. package/guides/pi-tools.md +34 -5
  122. package/guides/primitive-catch-consolidation.md +5 -5
  123. package/guides/primitive-history-consolidation.md +2 -2
  124. package/guides/primitive-status-consolidation.md +3 -3
  125. package/guides/primitive-storage-consolidation.md +1 -1
  126. package/guides/receive-primitive.md +90 -0
  127. package/guides/row-runtime-delivery-evidence.md +1 -1
  128. package/guides/row-storage-design.md +22 -28
  129. package/guides/square-api.md +314 -91
  130. package/guides/symmetric-domains.md +92 -0
  131. package/package.json +14 -1
  132. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  133. package/plugins/claude/skills/square/SKILL.md +2 -6
  134. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  135. package/plugins/opencode/delivery.ts +27 -17
  136. package/plugins/paseo/client/palette.ts +18 -18
  137. package/plugins/paseo/client/square-message.tsx +17 -23
  138. package/plugins/pi/square-pi.js +17 -17
  139. package/skills/square/SKILL.md +2 -6
  140. package/dist/square-backend.d.ts +0 -13
  141. package/dist/square-backend.js +0 -1
  142. package/dist/square-stdio.d.ts +0 -22
  143. package/dist/square-stdio.js +0 -235
  144. package/dist/stdio-server.d.ts +0 -3
  145. package/dist/stdio-server.js +0 -212
  146. package/guides/backends.md +0 -193
@@ -1,51 +1,22 @@
1
1
  import { z } from 'zod/v4';
2
2
  import { isSquareError } from './model.js';
3
- import { CATCH_DEFAULT_LIMIT } from './square-api.js';
3
+ import { requireOwner, requireJoinName, selectOwnSeat, CATCH_DEFAULT_LIMIT } from './square-api.js';
4
4
  import { renderCatchOutcome, renderExpression, renderFailureLines, renderHistoryResult, renderJoinResult, renderLifecycleResult, renderListenerChange, renderStatusResult, } from './operation-presentation.js';
5
- const location = z.string().optional().describe('Which square: a .square file path, or a backend URI like stdio://name/key. Relative paths start from the working directory. Leave it out for the configured square.');
5
+ const location = z.string().optional().describe('Which square: a .square file path. Relative paths start from the working directory. Leave it out for the configured square.');
6
6
  const participant = z.string().optional().describe('Your name in the square. Leave it out if you hold exactly one name here.');
7
7
  const activityId = z.string().regex(/^act\/(0|[1-9][0-9]*)$/);
8
8
  const stringArray = z.array(z.string());
9
- /** Keep the observation request internal: it is not an argument a caller decides. */
10
- function observation(control) {
11
- return control.observe === true ? { observe: true } : {};
12
- }
13
- /** Tool receipts are plain JSON; each renderer re-declares the facts it reads. */
9
+ /** Each renderer declares the operation facts it reads. */
14
10
  function rendering(render) {
15
11
  return (result, args) => render(result, args);
16
12
  }
17
- /**
18
- * Tool receipts are the one machine surface without activity metadata: a receipt copy drops every
19
- * property named `metadata` while domain activities keep theirs for SDK, CLI, stdio and stream readers.
20
- */
21
- function withoutActivityMetadata(value) {
22
- if (Array.isArray(value))
23
- return value.map(withoutActivityMetadata);
24
- if (value === null || typeof value !== 'object')
25
- return value;
26
- const prototype = Object.getPrototypeOf(value);
27
- if (prototype !== Object.prototype && prototype !== null)
28
- return value;
29
- return Object.fromEntries(Object.entries(value).filter(([key]) => key !== 'metadata')
30
- .map(([key, item]) => [key, withoutActivityMetadata(item)]));
31
- }
32
- /**
33
- * Human text renders from the unfiltered receipt, so metadata-only says keep the placeholder the
34
- * CLI prints and every other sentence stays identical. Machines only ever receive the filtered copy.
35
- */
36
- const renderingReceipts = new WeakMap();
37
- function renderingReceipt(receipt) {
38
- return (renderingReceipts.get(receipt) ?? receipt);
39
- }
40
13
  function tool(name, description, shape, run, render) {
41
14
  const inputSchema = z.object({ ...shape, location }).strict();
42
- return { name, description, inputSchema, render: (receipt, args) => render(renderingReceipt(receipt), args),
43
- run: async (square, args, control) => {
15
+ return { name, description, inputSchema,
16
+ run: async (square, args, control, identity) => {
44
17
  const { location: _location, ...operationArgs } = inputSchema.parse(args);
45
- const receipt = await run(square, operationArgs, control);
46
- const filtered = withoutActivityMetadata(receipt);
47
- renderingReceipts.set(filtered, receipt);
48
- return filtered;
18
+ const receipt = await run(square, operationArgs, control, identity);
19
+ return render(receipt, operationArgs) ?? '';
49
20
  } };
50
21
  }
51
22
  export const SQUARE_TOOL_INSTRUCTIONS = 'Join once. Then express to speak, and catch to take in what reached you. History looks back without taking anything in.';
@@ -53,10 +24,7 @@ const target = z.string().describe('The participant to turn toward or away from.
53
24
  export const SQUARE_TOOLS = [
54
25
  tool('join', 'Step into the square under your name. Do this once before you speak or catch. You arrive seeing what was said most recently.', {
55
26
  as: z.string().optional().describe('Your name in the square. Give it explicitly when joining.'), takeover: z.boolean().optional().describe('Claim this name even though another session holds it now. That session loses it.'),
56
- }, async (square, { as, takeover }, control) => {
57
- const { participant, ...receipt } = await square.join(as, { takeover, ...observation(control) }, control);
58
- return { ...receipt, participant: participant.name };
59
- }, rendering((result) => renderJoinResult(result))),
27
+ }, (square, { as, takeover }, control, identity) => square.join(requireJoinName(as), { ...identity, owner: requireOwner(identity.owner), takeover, observe: true }, control), rendering((result) => renderJoinResult(result))),
60
28
  tool('express', 'Speak or act in the square. Plain words, half-formed ideas and *actions* all belong here. Mention or reply to someone to be heard by them; ring the bell to be heard by everyone. Said to no one in particular, your words reach those who listen to you.', {
61
29
  as: participant,
62
30
  body: z.string().describe('What you say or do. Markdown works; *asterisks* mark an action.'),
@@ -65,11 +33,8 @@ export const SQUARE_TOOLS = [
65
33
  reply: activityId.optional().describe('The activity you are answering, like act/12. Its author hears you if still here.'),
66
34
  noMention: z.boolean().optional().describe('Leave @names in the body as plain text. Mentions and reply still reach people.'),
67
35
  force: z.boolean().optional().describe('Speak now, even if something reached you that you have not caught yet.'),
68
- noWait: z.boolean().optional().describe('If a hand is raised or you are speaking too fast, return at once instead of waiting.'),
69
- }, async (square, { body, as, ...options }, control) => {
70
- const { delivery: _delivery, ...result } = await (await square.participant(as)).express(body, { ...options, ...observation(control) }, control);
71
- return result;
72
- }, rendering((result) => renderExpression(result.observation?.location ?? '', result))),
36
+ noWait: z.boolean().optional().describe('If you are speaking too fast, return at once instead of waiting.'),
37
+ }, async (square, { body, as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).express(body, { ...options, observe: true }, control), rendering((result) => renderExpression(result.observation?.location ?? '', result))),
73
38
  tool('catch', 'Take in what reached you since you last looked: mentions, replies, bells, and words from those you listen to. What you catch no longer waits for you; history lets you look back.', {
74
39
  as: participant,
75
40
  id: activityId.optional().describe('Take in this activity in full, like act/12, even if you caught it before. Do not combine with idle, from, mention or limit.'),
@@ -77,7 +42,7 @@ export const SQUARE_TOOLS = [
77
42
  from: stringArray.optional().describe('Only what these participants said.'),
78
43
  mention: z.boolean().optional().describe('Only what was addressed to you, and bells.'),
79
44
  limit: z.number().int().min(1).optional().describe('At most this many at once (default 10, up to 100). The rest keeps waiting.'),
80
- }, async (square, { as, ...options }, control) => (await square.participant(as)).catch({ ...options, ...observation(control) }, control), rendering((result, args) => {
45
+ }, async (square, { as, ...options }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).catch({ ...options, observe: true }, control), rendering((result, args) => {
81
46
  // The CLI always spells the default cap in its continuation, so the tool does too.
82
47
  const options = args;
83
48
  const renderOptions = options.id === undefined ? { ...options, limit: options.limit ?? CATCH_DEFAULT_LIMIT } : options;
@@ -92,25 +57,20 @@ export const SQUARE_TOOLS = [
92
57
  fixed: z.string().optional().describe('Only activities containing this exact text.'),
93
58
  from: stringArray.optional().describe('Only what these participants said.'),
94
59
  mention: z.string().optional().describe('Only what was addressed to this participant.'),
95
- }, (square, args, control) => square.history({ ...args, ...observation(control) }, control), rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: args }))),
60
+ }, (square, args, control) => square.history({ ...args, observe: true }, control), rendering((result, args) => renderHistoryResult(result.observation?.location ?? '', result, { query: args }))),
96
61
  tool('attention', 'Turn toward someone or away from them, from now on. Listen: hear everything they say, even to others. Unlisten: stop listening without clearing an ignore; mentions and replies still reach you unless ignored. Ignore: hear nothing from them but bells. What was already said stays as it was.', {
97
62
  as: participant, action: z.enum(['listen', 'unlisten', 'ignore']).describe('"listen", "unlisten" or "ignore". Listening again ends an ignore.'), target,
98
- }, async (square, { as, action, target }, control) => {
99
- const self = await square.participant(as);
100
- return self[action](target, observation(control), control);
63
+ }, async (square, { as, action, target }, control, identity) => {
64
+ const self = await selectOwnSeat(square, identity.owner, as);
65
+ return self[action](target, { observe: true }, control);
101
66
  }, rendering((result, args) => renderListenerChange(result.observation?.location ?? '', args.action, result))),
102
- tool('hold', 'Raise a hand to pause the conversation; lower it with release. While a hand is up, new words wait to land.', {
103
- as: participant,
104
- reason: z.string().optional().describe('Why you are pausing. Everyone sees it.'),
105
- release: z.boolean().optional().describe('true lowers the raised hand so the conversation goes on.'),
106
- }, async (square, { as, reason, release }, control) => {
107
- const self = await square.participant(as);
108
- return release === true ? self.resume(observation(control), control) : self.hold(reason, observation(control), control);
109
- }, rendering((result, args) => renderLifecycleResult(result.observation?.location ?? '', args.release === true ? 'resume' : 'hold', result))),
110
- tool('done', 'Step out of the square. Everyone sees you leave, and you stop listening to anyone. Leaving carries no words, so say goodbye with express first. Stay instead if someone may still need you; you can join again later.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).done(undefined, observation(control), control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', 'done', result))),
111
- tool('status', 'Look around the square: who is here, whether a hand is raised, the latest activity, and who you listen to.', {
67
+ tool('done', 'Step out of the square. Everyone sees you leave, and you stop listening to anyone. Leaving carries no words, so say goodbye with express first. Stay instead if someone may still need you; you can join again later.', { as: participant }, async (square, { as }, control, identity) => (await selectOwnSeat(square, identity.owner, as)).done(undefined, { observe: true }, control), rendering((result) => renderLifecycleResult(result.observation?.location ?? '', result))),
68
+ tool('status', 'Look around the square: who is here, the latest activity, and who you listen to.', {
112
69
  as: z.string().optional().describe('Whose listening to show. Leave it out if you hold exactly one name here.'),
113
- }, (square, { as }, control) => square.status({ viewer: as, ...observation(control) }, control), rendering((result) => renderStatusResult(result.observation?.location ?? '', result))),
70
+ }, async (square, { as }, control, identity) => {
71
+ const viewer = (await selectOwnSeat(square, identity.owner, as, { optional: true }))?.name;
72
+ return square.status({ viewer, observe: true }, control);
73
+ }, rendering((result) => renderStatusResult(result.observation?.location ?? '', result))),
114
74
  ];
115
75
  export function squareToolFailure(error, signal) {
116
76
  const normalized = signal?.aborted
package/dist/stream.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs/promises';
2
2
  import { SquareError } from './model.js';
3
3
  import { createArtifactPort, observeSquareRevision } from './artifact.js';
4
- import { streamProjection, streamTailProjection } from './views.js';
4
+ import { parseRequiredActivityId, streamProjection, streamTailProjection } from './views.js';
5
5
  /** Pull-based observation; return interrupts an outstanding pull before awaiting its cleanup. */
6
6
  export function streamSquare(squarePath, options = {}, control, borrowed) {
7
7
  return {
@@ -28,7 +28,7 @@ export function streamSquare(squarePath, options = {}, control, borrowed) {
28
28
  }
29
29
  signal.throwIfAborted();
30
30
  const start = options.start ?? { kind: 'tail', last: 10 };
31
- let cursor = start.kind === 'after' ? start.after : -1;
31
+ let cursor = start.kind === 'after' ? parseRequiredActivityId(start.after) : -1;
32
32
  let initial = start.kind === 'tail' ? start : undefined;
33
33
  const observer = await observeSquareRevision(squarePath);
34
34
  const close = () => observer.close();
@@ -60,10 +60,9 @@ export function streamSquare(squarePath, options = {}, control, borrowed) {
60
60
  await observer.hinted(generation, Infinity, signal);
61
61
  continue;
62
62
  }
63
- for (const item of projection.activities) {
63
+ for (const activity of projection.activities) {
64
64
  signal.throwIfAborted();
65
- yield { seq: item.activity.index, square: squarePath, ...item.activity,
66
- ...(item.route === undefined ? {} : { route: item.route }) };
65
+ yield activity;
67
66
  }
68
67
  cursor = projection.cursor;
69
68
  initial = undefined;
package/dist/views.d.ts CHANGED
@@ -1,13 +1,13 @@
1
1
  import { type ActivityId } from './square-core.js';
2
2
  import { type StoredAct } from './model.js';
3
3
  import type { ActivityFilter, SquareRead } from './ports.js';
4
- import type { Activity, ArchiveRecord, HistoryQuery, HistoryContinuation, ParticipantsResult, SquareSnapshot, EntryPresentation, ListPresentation, StatusResult, SquareObservation, CatchPresence, StreamProjection } from './square-facade.js';
5
- export type { EntryPresentation, ListPresentation, CatchPresence, StreamProjection } from './square-facade.js';
4
+ import type { Activity, ArchiveRecord, PerceivedActivity, HistoryQuery, HistoryContinuation, ParticipantsResult, SquareSnapshot, EntryPresentation, StatusResult, SquareObservation, CatchPresence } from './square-facade.js';
6
5
  export declare const HISTORY_DEFAULT_LIMIT = 10;
7
6
  export declare const HISTORY_MAX_LIMIT = 100;
8
- export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume", "listen", "ignore", "unlisten"];
9
- /** The one StoredAct-to-public-Activity field mapping. Rejects read and actorless acts. */
7
+ export declare const NON_READ: readonly ["join", "done", "say", "listen", "ignore", "unlisten"];
8
+ /** The one StoredAct-to-public-Activity field mapping. Rejects read bookkeeping. */
10
9
  export declare function toPublicActivity(stored: StoredAct): Activity;
10
+ export declare function toPerceivedActivity(facts: SquareRead, stored: StoredAct, viewer: string): PerceivedActivity;
11
11
  export declare function parseRequiredActivityId(id: ActivityId): number;
12
12
  /** Compile the public query directly to typed selectors; only search hydrates unselected bodies. */
13
13
  export declare function selectHistory(facts: SquareRead, query: HistoryQuery, limit: number | null, kinds: ActivityFilter['kinds'], presentation?: boolean): {
@@ -19,20 +19,30 @@ export declare function selectHistory(facts: SquareRead, query: HistoryQuery, li
19
19
  export declare function publicActivities(activities: readonly ArchiveRecord[]): Activity[];
20
20
  export declare function snapshot(facts: SquareRead, now: number): SquareSnapshot;
21
21
  export declare function recoveryObservation(facts: SquareRead, name: string, now: number): {
22
- ownActivityCount: number;
23
- hardCap: import("./model.js").HardCap;
24
- holdReason?: string | undefined;
25
22
  participant: string;
26
- roster: string[];
27
23
  activitySummaries: import("./square-facade.js").UnreadActivitySummary[];
28
24
  standingCount: number;
29
- held: boolean;
25
+ ownActivityCount: number;
26
+ hardCap: import("./model.js").HardCap;
30
27
  };
31
28
  export declare function entryPresentation(facts: SquareRead, name: string, lastN?: number | null): EntryPresentation;
32
- type ParticipantQueryContext = Pick<SquareObservation, 'location' | 'now' | 'automaticDeliveryAvailable'>;
29
+ type ParticipantQueryContext = Pick<SquareObservation, 'location' | 'now'>;
33
30
  export declare function queryParticipants(facts: SquareRead, context: ParticipantQueryContext, observe?: boolean): ParticipantsResult;
34
- export declare function listPresentation(facts: SquareRead): ListPresentation;
31
+ export declare function listPresentation(facts: SquareRead): {
32
+ context: readonly string[];
33
+ participants: string[];
34
+ activities: number;
35
+ };
35
36
  export declare function queryStatus(facts: SquareRead, context: ParticipantQueryContext, viewer?: string, observe?: boolean): StatusResult;
36
37
  export declare function catchPresence(facts: SquareRead, now: number): CatchPresence;
37
- export declare function streamProjection(facts: SquareRead, cursor: number, recipient?: string): StreamProjection;
38
- export declare function streamTailProjection(facts: SquareRead, last?: number, recipient?: string): StreamProjection;
38
+ export declare function streamProjection(facts: SquareRead, cursor: number, recipient?: string): {
39
+ activities: Activity[];
40
+ cursor: number;
41
+ hasMore: boolean;
42
+ };
43
+ export declare function streamTailProjection(facts: SquareRead, last?: number, recipient?: string): {
44
+ activities: Activity[];
45
+ cursor: number;
46
+ hasMore: boolean;
47
+ };
48
+ export {};
package/dist/views.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { formatActivityId, parseActivityId } from './square-core.js';
2
2
  import { compareParticipantActivity, coreParticipants, resolveKnownName, unreadActivitySummaries } from './decisions.js';
3
3
  import { SquareError, nameKey } from './model.js';
4
- import { inSquareCount, readCursor, rosterNames } from './runtime.js';
4
+ import { inSquareCount } from './runtime.js';
5
5
  import { perceiveActivity } from './perception-projection.js';
6
6
  import { compileSearchPattern } from './search.js';
7
7
  const PAGE_SIZE = 100;
@@ -9,11 +9,11 @@ export const HISTORY_DEFAULT_LIMIT = 10;
9
9
  export const HISTORY_MAX_LIMIT = 100;
10
10
  const STATUS_HERE_WINDOW_MS = 30 * 60_000;
11
11
  const PRESENCE_WINDOW_MS = 8 * 60 * 60_000;
12
- export const NON_READ = ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore', 'unlisten'];
13
- const ARCHIVE = new Set(['join', 'done', 'say', 'hold', 'resume']);
14
- /** The one StoredAct-to-public-Activity field mapping. Rejects read and actorless acts. */
12
+ export const NON_READ = ['join', 'done', 'say', 'listen', 'ignore', 'unlisten'];
13
+ const ARCHIVE = new Set(['join', 'done', 'say']);
14
+ /** The one StoredAct-to-public-Activity field mapping. Rejects read bookkeeping. */
15
15
  export function toPublicActivity(stored) {
16
- if (stored.kind === 'read' || stored.actor === undefined)
16
+ if (stored.kind === 'read')
17
17
  throw new Error(`Cannot expose stored activity ${formatActivityId(stored.index)}`);
18
18
  return { id: formatActivityId(stored.index), at: stored.at, kind: stored.kind, actor: stored.actor,
19
19
  ...(stored.kind === 'say' && stored.reach !== undefined ? { reach: stored.reach } : {}),
@@ -22,6 +22,11 @@ export function toPublicActivity(stored) {
22
22
  mentions: stored.kind === 'say' ? stored.mentions ?? [] : [], ...('target' in stored ? { target: stored.target } : {}),
23
23
  ...(stored.kind === 'say' && stored.reply !== undefined ? { reply: formatActivityId(stored.reply) } : {}) };
24
24
  }
25
+ export function toPerceivedActivity(facts, stored, viewer) {
26
+ const via = facts.directedAt(viewer, stored.index)?.route;
27
+ return { ...toPublicActivity(stored), perception: perceiveActivity(facts, stored, viewer),
28
+ ...(via === undefined ? {} : { via }) };
29
+ }
25
30
  export function parseRequiredActivityId(id) {
26
31
  const index = parseActivityId(id);
27
32
  if (index === undefined)
@@ -103,50 +108,44 @@ export function selectHistory(facts, query, limit, kinds, presentation = true) {
103
108
  return { activities, visibleActivities, totalMatches, ...(continuation === undefined ? {} : { continuation }) };
104
109
  }
105
110
  export function publicActivities(activities) {
106
- return activities.flatMap((activity) => activity.kind === 'read' || activity.actor === undefined ? [] : [toPublicActivity(activity)]);
107
- }
108
- function statuses(facts, now) {
109
- return coreParticipants(facts, now).filter((p) => p.state !== 'not joined').map((p) => {
110
- const cursor = readCursor(facts, p.name);
111
- return { name: p.name, state: p.state === 'done' ? 'done' : 'joined', consumedThrough: cursor < 0 ? null : formatActivityId(cursor), watching: p.presence === 'watching', listening: p.listening };
112
- });
111
+ return activities.flatMap((activity) => activity.kind === 'read' ? [] : [toPublicActivity(activity)]);
113
112
  }
114
113
  export function snapshot(facts, now) {
115
- const header = facts.header(), hold = facts.hold();
114
+ const header = facts.header();
116
115
  const names = new Map(facts.roster().map((p) => [nameKey(p.name), p.name]));
117
116
  const receipts = new Set(facts.receiptAssignments().map((r) => JSON.stringify([r.owner, r.index])));
118
117
  return { context: [...header.preamble, ...header.warmup].join('\n'), actCount: facts.activityCount({ kinds: NON_READ }), hardCap: header.hardCap,
119
118
  ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
120
- held: hold.active && hold.actor !== undefined ? { by: hold.actor, ...(hold.reason === undefined ? {} : { reason: hold.reason }) } : null,
121
- participants: statuses(facts, now), delivered(name, id) { return receipts.has(JSON.stringify([names.get(nameKey(name)) ?? name, parseRequiredActivityId(id)])); } };
119
+ participants: coreParticipants(facts, now).filter((p) => p.presence !== 'never-joined').map(({ name, presence, listening }) => ({ name, presence, listening })),
120
+ delivered(name, id) { return receipts.has(JSON.stringify([names.get(nameKey(name)) ?? name, parseRequiredActivityId(id)])); } };
122
121
  }
123
122
  export function recoveryObservation(facts, name, now) {
124
- const known = resolveKnownName(facts, name), hold = facts.hold();
125
- return { participant: known, roster: rosterNames(facts), activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts), held: hold.active,
126
- ...(hold.reason === undefined ? {} : { holdReason: hold.reason }), ownActivityCount: facts.participant(known).sayCount, hardCap: facts.header().hardCap };
123
+ const known = resolveKnownName(facts, name);
124
+ return { participant: known, activitySummaries: unreadActivitySummaries(facts, known, now), standingCount: inSquareCount(facts),
125
+ ownActivityCount: facts.participant(known).sayCount, hardCap: facts.header().hardCap };
127
126
  }
128
127
  export function entryPresentation(facts, name, lastN = HISTORY_DEFAULT_LIMIT) {
129
128
  const header = facts.header();
130
129
  const recentActivities = selectHistory(facts, {}, lastN, ['say', 'done']).activities;
131
130
  return { joinedBefore: facts.activityCount({ kinds: ['join'], actors: [name] }) > 1,
132
131
  scene: header.warmup.join('\n').trim(),
133
- joinContext: (header.preamble.at(-1) === '---' ? header.preamble.slice(0, -1) : header.preamble).join('\n').trim(), recentActivities,
134
- perceptions: Object.fromEntries(recentActivities.map((a) => [a.index, perceiveActivity(facts, a, name)])),
135
- sayNumbers: facts.sayOrdinals(recentActivities.map((a) => a.index)) };
132
+ joinContext: (header.preamble.at(-1) === '---' ? header.preamble.slice(0, -1) : header.preamble).join('\n').trim(),
133
+ recentActivities: recentActivities.map((activity) => ({ ...toPublicActivity(activity), perception: perceiveActivity(facts, activity, name) })) };
136
134
  }
137
135
  function classifyPresence(participants, now) {
138
- const here = participants.filter((participant) => participant.state === 'active' && (participant.presence === 'watching'
136
+ const standing = (participant) => participant.presence === 'active' || participant.presence === 'watching';
137
+ const here = participants.filter((participant) => standing(participant) && (participant.presence === 'watching'
139
138
  || (participant.lastActiveAt !== undefined && now - participant.lastActiveAt <= STATUS_HERE_WINDOW_MS)));
140
139
  const hereNames = new Set(here.map((participant) => participant.name));
141
140
  return { orderedHereNames: [...here.filter((participant) => participant.presence === 'watching'), ...here.filter((participant) => participant.presence !== 'watching')].map((participant) => participant.name),
142
- lingeringCount: participants.filter((participant) => participant.state === 'active' && !hereNames.has(participant.name)).length,
143
- classificationByName: Object.fromEntries(participants.map((participant) => [participant.name, participant.state === 'done' ? 'done'
144
- : participant.state === 'not joined' ? 'not-joined' : hereNames.has(participant.name) ? 'here' : 'lingering'])) };
141
+ lingeringCount: participants.filter((participant) => standing(participant) && !hereNames.has(participant.name)).length,
142
+ classificationByName: Object.fromEntries(participants.map((participant) => [participant.name, participant.presence === 'done' ? 'done'
143
+ : participant.presence === 'never-joined' ? 'not-joined' : participant.presence === 'away' ? 'away' : hereNames.has(participant.name) ? 'here' : 'lingering'])) };
145
144
  }
146
145
  export function queryParticipants(facts, context, observe = false) {
147
146
  const participants = coreParticipants(facts, context.now);
148
147
  return { participants, ...(!observe ? {} : { observation: { ...context, ...classifyPresence(participants, context.now),
149
- standingCount: inSquareCount(facts), held: facts.hold().active } }) };
148
+ standingCount: inSquareCount(facts) } }) };
150
149
  }
151
150
  export function listPresentation(facts) {
152
151
  const participants = facts.roster().filter((p) => p.joined).map((p) => {
@@ -157,62 +156,57 @@ export function listPresentation(facts) {
157
156
  }
158
157
  export function queryStatus(facts, context, viewer = '', observe = false) {
159
158
  const participants = coreParticipants(facts, context.now);
160
- const header = facts.header(), hold = facts.hold(), standingCount = inSquareCount(facts);
161
- const listener = participants.find((p) => p.state !== 'not joined' && nameKey(p.name) === nameKey(viewer));
159
+ const header = facts.header(), standingCount = inSquareCount(facts);
160
+ const listener = participants.find((p) => p.presence !== 'never-joined' && nameKey(p.name) === nameKey(viewer));
162
161
  const latest = facts.activityHeads({ kinds: ['say', 'done'] }, { edge: 'newest', limit: 1 }).items[0];
163
162
  const latestAct = latest === undefined ? undefined : facts.activity(latest.index);
164
163
  const data = {
165
164
  hardCap: header.hardCap,
166
165
  ...(header.throttlePerMinute === undefined ? {} : { throttlePerMinute: header.throttlePerMinute }),
167
- activeCount: standingCount, doneCount: participants.filter((p) => p.state === 'done').length,
168
- hold: hold.active ? {
169
- ...(hold.actor === undefined ? {} : { by: hold.actor }),
170
- ...(hold.reason === undefined ? {} : { reason: hold.reason }),
171
- ...(hold.at === undefined ? {} : { at: hold.at }),
172
- } : null,
173
166
  ...(listener === undefined ? {} : { listening: listener.listening }),
174
- participants: participants.filter((p) => p.state !== 'not joined'), now: context.now,
175
- ...(latestAct === undefined || latestAct.actor === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
167
+ participants: participants.filter((p) => p.presence !== 'never-joined'), now: context.now,
168
+ ...(latestAct === undefined || latestAct.kind === 'read' ? {} : { latestActivity: toPublicActivity(latestAct) }),
176
169
  };
177
170
  if (!observe)
178
171
  return data;
179
172
  const afterIndex = latestAct?.index ?? -1;
180
- const joins = facts.activities(facts.activityHeads({ kinds: ['join'], afterIndex }, { edge: 'newest', limit: 3 }).items.map((h) => h.index)).map((activity) => ({ activity,
181
- rejoined: facts.activityCount({ kinds: ['done'], actors: [activity.actor], actorMatch: 'exact', beforeIndex: activity.index }) > 0 }));
173
+ const joins = facts.activities(facts.activityHeads({ kinds: ['join'], afterIndex }, { edge: 'newest', limit: 3 }).items.map((h) => h.index)).map((activity) => ({
174
+ rejoined: facts.activityCount({ kinds: ['done'], actors: [activity.actor], actorMatch: 'exact', beforeIndex: activity.index }) > 0,
175
+ activity: toPublicActivity(activity)
176
+ }));
182
177
  const canonicalViewer = facts.participant(viewer)?.name ?? viewer;
183
178
  return {
184
179
  ...data,
185
180
  observation: {
186
- ...context, ...classifyPresence(participants, context.now), standingCount, held: hold.active,
181
+ location: context.location,
182
+ ...classifyPresence(participants, context.now), standingCount,
187
183
  knownParticipantCount: participants.length, viewer: canonicalViewer,
188
184
  ...(canonicalViewer === '' ? {} : { participant: canonicalViewer }),
189
185
  joins, joinCount: facts.activityCount({ kinds: ['join'], afterIndex }),
190
186
  attentionNames: participants.filter((p) => canonicalViewer === '' || nameKey(p.name) === nameKey(canonicalViewer)).map((p) => p.name),
191
187
  ...(latestAct === undefined ? {} : { latestPerception: perceiveActivity(facts, latestAct, canonicalViewer) }),
192
- ...(latestAct?.kind === 'say' ? { latestActNumber: facts.sayOrdinals([latestAct.index])[latestAct.index] } : {}),
193
188
  },
194
189
  };
195
190
  }
196
191
  export function catchPresence(facts, now) {
197
- const participants = coreParticipants(facts, now).filter((participant) => participant.state === 'done' || participant.presence === 'watching'
192
+ const participants = coreParticipants(facts, now).filter((participant) => participant.presence === 'done' || participant.presence === 'away' || participant.presence === 'watching'
198
193
  || (participant.lastActiveAt !== undefined && now - participant.lastActiveAt <= PRESENCE_WINDOW_MS));
199
194
  return { participants, now };
200
195
  }
201
196
  export function streamProjection(facts, cursor, recipient) {
202
- const page = facts.activityHeads({ afterIndex: cursor }, { edge: 'oldest', limit: PAGE_SIZE });
203
- const heads = page.items.flatMap((head) => {
204
- const route = recipient === undefined ? undefined : facts.directedAt(recipient, head.index)?.route;
205
- return recipient !== undefined && route === undefined ? [] : [{ index: head.index, route }];
206
- });
207
- return { activities: facts.activities(heads.map((h) => h.index)).map((activity) => {
208
- const route = heads.find((h) => h.index === activity.index).route;
209
- return { activity, ...(route === undefined ? {} : { route }) };
210
- }), cursor: page.items.at(-1)?.index ?? cursor, hasMore: page.hasMore };
197
+ const page = facts.activityHeads({ afterIndex: cursor, kinds: NON_READ }, { edge: 'oldest', limit: PAGE_SIZE });
198
+ const heads = recipient === undefined ? page.items : page.items.filter((head) => facts.directedAt(recipient, head.index) !== undefined);
199
+ const activities = facts.activities(heads.map((head) => head.index)).map((activity) => recipient === undefined
200
+ ? toPublicActivity(activity) : toPerceivedActivity(facts, activity, recipient));
201
+ return { activities, cursor: page.items.at(-1)?.index ?? cursor, hasMore: page.hasMore };
211
202
  }
212
203
  export function streamTailProjection(facts, last = HISTORY_DEFAULT_LIMIT, recipient) {
213
204
  if (!Number.isSafeInteger(last) || last < 0 || last > PAGE_SIZE)
214
205
  throw new SquareError('invalid_args', `Invalid stream tail: expected a non-negative safe integer no greater than ${PAGE_SIZE}.`);
215
- const heads = last === 0 ? [] : recipient === undefined ? facts.activityHeads({}, { edge: 'newest', limit: last }).items : facts.directed(recipient, {}, { edge: 'newest', limit: last }).items;
216
- return { activities: facts.activities(heads.map((h) => h.index)).map((activity) => ({ activity,
217
- ...(recipient === undefined ? {} : { route: facts.directedAt(recipient, activity.index).route }) })), cursor: facts.header().lastActIndex, hasMore: false };
206
+ const heads = last === 0 ? [] : recipient === undefined
207
+ ? facts.activityHeads({ kinds: NON_READ }, { edge: 'newest', limit: last }).items
208
+ : facts.directed(recipient, {}, { edge: 'newest', limit: last }).items;
209
+ const activities = facts.activities(heads.map((head) => head.index)).map((activity) => recipient === undefined
210
+ ? toPublicActivity(activity) : toPerceivedActivity(facts, activity, recipient));
211
+ return { activities, cursor: facts.header().lastActIndex, hasMore: false };
218
212
  }
package/dist/wakes.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { type WatchLease, type WatchOptions } from './model.js';
1
+ import { type WatchOptions } from './model.js';
2
2
  import type { SquareArtifactPort } from './ports.js';
3
3
  import type { Seat } from './model.js';
4
4
  export type WatchLeaseStart = {
@@ -8,13 +8,9 @@ export type WatchLeaseStart = {
8
8
  readonly heartbeatAt: number;
9
9
  } | {
10
10
  readonly type: 'active';
11
- readonly lease: WatchLease;
12
11
  };
13
12
  export type WatchLeasePulse = {
14
13
  readonly type: 'replaced';
15
- } | {
16
- readonly type: 'held';
17
- readonly heartbeatAt?: number;
18
14
  } | {
19
15
  readonly type: 'terminal';
20
16
  readonly status: 'capped' | 'quorum';
package/dist/wakes.js CHANGED
@@ -9,7 +9,7 @@ export async function acquireWatchLease(artifact, seat, at, leaseId, options, si
9
9
  const { value } = await artifact.transactAs({ kind: 'participant', seat }, facts => {
10
10
  const known = resolveKnownName(facts, name), existing = freshWatchLease(facts, known, at);
11
11
  if (existing !== undefined && !options.replace)
12
- return { type: 'active', lease: existing };
12
+ return { type: 'active' };
13
13
  facts.putLease(known, newLease(leaseId, at, options), seat.generation);
14
14
  return { type: 'started', leaseId, replaced: existing !== undefined, heartbeatAt: at };
15
15
  }, signal);
@@ -21,15 +21,13 @@ export async function pulseWatchLease(artifact, seat, at, leaseId, options, hear
21
21
  const known = resolveKnownName(facts, name), lease = freshWatchLease(facts, known, at);
22
22
  if (lease?.leaseId !== leaseId)
23
23
  return { type: 'replaced' };
24
- const held = facts.hold().active;
25
- const terminal = held ? undefined : watchTerminalStatus(facts, known);
24
+ const terminal = watchTerminalStatus(facts, known);
26
25
  if (terminal !== undefined)
27
26
  return { type: 'terminal', status: terminal };
28
- const type = held ? 'held' : 'sleep';
29
27
  if (!heartbeatDue)
30
- return { type };
28
+ return { type: 'sleep' };
31
29
  facts.putLease(known, newLease(leaseId, at, options), seat.generation);
32
- return { type, heartbeatAt: at };
30
+ return { type: 'sleep', heartbeatAt: at };
33
31
  }, signal);
34
32
  return value;
35
33
  }