@astrosheep/square 0.3.20 → 0.3.22
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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/skills/square/SKILL.md +46 -38
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/codex-plugin/hooks/hooks.json +12 -0
- package/dist/attention-presentation.d.ts +1 -0
- package/dist/attention-presentation.js +9 -3
- package/dist/automatic-session.d.ts +1 -1
- package/dist/automatic-session.js +2 -11
- package/dist/boundary-presentation.d.ts +1 -1
- package/dist/boundary-presentation.js +30 -37
- package/dist/cli/square-commands.js +1 -12
- package/dist/codex-hook.js +10 -3
- package/dist/decisions.js +3 -3
- package/dist/help.js +3 -3
- package/dist/inbox.d.ts +9 -0
- package/dist/inbox.js +53 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/notifications.js +12 -9
- package/dist/participant-identity.d.ts +4 -0
- package/dist/participant-identity.js +32 -0
- package/dist/paseo-delivery.js +4 -4
- package/dist/presentation.js +1 -1
- package/dist/presented.d.ts +2 -2
- package/dist/presented.js +17 -8
- package/dist/registry.d.ts +4 -4
- package/dist/registry.js +8 -8
- package/dist/routes.d.ts +3 -3
- package/dist/routes.js +3 -3
- package/dist/square-core.d.ts +3 -0
- package/dist/square-core.js +20 -2
- package/dist/square-file-adapter.d.ts +11 -1
- package/dist/square-file-adapter.js +48 -0
- package/dist/square-wiring.d.ts +4 -0
- package/dist/square-wiring.js +4 -0
- package/dist/views.d.ts +0 -1
- package/dist/views.js +1 -2
- package/dist/wake-port.d.ts +1 -1
- package/dist/wake-port.js +1 -1
- package/extensions/square-pi.js +93 -0
- package/package.json +1 -1
- package/skills/square/SKILL.md +46 -38
package/dist/notifications.js
CHANGED
|
@@ -8,8 +8,7 @@ import { hasPresentedAttention } from './presented.js';
|
|
|
8
8
|
import { SquareError } from './model.js';
|
|
9
9
|
import { SLEEP_MS, matchesMentionTarget } from './runtime.js';
|
|
10
10
|
import { parseActivityId } from './square-core.js';
|
|
11
|
-
import {
|
|
12
|
-
import { renderAttentionPreview } from './attention-presentation.js';
|
|
11
|
+
import { displayAttentionPath, renderAttentionPreview } from './attention-presentation.js';
|
|
13
12
|
import { lookupParticipant } from './registry.js';
|
|
14
13
|
import { retireWakeRoute } from './routes.js';
|
|
15
14
|
import { openSquare } from './square-file-adapter.js';
|
|
@@ -30,12 +29,17 @@ export function wakeGraceMs(env = process.env) {
|
|
|
30
29
|
}
|
|
31
30
|
return value;
|
|
32
31
|
}
|
|
33
|
-
function
|
|
34
|
-
|
|
32
|
+
function wakeLabel(kind) {
|
|
33
|
+
if (kind === 'paseo')
|
|
34
|
+
return 'paseo';
|
|
35
|
+
if (kind.startsWith('codex'))
|
|
36
|
+
return 'codex-queue';
|
|
37
|
+
return kind;
|
|
35
38
|
}
|
|
36
|
-
function renderWakePayload(request, body) {
|
|
39
|
+
function renderWakePayload(request, body, kind) {
|
|
37
40
|
return [
|
|
38
|
-
|
|
41
|
+
`<system-reminder source="square" wake="${wakeLabel(kind)}">`,
|
|
42
|
+
`square: ${displayAttentionPath(request.squarePath)}`,
|
|
39
43
|
renderAttentionPreview({
|
|
40
44
|
squarePath: request.squarePath,
|
|
41
45
|
actIndex: request.actIndex,
|
|
@@ -43,9 +47,8 @@ function renderWakePayload(request, body) {
|
|
|
43
47
|
actor: request.actor,
|
|
44
48
|
route: request.route,
|
|
45
49
|
body,
|
|
50
|
+
compact: true,
|
|
46
51
|
}),
|
|
47
|
-
'The native adapter presented this attention. If you have not acted on it, pull from the square yourself.',
|
|
48
|
-
`\`${catchCommand(request.squarePath, request.recipient)}\``,
|
|
49
52
|
'</system-reminder>',
|
|
50
53
|
].join('\n');
|
|
51
54
|
}
|
|
@@ -181,7 +184,7 @@ async function processNotification(squarePath, notification, opts) {
|
|
|
181
184
|
actor: notification.item.actor,
|
|
182
185
|
route: notification.route,
|
|
183
186
|
};
|
|
184
|
-
await port.dispatch(evidence.attemptableRoutes, renderWakePayload(request, notification.item.body), {
|
|
187
|
+
await port.dispatch(evidence.attemptableRoutes, (route) => renderWakePayload(request, notification.item.body, route.kind), {
|
|
185
188
|
nextAttemptN: () => nextWakeAttemptNumber(attention, { env, now: now() }),
|
|
186
189
|
beforeSend: async (route, attemptN) => {
|
|
187
190
|
if (await waitForCatch(route, request, notification.item.body))
|
|
@@ -1 +1,5 @@
|
|
|
1
1
|
export declare function participantIdentity(name: string): string;
|
|
2
|
+
export type AutomaticProvider = 'codex' | 'claude' | 'opencode' | 'pi';
|
|
3
|
+
export declare function automaticParticipant(provider: AutomaticProvider, sessionId: string, env: NodeJS.ProcessEnv): string;
|
|
4
|
+
/** Compute the current harness participant without reading a Square or registry artifact. */
|
|
5
|
+
export declare function squareAssignedParticipantName(env?: NodeJS.ProcessEnv): string | undefined;
|
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { validateName } from './model.js';
|
|
1
3
|
export function participantIdentity(name) {
|
|
2
4
|
return `@${name}`;
|
|
3
5
|
}
|
|
6
|
+
const nativeSessions = [
|
|
7
|
+
{ variable: 'CLAUDE_CODE_SESSION_ID', provider: 'claude' },
|
|
8
|
+
{ variable: 'CODEX_THREAD_ID', provider: 'codex' },
|
|
9
|
+
{ variable: 'OPENCODE_SESSION_ID', provider: 'opencode' },
|
|
10
|
+
{ variable: 'SQUARE_PI_SESSION_ID', provider: 'pi' },
|
|
11
|
+
];
|
|
12
|
+
export function automaticParticipant(provider, sessionId, env) {
|
|
13
|
+
const configured = env.SQUARE_PARTICIPANT_NAME?.trim();
|
|
14
|
+
if (configured) {
|
|
15
|
+
validateName(configured);
|
|
16
|
+
return configured;
|
|
17
|
+
}
|
|
18
|
+
const digest = createHash('sha256').update(sessionId, 'utf8').digest('hex').slice(0, 12);
|
|
19
|
+
return `${provider}-${digest}`;
|
|
20
|
+
}
|
|
21
|
+
/** Compute the current harness participant without reading a Square or registry artifact. */
|
|
22
|
+
export function squareAssignedParticipantName(env = process.env) {
|
|
23
|
+
const configured = env.SQUARE_PARTICIPANT_NAME?.trim();
|
|
24
|
+
if (configured) {
|
|
25
|
+
validateName(configured);
|
|
26
|
+
return configured;
|
|
27
|
+
}
|
|
28
|
+
const names = new Set(nativeSessions
|
|
29
|
+
.map(({ variable, provider }) => {
|
|
30
|
+
const sessionId = env[variable]?.trim();
|
|
31
|
+
return sessionId === undefined || sessionId === '' ? undefined : automaticParticipant(provider, sessionId, env);
|
|
32
|
+
})
|
|
33
|
+
.filter((name) => name !== undefined));
|
|
34
|
+
return names.size === 1 ? [...names][0] : undefined;
|
|
35
|
+
}
|
package/dist/paseo-delivery.js
CHANGED
|
@@ -51,12 +51,12 @@ export class PaseoAdapter {
|
|
|
51
51
|
};
|
|
52
52
|
}
|
|
53
53
|
const agent = discovery.agents.find((candidate) => candidate.id === agentId);
|
|
54
|
-
if (agent === undefined ||
|
|
54
|
+
if (agent === undefined || agent.status !== 'idle') {
|
|
55
55
|
return {
|
|
56
56
|
outcome: 'unavailable',
|
|
57
|
-
signature: agent === undefined ? 'address_not_found' : '
|
|
58
|
-
message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle
|
|
59
|
-
diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : '
|
|
57
|
+
signature: agent === undefined ? 'address_not_found' : 'agent_not_idle',
|
|
58
|
+
message: agent === undefined ? 'The registered Paseo agent was not found.' : 'The registered Paseo agent is not idle.',
|
|
59
|
+
diagnostic: diagnostic('selection', address, agent === undefined ? 'not_found' : 'not_idle'),
|
|
60
60
|
...(agent === undefined ? {} : { retainRoute: true }),
|
|
61
61
|
};
|
|
62
62
|
}
|
package/dist/presentation.js
CHANGED
|
@@ -179,7 +179,7 @@ function renderPresenceOnlySay(event, opts = {}) {
|
|
|
179
179
|
const audience = audienceOf(event);
|
|
180
180
|
const targets = audience.kind === 'bell' ? [] : audience.names;
|
|
181
181
|
if (targets.length === 0) {
|
|
182
|
-
return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}
|
|
182
|
+
return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}`;
|
|
183
183
|
}
|
|
184
184
|
const dest = ` ${targets.map((name) => participantIdentity(name)).join(' and ')}`;
|
|
185
185
|
return `● ${participantIdentity(event.actor)} #${opts.actNumber ?? 1} · ${actId(event)} · ${formatRelativeTime(event.at, opts.now)}\n talked to${dest}`;
|
package/dist/presented.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ export declare function recordPresentedForOwner(ownerId: string, squarePath: str
|
|
|
8
8
|
/**
|
|
9
9
|
* Serialize presentation only for the affected participants. Delivery runs
|
|
10
10
|
* outside the short ledger-write lock, so unrelated owners never wait on an
|
|
11
|
-
* adapter. A throwing callback leaves no row and remains unpresented.
|
|
11
|
+
* adapter. A throwing or rejecting callback leaves no row and remains unpresented.
|
|
12
12
|
*/
|
|
13
|
-
export declare function presentOnce<T>(sessionId: string, lookup: (sessionId: string) => InboxMembership[]
|
|
13
|
+
export declare function presentOnce<T>(sessionId: string, lookup: (sessionId: string) => InboxMembership[] | Promise<InboxMembership[]>, deliver: (inbox: InboxMembership[]) => T | Promise<T>, env?: NodeJS.ProcessEnv, at?: number): Promise<T | undefined>;
|
package/dist/presented.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { withFileLockSync } from './file-lock.js';
|
|
5
|
+
import { withFileLock, withFileLockSync } from './file-lock.js';
|
|
6
6
|
import { canonicalSquarePath, lookupParticipant, lookupSessionBindings } from './registry.js';
|
|
7
7
|
import { sameName } from './model.js';
|
|
8
8
|
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -72,6 +72,15 @@ function withAttentionLocks(filePath, inbox, fn) {
|
|
|
72
72
|
}
|
|
73
73
|
return acquire(0);
|
|
74
74
|
}
|
|
75
|
+
async function withAttentionLocksAsync(filePath, inbox, fn) {
|
|
76
|
+
const lockPaths = [...new Set(inbox.map((membership) => attentionLockPath(filePath, membership)))].sort();
|
|
77
|
+
async function acquire(index) {
|
|
78
|
+
if (index >= lockPaths.length)
|
|
79
|
+
return fn();
|
|
80
|
+
return withFileLock(lockPaths[index], { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => acquire(index + 1));
|
|
81
|
+
}
|
|
82
|
+
return acquire(0);
|
|
83
|
+
}
|
|
75
84
|
function ownerFor(sessionId, membership) {
|
|
76
85
|
const squarePath = canonicalSquarePath(membership.squarePath);
|
|
77
86
|
const binding = lookupSessionBindings(sessionId).find((candidate) => candidate.squarePath === squarePath && sameName(candidate.name, membership.name));
|
|
@@ -129,21 +138,21 @@ export function recordPresentedForOwner(ownerId, squarePath, name, actIndex, env
|
|
|
129
138
|
/**
|
|
130
139
|
* Serialize presentation only for the affected participants. Delivery runs
|
|
131
140
|
* outside the short ledger-write lock, so unrelated owners never wait on an
|
|
132
|
-
* adapter. A throwing callback leaves no row and remains unpresented.
|
|
141
|
+
* adapter. A throwing or rejecting callback leaves no row and remains unpresented.
|
|
133
142
|
*/
|
|
134
|
-
export function presentOnce(sessionId, lookup, deliver, env = process.env, at = Date.now()) {
|
|
143
|
+
export async function presentOnce(sessionId, lookup, deliver, env = process.env, at = Date.now()) {
|
|
135
144
|
const filePath = presentedPath(env);
|
|
136
|
-
const initial = lookup(sessionId).filter((membership) => membership.notifications.length > 0);
|
|
145
|
+
const initial = (await lookup(sessionId)).filter((membership) => membership.notifications.length > 0);
|
|
137
146
|
if (initial.length === 0)
|
|
138
147
|
return undefined;
|
|
139
148
|
const lockedMemberships = new Set(initial.map(membershipKey));
|
|
140
|
-
return
|
|
141
|
-
const current = lookup(sessionId).filter((membership) => lockedMemberships.has(membershipKey(membership)));
|
|
149
|
+
return withAttentionLocksAsync(filePath, initial, async () => {
|
|
150
|
+
const current = (await lookup(sessionId)).filter((membership) => lockedMemberships.has(membershipKey(membership)));
|
|
142
151
|
const selected = selectUnpresented(sessionId, current, readRows(filePath, at));
|
|
143
152
|
if (selected.length === 0)
|
|
144
153
|
return undefined;
|
|
145
|
-
const result = deliver(selected.map(({ membership }) => membership));
|
|
146
|
-
|
|
154
|
+
const result = await deliver(selected.map(({ membership }) => membership));
|
|
155
|
+
await withFileLock(`${filePath}.lock`, { retryMs: LOCK_RETRY_MS, staleMs: LOCK_STALE_MS }, () => {
|
|
147
156
|
const rows = readRows(filePath, at);
|
|
148
157
|
const known = new Set(rows.map(rowKey));
|
|
149
158
|
for (const { membership, ownerId } of selected) {
|
package/dist/registry.d.ts
CHANGED
|
@@ -37,15 +37,15 @@ export declare function lookupParticipant(squarePath: string, name: string, now?
|
|
|
37
37
|
/** Resolve the current local harness owner for a participant, if one is registered. */
|
|
38
38
|
export declare function localParticipantOwner(squarePath: string, name: string, env?: NodeJS.ProcessEnv, now?: number): string | undefined;
|
|
39
39
|
export declare function localParticipantName(squarePath: string, env?: NodeJS.ProcessEnv): string | undefined;
|
|
40
|
-
/**
|
|
41
|
-
export declare function
|
|
40
|
+
/** Compute the current participant from the live harness identity, without registry history. */
|
|
41
|
+
export declare function squareAssignedParticipantName(env?: NodeJS.ProcessEnv): string | undefined;
|
|
42
42
|
export type CurrentParticipantBinding = Readonly<{
|
|
43
43
|
created: boolean;
|
|
44
44
|
ownerId: string;
|
|
45
45
|
}>;
|
|
46
|
-
/** Bind
|
|
46
|
+
/** Bind the current harness to one explicit Square participant. */
|
|
47
47
|
export declare function bindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): CurrentParticipantBinding;
|
|
48
|
-
/** Retire only
|
|
48
|
+
/** Retire only this harness's binding for one Square participant. */
|
|
49
49
|
export declare function unbindCurrentParticipant(squarePath: string, name: string, env?: NodeJS.ProcessEnv): boolean;
|
|
50
50
|
export interface RegistryPruneResult {
|
|
51
51
|
removed: number;
|
package/dist/registry.js
CHANGED
|
@@ -12,6 +12,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
12
12
|
import { nameKey, sameName, SquareError } from './model.js';
|
|
13
13
|
import { isCurrentlyJoined } from './runtime.js';
|
|
14
14
|
import { publishWakeRoutes, retireOwnerWakeRoutes } from './routes.js';
|
|
15
|
+
import { squareAssignedParticipantName as computeSquareAssignedParticipantName } from './participant-identity.js';
|
|
15
16
|
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
16
17
|
const COMPACT_BYTES = 64 * 1024;
|
|
17
18
|
const COMPACT_LINES = 1000;
|
|
@@ -236,15 +237,14 @@ export function localParticipantName(squarePath, env = process.env) {
|
|
|
236
237
|
const names = new Set(identities.flatMap((identity) => lookupSession(identity.sessionId).filter((item) => canonicalSquarePath(item.squarePath) === canonicalSquarePath(squarePath)).map((item) => item.name)));
|
|
237
238
|
return names.size === 1 ? [...names][0] : undefined;
|
|
238
239
|
}
|
|
239
|
-
/**
|
|
240
|
-
export function
|
|
241
|
-
|
|
242
|
-
return names.size === 1 ? [...names][0] : undefined;
|
|
240
|
+
/** Compute the current participant from the live harness identity, without registry history. */
|
|
241
|
+
export function squareAssignedParticipantName(env = process.env) {
|
|
242
|
+
return computeSquareAssignedParticipantName(env);
|
|
243
243
|
}
|
|
244
|
-
/** Bind
|
|
244
|
+
/** Bind the current harness to one explicit Square participant. */
|
|
245
245
|
export function bindCurrentParticipant(squarePath, name, env = process.env) {
|
|
246
|
-
if (
|
|
247
|
-
throw new SquareError('invalid_args', `The current session is not
|
|
246
|
+
if (squareAssignedParticipantName(env) !== name) {
|
|
247
|
+
throw new SquareError('invalid_args', `The current session is not assigned ${name}`);
|
|
248
248
|
}
|
|
249
249
|
const localOwner = localParticipantOwner(squarePath, name, env);
|
|
250
250
|
if (localOwner !== undefined)
|
|
@@ -259,7 +259,7 @@ export function bindCurrentParticipant(squarePath, name, env = process.env) {
|
|
|
259
259
|
throw new Error(`Current participant binding did not commit for ${name}`);
|
|
260
260
|
return { created: true, ownerId };
|
|
261
261
|
}
|
|
262
|
-
/** Retire only
|
|
262
|
+
/** Retire only this harness's binding for one Square participant. */
|
|
263
263
|
export function unbindCurrentParticipant(squarePath, name, env = process.env) {
|
|
264
264
|
const identities = new Set(localSessionIdentities(env).map((identity) => identity.sessionId));
|
|
265
265
|
const current = lookupParticipant(squarePath, name).filter((binding) => identities.has(binding.sessionId));
|
package/dist/routes.d.ts
CHANGED
|
@@ -30,9 +30,9 @@ export interface WakeRouteEvidence {
|
|
|
30
30
|
export type WakeRouteProbe = (env: NodeJS.ProcessEnv) => WakeRouteEvidence | undefined;
|
|
31
31
|
/**
|
|
32
32
|
* One probe per kind. A probe publishes only when its provider's complete
|
|
33
|
-
* endpoint evidence is present;
|
|
34
|
-
*
|
|
35
|
-
* their probes publish nothing until those transports land.
|
|
33
|
+
* endpoint evidence is present; Codex uses its thread id as that evidence.
|
|
34
|
+
* The other native transports have no endpoint lifecycle in this delivery,
|
|
35
|
+
* so their probes publish nothing until those transports land.
|
|
36
36
|
*/
|
|
37
37
|
export declare const WAKE_ROUTE_PROBES: Readonly<Record<WakeRouteKind, WakeRouteProbe>>;
|
|
38
38
|
/** The kind-neutral publication loop; probes supply complete evidence per kind. */
|
package/dist/routes.js
CHANGED
|
@@ -139,9 +139,9 @@ function completeRouteEvidence(value) {
|
|
|
139
139
|
}
|
|
140
140
|
/**
|
|
141
141
|
* One probe per kind. A probe publishes only when its provider's complete
|
|
142
|
-
* endpoint evidence is present;
|
|
143
|
-
*
|
|
144
|
-
* their probes publish nothing until those transports land.
|
|
142
|
+
* endpoint evidence is present; Codex uses its thread id as that evidence.
|
|
143
|
+
* The other native transports have no endpoint lifecycle in this delivery,
|
|
144
|
+
* so their probes publish nothing until those transports land.
|
|
145
145
|
*/
|
|
146
146
|
export const WAKE_ROUTE_PROBES = {
|
|
147
147
|
'opencode-server': () => undefined,
|
package/dist/square-core.d.ts
CHANGED
|
@@ -79,6 +79,8 @@ export interface FoldedSquareState {
|
|
|
79
79
|
throttleActivityAts: number[];
|
|
80
80
|
bellSayAtsByActor: Map<string, number[]>;
|
|
81
81
|
listening: Map<string, Participant[]>;
|
|
82
|
+
/** Derived sender blocks; kept outside the artifact schema. */
|
|
83
|
+
ignored: Map<string, Participant[]>;
|
|
82
84
|
}
|
|
83
85
|
export type ValidationResult = {
|
|
84
86
|
ok: true;
|
|
@@ -117,6 +119,7 @@ export declare function audienceOf(say: {
|
|
|
117
119
|
export declare function audienceIncludes(audience: Audience, name: string): boolean;
|
|
118
120
|
export declare function resolveAudience(audience: Audience, candidateNames: readonly string[]): string[];
|
|
119
121
|
export declare function activeListeners(state: FoldedSquareState, sender: string): string[];
|
|
122
|
+
export declare function isIgnored(state: FoldedSquareState, listener: string, sender: string): boolean;
|
|
120
123
|
export declare function audienceBefore(acts: readonly Act[], say: Extract<Act, {
|
|
121
124
|
kind: 'say';
|
|
122
125
|
}>): string[];
|
package/dist/square-core.js
CHANGED
|
@@ -59,18 +59,26 @@ export function resolveAudience(audience, candidateNames) {
|
|
|
59
59
|
}
|
|
60
60
|
export function activeListeners(state, sender) {
|
|
61
61
|
return state.participants
|
|
62
|
-
.filter((participant) => participant.joined
|
|
62
|
+
.filter((participant) => participant.joined
|
|
63
|
+
&& !(state.ignored.get(nameKey(participant.name)) ?? []).some((target) => sameName(target, sender))
|
|
64
|
+
&& (state.listening.get(nameKey(participant.name)) ?? []).some((target) => sameName(target, sender)))
|
|
63
65
|
.map((participant) => participant.name);
|
|
64
66
|
}
|
|
67
|
+
export function isIgnored(state, listener, sender) {
|
|
68
|
+
return (state.ignored.get(nameKey(listener)) ?? []).some((target) => sameName(target, sender));
|
|
69
|
+
}
|
|
65
70
|
export function audienceBefore(acts, say) {
|
|
66
71
|
const position = acts.findIndex((act) => act === say || ('index' in act && 'index' in say && act.index === say.index));
|
|
67
72
|
const before = fold(position < 0 ? acts : acts.slice(0, position));
|
|
68
|
-
const
|
|
73
|
+
const audience = audienceOf(say);
|
|
74
|
+
const mentionTargets = resolveAudience(audience, before.joined);
|
|
69
75
|
const listeners = activeListeners(before, say.actor);
|
|
70
76
|
const recipients = [];
|
|
71
77
|
for (const name of [...mentionTargets, ...listeners]) {
|
|
72
78
|
if (sameName(name, say.actor) || recipients.some((existing) => sameName(existing, name)))
|
|
73
79
|
continue;
|
|
80
|
+
if (audience.kind !== 'bell' && isIgnored(before, name, say.actor))
|
|
81
|
+
continue;
|
|
74
82
|
recipients.push(name);
|
|
75
83
|
}
|
|
76
84
|
return recipients;
|
|
@@ -146,6 +154,7 @@ export function fold(acts) {
|
|
|
146
154
|
throttleActivityAts: [],
|
|
147
155
|
bellSayAtsByActor: new Map(),
|
|
148
156
|
listening: new Map(),
|
|
157
|
+
ignored: new Map(),
|
|
149
158
|
};
|
|
150
159
|
for (const act of acts) {
|
|
151
160
|
const actor = actorOf(act);
|
|
@@ -172,6 +181,11 @@ export function fold(acts) {
|
|
|
172
181
|
if (!targets.some((target) => sameName(target, act.target)))
|
|
173
182
|
targets.push(act.target);
|
|
174
183
|
state.listening.set(key, targets);
|
|
184
|
+
const ignored = (state.ignored.get(key) ?? []).filter((target) => !sameName(target, act.target));
|
|
185
|
+
if (ignored.length === 0)
|
|
186
|
+
state.ignored.delete(key);
|
|
187
|
+
else
|
|
188
|
+
state.ignored.set(key, ignored);
|
|
175
189
|
break;
|
|
176
190
|
}
|
|
177
191
|
case 'ignore': {
|
|
@@ -181,6 +195,10 @@ export function fold(acts) {
|
|
|
181
195
|
state.listening.delete(key);
|
|
182
196
|
else
|
|
183
197
|
state.listening.set(key, targets);
|
|
198
|
+
const ignored = state.ignored.get(key) ?? [];
|
|
199
|
+
if (!ignored.some((target) => sameName(target, act.target)))
|
|
200
|
+
ignored.push(act.target);
|
|
201
|
+
state.ignored.set(key, ignored);
|
|
184
202
|
break;
|
|
185
203
|
}
|
|
186
204
|
case 'say':
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type BuildOptions, type HardCap } from './model.js';
|
|
2
|
-
import type
|
|
2
|
+
import { type OpenSquare } from './open-square.js';
|
|
3
3
|
import type { WakeNotifier } from './square-facade.js';
|
|
4
4
|
/** File-owned artifact creation for the CLI and path-backed public facade. */
|
|
5
5
|
export declare function createSquare(squarePath: string, options: BuildOptions & {
|
|
@@ -16,3 +16,13 @@ export declare function openSquare(squarePath: string, options?: Pick<SquareBuil
|
|
|
16
16
|
export declare function probeSquare(squarePath: string): OpenSquare | undefined;
|
|
17
17
|
export declare function buildSquare(squarePath: string, options: SquareBuildOptions): Promise<OpenSquare>;
|
|
18
18
|
export declare function buildMemorySquare(options: SquareBuildOptions): OpenSquare;
|
|
19
|
+
/** Wait for any bound artifact to change; delivery callers re-project after the edge. */
|
|
20
|
+
export type SquareChangeWaitResult<T> = {
|
|
21
|
+
status: 'ready';
|
|
22
|
+
value: T;
|
|
23
|
+
} | {
|
|
24
|
+
status: 'changed';
|
|
25
|
+
} | {
|
|
26
|
+
status: 'expired';
|
|
27
|
+
};
|
|
28
|
+
export declare function waitForSquareChanges<T>(squarePaths: readonly string[], timeoutMs: number, signal?: AbortSignal, afterReady?: () => Promise<T | undefined>): Promise<SquareChangeWaitResult<T>>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { createSquareState, probeSquareFile, writeSquareSnapshot, withSquareFileLock, openSquareCell, createMemoryCell, } from './square-storage.js';
|
|
3
3
|
import { InternalSquareError, SquareError, } from './model.js';
|
|
4
|
+
import { closeOpenSquare } from './open-square.js';
|
|
4
5
|
/** The current CLI file mutation boundary. */
|
|
5
6
|
function writeSquareState(squarePath, squareState) {
|
|
6
7
|
writeSquareSnapshot(squarePath, squareState);
|
|
@@ -68,3 +69,50 @@ export function buildMemorySquare(options) {
|
|
|
68
69
|
}, options.markdown);
|
|
69
70
|
return { cell: createMemoryCell(squareState), clock: options.clock ?? Date.now, ...(options.notifier === undefined ? {} : { notifier: options.notifier }), location: 'memory' };
|
|
70
71
|
}
|
|
72
|
+
export async function waitForSquareChanges(squarePaths, timeoutMs, signal, afterReady) {
|
|
73
|
+
if (timeoutMs <= 0 || signal?.aborted || squarePaths.length === 0)
|
|
74
|
+
return { status: 'expired' };
|
|
75
|
+
const squares = [];
|
|
76
|
+
try {
|
|
77
|
+
for (const squarePath of [...new Set(squarePaths)]) {
|
|
78
|
+
try {
|
|
79
|
+
squares.push(await openSquare(squarePath));
|
|
80
|
+
}
|
|
81
|
+
catch { /* stale binding */ }
|
|
82
|
+
}
|
|
83
|
+
if (squares.length === 0)
|
|
84
|
+
return { status: 'expired' };
|
|
85
|
+
const baselines = await Promise.all(squares.map(async (square) => ({
|
|
86
|
+
square,
|
|
87
|
+
version: (await square.cell.read()).version,
|
|
88
|
+
})));
|
|
89
|
+
const ready = await afterReady?.();
|
|
90
|
+
if (ready !== undefined)
|
|
91
|
+
return { status: 'ready', value: ready };
|
|
92
|
+
const waits = baselines.map(async ({ square, version }) => {
|
|
93
|
+
const changed = await square.cell.changed(version, timeoutMs).catch(() => false);
|
|
94
|
+
if (changed)
|
|
95
|
+
return true;
|
|
96
|
+
throw new Error('square wait expired');
|
|
97
|
+
});
|
|
98
|
+
let abortWait;
|
|
99
|
+
const abort = new Promise((resolve) => {
|
|
100
|
+
abortWait = () => resolve(false);
|
|
101
|
+
if (signal?.aborted)
|
|
102
|
+
abortWait();
|
|
103
|
+
else
|
|
104
|
+
signal?.addEventListener('abort', abortWait, { once: true });
|
|
105
|
+
});
|
|
106
|
+
try {
|
|
107
|
+
const changed = await Promise.race([Promise.any(waits).catch(() => false), abort]);
|
|
108
|
+
return changed ? { status: 'changed' } : { status: 'expired' };
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
if (abortWait)
|
|
112
|
+
signal?.removeEventListener('abort', abortWait);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
await Promise.all(squares.map((square) => closeOpenSquare(square)));
|
|
117
|
+
}
|
|
118
|
+
}
|
package/dist/square-wiring.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export declare class Square {
|
|
|
9
9
|
static build(input: SquareBuildInput): Promise<Square>;
|
|
10
10
|
static inMemory(input: Omit<SquareBuildInput, 'path'>): Square;
|
|
11
11
|
join(name: string): Promise<Participant>;
|
|
12
|
+
joinWithActivity(name: string): Promise<{
|
|
13
|
+
readonly participant: Participant;
|
|
14
|
+
readonly activity: Activity | null;
|
|
15
|
+
}>;
|
|
12
16
|
implicitJoin(name: string): Promise<{
|
|
13
17
|
readonly state: 'joined' | 'active' | 'done';
|
|
14
18
|
readonly participant?: Participant;
|
package/dist/square-wiring.js
CHANGED
|
@@ -59,6 +59,10 @@ export class Square {
|
|
|
59
59
|
const joined = await join(this.square, name);
|
|
60
60
|
return new ParticipantHandle(joined.name, this.square);
|
|
61
61
|
}
|
|
62
|
+
async joinWithActivity(name) {
|
|
63
|
+
const joined = await join(this.square, name);
|
|
64
|
+
return { participant: new ParticipantHandle(joined.name, this.square), activity: joined.activity };
|
|
65
|
+
}
|
|
62
66
|
async implicitJoin(name) {
|
|
63
67
|
const joined = await implicitJoin(this.square, name);
|
|
64
68
|
return joined.state === 'done'
|
package/dist/views.d.ts
CHANGED
|
@@ -79,7 +79,6 @@ export declare function resolveParticipant(square: OpenSquare, name: string): Pr
|
|
|
79
79
|
readonly roster: readonly string[];
|
|
80
80
|
}>;
|
|
81
81
|
export declare function currentParticipant(square: OpenSquare, name: string): Promise<string | undefined>;
|
|
82
|
-
export declare function admitsBareExpress(square: OpenSquare, name: string, body: string): Promise<boolean>;
|
|
83
82
|
export declare function participants(square: OpenSquare): Promise<ParticipantStatus[]>;
|
|
84
83
|
export declare function snapshot(square: OpenSquare): Promise<SquareSnapshot>;
|
|
85
84
|
export declare function activityPresentation(square: OpenSquare, name: string): Promise<ActivityPresentation>;
|
package/dist/views.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { extractMentions, formatActivityId, parseActivityId } from './square-core.js';
|
|
2
2
|
import { deliveryDelta, directedPeerSays } from './activity-feed.js';
|
|
3
3
|
import { coreActivities, coreParticipants, coreStatus, resolveKnownName } from './decisions.js';
|
|
4
4
|
import { deriveDeliveryModel, isActivitySeen, perceiveActivity, planActNotifications } from './delivery.js';
|
|
@@ -62,7 +62,6 @@ export async function history(square, query = {}) { const { state } = await squa
|
|
|
62
62
|
export async function participantHistory(square, name, query = {}) { const { state } = await square.cell.read(); const viewer = resolveKnownName(state, name); const effective = query.all === true || query.limit !== undefined ? query : { ...query, limit: 10 }; return selectHistory(coreActivities(state, historyOptions(effective, viewer)), effective).map((activity) => exposePerceived(state, activity, viewer)); }
|
|
63
63
|
export async function resolveParticipant(square, name) { const { state } = await square.cell.read(); return { name: resolveKnownName(state, name), roster: rosterNames(state) }; }
|
|
64
64
|
export async function currentParticipant(square, name) { const { state } = await square.cell.read(); const known = resolveRosterName(state, name); return known !== undefined && isCurrentlyJoined(state.acts, known) ? known : undefined; }
|
|
65
|
-
export async function admitsBareExpress(square, name, body) { const { state } = await square.cell.read(); const actor = resolveKnownName(state, name); return extractMentions(body).length > 0 || activeListeners(foldedState(state), actor).length > 0; }
|
|
66
65
|
export async function participants(square) { const { state } = await square.cell.read(); return statuses(square, state); }
|
|
67
66
|
export async function snapshot(square) { const { state } = await square.cell.read(); const folded = foldedState(state); return { context: [...state.preamble, ...state.warmup].join('\n'), actCount: state.acts.filter((activity) => activity.kind !== 'read').length, hardCap: state.hardCap, ...(state.throttlePerMinute === undefined ? {} : { throttlePerMinute: state.throttlePerMinute }), held: folded.hold.active && folded.hold.actor !== undefined ? { by: folded.hold.actor, ...(folded.hold.reason === undefined ? {} : { reason: folded.hold.reason }) } : null, participants: statuses(square, state), delivered(name, id) { return isActivitySeen(state, name, parseRequiredActivityId(id)); } }; }
|
|
68
67
|
export async function activityPresentation(square, name) { const { state } = await square.cell.read(); const known = resolveKnownName(state, name); const delta = deliveryDelta(state, known); const hold = currentHold(state.acts); return { name: known, roster: rosterNames(state), pendingPublic: directedPeerSays(state, delta, known), pendingRoomChanges: [], activities: state.acts, state, participantCount: inSquareCount(state), held: hold.active, ...(hold.reason === undefined ? {} : { holdReason: hold.reason }), ownActivityCount: countSays(state.acts, known), hardCap: state.hardCap }; }
|
package/dist/wake-port.d.ts
CHANGED
|
@@ -19,5 +19,5 @@ export type WakePortResult = {
|
|
|
19
19
|
export declare class WakePort {
|
|
20
20
|
private readonly adapters;
|
|
21
21
|
constructor(adapters: WakeAdapter[]);
|
|
22
|
-
dispatch(routes: readonly WakeRoute[], payload: string, hooks: WakePortHooks): Promise<WakePortResult>;
|
|
22
|
+
dispatch(routes: readonly WakeRoute[], payload: string | ((route: WakeRoute) => string), hooks: WakePortHooks): Promise<WakePortResult>;
|
|
23
23
|
}
|
package/dist/wake-port.js
CHANGED
|
@@ -10,7 +10,7 @@ export class WakePort {
|
|
|
10
10
|
if (adapter === undefined)
|
|
11
11
|
continue;
|
|
12
12
|
const attemptN = hooks.nextAttemptN();
|
|
13
|
-
const result = await adapter.dispatch(route.address, payload, () => hooks.beforeSend(route, attemptN));
|
|
13
|
+
const result = await adapter.dispatch(route.address, typeof payload === 'function' ? payload(route) : payload, () => hooks.beforeSend(route, attemptN));
|
|
14
14
|
if (result.outcome === 'cancelled')
|
|
15
15
|
return result;
|
|
16
16
|
if (result.outcome === 'unavailable') {
|
package/extensions/square-pi.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { presentPendingAtBoundary, renderPendingAtBoundary } from '../dist/boundary-presentation.js';
|
|
2
2
|
import { automaticSessionEnd, automaticSessionStart } from '../dist/automatic-session.js';
|
|
3
|
+
import { waitForSessionPending } from '../dist/inbox.js';
|
|
3
4
|
|
|
4
5
|
export function pendingInbox(inbox) {
|
|
5
6
|
return inbox.filter((item) => item.notifications?.length > 0);
|
|
@@ -20,14 +21,97 @@ export default function squarePiExtension(pi) {
|
|
|
20
21
|
let sessionCwd;
|
|
21
22
|
let joiningContext;
|
|
22
23
|
let previousSessionId;
|
|
24
|
+
let watcher;
|
|
25
|
+
let watcherAbort;
|
|
26
|
+
let generation = 0;
|
|
27
|
+
let presenting = false;
|
|
28
|
+
let settledSerial = 0;
|
|
29
|
+
let settledWaiters = [];
|
|
30
|
+
const handledPending = new Set();
|
|
31
|
+
let retryAfterChange = false;
|
|
23
32
|
const present = (deliver) => sessionId === undefined ? undefined : presentPendingAtBoundary(sessionId, deliver);
|
|
24
33
|
|
|
34
|
+
const waitForSettled = (serial, signal) => {
|
|
35
|
+
if (settledSerial !== serial) return Promise.resolve();
|
|
36
|
+
return new Promise((resolve) => {
|
|
37
|
+
const waiter = { serial, resolve };
|
|
38
|
+
settledWaiters.push(waiter);
|
|
39
|
+
if (signal) signal.addEventListener('abort', () => {
|
|
40
|
+
const index = settledWaiters.indexOf(waiter);
|
|
41
|
+
if (index >= 0) settledWaiters.splice(index, 1);
|
|
42
|
+
resolve();
|
|
43
|
+
}, { once: true });
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const wake = async (piContext, token, signal) => {
|
|
48
|
+
while (sessionId !== undefined && token === generation && !signal.aborted) {
|
|
49
|
+
const deferredRetry = retryAfterChange;
|
|
50
|
+
const pending = await waitForSessionPending(sessionId, 30_000, {
|
|
51
|
+
signal,
|
|
52
|
+
excludeKeys: handledPending,
|
|
53
|
+
skipImmediate: deferredRetry,
|
|
54
|
+
});
|
|
55
|
+
if (sessionId === undefined || token !== generation || signal.aborted) return;
|
|
56
|
+
if (pending.length === 0) {
|
|
57
|
+
if (deferredRetry) retryAfterChange = false;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
retryAfterChange = false;
|
|
61
|
+
if (!piContext.isIdle()) {
|
|
62
|
+
const serial = settledSerial;
|
|
63
|
+
await waitForSettled(serial, signal);
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (presenting) continue;
|
|
67
|
+
const keys = inboxKeys(pending);
|
|
68
|
+
presenting = true;
|
|
69
|
+
try {
|
|
70
|
+
const delivered = await presentPendingAtBoundary(
|
|
71
|
+
sessionId,
|
|
72
|
+
async (content) => {
|
|
73
|
+
await pi.sendMessage(
|
|
74
|
+
{ customType: 'square', content, display: true },
|
|
75
|
+
{ deliverAs: 'steer', triggerTurn: true },
|
|
76
|
+
);
|
|
77
|
+
return true;
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
if (delivered === true || delivered === undefined) {
|
|
81
|
+
for (const key of keys) handledPending.add(key);
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// Leave pending evidence untouched so the next state change can retry.
|
|
85
|
+
retryAfterChange = true;
|
|
86
|
+
} finally {
|
|
87
|
+
presenting = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
|
|
25
92
|
pi.on('session_start', async (_event, ctx) => {
|
|
93
|
+
generation += 1;
|
|
94
|
+
watcherAbort?.abort();
|
|
95
|
+
if (watcher) await watcher;
|
|
96
|
+
watcher = undefined;
|
|
97
|
+
watcherAbort = undefined;
|
|
98
|
+
handledPending.clear();
|
|
99
|
+
retryAfterChange = false;
|
|
26
100
|
sessionId = ctx.sessionManager.getSessionId();
|
|
27
101
|
sessionCwd = ctx.cwd || process.cwd();
|
|
28
102
|
previousSessionId = process.env.SQUARE_PI_SESSION_ID;
|
|
29
103
|
process.env.SQUARE_PI_SESSION_ID = sessionId;
|
|
30
104
|
try { joiningContext = await automaticSessionStart('pi', sessionId, sessionCwd); } catch { joiningContext = undefined; }
|
|
105
|
+
watcherAbort = new AbortController();
|
|
106
|
+
const token = generation;
|
|
107
|
+
watcher = wake(ctx, token, watcherAbort.signal).catch(() => undefined);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
pi.on('agent_settled', async () => {
|
|
111
|
+
settledSerial += 1;
|
|
112
|
+
const waiters = settledWaiters;
|
|
113
|
+
settledWaiters = [];
|
|
114
|
+
for (const waiter of waiters) waiter.resolve();
|
|
31
115
|
});
|
|
32
116
|
|
|
33
117
|
pi.on('before_agent_start', async () => {
|
|
@@ -37,6 +121,7 @@ export default function squarePiExtension(pi) {
|
|
|
37
121
|
joiningContext = undefined;
|
|
38
122
|
return { message: { customType: 'square', content: context, display: true } };
|
|
39
123
|
}
|
|
124
|
+
if (presenting) return undefined;
|
|
40
125
|
return present((context) => ({ message: { customType: 'square', content: context, display: true } }));
|
|
41
126
|
} catch {
|
|
42
127
|
return undefined;
|
|
@@ -44,6 +129,13 @@ export default function squarePiExtension(pi) {
|
|
|
44
129
|
});
|
|
45
130
|
|
|
46
131
|
pi.on('session_shutdown', async () => {
|
|
132
|
+
generation += 1;
|
|
133
|
+
watcherAbort?.abort();
|
|
134
|
+
if (watcher) await watcher;
|
|
135
|
+
watcher = undefined;
|
|
136
|
+
watcherAbort = undefined;
|
|
137
|
+
for (const waiter of settledWaiters) waiter.resolve();
|
|
138
|
+
settledWaiters = [];
|
|
47
139
|
if (sessionId && sessionCwd) await automaticSessionEnd('pi', sessionId, sessionCwd);
|
|
48
140
|
if (process.env.SQUARE_PI_SESSION_ID === sessionId) {
|
|
49
141
|
if (previousSessionId === undefined) delete process.env.SQUARE_PI_SESSION_ID;
|
|
@@ -52,5 +144,6 @@ export default function squarePiExtension(pi) {
|
|
|
52
144
|
sessionId = undefined;
|
|
53
145
|
sessionCwd = undefined;
|
|
54
146
|
joiningContext = undefined;
|
|
147
|
+
presenting = false;
|
|
55
148
|
});
|
|
56
149
|
}
|