@astrosheep/square 0.3.95 → 0.3.97

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 (51) hide show
  1. package/dist/artifact.d.ts +1 -1
  2. package/dist/artifact.js +11 -17
  3. package/dist/attention-presentation.d.ts +5 -4
  4. package/dist/attention-presentation.js +18 -8
  5. package/dist/boundary-presentation.js +31 -26
  6. package/dist/channel.d.ts +74 -0
  7. package/dist/channel.js +92 -0
  8. package/dist/claude-delivery.js +3 -9
  9. package/dist/cli/observation-commands.js +2 -2
  10. package/dist/codex-hook.d.ts +0 -2
  11. package/dist/codex-hook.js +0 -1
  12. package/dist/decisions.d.ts +2 -11
  13. package/dist/decisions.js +3 -8
  14. package/dist/delivery-health.js +3 -13
  15. package/dist/delivery.d.ts +1 -11
  16. package/dist/delivery.js +2 -9
  17. package/dist/identity.d.ts +0 -1
  18. package/dist/identity.js +0 -1
  19. package/dist/inbox.js +11 -7
  20. package/dist/mcp.js +65 -63
  21. package/dist/model.d.ts +6 -0
  22. package/dist/model.js +19 -0
  23. package/dist/notifications.d.ts +1 -1
  24. package/dist/ports.d.ts +5 -5
  25. package/dist/presentation-operations.d.ts +1 -0
  26. package/dist/sqlite-square.d.ts +3 -2
  27. package/dist/sqlite-square.js +91 -9
  28. package/dist/square-core.d.ts +1 -0
  29. package/dist/square-domain.js +22 -7
  30. package/dist/square-facade.d.ts +36 -10
  31. package/dist/square-sqlite.js +33 -4
  32. package/dist/square-tools.d.ts +1 -0
  33. package/dist/square-tools.js +9 -1
  34. package/dist/views.d.ts +1 -2
  35. package/dist/views.js +1 -9
  36. package/guides/operation-cancellation.md +5 -1
  37. package/guides/primitive-status-consolidation.md +1 -1
  38. package/guides/receive-primitive.md +4 -2
  39. package/guides/row-runtime-delivery-evidence.md +3 -3
  40. package/guides/row-storage-design.md +5 -1
  41. package/guides/square-api.md +63 -7
  42. package/package.json +5 -2
  43. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  44. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  45. package/plugins/codex/hooks/hooks.json +6 -6
  46. package/plugins/paseo/client/marble.ts +7 -0
  47. package/plugins/paseo/client/markdown.tsx +65 -9
  48. package/plugins/paseo/client/palette.ts +43 -20
  49. package/plugins/paseo/client/square-message.tsx +57 -48
  50. package/plugins/paseo/shared/message.ts +1 -2
  51. package/plugins/pi/square-pi.js +6 -4
@@ -8,6 +8,5 @@ export declare const SQUARE_IDENTITY: Readonly<{
8
8
  cliName: "square";
9
9
  pluginId: "square@astrosheep";
10
10
  hookCommand: "square codex-hook";
11
- hookMarker: "square-codex-hook";
12
11
  clientName: "astrosheep_square";
13
12
  }>;
package/dist/identity.js CHANGED
@@ -22,6 +22,5 @@ export const SQUARE_IDENTITY = Object.freeze({
22
22
  cliName: 'square',
23
23
  pluginId: 'square@astrosheep',
24
24
  hookCommand: 'square codex-hook',
25
- hookMarker: 'square-codex-hook',
26
25
  clientName: 'astrosheep_square',
27
26
  });
package/dist/inbox.js CHANGED
@@ -3,7 +3,7 @@ import { indexedSquareEntries, observeSquareIndex } from './square-index.js';
3
3
  import { canonicalPath } from './canonical-path.js';
4
4
  import { formatActivityId, parseActivityId } from './square-core.js';
5
5
  import { freshWatchLease, WATCH_STALE_MS } from './runtime.js';
6
- import { pendingHead, pendingNotifications, leaseOwnsNotification } from './delivery.js';
6
+ import { pendingHead, pendingHeads, leaseOwnsNotification } from './delivery.js';
7
7
  import { deliveryDeferralExpiry } from './delivery-deferral.js';
8
8
  import { attentionBodyIsClipped, renderAttentionPreview, renderSquareReminder } from './attention-presentation.js';
