@astrosheep/square 0.4.4 → 0.4.5
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-store.d.ts +1 -1
- package/dist/artifact-store.js +32 -15
- package/dist/artifact.d.ts +1 -4
- package/dist/artifact.js +0 -28
- package/dist/attention-presentation.js +1 -1
- package/dist/boundary-presentation.js +2 -2
- package/dist/claude-boundary-state.d.ts +6 -6
- package/dist/claude-boundary-state.js +13 -13
- package/dist/claude-hook.js +25 -24
- package/dist/claude-mcp-context.d.ts +2 -2
- package/dist/claude-mcp-context.js +6 -6
- package/dist/cli/context.js +3 -6
- package/dist/cli/json.js +1 -1
- package/dist/cli/mcp-server-command.js +3 -3
- package/dist/cli/observation-commands.d.ts +2 -2
- package/dist/cli/observation-commands.js +23 -51
- package/dist/cli/perceive-presentation.d.ts +3 -0
- package/dist/cli/{catch-presentation.js → perceive-presentation.js} +3 -3
- package/dist/cli/registry.js +2 -2
- package/dist/codex-address.d.ts +3 -0
- package/dist/codex-address.js +7 -0
- package/dist/codex-hook.d.ts +1 -1
- package/dist/codex-hook.js +11 -8
- package/dist/codex-queue.d.ts +3 -2
- package/dist/codex-queue.js +8 -11
- package/dist/codex-watch.d.ts +6 -1
- package/dist/codex-watch.js +27 -29
- package/dist/delivery-ledger-sqlite.d.ts +25 -20
- package/dist/delivery-ledger-sqlite.js +76 -61
- package/dist/delivery-ledger.d.ts +24 -19
- package/dist/delivery-ledger.js +150 -81
- package/dist/delivery-lifetime.d.ts +2 -2
- package/dist/delivery-lifetime.js +6 -18
- package/dist/help.js +12 -12
- package/dist/hints.d.ts +2 -2
- package/dist/hints.js +7 -7
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/list.d.ts +2 -2
- package/dist/list.js +4 -3
- package/dist/mcp.d.ts +2 -2
- package/dist/mcp.js +8 -32
- package/dist/neutral.d.ts +5 -5
- package/dist/notifications.js +12 -6
- package/dist/operation-presentation.d.ts +8 -3
- package/dist/operation-presentation.js +50 -12
- package/dist/packages/agent-delivery/src/file-lock.js +4 -1
- package/dist/perceive-decisions.d.ts +28 -0
- package/dist/{catch-decisions.js → perceive-decisions.js} +24 -24
- package/dist/{catch-selection.d.ts → perceive-selection.d.ts} +2 -2
- package/dist/{catch-selection.js → perceive-selection.js} +3 -3
- package/dist/plugins/opencode/index.js +4 -4
- package/dist/plugins/opencode-shared/delivery.d.ts +6 -6
- package/dist/plugins/opencode-shared/delivery.js +27 -21
- package/dist/plugins/opencode-v1/runtime.js +3 -3
- package/dist/ports.d.ts +0 -4
- package/dist/presentation.d.ts +1 -1
- package/dist/presentation.js +13 -13
- package/dist/session-lifecycle.d.ts +2 -2
- package/dist/session-lifecycle.js +11 -11
- package/dist/sqlite-square.d.ts +2 -4
- package/dist/sqlite-square.js +47 -54
- package/dist/sqlite-transaction.d.ts +2 -0
- package/dist/sqlite-transaction.js +25 -3
- package/dist/square-api.d.ts +1 -1
- package/dist/square-api.js +1 -1
- package/dist/square-domain.js +1 -1
- package/dist/square-facade.d.ts +15 -13
- package/dist/square-location.js +1 -1
- package/dist/square-posting.d.ts +5 -5
- package/dist/square-posting.js +39 -38
- package/dist/square-tools.d.ts +15 -4
- package/dist/square-tools.js +35 -17
- package/dist/views.d.ts +7 -3
- package/dist/views.js +6 -3
- package/guides/architect.md +2 -2
- package/guides/change-waits.md +1 -1
- package/guides/codex-delivery.md +40 -35
- package/guides/delivery-primitives.md +22 -7
- package/guides/mcp.md +4 -2
- package/guides/operation-cancellation.md +11 -3
- package/guides/pi-native-delivery.md +1 -1
- package/guides/pi-tools.md +11 -6
- package/guides/receive-primitive.md +3 -3
- package/guides/square-api.md +15 -8
- package/package.json +2 -2
- package/plugins/claude/.claude-plugin/plugin.json +1 -1
- package/plugins/claude/skills/square/SKILL.md +10 -10
- package/plugins/codex/.codex-plugin/plugin.json +2 -2
- package/plugins/codex/hooks/hooks.json +3 -0
- package/plugins/opencode/index.ts +4 -4
- package/plugins/opencode-shared/delivery.ts +27 -21
- package/plugins/opencode-v1/runtime.ts +3 -3
- package/plugins/pi/square-pi.js +18 -12
- package/skills/square/SKILL.md +10 -10
- package/dist/catch-decisions.d.ts +0 -28
- package/dist/cli/catch-presentation.d.ts +0 -3
- package/guides/mcp-connection-lifetime.md +0 -22
package/dist/mcp.js
CHANGED
|
@@ -5,23 +5,20 @@ import { operationEnv } from './participant-identity.js';
|
|
|
5
5
|
import { codexWakeAddress } from './codex-address.js';
|
|
6
6
|
import { readClaudeMcpCall } from './claude-mcp-context.js';
|
|
7
7
|
import { SQUARE_IDENTITY } from './identity.js';
|
|
8
|
-
import { Square,
|
|
8
|
+
import { Square, discover, squareClock } from './square-api.js';
|
|
9
9
|
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
13
|
const toolsEnabled = squareToolsEnabled(baseEnv);
|
|
14
|
-
const resources = createSquareResourceOwner();
|
|
15
14
|
const invoke = createSquareToolRunner();
|
|
16
|
-
// Calls to the configured square borrow one server-owned connection.
|
|
17
|
-
const inFlight = new Set();
|
|
18
15
|
const server = new McpServer({ name: 'square', version: SQUARE_IDENTITY.packageVersion }, {
|
|
19
16
|
...(toolsEnabled ? { instructions: SQUARE_TOOL_INSTRUCTIONS } : {}),
|
|
20
17
|
});
|
|
21
18
|
if (toolsEnabled)
|
|
22
19
|
for (const tool of SQUARE_TOOLS) {
|
|
23
20
|
server.registerTool(tool.name, { description: tool.description, inputSchema: tool.inputSchema }, (args, extra) => {
|
|
24
|
-
|
|
21
|
+
return invoke(tool, args, { signal: extra.signal }, async () => {
|
|
25
22
|
const threadId = extra._meta?.threadId;
|
|
26
23
|
const opencodeSessionId = extra._meta?.['ai.opencode/sessionID'];
|
|
27
24
|
// Claude's bundled plugin requires a per-call native hook association; startup identity is not a fallback.
|
|
@@ -51,38 +48,17 @@ export function createSquareMcpServer(options) {
|
|
|
51
48
|
return {
|
|
52
49
|
identity: { owner, wakeAddress: claude === undefined && typeof threadId === 'string' && threadId.trim()
|
|
53
50
|
? { ...codexWakeAddress(threadId, env), kind: 'codex-queue' } : callWake },
|
|
54
|
-
open: (location, control) => Square.at({
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
}, control),
|
|
51
|
+
open: (location, control) => Square.at({ ...resourceOptions, env, cwd: claude?.cwd ?? resourceOptions.cwd, path: location }, control),
|
|
52
|
+
discover: async (discovery, control) => ({
|
|
53
|
+
result: await discover({ ...resourceOptions, env, cwd: claude?.cwd ?? resourceOptions.cwd }, discovery, control),
|
|
54
|
+
now: squareClock(env, resourceOptions.clock)(),
|
|
55
|
+
}),
|
|
60
56
|
};
|
|
61
|
-
}
|
|
57
|
+
}).then(({ text, isError }) => ({
|
|
62
58
|
...(isError ? { isError } : {}), content: [{ type: 'text', text }],
|
|
63
59
|
}));
|
|
64
|
-
inFlight.add(call);
|
|
65
|
-
return call.finally(() => { inFlight.delete(call); });
|
|
66
60
|
});
|
|
67
61
|
}
|
|
68
|
-
const close = server.close.bind(server);
|
|
69
|
-
// One idempotent shutdown for both close paths: an explicit server.close() and a
|
|
70
|
-
// transport-initiated disconnect, where the SDK protocol fires onclose by itself.
|
|
71
|
-
let draining;
|
|
72
|
-
const drain = () => draining ??= (async () => {
|
|
73
|
-
// The SDK aborts in-flight request signals before onclose/close returns; wait for
|
|
74
|
-
// those handlers to release the shared connection (their cleanup writes included).
|
|
75
|
-
await Promise.allSettled([...inFlight]);
|
|
76
|
-
await resources.close();
|
|
77
|
-
})();
|
|
78
|
-
server.close = async () => { await close(); await drain(); };
|
|
79
|
-
const priorOnclose = server.server.onclose;
|
|
80
|
-
server.server.onclose = () => {
|
|
81
|
-
// onclose is synchronous, so the disconnect path starts the drain without awaiting it;
|
|
82
|
-
// the handler's own catch keeps an unexpected close failure from crashing the process.
|
|
83
|
-
void drain().catch(() => undefined);
|
|
84
|
-
priorOnclose?.();
|
|
85
|
-
};
|
|
86
62
|
return server;
|
|
87
63
|
}
|
|
88
64
|
export async function runSquareMcpStdio(options) {
|
package/dist/neutral.d.ts
CHANGED
|
@@ -9,7 +9,7 @@ export interface SquareErrorFacts {
|
|
|
9
9
|
activityUnsent?: true;
|
|
10
10
|
/** Refusal reason, including absent participants, conflicting reach, and inaccessible activities. */
|
|
11
11
|
reason?: 'never_joined' | 'not_standing' | 'bell_mention_conflict' | 'not_addressed' | 'invalid_reply' | 'invalid_done_body' | 'needs_seat';
|
|
12
|
-
/** The activity id a `
|
|
12
|
+
/** The activity id a `perceive --id` refusal named. */
|
|
13
13
|
activityId?: string;
|
|
14
14
|
/** Requested recipient spellings, without parsing presentation copy. */
|
|
15
15
|
targets?: readonly string[];
|
|
@@ -18,7 +18,7 @@ export interface SquareErrorFacts {
|
|
|
18
18
|
pending?: number;
|
|
19
19
|
retryAfterMs?: number;
|
|
20
20
|
}
|
|
21
|
-
export interface
|
|
21
|
+
export interface PerceiveWaitStart {
|
|
22
22
|
readonly idleMs: number;
|
|
23
23
|
readonly replacementRequested: boolean;
|
|
24
24
|
readonly replaced: boolean;
|
|
@@ -28,15 +28,15 @@ export type OperationProgress = {
|
|
|
28
28
|
readonly reason: 'throttled';
|
|
29
29
|
readonly delayMs?: number;
|
|
30
30
|
} | ({
|
|
31
|
-
readonly kind: '
|
|
31
|
+
readonly kind: 'perceive-started';
|
|
32
32
|
readonly participant: string;
|
|
33
|
-
} &
|
|
33
|
+
} & PerceiveWaitStart);
|
|
34
34
|
export interface OperationControl {
|
|
35
35
|
readonly signal?: AbortSignal;
|
|
36
36
|
readonly onProgress?: (progress: OperationProgress) => void;
|
|
37
37
|
}
|
|
38
38
|
export interface OperationFailureDetail {
|
|
39
|
-
readonly operation: 'express' | 'join' | '
|
|
39
|
+
readonly operation: 'express' | 'join' | 'perceive' | 'history' | 'other';
|
|
40
40
|
readonly subject?: 'caller' | 'filter' | 'recipient';
|
|
41
41
|
readonly participant?: string;
|
|
42
42
|
readonly expressEffect?: 'unsent' | 'landed' | 'unknown';
|
package/dist/notifications.js
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { createDefaultDeliveryTransports } from './wake-transports.js';
|
|
2
2
|
export { createDefaultDeliveryTransports } from './wake-transports.js';
|
|
3
|
+
function deliveryWaitMs(env = process.env) {
|
|
4
|
+
const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
|
|
5
|
+
return Number.isSafeInteger(configured) && configured > 0 ? configured : 5000;
|
|
6
|
+
}
|
|
3
7
|
/** Best-effort dispatch for currently eligible cards, including cards restored by release. */
|
|
4
8
|
export async function dispatchEligibleCards(ledger, owner, options = {}) {
|
|
5
|
-
|
|
9
|
+
options = { ...options, signal: options.signal ?? AbortSignal.timeout(deliveryWaitMs(options.env)) };
|
|
10
|
+
const session = await ledger.session(owner, options.signal);
|
|
6
11
|
if (!session || session.state !== 'attached' || options.signal?.aborted)
|
|
7
12
|
return;
|
|
8
|
-
const pending = await ledger.pending(owner, { generation: session.generation });
|
|
13
|
+
const pending = await ledger.pending(owner, { generation: session.generation }, options.signal);
|
|
9
14
|
for (const card of pending)
|
|
10
15
|
await dispatchPostedCard(ledger, owner, card.key, options.track ?? (() => undefined), options.env, options.signal, options.transports);
|
|
11
16
|
}
|
|
12
17
|
/** Post, then best-effort dispatch without consulting producer storage. */
|
|
13
18
|
export async function postAndDispatch(ledger, input, options = {}) {
|
|
14
|
-
|
|
19
|
+
options = { ...options, signal: options.signal ?? AbortSignal.timeout(deliveryWaitMs(options.env)) };
|
|
20
|
+
const result = await ledger.post(input, options.signal);
|
|
15
21
|
if (result.status === 'posted' || result.status === 'duplicate') {
|
|
16
22
|
try {
|
|
17
23
|
await dispatchEligibleCards(ledger, input.owner, options);
|
|
@@ -24,12 +30,12 @@ export async function postAndDispatch(ledger, input, options = {}) {
|
|
|
24
30
|
export async function dispatchPostedCard(ledger, owner, key, track, env = process.env, signal, suppliedTransports) {
|
|
25
31
|
if (signal?.aborted)
|
|
26
32
|
return;
|
|
27
|
-
const
|
|
33
|
+
const timeoutMs = deliveryWaitMs(env);
|
|
34
|
+
signal ??= AbortSignal.timeout(timeoutMs);
|
|
35
|
+
const session = await ledger.session(owner, signal);
|
|
28
36
|
if (!session || session.state !== 'attached')
|
|
29
37
|
return;
|
|
30
38
|
const transports = suppliedTransports ?? await createDefaultDeliveryTransports(env);
|
|
31
|
-
const configured = Number(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? 5000);
|
|
32
|
-
const timeoutMs = Number.isSafeInteger(configured) && configured > 0 ? configured : 5000;
|
|
33
39
|
for (const route of session.routes) {
|
|
34
40
|
// Claude and Codex custody belongs to their native receivers, including for
|
|
35
41
|
// callers that supply transports explicitly to the producer API.
|
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { PerceiveOptions, PerceiveResult, ExpressResult, HistoryQuery, HistoryResult, JoinResult, LifecycleResult, ListenerChangeResult, ListeningResult, OperationFailureDetail, ParticipantsResult, StatusResult, DiscoveryResult } from './square-facade.js';
|
|
2
|
+
export declare function renderSquareList(data: DiscoveryResult, projection: {
|
|
3
|
+
now: number;
|
|
4
|
+
depth: number;
|
|
5
|
+
limit: number;
|
|
6
|
+
}): string;
|
|
2
7
|
export declare const PARTICIPANTS_DEFAULT_LIMIT = 20;
|
|
3
8
|
export declare const PARTICIPANTS_MAX_LIMIT = 100;
|
|
4
9
|
/** A join receipt whose participant is already reduced to its serializable name. */
|
|
@@ -9,8 +14,8 @@ export type JoinPresentation = Omit<JoinResult, 'participant'> & {
|
|
|
9
14
|
};
|
|
10
15
|
export declare function renderJoinResult(data: JoinPresentation, owner?: string): string | undefined;
|
|
11
16
|
export declare function renderExpression(squarePath: string, data: ExpressResult, owner?: string): string | undefined;
|
|
12
|
-
/** Render settled
|
|
13
|
-
export declare function
|
|
17
|
+
/** Render settled perceive facts; no selection, terminal decisions, receipts or wait machinery. */
|
|
18
|
+
export declare function renderPerceiveOutcome(squarePath: string, data: PerceiveResult, options?: PerceiveOptions, owner?: string): {
|
|
14
19
|
stdout: string;
|
|
15
20
|
exitCode: number;
|
|
16
21
|
};
|
|
@@ -2,9 +2,47 @@
|
|
|
2
2
|
// render through these functions, so a person sees the same square either way.
|
|
3
3
|
import { sameName } from './model.js';
|
|
4
4
|
import { formatActivityId } from './square-core.js';
|
|
5
|
-
import { commandPrefix, joinRecoveryCommand, participantCommandPrefix, participantIdentity, participantsRecoveryCommand, quoteShell, renderAmbientEvent, renderAmbientEventFacts, renderEventCli, renderGrepActivitiesView, renderPresenceAnchor, renderWatchAlreadyActive, renderWatchReplaced, renderWatchStatus, takeoverRecoveryLines, withPathOutput, } from './presentation.js';
|
|
5
|
+
import { commandPrefix, joinRecoveryCommand, participantCommandPrefix, participantIdentity, participantsRecoveryCommand, quoteShell, renderAmbientEvent, renderAmbientEventFacts, renderEventCli, renderGrepActivitiesView, renderPresenceAnchor, renderWatchAlreadyActive, renderWatchReplaced, renderWatchStatus, takeoverRecoveryLines, withPathOutput, truncateChars, } from './presentation.js';
|
|
6
6
|
import { formatRelativeTime, formatTimestamp } from './time.js';
|
|
7
7
|
import { style } from './tty-style.js';
|
|
8
|
+
function listDisplay(value) {
|
|
9
|
+
const truncated = truncateChars(value, 159);
|
|
10
|
+
return truncated.remaining === 0 ? truncated.text : `${truncated.text}…`;
|
|
11
|
+
}
|
|
12
|
+
export function renderSquareList(data, projection) {
|
|
13
|
+
const { items, stopped, remaining, next } = data;
|
|
14
|
+
if (data.total === 0) {
|
|
15
|
+
return ['(no squares found)', ...(stopped ? [style('dim', '○ the walk stopped early — deeper squares may still be hiding')] : [])].join('\n') + '\n';
|
|
16
|
+
}
|
|
17
|
+
const lines = ['squares'];
|
|
18
|
+
for (const item of items) {
|
|
19
|
+
const glyph = item.activities > 0 ? '●' : style('dim', '○');
|
|
20
|
+
lines.push(`${glyph} ${listDisplay(item.path)}${style('dim', ` · ${formatRelativeTime(item.lastActiveAt, projection.now)} · ${item.participants.length} in the square · ${item.activities} ${item.activities === 1 ? 'activity' : 'activities'}`)}`);
|
|
21
|
+
const context = item.context.slice(0, 2);
|
|
22
|
+
if (context.length === 0)
|
|
23
|
+
lines.push(` ${style('dim', 'context')} · (none)`);
|
|
24
|
+
else {
|
|
25
|
+
context.forEach((line, index) => lines.push(` ${index === 0 ? style('dim', 'context') : ' '} · ${listDisplay(line)}`));
|
|
26
|
+
const hidden = item.context.length - context.length;
|
|
27
|
+
if (hidden > 0)
|
|
28
|
+
lines.push(style('dim', ` · … ${hidden} more ${hidden === 1 ? 'line' : 'lines'}`));
|
|
29
|
+
}
|
|
30
|
+
const participants = item.participants.slice(0, 3);
|
|
31
|
+
const hidden = item.participants.length - participants.length;
|
|
32
|
+
const names = participants.length === 0 ? 'nobody' : participants.map((name) => participantIdentity(listDisplay(name))).join(' · ');
|
|
33
|
+
lines.push(` ${style('dim', 'participants')} · ${names}${hidden > 0 ? style('dim', ` · … ${hidden} more`) : ''}`);
|
|
34
|
+
if (item.joined?.length)
|
|
35
|
+
lines.push(` ${style('dim', 'joined')} · ${item.joined.map(seat => `${participantIdentity(listDisplay(seat.name))}${seat.pending > 0 ? style('dim', ` (${seat.pending} waiting)`) : ''}`).join(' · ')}`);
|
|
36
|
+
}
|
|
37
|
+
if (remaining > 0) {
|
|
38
|
+
lines.push(style('dim', stopped ? ' … more squares' : ` … ${remaining} more ${remaining === 1 ? 'square' : 'squares'}`));
|
|
39
|
+
if (next !== undefined)
|
|
40
|
+
lines.push(`square list --depth ${projection.depth} --limit ${projection.limit} --after ${quoteShell(next)}`);
|
|
41
|
+
}
|
|
42
|
+
if (stopped)
|
|
43
|
+
lines.push(style('dim', '○ the walk stopped early — deeper squares may still be hiding'));
|
|
44
|
+
return lines.join('\n') + '\n';
|
|
45
|
+
}
|
|
8
46
|
export const PARTICIPANTS_DEFAULT_LIMIT = 20;
|
|
9
47
|
export const PARTICIPANTS_MAX_LIMIT = 100;
|
|
10
48
|
const STATUS_PARTICIPANT_PREVIEW_LIMIT = 10;
|
|
@@ -18,8 +56,8 @@ export function renderJoinResult(data, owner) {
|
|
|
18
56
|
now: after.now, preview: 200, perception: event.perception,
|
|
19
57
|
})).filter(Boolean).join('\n\n');
|
|
20
58
|
const fallback = after.deliveryAvailable ? ['', '· when someone calls you, you’ll hear them here'] : [
|
|
21
|
-
'', `${participantCommandPrefix(after.location, name, owner)}
|
|
22
|
-
' · stay within earshot — keep this
|
|
59
|
+
'', `${participantCommandPrefix(after.location, name, owner)} perceive --idle 30m`,
|
|
60
|
+
' · stay within earshot — keep this perceive open for new activity',
|
|
23
61
|
];
|
|
24
62
|
const entryLine = data.kind === 'joined' ? '● you stepped into the square'
|
|
25
63
|
: data.kind === 'reconnected' ? '● you are already in the square'
|
|
@@ -49,7 +87,7 @@ export function renderExpression(squarePath, data, owner) {
|
|
|
49
87
|
'Unread context · preview only, not consumed',
|
|
50
88
|
...context.activities.map(item => `· ${participantIdentity(item.actor)} · ${item.id}\n ${item.preview}${item.truncated ? '… (truncated)' : ''}`),
|
|
51
89
|
...(context.remaining === 0 ? [] : [`· ${context.remaining} more unread activities`]),
|
|
52
|
-
`${participantCommandPrefix(projection.location, name, owner)}
|
|
90
|
+
`${participantCommandPrefix(projection.location, name, owner)} perceive`,
|
|
53
91
|
].join('\n');
|
|
54
92
|
const ownCount = projection.ownActivityCount;
|
|
55
93
|
const capCount = projection.hardCap === null ? '' : ` · ${ownCount}/${projection.hardCap}`;
|
|
@@ -73,10 +111,10 @@ function continuationCommand(squarePath, name, options, owner) {
|
|
|
73
111
|
args.push('--mention');
|
|
74
112
|
if (options.limit !== undefined)
|
|
75
113
|
args.push('--limit', String(options.limit));
|
|
76
|
-
return `${participantCommandPrefix(squarePath, name, owner)}
|
|
114
|
+
return `${participantCommandPrefix(squarePath, name, owner)} perceive ${args.join(' ')}`;
|
|
77
115
|
}
|
|
78
|
-
/** Render settled
|
|
79
|
-
export function
|
|
116
|
+
/** Render settled perceive facts; no selection, terminal decisions, receipts or wait machinery. */
|
|
117
|
+
export function renderPerceiveOutcome(squarePath, data, options = {}, owner) {
|
|
80
118
|
const projection = data.observation;
|
|
81
119
|
if (projection === undefined)
|
|
82
120
|
return { stdout: '', exitCode: 0 };
|
|
@@ -86,7 +124,7 @@ export function renderCatchOutcome(squarePath, data, options = {}, owner) {
|
|
|
86
124
|
const header = { participantCount: projection.standingCount };
|
|
87
125
|
const statusOptions = {
|
|
88
126
|
squarePath: location, name, owner, presence: projection.presence,
|
|
89
|
-
|
|
127
|
+
showPerceiveHint: !projection.deliveryAvailable,
|
|
90
128
|
ownActivityCount: projection.ownActivityCount, hardCap: projection.hardCap,
|
|
91
129
|
};
|
|
92
130
|
if (stop === 'already-active' || stop === 'replaced') {
|
|
@@ -106,7 +144,7 @@ export function renderCatchOutcome(squarePath, data, options = {}, owner) {
|
|
|
106
144
|
...(stop === 'idle' && options.idle !== undefined ? { idleMs: options.idle } : {}),
|
|
107
145
|
});
|
|
108
146
|
const fallback = stop === undefined && !projection.deliveryAvailable && data.remaining === 0
|
|
109
|
-
? `${participantCommandPrefix(location, name, owner)}
|
|
147
|
+
? `${participantCommandPrefix(location, name, owner)} perceive --idle 30m\n stay available for new activity` : '';
|
|
110
148
|
const hint = data.hint === undefined ? '' : style('dim', `· ${data.hint}`);
|
|
111
149
|
return {
|
|
112
150
|
stdout: withPathOutput(location, [terminal, rendered, hint, remaining, fallback].filter(Boolean).join('\n\n'), header),
|
|
@@ -187,7 +225,7 @@ export function renderParticipantsResult(squarePath, data, limit = PARTICIPANTS_
|
|
|
187
225
|
const participants = data.participants;
|
|
188
226
|
const lines = participants.slice(0, limit).map((participant) => {
|
|
189
227
|
const glyph = participant.presence === 'done' ? '○' : participant.presence === 'watching' ? '◎' : participant.activityCount > 0 && projection.classificationByName[participant.name] === 'here' ? '●' : '○';
|
|
190
|
-
const state = participant.presence === 'watching' ? '
|
|
228
|
+
const state = participant.presence === 'watching' ? 'perceiving' : participant.presence === 'never-joined' ? 'not joined' : participant.presence;
|
|
191
229
|
const last = participant.lastActiveAt === undefined ? '—' : formatRelativeTime(participant.lastActiveAt, now);
|
|
192
230
|
return ` ${glyph} ${participant.name}${style('dim', ` · ${state} · ${participant.activityCount} ${participant.activityCount === 1 ? 'activity' : 'activities'} · ${last}`)}`;
|
|
193
231
|
});
|
|
@@ -223,7 +261,7 @@ export function renderStatusResult(squarePath, data) {
|
|
|
223
261
|
? '◎'
|
|
224
262
|
: participant.activityCount > 0 ? '●' : '○';
|
|
225
263
|
const summary = participant.presence === 'watching'
|
|
226
|
-
? 'is nearby ·
|
|
264
|
+
? 'is nearby · perceiving'
|
|
227
265
|
: participant.activityCount > 0
|
|
228
266
|
? `is nearby · ${formatRelativeTime(participant.lastActiveAt ?? data.now, data.now)}`
|
|
229
267
|
: `is nearby · quiet · ${participant.lastActiveAt === undefined ? 'just now' : formatRelativeTime(participant.lastActiveAt, data.now)}`;
|
|
@@ -234,7 +272,7 @@ export function renderStatusResult(squarePath, data) {
|
|
|
234
272
|
? `${participant.pendingMentionCount} ${participant.pendingMentionCount === 1 ? 'mention' : 'mentions'} waiting`
|
|
235
273
|
: participant.unreadActivityCount > 0
|
|
236
274
|
? `${participant.unreadActivityCount} change${participant.unreadActivityCount === 1 ? '' : 's'} waiting`
|
|
237
|
-
: '
|
|
275
|
+
: 'up to date';
|
|
238
276
|
return ` ${glyph} ${participantIdentity(participant.name)}${style('dim', ` ${summary}${attention === '' ? '' : ` · ${attention}`}`)}`;
|
|
239
277
|
}));
|
|
240
278
|
people.push(...away.slice(0, STATUS_PARTICIPANT_PREVIEW_LIMIT).map(participant => ` ○ ${participantIdentity(participant.name)}${style('dim', ' · away')}`));
|
|
@@ -12,7 +12,10 @@ export class FileLockError extends Error {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
function isBusy(error) {
|
|
15
|
-
|
|
15
|
+
const code = error?.code;
|
|
16
|
+
return typeof code === 'string' && /^SQLITE_BUSY(?:_|$)/.test(code)
|
|
17
|
+
|| typeof error?.errcode === 'number' && (error.errcode & 0xff) === 5
|
|
18
|
+
|| error instanceof Error && /database is locked|database is busy|SQLITE_BUSY/i.test(error.message);
|
|
16
19
|
}
|
|
17
20
|
function isNotADatabase(error) {
|
|
18
21
|
return error instanceof Error && /not a database|file is not a database/i.test(error.message);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type HydratedAct } from './model.js';
|
|
2
|
+
import type { ActivityIndex, DirectedFilter, SquareRead, SquareWrite } from './ports.js';
|
|
3
|
+
import type { PerceiveOptions, PerceiveReason, PerceivedActivity } from './square-facade.js';
|
|
4
|
+
export interface PerceiveSelection {
|
|
5
|
+
readonly participant: string;
|
|
6
|
+
readonly activities: readonly PerceivedActivity[];
|
|
7
|
+
readonly remaining: number;
|
|
8
|
+
readonly standingCount: number;
|
|
9
|
+
readonly deliveryAvailable: boolean;
|
|
10
|
+
readonly now: number;
|
|
11
|
+
readonly ownActivityCount: number;
|
|
12
|
+
readonly hardCap: number | null;
|
|
13
|
+
readonly terminalStatus: Extract<PerceiveReason, 'capped' | 'quorum'> | undefined;
|
|
14
|
+
readonly hint?: string;
|
|
15
|
+
}
|
|
16
|
+
/** Capture a selected page (or an empty completion) without opening another resource. */
|
|
17
|
+
export declare function observePerceiveSelection(facts: SquareRead, participant: string, activities: readonly HydratedAct[], filter: DirectedFilter, at: number, remaining?: number): PerceiveSelection;
|
|
18
|
+
export declare const PERCEIVE_DEFAULT_LIMIT = 10;
|
|
19
|
+
export declare const PERCEIVE_MAX_LIMIT = 100;
|
|
20
|
+
export declare function perceiveLimit(value: number | undefined): number;
|
|
21
|
+
/** Select non-ID perceive coordinates without hydrating bodies or recording receipts. */
|
|
22
|
+
export declare function selectPerceivePage(facts: SquareRead, requestedName: string, options: PerceiveOptions): {
|
|
23
|
+
readonly participant: string;
|
|
24
|
+
readonly indexes: readonly ActivityIndex[];
|
|
25
|
+
readonly filter: DirectedFilter;
|
|
26
|
+
};
|
|
27
|
+
/** Selection, receipts and resulting cursor belong to one artifact write transaction. */
|
|
28
|
+
export declare function decidePerceive(facts: SquareWrite, requestedName: string, options: PerceiveOptions, at: number): PerceiveSelection;
|
|
@@ -3,13 +3,13 @@ import { terminalStatusFor } from './runtime.js';
|
|
|
3
3
|
import { SquareError } from './model.js';
|
|
4
4
|
import { resolveKnownName } from './decisions.js';
|
|
5
5
|
import { toPerceivedActivity } from './views.js';
|
|
6
|
-
import {
|
|
6
|
+
import { perceiveHint } from './hints.js';
|
|
7
7
|
import { consumeReceived, ReceiveError, selectReceivedHeads } from './receive.js';
|
|
8
8
|
import { squareReceiveRead } from './square-receive.js';
|
|
9
9
|
/** Capture a selected page (or an empty completion) without opening another resource. */
|
|
10
|
-
export function
|
|
10
|
+
export function observePerceiveSelection(facts, participant, activities, filter, at, remaining) {
|
|
11
11
|
if (typeof at !== 'number' || !Number.isFinite(at))
|
|
12
|
-
throw new SquareError('invalid_args', '
|
|
12
|
+
throw new SquareError('invalid_args', 'Perceive time must be a finite number.');
|
|
13
13
|
const counts = facts.participantCounts(), header = facts.header(), current = facts.participant(participant);
|
|
14
14
|
return {
|
|
15
15
|
participant, activities: activities.map(activity => toPerceivedActivity(facts, activity, participant)),
|
|
@@ -20,44 +20,44 @@ export function observeCatchSelection(facts, participant, activities, filter, at
|
|
|
20
20
|
terminalStatus: terminalStatusFor(current, header.hardCap, counts),
|
|
21
21
|
};
|
|
22
22
|
}
|
|
23
|
-
export const
|
|
24
|
-
export const
|
|
25
|
-
export function
|
|
26
|
-
const limit = value ??
|
|
27
|
-
if (!Number.isSafeInteger(limit) || limit < 1 || limit >
|
|
28
|
-
throw new SquareError('invalid_args', `
|
|
23
|
+
export const PERCEIVE_DEFAULT_LIMIT = 10;
|
|
24
|
+
export const PERCEIVE_MAX_LIMIT = 100;
|
|
25
|
+
export function perceiveLimit(value) {
|
|
26
|
+
const limit = value ?? PERCEIVE_DEFAULT_LIMIT;
|
|
27
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > PERCEIVE_MAX_LIMIT) {
|
|
28
|
+
throw new SquareError('invalid_args', `Perceive limit must be a positive integer no greater than ${PERCEIVE_MAX_LIMIT}.`);
|
|
29
29
|
}
|
|
30
30
|
return limit;
|
|
31
31
|
}
|
|
32
|
-
function
|
|
32
|
+
function perceivePageSelection(facts, requestedName, options) {
|
|
33
33
|
const participant = resolveKnownName(facts, requestedName);
|
|
34
|
-
const limit =
|
|
34
|
+
const limit = perceiveLimit(options.limit);
|
|
35
35
|
const senders = options.from?.map(name => resolveKnownName(facts, name));
|
|
36
36
|
const filter = { kinds: ['say'], unseenOnly: true, ...(senders === undefined ? {} : { senders }), ...(options.mention === true ? { attentionOnly: true } : {}) };
|
|
37
37
|
return { participant, filter, selection: { kind: 'page', query: filter, limit } };
|
|
38
38
|
}
|
|
39
|
-
/** Select non-ID
|
|
40
|
-
export function
|
|
41
|
-
const { participant, filter, selection } =
|
|
39
|
+
/** Select non-ID perceive coordinates without hydrating bodies or recording receipts. */
|
|
40
|
+
export function selectPerceivePage(facts, requestedName, options) {
|
|
41
|
+
const { participant, filter, selection } = perceivePageSelection(facts, requestedName, options);
|
|
42
42
|
return { participant, indexes: selectReceivedHeads(squareReceiveRead(facts), participant, selection).map(item => item.key), filter };
|
|
43
43
|
}
|
|
44
44
|
/** Selection, receipts and resulting cursor belong to one artifact write transaction. */
|
|
45
|
-
export function
|
|
45
|
+
export function decidePerceive(facts, requestedName, options, at) {
|
|
46
46
|
let viewer;
|
|
47
47
|
let filter = { kinds: ['say'], unseenOnly: true };
|
|
48
48
|
let selection;
|
|
49
49
|
if (options.id !== undefined) {
|
|
50
50
|
viewer = resolveKnownName(facts, requestedName);
|
|
51
|
-
|
|
51
|
+
perceiveLimit(options.limit);
|
|
52
52
|
if (['idle', 'from', 'mention', 'limit'].some((key) => Object.hasOwn(options, key)))
|
|
53
|
-
throw new SquareError('invalid_args', '
|
|
53
|
+
throw new SquareError('invalid_args', 'Perceive id cannot be combined with idle, from, mention or limit.');
|
|
54
54
|
const index = parseActivityId(options.id);
|
|
55
55
|
if (index === undefined)
|
|
56
|
-
throw new SquareError('invalid_args', 'Invalid
|
|
56
|
+
throw new SquareError('invalid_args', 'Invalid perceive id: expected an activity id like act/12.');
|
|
57
57
|
selection = { kind: 'exact', key: index, remainingQuery: filter };
|
|
58
58
|
}
|
|
59
59
|
else {
|
|
60
|
-
const page =
|
|
60
|
+
const page = perceivePageSelection(facts, requestedName, options);
|
|
61
61
|
viewer = page.participant;
|
|
62
62
|
filter = page.filter;
|
|
63
63
|
selection = page.selection;
|
|
@@ -87,10 +87,10 @@ export function decideCatch(facts, requestedName, options, at) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
const delivered = consumed.items.map(item => item.data);
|
|
90
|
-
const observed =
|
|
91
|
-
// Receipts are the durable
|
|
92
|
-
const
|
|
93
|
-
// An exact id
|
|
94
|
-
const hint = options.id === undefined ?
|
|
90
|
+
const observed = observePerceiveSelection(facts, viewer, delivered, filter, at, consumed.remaining);
|
|
91
|
+
// Receipts are the durable perceive count: everything directed here minus what is still unseen.
|
|
92
|
+
const perceived = facts.directedCount(viewer, { kinds: ['say'] }) - facts.directedCount(viewer, { kinds: ['say'], unseenOnly: true });
|
|
93
|
+
// An exact id perceive is a targeted fetch and stays repeatable, so it carries no nudge.
|
|
94
|
+
const hint = options.id === undefined ? perceiveHint(perceived - consumed.newlyConsumed, perceived) : undefined;
|
|
95
95
|
return hint === undefined ? observed : { ...observed, hint };
|
|
96
96
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Audience } from './square-core.js';
|
|
2
2
|
/** Pure filter over settled audience facts; unread/directed eligibility and paging belong to callers. */
|
|
3
|
-
export declare function
|
|
3
|
+
export declare function matchesPerceiveSelection(actor: string, audience: Audience, filter: {
|
|
4
4
|
readonly participants?: readonly string[];
|
|
5
5
|
readonly mention?: string;
|
|
6
6
|
}, replyAuthor?: string): boolean;
|
|
@@ -10,5 +10,5 @@ export interface RoutedNotification {
|
|
|
10
10
|
readonly route: import('./model.js').DirectedNotificationRoute;
|
|
11
11
|
readonly recipient: string;
|
|
12
12
|
}
|
|
13
|
-
/** A live
|
|
13
|
+
/** A live perceive owns every matching notification, independent of its page size. */
|
|
14
14
|
export declare function leaseOwnsNotification(lease: import('./model.js').WatchLease, notification: RoutedNotification): boolean;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { sameName } from './model.js';
|
|
2
2
|
import { audienceIncludes } from './square-core.js';
|
|
3
3
|
/** Pure filter over settled audience facts; unread/directed eligibility and paging belong to callers. */
|
|
4
|
-
export function
|
|
4
|
+
export function matchesPerceiveSelection(actor, audience, filter, replyAuthor) {
|
|
5
5
|
if (filter.participants !== undefined && !filter.participants.some((participant) => sameName(participant, actor))) {
|
|
6
6
|
return false;
|
|
7
7
|
}
|
|
@@ -12,8 +12,8 @@ export function matchesCatchSelection(actor, audience, filter, replyAuthor) {
|
|
|
12
12
|
return audienceIncludes(audience, filter.mention)
|
|
13
13
|
|| (replyAuthor !== undefined && sameName(replyAuthor, filter.mention));
|
|
14
14
|
}
|
|
15
|
-
/** A live
|
|
15
|
+
/** A live perceive owns every matching notification, independent of its page size. */
|
|
16
16
|
export function leaseOwnsNotification(lease, notification) {
|
|
17
|
-
return
|
|
17
|
+
return matchesPerceiveSelection(notification.actor, notification.route === 'bell' ? { kind: 'bell' }
|
|
18
18
|
: { kind: 'mentions', names: notification.route === 'mention' ? [notification.recipient] : [] }, lease.filter ?? {}, notification.route === 'reply' ? notification.recipient : undefined);
|
|
19
19
|
}
|
|
@@ -48,8 +48,8 @@ export default {
|
|
|
48
48
|
return async () => { };
|
|
49
49
|
}
|
|
50
50
|
const env = { ...process.env };
|
|
51
|
-
const discovery = await observeOpenCodeSessions(env);
|
|
52
51
|
const lifetime = new AbortController();
|
|
52
|
+
const discovery = await observeOpenCodeSessions(env, lifetime.signal);
|
|
53
53
|
const receivers = new Map(); // Operational handles only; presence owns membership.
|
|
54
54
|
const interrupting = new Set();
|
|
55
55
|
const cancellationFailed = new Set();
|
|
@@ -80,7 +80,7 @@ export default {
|
|
|
80
80
|
return target;
|
|
81
81
|
};
|
|
82
82
|
const reconcile = async (activeSession) => {
|
|
83
|
-
const candidates = new Set(await listOpenCodeSessions(env));
|
|
83
|
+
const candidates = new Set(await listOpenCodeSessions(env, lifetime.signal));
|
|
84
84
|
if (activeSession)
|
|
85
85
|
candidates.add(activeSession);
|
|
86
86
|
for (const sessionId of candidates) {
|
|
@@ -93,7 +93,7 @@ export default {
|
|
|
93
93
|
const receiver = receivers.get(sessionId);
|
|
94
94
|
stop(sessionId);
|
|
95
95
|
if (receiver)
|
|
96
|
-
await detachOpenCodeSession(sessionId, receiver.generation, env);
|
|
96
|
+
await detachOpenCodeSession(sessionId, receiver.generation, env, lifetime.signal);
|
|
97
97
|
continue;
|
|
98
98
|
}
|
|
99
99
|
const existing = receivers.get(sessionId);
|
|
@@ -171,7 +171,7 @@ export default {
|
|
|
171
171
|
const receiver = receivers.get(sessionId);
|
|
172
172
|
stop(sessionId);
|
|
173
173
|
if (receiver)
|
|
174
|
-
await detachOpenCodeSession(sessionId, receiver.generation, env);
|
|
174
|
+
await detachOpenCodeSession(sessionId, receiver.generation, env, lifetime.signal);
|
|
175
175
|
await reconcile();
|
|
176
176
|
});
|
|
177
177
|
else if (event.type === 'session.execution.interrupted' && event.data.reason === 'user') {
|
|
@@ -7,17 +7,17 @@ export interface OpenCodeContextMessage {
|
|
|
7
7
|
readonly text?: string;
|
|
8
8
|
}[];
|
|
9
9
|
}
|
|
10
|
-
export declare function listOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<readonly string[]>;
|
|
10
|
+
export declare function listOpenCodeSessions(env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<readonly string[]>;
|
|
11
11
|
/** The file observer is a hint; callers must always reread the ledger after an edge. */
|
|
12
|
-
export declare function observeOpenCodeSessions(env: NodeJS.ProcessEnv): Promise<{
|
|
12
|
+
export declare function observeOpenCodeSessions(env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<{
|
|
13
13
|
observer: import("../../file-changes.js").VersionObserver<string>;
|
|
14
14
|
close: () => Promise<void>;
|
|
15
15
|
}>;
|
|
16
16
|
export declare function publishOpenCodeAddress(sessionId: string, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<string | undefined>;
|
|
17
|
-
export declare function currentOpenCodeGeneration(sessionId: string, env: NodeJS.ProcessEnv): Promise<string | undefined>;
|
|
18
|
-
export declare function detachOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void>;
|
|
19
|
-
export declare function endOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<void>;
|
|
20
|
-
export declare function cancelOpenCodeDeliveries(sessionId: string, generation: string, env: NodeJS.ProcessEnv): Promise<boolean>;
|
|
17
|
+
export declare function currentOpenCodeGeneration(sessionId: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<string | undefined>;
|
|
18
|
+
export declare function detachOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<void>;
|
|
19
|
+
export declare function endOpenCodeSession(sessionId: string, generation: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<void>;
|
|
20
|
+
export declare function cancelOpenCodeDeliveries(sessionId: string, generation: string, env: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<boolean>;
|
|
21
21
|
/** Confirmation is observer-level: another host plugin may subsequently remove the message. */
|
|
22
22
|
export declare function observeOpenCodeContext(sessionId: string, generation: string, messages: readonly OpenCodeContextMessage[], env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void>;
|
|
23
23
|
export declare function receiveOpenCodePending(sessionId: string, generation: string, transport: DeliveryTransport, env: NodeJS.ProcessEnv, signal: AbortSignal): Promise<void>;
|