@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.
- package/dist/artifact.d.ts +1 -1
- package/dist/artifact.js +11 -17
- package/dist/attention-presentation.d.ts +5 -4
- package/dist/attention-presentation.js +18 -8
- package/dist/boundary-presentation.js +31 -26
- package/dist/channel.d.ts +74 -0
- package/dist/channel.js +92 -0
- package/dist/claude-delivery.js +3 -9
- package/dist/cli/observation-commands.js +2 -2
- package/dist/codex-hook.d.ts +0 -2
- package/dist/codex-hook.js +0 -1
- package/dist/decisions.d.ts +2 -11
- package/dist/decisions.js +3 -8
- package/dist/delivery-health.js +3 -13
- package/dist/delivery.d.ts +1 -11
- package/dist/delivery.js +2 -9
- package/dist/identity.d.ts +0 -1
- package/dist/identity.js +0 -1
- package/dist/inbox.js +11 -7
- package/dist/mcp.js +65 -63
- package/dist/model.d.ts +6 -0
- package/dist/model.js +19 -0
- package/dist/notifications.d.ts +1 -1
- package/dist/ports.d.ts +5 -5
- package/dist/presentation-operations.d.ts +1 -0
- package/dist/sqlite-square.d.ts +3 -2
- package/dist/sqlite-square.js +91 -9
- package/dist/square-core.d.ts +1 -0
- package/dist/square-domain.js +22 -7
- package/dist/square-facade.d.ts +36 -10
- package/dist/square-sqlite.js +33 -4
- package/dist/square-tools.d.ts +1 -0
- package/dist/square-tools.js +9 -1
- package/dist/views.d.ts +1 -2
- package/dist/views.js +1 -9
- package/guides/operation-cancellation.md +5 -1
- package/guides/primitive-status-consolidation.md +1 -1
- package/guides/receive-primitive.md +4 -2
- package/guides/row-runtime-delivery-evidence.md +3 -3
- package/guides/row-storage-design.md +5 -1
- package/guides/square-api.md +63 -7
- package/package.json +5 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/codex/.codex-plugin/plugin.json +1 -1
- package/plugins/codex/hooks/hooks.json +6 -6
- package/plugins/paseo/client/marble.ts +7 -0
- package/plugins/paseo/client/markdown.tsx +65 -9
- package/plugins/paseo/client/palette.ts +43 -20
- package/plugins/paseo/client/square-message.tsx +57 -48
- package/plugins/paseo/shared/message.ts +1 -2
- package/plugins/pi/square-pi.js +6 -4
package/dist/identity.d.ts
CHANGED
package/dist/identity.js
CHANGED
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,
|
|
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
|
-
|
|
151
|
-
|
|
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)' })]
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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. */
|
package/dist/notifications.d.ts
CHANGED
|
@@ -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;
|
package/dist/sqlite-square.d.ts
CHANGED
|
@@ -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>;
|
package/dist/sqlite-square.js
CHANGED
|
@@ -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 {
|
|
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,
|
|
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 =
|
|
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
|
|
436
|
-
if (
|
|
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(
|
|
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}`);
|
package/dist/square-core.d.ts
CHANGED
package/dist/square-domain.js
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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,
|