9
9
  export function pendingKey(membership, actIndex) {
@@ -146,11 +146,14 @@ async function projectPending(candidates, cache, signal) {
146
146
  const header = facts.header();
147
147
  let cached = cache?.get(location);
148
148
  if (cached?.coordinate !== coordinate || cached.version !== header.revision) {
149
- cached = { coordinate, version: header.revision, value: locatedSeats(facts, location, seats).map(membership => ({
150
- ...membership, notifications: pendingNotifications(facts, membership.seat.participant).map(({ item, route }) => ({
151
- actIndex: item.index, actor: item.actor, at: item.at, body: item.body ?? '(structured activity)', route,
152
- })),
153
- })) };
149
+ cached = { coordinate, version: header.revision, value: locatedSeats(facts, location, seats).map(membership => {
150
+ const heads = pendingHeads(facts, membership.seat.participant);
151
+ const routes = new Map(heads.map(head => [head.index, head.route]));
152
+ return { ...membership, notifications: facts.activities(heads.map(head => head.index)).flatMap(item => item.kind === 'say'
153
+ ? [{ actIndex: item.index, actor: item.actor, at: item.at, body: item.body ?? '(structured activity)',
154
+ route: routes.get(item.index), ...(item.delivery === undefined ? {} : { delivery: item.delivery }) }]
155
+ : []) };
156
+ }) };
154
157
  }
155
158
  // Cache conversation content, never leases or delivery eligibility: evidence can change
156
159
  // without a conversation revision and must not force body hydration or self-wake retries.
@@ -197,7 +200,8 @@ export function nativePendingPreview(facts, input) {
197
200
  if (item?.kind !== 'say' || deliveryDeferralExpiry(facts, seat, input.actIndex, input.now) !== undefined)
198
201
  return undefined;
199
202
  return { payload: renderSquareReminder([renderAttentionPreview({ squarePath: input.location, recipient: input.participant, actIndex: item.index,
200
- actor: item.actor, route: head.route, body: item.body ?? '(structured activity)' })]), clipped: attentionBodyIsClipped(item.body ?? '(structured activity)') };
203
+ actor: item.actor, route: head.route, body: item.body ?? '(structured activity)', delivery: item.delivery })], [item.delivery]),
204
+ clipped: attentionBodyIsClipped(item.body ?? '(structured activity)', item.delivery) };
201
205
  }
202
206
  export async function sessionInbox(owner, env = process.env, signal) {
203
207
  return projectPending(await projectSessionCandidates({ owner, env }), undefined, signal);
package/dist/mcp.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
- import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure, squareToolFailureText } from './square-tools.js';
3
+ import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolsEnabled, squareToolFailure, squareToolFailureText } from './square-tools.js';
4
4
  import { operationEnv } from './participant-identity.js';
5
5
  import { codexWakeAddress } from './codex-address.js';
6
6
  import { readClaudeMcpCall } from './claude-mcp-context.js';
