@astrosheep/square 0.3.45 → 0.3.47
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 +6 -1
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +1 -3
- package/dist/artifact.d.ts +28 -7
- package/dist/artifact.js +294 -253
- package/dist/catch-decisions.d.ts +2 -6
- package/dist/catch-decisions.js +27 -12
- package/dist/cli/observation-commands.js +19 -3
- package/dist/cli/square-commands.js +2 -0
- package/dist/delivery.js +8 -18
- package/dist/help.js +3 -3
- package/dist/host-ledger-file-adapter.js +14 -1
- package/dist/model.d.ts +2 -1
- package/dist/participant-host.d.ts +19 -0
- package/dist/participant-host.js +78 -0
- package/dist/perception-projection.d.ts +15 -0
- package/dist/perception-projection.js +28 -0
- package/dist/ports.d.ts +1 -4
- package/dist/presence.d.ts +0 -4
- package/dist/presence.js +0 -13
- package/dist/routes.js +2 -1
- package/dist/square-actions.d.ts +4 -8
- package/dist/square-actions.js +3 -83
- package/dist/square-facade.d.ts +1 -0
- package/dist/square-file-adapter.js +9 -8
- package/dist/square-state.d.ts +6 -0
- package/dist/square-state.js +141 -0
- package/dist/square-storage.d.ts +7 -10
- package/dist/square-storage.js +83 -114
- package/dist/state-cell.d.ts +1 -4
- package/dist/watch.js +1 -0
- package/extensions/square-pi.js +27 -6
- package/guides/participant-ownership.md +3 -3
- package/package.json +2 -2
- package/skills/square/SKILL.md +6 -1
- package/dist/landing.d.ts +0 -26
- package/dist/landing.js +0 -42
package/dist/catch-decisions.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { audienceOf, formatActivityId,
|
|
2
|
-
import {
|
|
1
|
+
import { audienceOf, formatActivityId, parseActivityId } from './square-core.js';
|
|
2
|
+
import { derivePerceptionProjection } from './perception-projection.js';
|
|
3
|
+
import { matchesMentionTarget, recordObservation } from './runtime.js';
|
|
3
4
|
import { participantIdentity } from './participant-identity.js';
|
|
4
5
|
import { SquareError, sameName, validateName } from './model.js';
|
|
5
6
|
import { resolveRosterName } from './runtime.js';
|
|
@@ -12,15 +13,6 @@ function catchLimit(value) {
|
|
|
12
13
|
}
|
|
13
14
|
return limit;
|
|
14
15
|
}
|
|
15
|
-
function defaultProjection(state) {
|
|
16
|
-
const landed = replayLandedAudiences(state.acts);
|
|
17
|
-
return {
|
|
18
|
-
cursorFor: (name) => readCursor(state, name, landed),
|
|
19
|
-
directedTo: (activity, name) => activity.kind === 'say' && landed.includes(activity, name),
|
|
20
|
-
perceive: (activity, name) => activity.kind !== 'say' || sameName(activity.actor, name) || landed.includes(activity, name) ? 'full' : 'presence',
|
|
21
|
-
isSeen: (name, index) => observationFor(state, name, index)?.state === 'seen',
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
16
|
function resolveCatchName(state, requestedName) {
|
|
25
17
|
validateName(requestedName);
|
|
26
18
|
const known = resolveRosterName(state, requestedName);
|
|
@@ -30,10 +22,33 @@ function resolveCatchName(state, requestedName) {
|
|
|
30
22
|
return known;
|
|
31
23
|
}
|
|
32
24
|
/** Pure catch selection and acknowledgement over one SquareState snapshot. */
|
|
33
|
-
export function decideCatch(state, requestedName, options, at, project =
|
|
25
|
+
export function decideCatch(state, requestedName, options, at, project = derivePerceptionProjection) {
|
|
34
26
|
const viewer = resolveCatchName(state, requestedName);
|
|
35
27
|
const limit = catchLimit(options.limit);
|
|
36
28
|
const delivery = project(state);
|
|
29
|
+
if (options.id !== undefined) {
|
|
30
|
+
if (['idle', 'from', 'mention', 'limit'].some((key) => Object.hasOwn(options, key))) {
|
|
31
|
+
throw new SquareError('invalid_args', 'Catch id cannot be combined with idle, from, mention or limit.');
|
|
32
|
+
}
|
|
33
|
+
const index = parseActivityId(options.id);
|
|
34
|
+
if (index === undefined)
|
|
35
|
+
throw new SquareError('invalid_args', 'Invalid catch id: expected an activity id like act/12.');
|
|
36
|
+
const activity = state.acts.find((item) => item.index === index);
|
|
37
|
+
if (activity === undefined || activity.kind !== 'say' || !delivery.directedTo(activity, viewer)) {
|
|
38
|
+
throw new SquareError('invalid_args', 'That activity is not available to catch.');
|
|
39
|
+
}
|
|
40
|
+
const perception = delivery.perceive(activity, viewer);
|
|
41
|
+
const changed = recordObservation(state, viewer, index, 'seen', at);
|
|
42
|
+
const cursor = delivery.cursorFor(viewer);
|
|
43
|
+
return {
|
|
44
|
+
viewer,
|
|
45
|
+
delivered: [activity],
|
|
46
|
+
perceptions: new Map([[index, perception]]),
|
|
47
|
+
consumedThrough: cursor < 0 ? null : formatActivityId(cursor),
|
|
48
|
+
changed,
|
|
49
|
+
remaining: state.acts.filter((item) => item.kind === 'say' && delivery.directedTo(item, viewer) && !delivery.isSeen(viewer, item.index)).length,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
37
52
|
const from = options.from;
|
|
38
53
|
const mentionOnly = options.mention === true;
|
|
39
54
|
const delivered = [];
|
|
@@ -86,12 +86,22 @@ export const catchCommand = {
|
|
|
86
86
|
const name = requireParticipant(context.name);
|
|
87
87
|
let idleMs;
|
|
88
88
|
let mention;
|
|
89
|
-
let limit
|
|
89
|
+
let limit;
|
|
90
|
+
let id;
|
|
90
91
|
let replace = false;
|
|
91
92
|
let now = false;
|
|
92
93
|
const participants = [];
|
|
93
94
|
for (let index = 0; index < argv.length; index++) {
|
|
94
|
-
if (argv[index] === '--
|
|
95
|
+
if (argv[index] === '--id') {
|
|
96
|
+
if (id !== undefined)
|
|
97
|
+
fail('✕ catch accepts one --id\n» square catch --help');
|
|
98
|
+
const value = requireValue(argv, index, '--id');
|
|
99
|
+
if (parseActivityId(value) === undefined)
|
|
100
|
+
fail('✕ invalid --id: expected an activity id like act/12\n» square catch --help');
|
|
101
|
+
id = value;
|
|
102
|
+
index += 1;
|
|
103
|
+
}
|
|
104
|
+
else if (argv[index] === '--from') {
|
|
95
105
|
participants.push(...parseNameList(requireValue(argv, index, argv[index]), argv[index]));
|
|
96
106
|
index += 1;
|
|
97
107
|
}
|
|
@@ -113,6 +123,12 @@ export const catchCommand = {
|
|
|
113
123
|
else
|
|
114
124
|
fail(`✕ catch does not know ${argv[index]}\n» square catch --help`);
|
|
115
125
|
}
|
|
126
|
+
if (id !== undefined) {
|
|
127
|
+
if (argv.some((flag) => ['--idle', '--from', '--mention', '--limit', '--replace'].includes(flag))) {
|
|
128
|
+
fail('✕ --id cannot be combined with --idle, --from, --mention, --limit or --replace\n» square catch --help');
|
|
129
|
+
}
|
|
130
|
+
return { id, now: true };
|
|
131
|
+
}
|
|
116
132
|
if (now === (idleMs !== undefined))
|
|
117
133
|
fail('catch requires exactly one mode: --now or --idle <duration>.');
|
|
118
134
|
if (replace && now)
|
|
@@ -120,7 +136,7 @@ export const catchCommand = {
|
|
|
120
136
|
return {
|
|
121
137
|
...(participants.length > 0 ? { participants } : {}),
|
|
122
138
|
...(mention === undefined ? {} : { mention }),
|
|
123
|
-
limit,
|
|
139
|
+
limit: limit ?? CATCH_DEFAULT_LIMIT,
|
|
124
140
|
...(idleMs === undefined ? {} : { idleMs }),
|
|
125
141
|
...(replace ? { replace } : {}),
|
|
126
142
|
...(now ? { now } : {}),
|
|
@@ -147,6 +147,8 @@ export const joinCommand = {
|
|
|
147
147
|
: `✓ you banished the original ${participantIdentity(joinedName)} — the name is yours`;
|
|
148
148
|
const output = [
|
|
149
149
|
entryLine,
|
|
150
|
+
'',
|
|
151
|
+
"Don't treat Square as a public dumping ground. Speak here only when you genuinely think someone else needs to know; otherwise you are needlessly interrupting them.",
|
|
150
152
|
...(reconnect || scene === '' ? [] : ['', scene]),
|
|
151
153
|
...(isRejoin || contextText === '' ? [] : ['', 'context', contextText]),
|
|
152
154
|
...(isRejoin || activities === '' ? [] : ['', 'recent activity', activities]),
|
package/dist/delivery.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { findParticipantName, sameName, } from './model.js';
|
|
2
2
|
import { audienceOf, formatActivityId, replayLandedAudiences } from './square-core.js';
|
|
3
|
-
import {
|
|
3
|
+
import { derivePerceptionProjection } from './perception-projection.js';
|
|
4
|
+
import { matchesMentionTarget, recordObservation } from './runtime.js';
|
|
4
5
|
export function notificationMessageId(squarePath, actIndex) {
|
|
5
6
|
return `square:${squarePath}#${formatActivityId(actIndex)}`;
|
|
6
7
|
}
|
|
@@ -13,16 +14,13 @@ export function isActivitySeen(squareState, name, actOrIndex) {
|
|
|
13
14
|
*/
|
|
14
15
|
export function deriveDeliveryModel(squareState) {
|
|
15
16
|
const landed = replayLandedAudiences(squareState.acts);
|
|
17
|
+
const perception = derivePerceptionProjection(squareState, landed);
|
|
16
18
|
const roster = [...landed.joined];
|
|
17
19
|
const plannedByIndex = new Map();
|
|
18
20
|
let pendingByRecipient;
|
|
19
|
-
function canonicalRecipient(name) {
|
|
20
|
-
return landed.resolveParticipant(name) ?? name;
|
|
21
|
-
}
|
|
22
21
|
function isSeen(requestedRecipient, actOrIndex) {
|
|
23
|
-
const recipient = canonicalRecipient(requestedRecipient);
|
|
24
22
|
const index = typeof actOrIndex === 'number' ? actOrIndex : actOrIndex.index;
|
|
25
|
-
return
|
|
23
|
+
return perception.isSeen(requestedRecipient, index);
|
|
26
24
|
}
|
|
27
25
|
function plan(item) {
|
|
28
26
|
if (item.kind !== 'say')
|
|
@@ -64,25 +62,17 @@ export function deriveDeliveryModel(squareState) {
|
|
|
64
62
|
}
|
|
65
63
|
return [...(pendingByRecipient.get(recipient) ?? [])];
|
|
66
64
|
}
|
|
67
|
-
function directedTo(item, recipient) {
|
|
68
|
-
return item.kind === 'say' && landed.includes(item, recipient);
|
|
69
|
-
}
|
|
70
|
-
function perceive(item, viewer) {
|
|
71
|
-
if (item.kind !== 'say' || sameName(item.actor, viewer))
|
|
72
|
-
return 'full';
|
|
73
|
-
return directedTo(item, viewer) ? 'full' : 'presence';
|
|
74
|
-
}
|
|
75
65
|
return {
|
|
76
66
|
plan,
|
|
77
67
|
pendingFor,
|
|
78
|
-
directedTo,
|
|
79
|
-
perceive,
|
|
80
|
-
cursorFor:
|
|
68
|
+
directedTo: perception.directedTo,
|
|
69
|
+
perceive: perception.perceive,
|
|
70
|
+
cursorFor: perception.cursorFor,
|
|
81
71
|
isSeen,
|
|
82
72
|
knownParticipant: (name) => landed.resolveParticipant(name),
|
|
83
73
|
participants: () => landed.participants,
|
|
84
74
|
joinedRecipients: () => roster,
|
|
85
|
-
replayedActivityCount:
|
|
75
|
+
replayedActivityCount: perception.replayedActivityCount,
|
|
86
76
|
};
|
|
87
77
|
}
|
|
88
78
|
export function planActNotifications(squareState, item, delivery = deriveDeliveryModel(squareState)) {
|
package/dist/help.js
CHANGED
|
@@ -24,9 +24,9 @@ const COMMANDS = [
|
|
|
24
24
|
{ names: ['ignore'], usage: '--as <name> ignore <participant>', usesSquare: true, group: 'participant', summary: 'Ignore one participant\'s future mentions and bare says.' },
|
|
25
25
|
{ names: ['listening'], usage: '--as <name> listening', usesSquare: true, group: 'participant', summary: 'Show who you are turned toward.', details: ['Listening is future-only: the edge is fixed when a say lands; it never rewrites history.'] },
|
|
26
26
|
{
|
|
27
|
-
names: ['catch'], usage: '--as <name> catch (--now | --idle <duration>) [--from <names>] [--mention] [--limit <count>] [--replace]', usesSquare: true, group: 'participant',
|
|
27
|
+
names: ['catch'], usage: '--as <name> catch (--id <activity-id> [--now] | --now | --idle <duration>) [--from <names>] [--mention] [--limit <count>] [--replace]', usesSquare: true, group: 'participant',
|
|
28
28
|
summary: 'Catch directed conversation since you last looked.',
|
|
29
|
-
details: ['Modes:', ' --now Catch up immediately.', ' --idle <duration> Wait for something relevant, or for quiet to last this long.', '', 'Attention:', ' Mentions arrive when you are addressed; bells arrive for everyone; bare says require listen.', ' listen and ignore are future-only and fixed when each say lands.', '', 'Filters:', ' --from <names> Match only comma-separated participants.', ' --mention Match direct attention for your current participant (and bells).', ' --limit <count> Return one page (default 10, maximum 100).', '', 'Recovery:', ' --replace Replace another active catch for this participant.'],
|
|
29
|
+
details: ['Modes:', ' --id <activity-id> Catch exactly one activity addressed to you, with its full body; implies --now.', ' Marks only this activity seen; repeats return it again. No filters, limit, idle or replace.', ' history stays read-only; catch --id consumes the selected activity.', ' --now Catch up immediately.', ' --idle <duration> Wait for something relevant, or for quiet to last this long.', '', 'Attention:', ' Mentions arrive when you are addressed; bells arrive for everyone; bare says require listen.', ' listen and ignore are future-only and fixed when each say lands.', '', 'Filters:', ' --from <names> Match only comma-separated participants.', ' --mention Match direct attention for your current participant (and bells).', ' --limit <count> Return one page (default 10, maximum 100).', '', 'Recovery:', ' --replace Replace another active catch for this participant.'],
|
|
30
30
|
},
|
|
31
31
|
{ names: ['done'], usage: '--as <name> done [final | -]', usesSquare: true, group: 'participant', summary: 'Step out, optionally leaving a final note.' },
|
|
32
32
|
{
|
|
@@ -70,7 +70,7 @@ const COMMANDS = [
|
|
|
70
70
|
},
|
|
71
71
|
{
|
|
72
72
|
names: ['doctor'], usage: 'doctor', usesSquare: true, group: 'maintenance',
|
|
73
|
-
summary: 'Validate
|
|
73
|
+
summary: 'Validate artifact integrity.',
|
|
74
74
|
},
|
|
75
75
|
];
|
|
76
76
|
function definitionFor(command) {
|
|
@@ -2,11 +2,24 @@ import { promises as fs } from 'node:fs';
|
|
|
2
2
|
import fsSync from 'node:fs';
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
|
-
import { withFileLock } from './file-lock.js';
|
|
5
|
+
import { withFileLock as acquireFileLock } from './file-lock.js';
|
|
6
6
|
import { nameKey } from './model.js';
|
|
7
7
|
import { isCurrentlyJoined } from './runtime.js';
|
|
8
8
|
const LOCK = { retryMs: 10 };
|
|
9
9
|
const RETENTION = 7 * 86400000;
|
|
10
|
+
// Prepare before locking: lock databases and their journals are local runtime data too.
|
|
11
|
+
async function withFileLock(file, options, fn) {
|
|
12
|
+
const directory = path.dirname(file);
|
|
13
|
+
await fs.mkdir(directory, { recursive: true });
|
|
14
|
+
try {
|
|
15
|
+
await fs.writeFile(path.join(directory, '.gitignore'), '*\n', { flag: 'wx' });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (error.code !== 'EEXIST')
|
|
19
|
+
throw error;
|
|
20
|
+
}
|
|
21
|
+
return acquireFileLock(file, options, fn);
|
|
22
|
+
}
|
|
10
23
|
async function canon(value) { const absolute = path.resolve(value); try {
|
|
11
24
|
return await fs.realpath(absolute);
|
|
12
25
|
}
|
package/dist/model.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Act } from './square-core.js';
|
|
1
|
+
import type { Act, ActivityId } from './square-core.js';
|
|
2
2
|
export { formatActivityId, parseActivityId } from './square-core.js';
|
|
3
3
|
export type { Act, ActivityId, Audience, Reach } from './square-core.js';
|
|
4
4
|
export declare const WAKE_ROUTE_KINDS: readonly ["opencode-server", "codex-queue", "claude-native", "pi-extension", "paseo"];
|
|
@@ -108,6 +108,7 @@ export interface ActivitiesOptions {
|
|
|
108
108
|
json?: boolean;
|
|
109
109
|
}
|
|
110
110
|
export interface WatchOptions {
|
|
111
|
+
id?: ActivityId;
|
|
111
112
|
participants?: string[];
|
|
112
113
|
mention?: string;
|
|
113
114
|
limit?: number;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { HostLedgerPort, PresenceChannel, SquareArtifactPort } from './ports.js';
|
|
2
|
+
import { type WakeRoute } from './routes.js';
|
|
3
|
+
export interface HostContext {
|
|
4
|
+
readonly artifact: SquareArtifactPort;
|
|
5
|
+
readonly clock: () => number;
|
|
6
|
+
readonly location?: string;
|
|
7
|
+
readonly hostLedger?: HostLedgerPort;
|
|
8
|
+
readonly env?: NodeJS.ProcessEnv;
|
|
9
|
+
}
|
|
10
|
+
export declare function processIdentity(env: NodeJS.ProcessEnv): {
|
|
11
|
+
session: string;
|
|
12
|
+
channel: PresenceChannel;
|
|
13
|
+
};
|
|
14
|
+
export declare function identityRouteDraft(context: HostContext, participant: string): Promise<Omit<WakeRoute, 'updatedAt'> | undefined>;
|
|
15
|
+
export declare function publishIdentityRoute(context: HostContext, participant: string, epoch?: number): Promise<void>;
|
|
16
|
+
export declare function retireIdentityRoute(context: HostContext, participant: string, expectedEpoch?: number): Promise<void>;
|
|
17
|
+
export declare function assertLiveOwner(context: HostContext, participant: string, expectedEpoch?: number): Promise<boolean>;
|
|
18
|
+
/** Presence is best effort and runs only after the artifact mutation commits. */
|
|
19
|
+
export declare function ensureLocalPresence(context: HostContext, participant: string, epoch?: number): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { readParticipantOwner, sessionOwnsParticipant } from './registry.js';
|
|
2
|
+
import { defaultWakeRouteCapabilities, publishWakeRoute, retireWakeRouteFromArtifact, resolvePrimaryWakeRoute, ROUTE_FRESH_MS } from './routes.js';
|
|
3
|
+
export function processIdentity(env) {
|
|
4
|
+
const choices = [
|
|
5
|
+
[env.CLAUDE_CODE_SESSION_ID, 'claude-code'], [env.CODEX_THREAD_ID, 'codex'],
|
|
6
|
+
[env.OPENCODE_SESSION_ID, 'opencode'], [env.SQUARE_PI_SESSION_ID, 'pi'], [env.PASEO_AGENT_ID, 'paseo'],
|
|
7
|
+
];
|
|
8
|
+
const found = choices.find(([session]) => session?.trim());
|
|
9
|
+
return found === undefined ? { session: `process:${process.pid}`, channel: 'unknown' } : { session: found[0].trim(), channel: found[1] };
|
|
10
|
+
}
|
|
11
|
+
export async function identityRouteDraft(context, participant) {
|
|
12
|
+
if (context.location === undefined || context.location === 'memory')
|
|
13
|
+
return undefined;
|
|
14
|
+
const identity = processIdentity(context.env ?? process.env);
|
|
15
|
+
const provider = identity.channel === 'claude-code' ? 'claude' : identity.channel === 'opencode' ? 'opencode' : identity.channel === 'pi' ? 'pi' : identity.channel === 'paseo' ? 'paseo' : 'codex';
|
|
16
|
+
const capabilities = context.hostLedger === undefined ? { canUse: () => false } : await defaultWakeRouteCapabilities(context.hostLedger);
|
|
17
|
+
return resolvePrimaryWakeRoute({ location: context.location, participant, sessionId: identity.session, provider }, context.env ?? process.env, capabilities);
|
|
18
|
+
}
|
|
19
|
+
async function currentOwnerEpoch(context, participant) {
|
|
20
|
+
if (context.location === undefined || context.location === 'memory')
|
|
21
|
+
return undefined;
|
|
22
|
+
const owner = await readParticipantOwner(context.location, participant, context.env ?? process.env);
|
|
23
|
+
return owner?.epoch;
|
|
24
|
+
}
|
|
25
|
+
export async function publishIdentityRoute(context, participant, epoch) {
|
|
26
|
+
const route = await identityRouteDraft(context, participant);
|
|
27
|
+
if (route === undefined)
|
|
28
|
+
return;
|
|
29
|
+
const ownerEpoch = epoch ?? await currentOwnerEpoch(context, participant);
|
|
30
|
+
await publishWakeRoute(context.artifact, { ...route, ...(ownerEpoch === undefined ? {} : { epoch: ownerEpoch }) }, { at: context.clock(), requireCurrentSession: true }).catch(() => undefined);
|
|
31
|
+
}
|
|
32
|
+
export async function retireIdentityRoute(context, participant, expectedEpoch) {
|
|
33
|
+
if (context.location === undefined || context.location === 'memory')
|
|
34
|
+
return;
|
|
35
|
+
const identity = processIdentity(context.env ?? process.env);
|
|
36
|
+
await retireWakeRouteFromArtifact(context.artifact, { location: context.location, participant, sessionId: identity.session }, expectedEpoch === undefined ? {} : { expectedEpoch }).catch(() => undefined);
|
|
37
|
+
}
|
|
38
|
+
export async function assertLiveOwner(context, participant, expectedEpoch) {
|
|
39
|
+
if (context.hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
40
|
+
return true;
|
|
41
|
+
const identity = processIdentity(context.env ?? process.env);
|
|
42
|
+
// Library callers without a harness session are not ownership-fenced unless an epoch was supplied.
|
|
43
|
+
if (identity.channel === 'unknown' && expectedEpoch === undefined)
|
|
44
|
+
return true;
|
|
45
|
+
return sessionOwnsParticipant(context.location, participant, identity.session, context.env ?? process.env, expectedEpoch);
|
|
46
|
+
}
|
|
47
|
+
/** Presence is best effort and runs only after the artifact mutation commits. */
|
|
48
|
+
export async function ensureLocalPresence(context, participant, epoch) {
|
|
49
|
+
if (context.hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
50
|
+
return;
|
|
51
|
+
const identity = processIdentity(context.env ?? process.env);
|
|
52
|
+
const ownerEpoch = epoch ?? await currentOwnerEpoch(context, participant);
|
|
53
|
+
const now = context.clock();
|
|
54
|
+
// Semantic publish: an unexpired presence row already stands when it carries the same owner epoch.
|
|
55
|
+
try {
|
|
56
|
+
const existing = await context.hostLedger.listPresence({ location: context.location, participant, session: identity.session, scopes: ['local'], now });
|
|
57
|
+
if (existing.some((row) => row.channel === identity.channel
|
|
58
|
+
&& now - (row.updatedAt ?? 0) < ROUTE_FRESH_MS
|
|
59
|
+
&& (ownerEpoch === undefined || row.epoch === ownerEpoch)))
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
catch { /* fall through to the best-effort ensure below */ }
|
|
63
|
+
const result = await context.hostLedger.ensurePresence({
|
|
64
|
+
location: context.location,
|
|
65
|
+
participant,
|
|
66
|
+
session: identity.session,
|
|
67
|
+
channel: identity.channel,
|
|
68
|
+
// Presence rows are host-ledger wall-time evidence; the square clock belongs to artifact activities.
|
|
69
|
+
updatedAt: Date.now(),
|
|
70
|
+
...(ownerEpoch === undefined || ownerEpoch <= 0 ? {} : { epoch: ownerEpoch }),
|
|
71
|
+
}, 'local').catch((error) => ({
|
|
72
|
+
status: 'degraded',
|
|
73
|
+
record: { location: context.location, participant, session: identity.session, channel: identity.channel },
|
|
74
|
+
error,
|
|
75
|
+
}));
|
|
76
|
+
if (result.status === 'degraded')
|
|
77
|
+
process.stderr.write(`! host presence degraded: ${result.error instanceof Error ? result.error.message : String(result.error)}\n`);
|
|
78
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type LandedAudienceReplay, type Perception } from './square-core.js';
|
|
2
|
+
import { type SquareState, type StoredAct } from './model.js';
|
|
3
|
+
/**
|
|
4
|
+
* The small, state-only perception surface shared by catch and delivery.
|
|
5
|
+
* Notification planning deliberately remains outside this projection.
|
|
6
|
+
*/
|
|
7
|
+
export interface PerceptionProjection {
|
|
8
|
+
directedTo(activity: StoredAct, name: string): boolean;
|
|
9
|
+
perceive(activity: StoredAct, name: string): Perception;
|
|
10
|
+
cursorFor(name: string): number;
|
|
11
|
+
isSeen(name: string, index: number): boolean;
|
|
12
|
+
readonly replayedActivityCount: number;
|
|
13
|
+
}
|
|
14
|
+
/** Build perception over one audience replay of a SquareState snapshot. */
|
|
15
|
+
export declare function derivePerceptionProjection(state: SquareState, landed?: LandedAudienceReplay): PerceptionProjection;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { formatActivityId, replayLandedAudiences } from './square-core.js';
|
|
2
|
+
import { sameName } from './model.js';
|
|
3
|
+
import { readCursor } from './runtime.js';
|
|
4
|
+
/** Build perception over one audience replay of a SquareState snapshot. */
|
|
5
|
+
export function derivePerceptionProjection(state, landed = replayLandedAudiences(state.acts)) {
|
|
6
|
+
function canonicalName(name) {
|
|
7
|
+
return landed.resolveParticipant(name) ?? name;
|
|
8
|
+
}
|
|
9
|
+
function directedTo(activity, name) {
|
|
10
|
+
return activity.kind === 'say' && landed.includes(activity, name);
|
|
11
|
+
}
|
|
12
|
+
function perceive(activity, name) {
|
|
13
|
+
if (activity.kind !== 'say' || sameName(activity.actor, name))
|
|
14
|
+
return 'full';
|
|
15
|
+
return directedTo(activity, name) ? 'full' : 'presence';
|
|
16
|
+
}
|
|
17
|
+
function isSeen(name, index) {
|
|
18
|
+
const recipient = canonicalName(name);
|
|
19
|
+
return state.runtime.observations?.[recipient]?.[formatActivityId(index)]?.state === 'seen';
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
directedTo,
|
|
23
|
+
perceive,
|
|
24
|
+
cursorFor: (name) => readCursor(state, name, landed),
|
|
25
|
+
isSeen,
|
|
26
|
+
replayedActivityCount: landed.replayedActivityCount,
|
|
27
|
+
};
|
|
28
|
+
}
|
package/dist/ports.d.ts
CHANGED
|
@@ -11,10 +11,7 @@ export interface SquareArtifactPort {
|
|
|
11
11
|
transact<R>(fn: (state: SquareState, version: number) => {
|
|
12
12
|
state?: SquareState;
|
|
13
13
|
result: R;
|
|
14
|
-
}
|
|
15
|
-
state?: SquareState;
|
|
16
|
-
result: R;
|
|
17
|
-
}>): Promise<R>;
|
|
14
|
+
}): Promise<R>;
|
|
18
15
|
changed(sinceVersion: number, timeoutMs: number): Promise<boolean>;
|
|
19
16
|
close(): Promise<void>;
|
|
20
17
|
}
|
package/dist/presence.d.ts
CHANGED
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
import { type DeliveryModel } from './delivery.js';
|
|
2
|
-
import type { OpenSquare } from './open-square.js';
|
|
3
|
-
import type { CatchOptions, CatchResult } from './square-facade.js';
|
|
4
|
-
export declare function catchUp(square: OpenSquare, name: string, options?: CatchOptions, deriveDelivery?: (state: import('./model.js').SquareState) => DeliveryModel): Promise<CatchResult>;
|
|
5
1
|
/** Commit seen only for complete, actually rendered boundary bodies. */
|
|
6
2
|
export declare function markBoundarySeen(squarePath: string, name: string, actIndexes: readonly number[], at?: number): Promise<void>;
|
package/dist/presence.js
CHANGED
|
@@ -1,19 +1,6 @@
|
|
|
1
|
-
import { deriveDeliveryModel } from './delivery.js';
|
|
2
1
|
import { openSquare } from './square-file-adapter.js';
|
|
3
2
|
import { closeOpenSquare } from './open-square.js';
|
|
4
3
|
import { recordObservation } from './runtime.js';
|
|
5
|
-
import { catchUp as actionCatchUp } from './square-actions.js';
|
|
6
|
-
function operationContext(square) {
|
|
7
|
-
return 'artifact' in square
|
|
8
|
-
? { artifact: square.artifact, clock: square.clock, location: square.location, hostLedger: square.hostLedger, wakeTransport: square.wakeTransport, env: square.env }
|
|
9
|
-
: { artifact: square.cell, clock: square.clock };
|
|
10
|
-
}
|
|
11
|
-
export async function catchUp(square, name, options = {}, deriveDelivery = deriveDeliveryModel) {
|
|
12
|
-
const project = deriveDelivery === deriveDeliveryModel
|
|
13
|
-
? undefined
|
|
14
|
-
: (state) => deriveDelivery(state);
|
|
15
|
-
return actionCatchUp(operationContext(square), name, options, project);
|
|
16
|
-
}
|
|
17
4
|
/** Commit seen only for complete, actually rendered boundary bodies. */
|
|
18
5
|
export async function markBoundarySeen(squarePath, name, actIndexes, at = Date.now()) {
|
|
19
6
|
let square;
|
package/dist/routes.js
CHANGED
|
@@ -94,8 +94,9 @@ function sameRouteAddress(left, right) {
|
|
|
94
94
|
}
|
|
95
95
|
function canonicalRouteLocationSync(location) {
|
|
96
96
|
const absolute = path.resolve(location);
|
|
97
|
+
// Match fs.promises.realpath used by artifact and presence adapters, including Windows 8.3 aliases.
|
|
97
98
|
try {
|
|
98
|
-
return fs.realpathSync(absolute);
|
|
99
|
+
return fs.realpathSync.native(absolute);
|
|
99
100
|
}
|
|
100
101
|
catch {
|
|
101
102
|
return absolute;
|
package/dist/square-actions.d.ts
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import { type SquareState } from './model.js';
|
|
2
|
-
import type {
|
|
2
|
+
import type { WakeTransportPort } from './ports.js';
|
|
3
3
|
import type { Activity, CatchOptions, CatchResult, ExpressOptions, ExpressResult, OwnershipFenceOptions } from './square-facade.js';
|
|
4
4
|
import { type CatchProjection } from './catch-decisions.js';
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
readonly
|
|
8
|
-
readonly location?: string;
|
|
9
|
-
readonly hostLedger?: HostLedgerPort;
|
|
10
|
-
readonly wakeTransport?: import('./ports.js').WakeTransportPort;
|
|
11
|
-
readonly env?: NodeJS.ProcessEnv;
|
|
5
|
+
import { type HostContext } from './participant-host.js';
|
|
6
|
+
export interface OperationContext extends HostContext {
|
|
7
|
+
readonly wakeTransport?: WakeTransportPort;
|
|
12
8
|
}
|
|
13
9
|
export type { OwnershipFenceOptions };
|
|
14
10
|
export declare function catchUp(square: OperationContext, name: string, options?: CatchOptions, project?: (state: SquareState) => CatchProjection): Promise<CatchResult>;
|
package/dist/square-actions.js
CHANGED
|
@@ -4,89 +4,9 @@ import { isSquareError, nameKey, SquareError, validateName } from './model.js';
|
|
|
4
4
|
import { participantIdentity } from './participant-identity.js';
|
|
5
5
|
import { deliverPending } from './delivery-operations.js';
|
|
6
6
|
import { decideCatch } from './catch-decisions.js';
|
|
7
|
-
import { claimSessionParticipant, claimSessionTakeover, readParticipantOwner,
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
const choices = [
|
|
11
|
-
[env.CLAUDE_CODE_SESSION_ID, 'claude-code'], [env.CODEX_THREAD_ID, 'codex'],
|
|
12
|
-
[env.OPENCODE_SESSION_ID, 'opencode'], [env.SQUARE_PI_SESSION_ID, 'pi'], [env.PASEO_AGENT_ID, 'paseo'],
|
|
13
|
-
];
|
|
14
|
-
const found = choices.find(([session]) => session?.trim());
|
|
15
|
-
return found === undefined ? { session: `process:${process.pid}`, channel: 'unknown' } : { session: found[0].trim(), channel: found[1] };
|
|
16
|
-
}
|
|
17
|
-
async function identityRouteDraft(context, participant) {
|
|
18
|
-
if (context.location === undefined || context.location === 'memory')
|
|
19
|
-
return undefined;
|
|
20
|
-
const identity = processIdentity(context.env ?? process.env);
|
|
21
|
-
const provider = identity.channel === 'claude-code' ? 'claude' : identity.channel === 'opencode' ? 'opencode' : identity.channel === 'pi' ? 'pi' : identity.channel === 'paseo' ? 'paseo' : 'codex';
|
|
22
|
-
const capabilities = context.hostLedger === undefined ? { canUse: () => false } : await defaultWakeRouteCapabilities(context.hostLedger);
|
|
23
|
-
return resolvePrimaryWakeRoute({ location: context.location, participant, sessionId: identity.session, provider }, context.env ?? process.env, capabilities);
|
|
24
|
-
}
|
|
25
|
-
async function currentOwnerEpoch(context, participant) {
|
|
26
|
-
if (context.location === undefined || context.location === 'memory')
|
|
27
|
-
return undefined;
|
|
28
|
-
const owner = await readParticipantOwner(context.location, participant, context.env ?? process.env);
|
|
29
|
-
return owner?.epoch;
|
|
30
|
-
}
|
|
31
|
-
async function publishIdentityRoute(context, participant, epoch) {
|
|
32
|
-
if (context.location === undefined || context.location === 'memory')
|
|
33
|
-
return;
|
|
34
|
-
const identity = processIdentity(context.env ?? process.env);
|
|
35
|
-
const provider = identity.channel === 'claude-code' ? 'claude' : identity.channel === 'opencode' ? 'opencode' : identity.channel === 'pi' ? 'pi' : identity.channel === 'paseo' ? 'paseo' : 'codex';
|
|
36
|
-
const capabilities = context.hostLedger === undefined ? { canUse: () => false } : await defaultWakeRouteCapabilities(context.hostLedger);
|
|
37
|
-
const route = resolvePrimaryWakeRoute({ location: context.location, participant, sessionId: identity.session, provider }, context.env ?? process.env, capabilities);
|
|
38
|
-
if (route === undefined)
|
|
39
|
-
return;
|
|
40
|
-
const ownerEpoch = epoch ?? await currentOwnerEpoch(context, participant);
|
|
41
|
-
await publishWakeRoute(context.artifact, { ...route, ...(ownerEpoch === undefined ? {} : { epoch: ownerEpoch }) }, { at: context.clock(), requireCurrentSession: true }).catch(() => undefined);
|
|
42
|
-
}
|
|
43
|
-
async function retireIdentityRoute(context, participant, expectedEpoch) {
|
|
44
|
-
if (context.location === undefined || context.location === 'memory')
|
|
45
|
-
return;
|
|
46
|
-
const identity = processIdentity(context.env ?? process.env);
|
|
47
|
-
await retireWakeRouteFromArtifact(context.artifact, { location: context.location, participant, sessionId: identity.session }, expectedEpoch === undefined ? {} : { expectedEpoch }).catch(() => undefined);
|
|
48
|
-
}
|
|
49
|
-
async function assertLiveOwner(context, participant, expectedEpoch) {
|
|
50
|
-
if (context.hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
51
|
-
return true;
|
|
52
|
-
const identity = processIdentity(context.env ?? process.env);
|
|
53
|
-
// Library callers without a harness session are not ownership-fenced unless an epoch was supplied.
|
|
54
|
-
if (identity.channel === 'unknown' && expectedEpoch === undefined)
|
|
55
|
-
return true;
|
|
56
|
-
return sessionOwnsParticipant(context.location, participant, identity.session, context.env ?? process.env, expectedEpoch);
|
|
57
|
-
}
|
|
58
|
-
/** Presence is best effort and runs only after the artifact mutation commits. */
|
|
59
|
-
async function ensureLocalPresence(context, participant, epoch) {
|
|
60
|
-
if (context.hostLedger === undefined || context.location === undefined || context.location === 'memory')
|
|
61
|
-
return;
|
|
62
|
-
const identity = processIdentity(context.env ?? process.env);
|
|
63
|
-
const ownerEpoch = epoch ?? await currentOwnerEpoch(context, participant);
|
|
64
|
-
const now = context.clock();
|
|
65
|
-
// Semantic publish: an unexpired presence row already stands when it carries the same owner epoch.
|
|
66
|
-
try {
|
|
67
|
-
const existing = await context.hostLedger.listPresence({ location: context.location, participant, session: identity.session, scopes: ['local'], now });
|
|
68
|
-
if (existing.some((row) => row.channel === identity.channel
|
|
69
|
-
&& now - (row.updatedAt ?? 0) < ROUTE_FRESH_MS
|
|
70
|
-
&& (ownerEpoch === undefined || row.epoch === ownerEpoch)))
|
|
71
|
-
return;
|
|
72
|
-
}
|
|
73
|
-
catch { /* fall through to the best-effort ensure below */ }
|
|
74
|
-
const result = await context.hostLedger.ensurePresence({
|
|
75
|
-
location: context.location,
|
|
76
|
-
participant,
|
|
77
|
-
session: identity.session,
|
|
78
|
-
channel: identity.channel,
|
|
79
|
-
// Presence rows are host-ledger wall-time evidence; the square clock belongs to artifact activities.
|
|
80
|
-
updatedAt: Date.now(),
|
|
81
|
-
...(ownerEpoch === undefined || ownerEpoch <= 0 ? {} : { epoch: ownerEpoch }),
|
|
82
|
-
}, 'local').catch((error) => ({
|
|
83
|
-
status: 'degraded',
|
|
84
|
-
record: { location: context.location, participant, session: identity.session, channel: identity.channel },
|
|
85
|
-
error,
|
|
86
|
-
}));
|
|
87
|
-
if (result.status === 'degraded')
|
|
88
|
-
process.stderr.write(`! host presence degraded: ${result.error instanceof Error ? result.error.message : String(result.error)}\n`);
|
|
89
|
-
}
|
|
7
|
+
import { claimSessionParticipant, claimSessionTakeover, readParticipantOwner, withOwnershipClaimLock } from './registry.js';
|
|
8
|
+
import { assertLiveOwner, ensureLocalPresence, identityRouteDraft, processIdentity, publishIdentityRoute, retireIdentityRoute } from './participant-host.js';
|
|
9
|
+
import { applyWakeRouteToState, dropParticipantWakeRoutesFromState, dropSessionWakeRoutesFromState, sessionCanEndParticipant } from './routes.js';
|
|
90
10
|
function exposeCaught(activity, perception) {
|
|
91
11
|
if (activity.kind === 'read' || activity.actor === undefined)
|
|
92
12
|
throw new Error(`Cannot expose stored activity ${formatActivityId(activity.index)}`);
|
package/dist/square-facade.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
1
|
import path from 'node:path';
|
|
3
|
-
import { createSquareState, probeSquareFile, writeSquareSnapshot,
|
|
2
|
+
import { createSquareState, createSquareSnapshot, probeSquareFile, writeSquareSnapshot, openSquareCell, createMemoryCell, } from './square-storage.js';
|
|
4
3
|
import { InternalSquareError, SquareError, } from './model.js';
|
|
5
4
|
import { closeOpenSquare } from './open-square.js';
|
|
6
5
|
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
7
6
|
/** File-owned artifact creation for the CLI and path-backed public facade. */
|
|
8
7
|
export async function createSquare(squarePath, options, snippet) {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const state = createSquareState(options, snippet);
|
|
9
|
+
if (options.force) {
|
|
10
|
+
await writeSquareSnapshot(squarePath, state);
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
if (!await createSquareSnapshot(squarePath, state)) {
|
|
14
|
+
throw new InternalSquareError('conflict', `Refusing to overwrite existing square: ${squarePath}\nPass -f to overwrite.`);
|
|
15
|
+
}
|
|
15
16
|
}
|
|
16
17
|
function validateBuildOptions(options) {
|
|
17
18
|
if (options.hardCap !== undefined && options.hardCap !== null
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type BuildOptions, type HardCap, type SquareRuntimeState, type SquareState } from './model.js';
|
|
2
|
+
export declare function validateSquareState(value: unknown): SquareState;
|
|
3
|
+
export declare function emptyRuntimeState(nextActIndex?: number): SquareRuntimeState;
|
|
4
|
+
export declare function createSquareState(options: BuildOptions & {
|
|
5
|
+
hardCap: HardCap;
|
|
6
|
+
}, snippet: string): SquareState;
|