@@ -10,76 +10,78 @@ import { harnessOwner } from './square-api.js';
10
10
  export function createSquareMcpServer(options) {
11
11
  const { owner: configuredOwner, wakeAddress, ...resourceOptions } = options;
12
12
  const baseEnv = { ...(resourceOptions.env ?? process.env) };
13
+ const toolsEnabled = squareToolsEnabled(baseEnv);
13
14
  const resources = createSquareResourceOwner();
14
15
  // Calls to the configured square borrow one server-owned connection.
15
16
  const inFlight = new Set();
16
17
  const server = new McpServer({ name: 'square', version: SQUARE_IDENTITY.packageVersion }, {
17
- instructions: SQUARE_TOOL_INSTRUCTIONS,
18
+ ...(toolsEnabled ? { instructions: SQUARE_TOOL_INSTRUCTIONS } : {}),
18
19
  });
19
- for (const tool of SQUARE_TOOLS) {
20
- server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args, extra) => {
21
- const call = (async () => {
22
- let square;
23
- try {
24
- const { location } = args;
25
- const threadId = extra._meta?.threadId;
26
- const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
27
- // Claude's plugin endpoint requires the mod's call-local coordinate, never startup identity.
28
- const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
29
- ? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
30
- : undefined;
31
- const owner = claude !== undefined
32
- ? harnessOwner({ channel: 'claude-code', session: claude.sessionId, ...(claude.agentId === undefined ? {} : { agent: claude.agentId }) })
33
- : typeof threadId === 'string' && threadId.trim()
34
- ? harnessOwner({ channel: 'codex', session: threadId })
35
- : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
36
- ? harnessOwner({ channel: 'opencode', session: opencodeSessionId })
37
- : configuredOwner;
38
- const env = claude !== undefined
39
- ? { ...operationEnv('claude', claude.sessionId, baseEnv),
40
- // Preserve Claude's session + child flag semantics; an agent loop ID is not a session ID.
41
- CLAUDE_CODE_CHILD_SESSION: claude.agentId === undefined ? '' : '1' }
42
- : typeof threadId === 'string' && threadId.trim()
43
- ? operationEnv('codex', threadId, baseEnv)
44
- : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
45
- ? operationEnv('opencode', opencodeSessionId, baseEnv)
46
- : baseEnv;
47
- square = await Square.at({
48
- ...resourceOptions, env, cwd: claude?.cwd ?? resourceOptions.cwd,
49
- path: location ?? options.path,
50
- // The server owner is bound to its configured square; overrides close after each call.
51
- resources: location === undefined && claude === undefined ? resources : undefined,
52
- }, { signal: extra.signal });
53
- const callWake = wakeAddress !== undefined && (owner === configuredOwner
54
- || wakeAddress.kind === 'paseo' && wakeAddress.address.agentId === baseEnv.PASEO_AGENT_ID
55
- || wakeAddress.kind === 'claude-native' && wakeAddress.address.sessionId === claude?.sessionId
56
- || wakeAddress.kind === 'opencode-server' && wakeAddress.address.sessionId === opencodeSessionId)
57
- ? wakeAddress : undefined;
58
- let text;
20
+ if (toolsEnabled)
21
+ for (const tool of SQUARE_TOOLS) {
22
+ server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args, extra) => {
23
+ const call = (async () => {
24
+ let square;
59
25
  try {
60
- text = await tool.run(square, args, { signal: extra.signal }, {
61
- owner, wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
62
- ? codexWakeAddress(threadId, env) : callWake,
63
- });
26
+ const { location } = args;
27
+ const threadId = extra._meta?.threadId;
28
+ const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
29
+ // Claude's plugin endpoint requires the mod's call-local coordinate, never startup identity.
30
+ const claude = baseEnv.SQUARE_CLAUDE_MCP_CONTEXT === '1'
31
+ ? await readClaudeMcpCall(extra._meta?.['claudecode/toolUseId'], baseEnv)
32
+ : undefined;
33
+ const owner = claude !== undefined
34
+ ? harnessOwner({ channel: 'claude-code', session: claude.sessionId, ...(claude.agentId === undefined ? {} : { agent: claude.agentId }) })
35
+ : typeof threadId === 'string' && threadId.trim()
36
+ ? harnessOwner({ channel: 'codex', session: threadId })
37
+ : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
38
+ ? harnessOwner({ channel: 'opencode', session: opencodeSessionId })
39
+ : configuredOwner;
40
+ const env = claude !== undefined
41
+ ? { ...operationEnv('claude', claude.sessionId, baseEnv),
42
+ // Preserve Claude's session + child flag semantics; an agent loop ID is not a session ID.
43
+ CLAUDE_CODE_CHILD_SESSION: claude.agentId === undefined ? '' : '1' }
44
+ : typeof threadId === 'string' && threadId.trim()
45
+ ? operationEnv('codex', threadId, baseEnv)
46
+ : typeof opencodeSessionId === 'string' && opencodeSessionId.trim()
47
+ ? operationEnv('opencode', opencodeSessionId, baseEnv)
48
+ : baseEnv;
49
+ square = await Square.at({
50
+ ...resourceOptions, env, cwd: claude?.cwd ?? resourceOptions.cwd,
51
+ path: location ?? options.path,
52
+ // The server owner is bound to its configured square; overrides close after each call.
53
+ resources: location === undefined && claude === undefined ? resources : undefined,
54
+ }, { signal: extra.signal });
55
+ const callWake = wakeAddress !== undefined && (owner === configuredOwner
56
+ || wakeAddress.kind === 'paseo' && wakeAddress.address.agentId === baseEnv.PASEO_AGENT_ID
57
+ || wakeAddress.kind === 'claude-native' && wakeAddress.address.sessionId === claude?.sessionId
58
+ || wakeAddress.kind === 'opencode-server' && wakeAddress.address.sessionId === opencodeSessionId)
59
+ ? wakeAddress : undefined;
60
+ let text;
61
+ try {
62
+ text = await tool.run(square, args, { signal: extra.signal }, {
63
+ owner, wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
64
+ ? codexWakeAddress(threadId, env) : callWake,
65
+ });
66
+ }
67
+ finally {
68
+ await square.close();
69
+ }
70
+ return { content: [{ type: 'text', text }] };
64
71
  }
65
- finally {
66
- await square.close();
72
+ catch (error) {
73
+ const failure = squareToolFailure(error, extra.signal);
74
+ const participant = args.as;
75
+ return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, {
76
+ location: square?.location ?? options.path,
77
+ participant: typeof participant === 'string' ? participant : undefined,
78
+ }) }] };
67
79
  }
68
- return { content: [{ type: 'text', text }] };
69
- }
70
- catch (error) {
71
- const failure = squareToolFailure(error, extra.signal);
72
- const participant = args.as;
73
- return { isError: true, content: [{ type: 'text', text: squareToolFailureText(failure, {
74
- location: square?.location ?? options.path,
75
- participant: typeof participant === 'string' ? participant : undefined,
76
- }) }] };
77
- }
78
- })();
79
- inFlight.add(call);
80
- return call.finally(() => { inFlight.delete(call); });
81
- });
82
- }
80
+ })();
81
+ inFlight.add(call);
82
+ return call.finally(() => { inFlight.delete(call); });
83
+ });
84
+ }
83
85
  const close = server.close.bind(server);
84
86
  // One idempotent shutdown for both close paths: an explicit server.close() and a
85
87
  // transport-initiated disconnect, where the SDK protocol fires onclose by itself.
package/dist/model.d.ts CHANGED
@@ -7,6 +7,11 @@ export type JsonValue = null | boolean | number | string | readonly JsonValue[]
7
7
  readonly [key: string]: JsonValue;
8
8
  };
9
9
  export type ActivityMetadata = Readonly<Record<string, JsonValue>>;
10
+ export interface ActivityDelivery {
11
+ readonly mode: 'direct';
12
+ readonly preview?: string;
13
+ }
14
+ export declare function validateActivityDelivery(value: unknown): ActivityDelivery | undefined;
10
15
  export declare const ACTIVITY_METADATA_MAX_BYTES: number;
11
16
  export declare const ACTIVITY_METADATA_MAX_DEPTH = 256;
12
17
  /** Validate the bounded, lossless JSON object carried by a say. */
@@ -105,6 +110,7 @@ export interface InboxNotification {
105
110
  at: number;
106
111
  route: DirectedNotificationRoute;
107
112
  body: string;
113
+ readonly delivery?: ActivityDelivery;
108
114
  }
109
115
  export interface LocatedSeat {
110
116
  readonly location: string;
package/dist/model.js CHANGED
@@ -1,6 +1,25 @@
1
1
  // Shared model and constants for Square.
2
2
  export { formatActivityId, parseActivityId } from './square-core.js';
3
3
  export const WAKE_ROUTE_KINDS = ['opencode-server', 'codex-queue', 'claude-native', 'paseo', 'http'];
4
+ export function validateActivityDelivery(value) {
5
+ if (value === undefined)
6
+ return undefined;
7
+ if (value !== null && typeof value === 'object'
8
+ && [Object.prototype, null].includes(Object.getPrototypeOf(value))
9
+ && Object.getOwnPropertySymbols(value).length === 0) {
10
+ const keys = Object.getOwnPropertyNames(value);
11
+ const mode = Object.getOwnPropertyDescriptor(value, 'mode');
12
+ const preview = Object.getOwnPropertyDescriptor(value, 'preview');
13
+ if (mode?.enumerable && 'value' in mode && mode.value === 'direct') {
14
+ if (keys.length === 1)
15
+ return { mode: 'direct' };
16
+ if (keys.length === 2 && preview?.enumerable && 'value' in preview
17
+ && typeof preview.value === 'string' && preview.value.trim() !== '')
18
+ return { mode: 'direct', preview: preview.value };
19
+ }
20
+ }
21
+ throw new SquareError('invalid_args', "Activity delivery must be { mode: 'direct', preview?: string }.");
22
+ }
4
23
  export const ACTIVITY_METADATA_MAX_BYTES = 16 * 1024;
5
24
  export const ACTIVITY_METADATA_MAX_DEPTH = 256;
6
25
  /** Validate the bounded, lossless JSON object carried by a say. */
@@ -1,6 +1,5 @@
1
1
  import { type WakeRouteKind } from './model.js';
2
2
  import type { WakeTransportPort } from './ports.js';
3
- export type { PlannedNotification } from './delivery.js';
4
3
  export declare const PRIVILEGED_HOOK_BUDGET_MS = 3000;
5
4
  export declare function wakeGraceMs(env?: NodeJS.ProcessEnv): number;
6
5
  export type WakeTransports = Readonly<Partial<Record<WakeRouteKind, WakeTransportPort>>>;
@@ -24,3 +23,4 @@ export interface SweepPendingNotificationsOptions {
24
23
  }
25
24
  /** Select old pending attention at a bounded action boundary for an explicit executor. */
26
25
  export declare function sweepPendingNotifications(squarePath: string, opts?: SweepPendingNotificationsOptions): Promise<number[]>;
26
+ export {};
package/dist/ports.d.ts CHANGED
@@ -63,6 +63,8 @@ export interface DirectedFilter {
63
63
  readonly beforeIndex?: number;
64
64
  readonly throughIndex?: number;
65
65
  readonly unseenOnly?: true;
66
+ /** Exact own-property scalar equalities, applied before paging or counting. */
67
+ readonly metadataMatch?: MetadataMatch;
66
68
  readonly senders?: readonly string[];
67
69
  readonly attentionOnly?: true;
68
70
  }
@@ -78,12 +80,11 @@ export interface ReceiptFacts {
78
80
  readonly latestAt?: number;
79
81
  readonly latestActorActivityIndex?: number;
80
82
  }
81
- export interface ReceiptAssignment {
82
- readonly owner: string;
83
- readonly index: ActivityIndex;
84
- }
85
83
  export type SeatRef = Pick<Seat, 'participant' | 'owner' | 'generation'>;
86
84
  export type ArtifactWriteAuthority = {
85
+ readonly kind: 'send';
86
+ readonly actor: string;
87
+ } | {
87
88
  readonly kind: 'participant';
88
89
  readonly seat: SeatRef;
89
90
  } | {
@@ -199,7 +200,6 @@ export interface SquareRead {
199
200
  readonly at: number;
200
201
  } | undefined;
201
202
  receiptFacts(requestedName: string, afterIndex: number): ReceiptFacts;
202
- receiptAssignments(): readonly ReceiptAssignment[];
203
203
  lease(requestedName: string): WatchLease | undefined;
204
204
  deliveryDeferrals(participant: string, generation: ActivityId): readonly DeliveryDeferralFact[];
205
205
  }
@@ -6,6 +6,7 @@ export declare function preparePendingPresentation(input: Omit<PresentPendingInp
6
6
  at?: number;
7
7
  body?: string;
8
8
  metadata?: import("./model.js").ActivityMetadata;
9
+ delivery?: import("./model.js").ActivityDelivery;
9
10
  mentions?: readonly import("./square-core.js").Participant[];
10
11
  reach?: import("./square-core.js").Reach;
11
12
  reply?: number;
@@ -1,5 +1,5 @@
1
1
  import { type Owner } from './model.js';
2
- import type { HistoryQuery, HistoryResult, JoinOptions, JoinResult, LifecycleResult, ObservationOptions, OperationControl, Participant, SquareAtInput, SquareBuildInput, SquareLocation, StatusOptions, StatusResult, ParticipantsResult, RecoveryResult, DiscoveryOptions, InboxOptions, InboxResult, StreamOptions, StreamRecord, Activity, PerceivedActivity } from './square-facade.js';
2
+ import type { HistoryQuery, HistoryResult, JoinOptions, JoinResult, LifecycleResult, ObservationOptions, OperationControl, Participant, SquareAtInput, SquareBuildInput, SquareLocation, StatusOptions, StatusResult, ParticipantsResult, RecoveryResult, DiscoveryOptions, InboxOptions, InboxResult, StreamOptions, StreamRecord, Activity, PerceivedActivity, ChannelSendInput, ChannelSendResult } from './square-facade.js';
3
3
  export type * from './square-facade.js';
4
4
  export { createSquareResourceOwner } from './artifact.js';
5
5
  export { formatActivityId, parseActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
@@ -26,13 +26,14 @@ export declare class SqliteSquare {
26
26
  private contextObservation;
27
27
  private handle;
28
28
  private static readonly ParticipantHandle;
29
+ /** Direct messages use explicit recipients, not participant expression admission or body attention. */
30
+ send(input: ChannelSendInput, control?: OperationControl): Promise<ChannelSendResult>;
29
31
  participant(name: string, { owner }: {
30
32
  readonly owner: Owner;
31
33
  }): Promise<Participant>;
32
34
  join(requested: string, options: JoinOptions, control?: OperationControl): Promise<JoinResult>;
33
35
  /** Restore only a standing seat; a concurrent departure or takeover cannot be revived. */
34
36
  reattach(name: string, { owner, wakeAddress }: Pick<JoinOptions, 'owner' | 'wakeAddress'>, control?: OperationControl): Promise<Participant | null>;
35
- snapshot(control?: OperationControl): Promise<import("./square-facade.js").SquareSnapshot>;
36
37
  history(query?: HistoryQuery, control?: OperationControl): Promise<HistoryResult>;
37
38
  status(options?: StatusOptions, control?: OperationControl): Promise<StatusResult>;
38
39
  participants(options?: ObservationOptions, control?: OperationControl): Promise<ParticipantsResult>;
@@ -1,18 +1,19 @@
1
- import { formatActivityId } from './square-core.js';
1
+ import { formatActivityId, MAX_IDENTITY_SET_SIZE } from './square-core.js';
2
2
  import { assertCurrentSeat } from './square-domain.js';
3
3
  import { canonicalPath } from './canonical-path.js';
4
4
  import { validateMetadataMatch, validateDeferralDuration, validateDeferralId } from './delivery-deferral.js';
5
5
  import { squareLocation } from './square-location.js';
6
6
  import crypto from 'node:crypto';
7
7
  import { setTimeout as sleep } from 'node:timers/promises';
8
- import { InternalSquareError, SquareError, isSquareError, nameKey, validateName } from './model.js';
9
- import { coreDone, coreIgnore, coreListen, coreListening, coreUnlisten, decideAct, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
8
+ import { activityMetadataHasContent, InternalSquareError, SquareError, isSquareError, nameKey, sameName, validateActivityMetadata, validateName } from './model.js';
9
+ import { attentionChange, coreDone, coreListening, decideAct, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
10
10
  import { WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
11
11
  import { catchLimit, decideCatch, observeCatchSelection, selectCatchPage } from './catch-decisions.js';
12
12
  import { expressionContext } from './activity-feed.js';
13
13
  import { expressHint } from './hints.js';
14
+ import { ATTENTION_BODY_MAX } from './attention-presentation.js';
14
15
  import { asSeatRef, participantAuthority, requireOwner, requireJoinName } from './seats.js';
15
- import { recoveryObservation, entryPresentation, snapshot, queryStatus, queryParticipants, catchPresence, parseRequiredActivityId, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
16
+ import { recoveryObservation, entryPresentation, queryStatus, queryParticipants, catchPresence, parseRequiredActivityId, toPublicActivity, toPerceivedActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
16
17
  import { sessionInboxCounts } from './inbox.js';
17
18
  import { rememberSquarePath, squareIndexPath } from './square-index.js';
18
19
  import { deliverPending } from './delivery-operations.js';
@@ -133,6 +134,7 @@ export class SqliteSquare {
133
134
  this.owner = owner;
134
135
  this.seat = seat;
135
136
  }
137
+ get generation() { return this.seat.generation; }
136
138
  async express(bodyOrInput, optionsOrControl = {}, operationControl) {
137
139
  const square = this.owner;
138
140
  const input = typeof bodyOrInput === 'object' && bodyOrInput !== null ? bodyOrInput : undefined;
@@ -214,7 +216,7 @@ export class SqliteSquare {
214
216
  checkControl(control);
215
217
  const seat = this.seat, now = square.clock();
216
218
  return (await square.artifact.transactAs(participantAuthority(seat), facts => {
217
- const act = verb === 'listen' ? coreListen(facts, name, target, now) : verb === 'ignore' ? coreIgnore(facts, name, target, now) : coreUnlisten(facts, name, target, now);
219
+ const act = attentionChange(facts, verb, name, target, now);
218
220
  const activity = act === undefined ? null : toPublicActivity(facts.land(act));
219
221
  return { activity, ...(!options.observe ? {} : { observation: {
220
222
  ...square.observation(inSquareCount(facts), name, now), participant: name,
@@ -342,6 +344,37 @@ export class SqliteSquare {
342
344
  await releaseWatchLease(square.artifact, seat, leaseId).catch(() => undefined);
343
345
  }
344
346
  }
347
+ async pending(options = {}, control) {
348
+ checkControl(control);
349
+ const limit = options.limit ?? 100;
350
+ if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100)
351
+ throw new SquareError('invalid_args', 'Pending limit must be a positive integer no greater than 100.');
352
+ const match = options.match === undefined ? undefined : validateMetadataMatch(options.match);
353
+ const square = this.owner;
354
+ return (await square.artifact.read(facts => {
355
+ assertCurrentSeat(facts, asSeatRef(this.seat));
356
+ const filter = { unseenOnly: true, ...(match === undefined ? {} : { metadataMatch: match }) };
357
+ const heads = facts.directed(this.name, filter, { edge: 'oldest', limit });
358
+ const activities = facts.activities(heads.items.map(head => head.index))
359
+ .map(activity => toPerceivedActivity(facts, activity, this.name));
360
+ return { activities, remaining: facts.directedCount(this.name, filter) - activities.length };
361
+ }, control?.signal)).value;
362
+ }
363
+ async ack(ids, control) {
364
+ checkControl(control);
365
+ if (!Array.isArray(ids))
366
+ throw new SquareError('invalid_args', 'Ack requires an array of activity IDs.');
367
+ const indexes = [...new Set(Array.from(ids, parseRequiredActivityId))], square = this.owner;
368
+ return (await square.artifact.transactAs(participantAuthority(this.seat), facts => {
369
+ for (const index of indexes) {
370
+ if (facts.directedAt(this.name, index) === undefined)
371
+ throw new SquareError('invalid_args', `${formatActivityId(index)} is not addressed to you.`, {
372
+ reason: 'not_addressed', activityId: formatActivityId(index),
373
+ });
374
+ }
375
+ return { consumed: facts.markSeen(this.name, indexes, square.clock()) };
376
+ }, control?.signal)).value;
377
+ }
345
378
  async deferDelivery(input, control) {
346
379
  checkControl(control);
347
380
  const match = validateMetadataMatch(input?.match), duration = validateDeferralDuration(input?.durationMs);
@@ -382,6 +415,56 @@ export class SqliteSquare {
382
415
  }, control?.signal)).value;
383
416
  }
384
417
  };
418
+ /** Direct messages use explicit recipients, not participant expression admission or body attention. */
419
+ async send(input, control) {
420
+ checkControl(control);
421
+ const { from, to, body, truncate = ATTENTION_BODY_MAX } = input;
422
+ if (typeof from !== 'string')
423
+ throw new SquareError('invalid_args', 'Channel sender must be a name.');
424
+ validateName(from);
425
+ const targets = typeof to === 'string' ? [to] : to;
426
+ if (!Array.isArray(targets) || targets.length < 1 || targets.length > MAX_IDENTITY_SET_SIZE)
427
+ throw new SquareError('invalid_args', `Channel needs between 1 and ${MAX_IDENTITY_SET_SIZE} recipients.`);
428
+ const unique = [];
429
+ for (const target of targets) {
430
+ if (typeof target !== 'string')
431
+ throw new SquareError('invalid_args', 'Channel recipients must be names.');
432
+ validateName(target);
433
+ if (!unique.some(name => sameName(name, target)))
434
+ unique.push(target);
435
+ }
436
+ if (typeof body !== 'string' || body.trim() === '')
437
+ throw new SquareError('invalid_args', 'Channel body must be non-empty.');
438
+ if (truncate !== false && (!Number.isSafeInteger(truncate) || truncate < 1))
439
+ throw new SquareError('invalid_args', 'Channel truncate must be false or a positive integer.');
440
+ const fullBody = body.replace(/\r\n/g, '\n');
441
+ const preview = truncate !== false && fullBody.length > truncate
442
+ ? `${fullBody.slice(0, truncate).trimEnd()}… (truncated ${fullBody.length - truncate} chars)`
443
+ : undefined;
444
+ const delivery = preview === undefined ? { mode: 'direct' } : { mode: 'direct', preview };
445
+ const metadata = validateActivityMetadata(input.metadata);
446
+ const { stored, resolvedTo } = (await this.artifact.transactAs({ kind: 'send', actor: from }, facts => {
447
+ const standing = facts.roster().filter(p => p.joined), resolvedTo = [], missing = [];
448
+ for (const target of unique) {
449
+ const recipient = standing.find(p => sameName(p.name, target));
450
+ if (recipient === undefined)
451
+ missing.push(target);
452
+ else
453
+ resolvedTo.push(recipient.name);
454
+ }
455
+ if (missing.length > 0)
456
+ throw new SquareError('invalid_args', `Channel recipients are not standing: ${missing.join(', ')}`, {
457
+ reason: 'not_standing', targets: missing, activityUnsent: true,
458
+ });
459
+ return { stored: facts.land({ kind: 'say', actor: from, at: this.clock(), body: fullBody,
460
+ ...(activityMetadataHasContent(metadata) ? { metadata } : {}), delivery, mentions: resolvedTo }), resolvedTo };
461
+ }, control?.signal)).value;
462
+ if (this.wakeTransport !== undefined)
463
+ await deliverPending({ artifact: this.artifact, transport: this.wakeTransport, location: this.deliveryLocation,
464
+ activity: stored.index, now: stored.at }).catch(() => undefined);
465
+ return { id: formatActivityId(stored.index), at: stored.at, from: stored.actor, to: resolvedTo, body: preview ?? fullBody,
466
+ ...(stored.kind !== 'say' || stored.metadata === undefined ? {} : { metadata: stored.metadata }) };
467
+ }
385
468
  async participant(name, { owner }) {
386
469
  validateName(name);
387
470
  requireOwner(owner);
@@ -432,10 +515,10 @@ export class SqliteSquare {
432
515
  kind = 'reconnected';
433
516
  }
434
517
  if (kind !== 'reconnected') {
435
- const decision = decideJoin(facts, participant, now);
436
- if (decision.joinAct === undefined)
518
+ const joinAct = decideJoin(facts, participant, now);
519
+ if (joinAct === undefined)
437
520
  throw new SquareError('already_joined', `${participantIdentity(participant)} could not be joined`);
438
- activity = toPublicActivity(facts.land(decision.joinAct));
521
+ activity = toPublicActivity(facts.land(joinAct));
439
522
  }
440
523
  const seat = facts.seat(participant);
441
524
  if (seat === undefined)
@@ -480,7 +563,6 @@ export class SqliteSquare {
480
563
  await this.publishSeat(owner);
481
564
  return seat === null ? null : this.handle(seat);
482
565
  }
483
- async snapshot(control) { return (await this.artifact.read(facts => snapshot(facts, this.clock()), control?.signal)).value; }
484
566
  async history(query = {}, control) {
485
567
  if (query.limit !== undefined && query.limit !== null && (!Number.isSafeInteger(query.limit) || query.limit < 1 || query.limit > HISTORY_MAX_LIMIT))
486
568
  throw new SquareError('invalid_args', `History limit must be a positive integer no greater than ${HISTORY_MAX_LIMIT}`);
@@ -26,6 +26,7 @@ export type Act = {
26
26
  at?: number;
27
27
  body?: string;
28
28
  metadata?: import('./model.js').ActivityMetadata;
29
+ delivery?: import('./model.js').ActivityDelivery;
29
30
  mentions?: readonly Participant[];
30
31
  reach?: Reach;
31
32
  reply?: number;
@@ -1,7 +1,16 @@
1
- import { SquareError, formatActivityId, parseActivityId, sameName, validateOwner, validateName } from './model.js';
1
+ import { SquareError, formatActivityId, parseActivityId, sameName, validateActivityDelivery, validateOwner, validateName } from './model.js';
2
2
  import { resolveAudience } from './attention.js';
3
3
  /** The single landing transition, shared by both adapters and explicit rebuild. */
4
4
  export function deriveLanding(activity, facts) {
5
+ if (activity.kind === 'say' && activity.delivery?.mode === 'direct') {
6
+ const audience = [];
7
+ for (const mention of activity.mentions ?? []) {
8
+ const recipient = facts.standing.find(item => sameName(item.name, mention))?.name;
9
+ if (recipient !== undefined && !audience.some(item => sameName(item.recipient, recipient)))
10
+ audience.push({ recipient, route: 'mention' });
11
+ }
12
+ return { activity, audience };
13
+ }
5
14
  const audience = activity.kind === 'say' ? resolveAudience({
6
15
  actor: activity.actor, recipients: facts.standing.map((item) => item.name),
7
16
  mentions: activity.mentions, replyAuthor: facts.replyAuthor, reach: activity.reach,
@@ -60,15 +69,15 @@ export function wrapSquareWrite(read, writes, authority) {
60
69
  throw new SquareError('invalid_args', 'Artifact writes require an explicit authority.');
61
70
  const { applyLanding, ...mutations } = writes;
62
71
  const seated = authority.kind === 'participant' || authority.kind === 'host-cleanup';
63
- const expectedName = seated ? authority.seat?.participant : authority.participant;
64
- if (!['participant', 'host-cleanup', 'join', 'takeover'].includes(authority.kind) || typeof expectedName !== 'string')
72
+ const expectedName = seated ? authority.seat?.participant : authority.kind === 'send' ? authority.actor : authority.participant;
73
+ if (!['participant', 'host-cleanup', 'join', 'takeover', 'send'].includes(authority.kind) || typeof expectedName !== 'string')
65
74
  throw new SquareError('invalid_args', 'Artifact write authority is malformed.');
66
75
  validateName(expectedName);
67
76
  if (seated) {
68
77
  validateOwner(authority.seat.owner);
69
78
  validateGeneration(authority.seat.generation);
70
79
  }
71
- else {
80
+ else if (authority.kind !== 'send') {
72
81
  validateOwner(authority.owner);
73
82
  if (authority.kind === 'takeover')
74
83
  validateGeneration(authority.expectedGeneration);
@@ -100,7 +109,7 @@ export function wrapSquareWrite(read, writes, authority) {
100
109
  reconnectSeat = seat;
101
110
  }
102
111
  }
103
- else {
112
+ else if (authority.kind === 'takeover') {
104
113
  const current = read.seat(expectedName);
105
114
  if (current === undefined || current.generation !== authority.expectedGeneration)
106
115
  throw new SquareError('already_joined', 'The participant seat changed before takeover.');
@@ -114,7 +123,13 @@ export function wrapSquareWrite(read, writes, authority) {
114
123
  validateName(actor);
115
124
  if (!sameName(actor, expectedName))
116
125
  throw new SquareError('already_joined', 'Activity actor does not match the artifact authority.');
117
- if (authority.kind === 'participant') {
126
+ if (activity.kind === 'say')
127
+ validateActivityDelivery(activity.delivery);
128
+ if (authority.kind === 'send') {
129
+ if (activity.kind !== 'say' || activity.delivery?.mode !== 'direct' || activity.reach !== undefined || activity.reply !== undefined)
130
+ throw new SquareError('invalid_args', 'Send authority may only land direct messages without bell or reply.');
131
+ }
132
+ else if (authority.kind === 'participant') {
118
133
  assertSeat(authority.seat);
119
134
  if (activity.kind === 'join')
120
135
  throw new SquareError('invalid_args', 'A participant seat can only be re-established by join.');
@@ -150,7 +165,7 @@ export function wrapSquareWrite(read, writes, authority) {
150
165
  const stored = { ...activity, index };
151
166
  applyLanding(deriveLanding(stored, landingFacts(read, stored)));
152
167
  if (stored.kind === 'join') {
153
- if (seated)
168
+ if (authority.kind !== 'join' && authority.kind !== 'takeover')
154
169
  throw new SquareError('invalid_args', 'Seat authority cannot establish another seat.');
155
170
  const seat = {
156
171
  participant: stored.actor,