@astrosheep/square 0.3.29 → 0.3.31
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/hooks/hooks.json +0 -3
- package/claude-plugin/skills/square/SKILL.md +8 -7
- package/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +7 -3
- package/dist/artifact.js +7 -50
- package/dist/automatic-session.js +31 -14
- package/dist/boundary-presentation.d.ts +1 -1
- package/dist/boundary-presentation.js +58 -14
- package/dist/catch-decisions.d.ts +17 -0
- package/dist/catch-decisions.js +53 -0
- package/dist/claude-hook.d.ts +1 -1
- package/dist/cli/context.js +10 -2
- package/dist/cli/observation-commands.d.ts +5 -1
- package/dist/cli/observation-commands.js +68 -33
- package/dist/cli/square-commands.js +17 -10
- package/dist/codex-hook.d.ts +1 -1
- package/dist/decisions.js +2 -2
- package/dist/delivery-health.d.ts +1 -1
- package/dist/delivery-operations.d.ts +25 -0
- package/dist/delivery-operations.js +124 -0
- package/dist/help.js +1 -1
- package/dist/host-ledger-file-adapter.d.ts +34 -0
- package/dist/host-ledger-file-adapter.js +165 -0
- package/dist/host-ledger.d.ts +160 -0
- package/dist/host-ledger.js +1 -0
- package/dist/inbox.d.ts +2 -2
- package/dist/inbox.js +22 -14
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1 -0
- package/dist/landing.d.ts +18 -14
- package/dist/landing.js +29 -113
- package/dist/model.d.ts +6 -17
- package/dist/notifications.d.ts +6 -10
- package/dist/notifications.js +71 -192
- package/dist/open-square.d.ts +4 -4
- package/dist/open-square.js +1 -1
- package/dist/ports.d.ts +127 -0
- package/dist/ports.js +1 -0
- package/dist/presence.d.ts +1 -2
- package/dist/presence.js +13 -46
- package/dist/presentation-operations.d.ts +3 -0
- package/dist/presentation-operations.js +51 -0
- package/dist/presentation.d.ts +2 -2
- package/dist/presentation.js +11 -7
- package/dist/presented.d.ts +1 -12
- package/dist/presented.js +6 -75
- package/dist/registry.d.ts +10 -10
- package/dist/registry.js +48 -113
- package/dist/routes.d.ts +6 -33
- package/dist/routes.js +6 -170
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -4
- package/dist/square-actions.d.ts +32 -0
- package/dist/square-actions.js +167 -0
- package/dist/square-facade.d.ts +7 -7
- package/dist/square-file-adapter.d.ts +4 -3
- package/dist/square-file-adapter.js +22 -5
- package/dist/square-projections.d.ts +68 -0
- package/dist/square-projections.js +87 -0
- package/dist/square-storage.d.ts +2 -2
- package/dist/square-storage.js +14 -9
- package/dist/square-wiring.d.ts +3 -3
- package/dist/square-wiring.js +44 -29
- package/dist/views.d.ts +8 -2
- package/dist/views.js +28 -24
- package/dist/wake-attempts.d.ts +29 -22
- package/dist/wake-attempts.js +36 -119
- package/dist/wake-evidence.d.ts +6 -18
- package/dist/wake-evidence.js +17 -80
- package/dist/wakes.d.ts +2 -16
- package/dist/wakes.js +7 -27
- package/dist/watch.js +2 -3
- package/extensions/square-pi.js +7 -2
- package/package.json +1 -1
- package/skills/brainstorm/SKILL.md +2 -2
- package/skills/square/SKILL.md +8 -7
|
@@ -4,9 +4,6 @@
|
|
|
4
4
|
"SessionStart": [
|
|
5
5
|
{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }
|
|
6
6
|
],
|
|
7
|
-
"SessionResume": [
|
|
8
|
-
{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }
|
|
9
|
-
],
|
|
10
7
|
"SessionEnd": [
|
|
11
8
|
{ "hooks": [{ "type": "command", "command": "square claude-hook", "timeout": 5 }] }
|
|
12
9
|
],
|
|
@@ -61,7 +61,7 @@ The ownership boundary belongs here. @bob, does this match your read?
|
|
|
61
61
|
EOF
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
**Addressing.** Normally address whoever needs the activity with `@name`: mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. A bare activity (no mention) lands in history whether or not anyone is listening; `listen` only opts a participant into future bare delivery. Use `--bell` only when every participant needs it. Addressing is not a secrecy boundary —
|
|
64
|
+
**Addressing.** Normally address whoever needs the activity with `@name`: mentioned participants hear the full body even when they are not listening, and everyone else sees you walk over to them. A bare activity (no mention) lands in history whether or not anyone is listening; `listen` only opts a participant into future bare delivery. Use `--bell` only when every participant needs it. Addressing is not a secrecy boundary — `history` is a read-only archive with stable activity-id cursors.
|
|
65
65
|
|
|
66
66
|
**Discipline.** Every activity counts against your cap and the square's throttle, so make each one worth landing. Keep private progress and tool chatter out — express only when another participant needs the thought, question, or decision.
|
|
67
67
|
|
|
@@ -74,11 +74,11 @@ EOF
|
|
|
74
74
|
```bash
|
|
75
75
|
square --location <square> --as <name> catch --now # take in what is pending
|
|
76
76
|
square --location <square> --as <name> catch --idle 30m # wait until something relevant lands, or 30m of quiet
|
|
77
|
-
square --location <square> --as <name> catch --mention
|
|
78
|
-
square --location <square> --as <name> catch --from <names>
|
|
77
|
+
square --location <square> --as <name> catch --now --mention # take in pending mentions
|
|
78
|
+
square --location <square> --as <name> catch --now --from <names> # take in pending activity from named participants
|
|
79
79
|
```
|
|
80
80
|
|
|
81
|
-
Waiting with `catch --idle` is the normal way to stay present between expressions — `join` prints the exact command to keep open. Do not build a polling loop.
|
|
81
|
+
Every catch needs exactly one mode: `--now` or `--idle <duration>`. `--mention` and `--from` filter either mode; they do not replace it. Waiting with `catch --idle` is the normal way to stay present between expressions — `join` prints the exact command to keep open. Do not build a polling loop.
|
|
82
82
|
|
|
83
83
|
## Listen
|
|
84
84
|
|
|
@@ -98,13 +98,14 @@ square --location <square> --as <name> ignore <participant>
|
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
100
|
square history --limit 5 # most recent 5, oldest to newest
|
|
101
|
+
square history --before act/12 --limit 5 # the page before act/12
|
|
102
|
+
square history --after act/12 --limit 5 # the page after act/12
|
|
101
103
|
square history --limit 5 --order desc # newest first
|
|
102
|
-
square history --
|
|
103
|
-
square history --full # expand bodies in range
|
|
104
|
+
square history --no-truncate # expand preview bodies
|
|
104
105
|
square history --grep 'term' # search
|
|
105
106
|
```
|
|
106
107
|
|
|
107
|
-
See `square history --help` for advanced usage. Never read or parse the binary Square artifact directly
|
|
108
|
+
See `square history --help` for advanced usage. Never read or parse the binary Square artifact directly. Bodies are previews by default; follow the printed activity-id command to continue page by page.
|
|
108
109
|
|
|
109
110
|
## Hold
|
|
110
111
|
|
package/dist/activity.js
CHANGED
|
@@ -5,7 +5,6 @@ import { setTimeout as sleep } from 'node:timers/promises';
|
|
|
5
5
|
import { SquareError, isSquareError, validateName } from './model.js';
|
|
6
6
|
import { expressHintLine, renderActivityBlocked, renderActivityLimit, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, withPathOutput, } from './presentation.js';
|
|
7
7
|
import { nowMs, SLEEP_MS } from './runtime.js';
|
|
8
|
-
import { wakeNotifierForSquare } from './notifications.js';
|
|
9
8
|
import { openSquare } from './square-file-adapter.js';
|
|
10
9
|
import { closeOpenSquare } from './open-square.js';
|
|
11
10
|
import { Square } from './square-wiring.js';
|
|
@@ -67,7 +66,7 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
67
66
|
const noWait = opts.noWait ?? false;
|
|
68
67
|
const reach = opts.reach === 'bell' ? 'bell' : undefined;
|
|
69
68
|
let announcedWait;
|
|
70
|
-
const square = await Square.at({ path: squarePath, clock: nowMs
|
|
69
|
+
const square = await Square.at({ path: squarePath, clock: nowMs });
|
|
71
70
|
try {
|
|
72
71
|
const participant = await square.join(name);
|
|
73
72
|
while (true) {
|
|
@@ -76,11 +75,16 @@ export async function cmdActivity(squarePath, name, activity, resolveBody, opts)
|
|
|
76
75
|
const pendingPublic = before.pendingPublic;
|
|
77
76
|
const pendingRoomChanges = before.pendingRoomChanges;
|
|
78
77
|
try {
|
|
79
|
-
await participant.express(body, {
|
|
78
|
+
const result = await participant.express(body, {
|
|
80
79
|
force,
|
|
81
80
|
...(reach === undefined ? {} : { reach }),
|
|
82
81
|
...(opts.reply === undefined ? {} : { reply: formatActivityId(opts.reply) }),
|
|
83
82
|
});
|
|
83
|
+
try {
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// Compatibility wake is post-commit and cannot undo the activity.
|
|
87
|
+
}
|
|
84
88
|
const freshSquare = await openSquare(squarePath, { clock: nowMs });
|
|
85
89
|
const fresh = await activityPresentation(freshSquare, knownName).finally(() => closeOpenSquare(freshSquare));
|
|
86
90
|
const headerCount = fresh.participantCount;
|
package/dist/artifact.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'node:fs';
|
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { TextDecoder } from 'node:util';
|
|
5
5
|
import zlib from 'node:zlib';
|
|
6
|
-
import {
|
|
6
|
+
import { InternalSquareError, SquareError, } from './model.js';
|
|
7
7
|
import { parseActivityId } from './square-core.js';
|
|
8
8
|
const SQUARE_MAGIC = Buffer.from('SQUARE01', 'ascii');
|
|
9
9
|
const LENGTH_BYTES = 4;
|
|
@@ -37,16 +37,14 @@ function isStringArray(value) {
|
|
|
37
37
|
}
|
|
38
38
|
function validateObservation(value) {
|
|
39
39
|
return isObject(value)
|
|
40
|
-
&& hasExactKeys(value, ['state', 'at']
|
|
41
|
-
&&
|
|
42
|
-
&& isFiniteNumber(value.at)
|
|
43
|
-
&& (value.ownerId === undefined || isNonblankString(value.ownerId));
|
|
40
|
+
&& hasExactKeys(value, ['state', 'at'])
|
|
41
|
+
&& value.state === 'seen'
|
|
42
|
+
&& isFiniteNumber(value.at);
|
|
44
43
|
}
|
|
45
44
|
function validateWatchLease(value) {
|
|
46
45
|
if (!isObject(value)
|
|
47
|
-
|| !hasExactKeys(value, ['leaseId', 'heartbeatAt', 'expiresAt'], ['
|
|
46
|
+
|| !hasExactKeys(value, ['leaseId', 'heartbeatAt', 'expiresAt'], ['filter'])
|
|
48
47
|
|| !isNonblankString(value.leaseId)
|
|
49
|
-
|| (value.ownerId !== undefined && !isNonblankString(value.ownerId))
|
|
50
48
|
|| !isFiniteNumber(value.heartbeatAt)
|
|
51
49
|
|| !isFiniteNumber(value.expiresAt)
|
|
52
50
|
|| value.expiresAt < value.heartbeatAt)
|
|
@@ -58,49 +56,16 @@ function validateWatchLease(value) {
|
|
|
58
56
|
&& (value.filter.participants === undefined || isStringArray(value.filter.participants))
|
|
59
57
|
&& (value.filter.mention === undefined || typeof value.filter.mention === 'string');
|
|
60
58
|
}
|
|
61
|
-
function validateNotifyLease(value) {
|
|
62
|
-
if (!isObject(value)
|
|
63
|
-
|| !hasExactKeys(value, ['leaseId', 'expiresAt', 'phase'], ['attemptN', 'routeKind'])
|
|
64
|
-
|| !isNonblankString(value.leaseId)
|
|
65
|
-
|| !isFiniteNumber(value.expiresAt)
|
|
66
|
-
|| (value.phase !== 'claimed' && value.phase !== 'dispatching')
|
|
67
|
-
|| (value.attemptN !== undefined && (!Number.isSafeInteger(value.attemptN) || value.attemptN <= 0))
|
|
68
|
-
|| (value.routeKind !== undefined && !isWakeRouteKind(value.routeKind)))
|
|
69
|
-
return false;
|
|
70
|
-
return value.phase !== 'dispatching' || (value.attemptN !== undefined && value.routeKind !== undefined);
|
|
71
|
-
}
|
|
72
59
|
function validateRecord(value, item) {
|
|
73
60
|
return isObject(value)
|
|
74
61
|
&& Object.entries(value).every(([key, candidate]) => key.length > 0 && item(candidate));
|
|
75
62
|
}
|
|
76
|
-
function parseNotifyLeaseKey(key) {
|
|
77
|
-
let parsed;
|
|
78
|
-
try {
|
|
79
|
-
parsed = JSON.parse(key);
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
return undefined;
|
|
83
|
-
}
|
|
84
|
-
if (!Array.isArray(parsed) || parsed.length !== 2)
|
|
85
|
-
return undefined;
|
|
86
|
-
const [id, name] = parsed;
|
|
87
|
-
if (typeof id !== 'string' || typeof name !== 'string' || name.length === 0 || nameKey(name) !== name) {
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
const index = parseActivityId(id);
|
|
91
|
-
if (index === undefined)
|
|
92
|
-
return undefined;
|
|
93
|
-
if (JSON.stringify([id, name]) !== key)
|
|
94
|
-
return undefined;
|
|
95
|
-
return { index, name };
|
|
96
|
-
}
|
|
97
63
|
function validateRuntime(value) {
|
|
98
64
|
if (!isObject(value)
|
|
99
|
-
|| !hasExactKeys(value, ['nextActIndex', 'observations', 'leases'
|
|
65
|
+
|| !hasExactKeys(value, ['nextActIndex', 'observations', 'leases'])
|
|
100
66
|
|| !isNonNegativeInteger(value.nextActIndex)
|
|
101
67
|
|| !validateRecord(value.observations, (candidate) => isObject(candidate) && Object.entries(candidate).every(([id, observation]) => parseActivityId(id) !== undefined && validateObservation(observation)))
|
|
102
|
-
|| !validateRecord(value.leases, validateWatchLease)
|
|
103
|
-
|| !validateRecord(value.notifyLeases, validateNotifyLease))
|
|
68
|
+
|| !validateRecord(value.leases, validateWatchLease))
|
|
104
69
|
return false;
|
|
105
70
|
return true;
|
|
106
71
|
}
|
|
@@ -115,13 +80,6 @@ function validateAssignedRuntimeReferences(runtime) {
|
|
|
115
80
|
return 'future';
|
|
116
81
|
}
|
|
117
82
|
}
|
|
118
|
-
for (const key of Object.keys(runtime.notifyLeases)) {
|
|
119
|
-
const parsed = parseNotifyLeaseKey(key);
|
|
120
|
-
if (parsed === undefined)
|
|
121
|
-
return 'malformed';
|
|
122
|
-
if (parsed.index >= bound)
|
|
123
|
-
return 'future';
|
|
124
|
-
}
|
|
125
83
|
return 'ok';
|
|
126
84
|
}
|
|
127
85
|
function validateActor(value, required) {
|
|
@@ -253,7 +211,6 @@ export function emptyRuntimeState(nextActIndex = 0) {
|
|
|
253
211
|
nextActIndex,
|
|
254
212
|
observations: {},
|
|
255
213
|
leases: {},
|
|
256
|
-
notifyLeases: {},
|
|
257
214
|
};
|
|
258
215
|
}
|
|
259
216
|
function normalizedLines(value) {
|
|
@@ -4,8 +4,9 @@ import { openSquare } from './square-file-adapter.js';
|
|
|
4
4
|
import { closeOpenSquare } from './open-square.js';
|
|
5
5
|
import { Square } from './square-wiring.js';
|
|
6
6
|
import { entryPresentation } from './views.js';
|
|
7
|
-
import { canonicalSquarePath, lookupSessionBindings, recordSessionDone, recordSessionJoin } from './registry.js';
|
|
8
7
|
import { automaticParticipant } from './participant-identity.js';
|
|
8
|
+
import { createHostLedgerPort } from './host-ledger-file-adapter.js';
|
|
9
|
+
import { projectSessionBindings } from './square-projections.js';
|
|
9
10
|
export { automaticParticipant } from './participant-identity.js';
|
|
10
11
|
const providerEnv = {
|
|
11
12
|
codex: 'CODEX_THREAD_ID',
|
|
@@ -13,6 +14,16 @@ const providerEnv = {
|
|
|
13
14
|
opencode: 'OPENCODE_SESSION_ID',
|
|
14
15
|
pi: 'SQUARE_PI_SESSION_ID',
|
|
15
16
|
};
|
|
17
|
+
function operationEnv(provider, sessionId, env) {
|
|
18
|
+
return { ...env, [providerEnv[provider]]: sessionId };
|
|
19
|
+
}
|
|
20
|
+
function hostLedgerForEnv(env) {
|
|
21
|
+
const root = env.SQUARE_REGISTRY === undefined ? undefined : path.dirname(env.SQUARE_REGISTRY);
|
|
22
|
+
return createHostLedgerPort({
|
|
23
|
+
userPath: env.SQUARE_HOST_LEDGER_USER ?? root,
|
|
24
|
+
localPath: env.SQUARE_HOST_LEDGER_LOCAL ?? root,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
16
27
|
export function publicSquarePath(cwd) {
|
|
17
28
|
return path.join(cwd, '.square', 'PUBLIC.square');
|
|
18
29
|
}
|
|
@@ -38,16 +49,15 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
|
|
|
38
49
|
return undefined;
|
|
39
50
|
}
|
|
40
51
|
const name = automaticParticipant(provider, sessionId, env);
|
|
41
|
-
const canonicalPath = await canonicalSquarePath(squarePath);
|
|
42
|
-
const alreadyBound = (await lookupSessionBindings(sessionId)).some((binding) => binding.squarePath === canonicalPath && binding.name === name);
|
|
43
52
|
await closeOpenSquare(reader);
|
|
44
|
-
const
|
|
53
|
+
const scopedEnv = operationEnv(provider, sessionId, env);
|
|
54
|
+
const square = await Square.at({ path: squarePath, hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
|
|
45
55
|
try {
|
|
46
56
|
const implicit = await square.implicitJoin(name);
|
|
47
|
-
if (implicit.state === 'done'
|
|
57
|
+
if (implicit.state === 'done')
|
|
48
58
|
return undefined;
|
|
49
|
-
|
|
50
|
-
await
|
|
59
|
+
await hostLedgerForEnv(env).ensurePresence({ location: squarePath, participant: name, session: sessionId, channel: provider === 'claude' ? 'claude-code' : provider, updatedAt: Date.now() });
|
|
60
|
+
await square.reconcileBinding();
|
|
51
61
|
return undefined;
|
|
52
62
|
}
|
|
53
63
|
finally {
|
|
@@ -56,22 +66,29 @@ export async function automaticSessionStart(provider, sessionId, cwd, env = proc
|
|
|
56
66
|
}
|
|
57
67
|
export async function automaticSessionEnd(provider, sessionId, cwd, env = process.env) {
|
|
58
68
|
const squarePath = publicSquarePath(cwd);
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
const
|
|
69
|
+
const scopedEnv = operationEnv(provider, sessionId, env);
|
|
70
|
+
const hostLedger = hostLedgerForEnv(scopedEnv);
|
|
71
|
+
const probe = await openSquare(squarePath, { hostLedger, env: scopedEnv });
|
|
72
|
+
const binding = (await projectSessionBindings({
|
|
73
|
+
hostLedger,
|
|
74
|
+
sessionId,
|
|
75
|
+
location: probe.location,
|
|
76
|
+
scopes: ['user', 'local'],
|
|
77
|
+
}))[0];
|
|
78
|
+
await closeOpenSquare(probe);
|
|
62
79
|
if (binding === undefined || !await squareExists(squarePath))
|
|
63
80
|
return;
|
|
64
81
|
const reader = await openSquare(squarePath);
|
|
65
|
-
const joined = await entryPresentation(reader, binding.
|
|
82
|
+
const joined = await entryPresentation(reader, binding.participant).finally(() => closeOpenSquare(reader));
|
|
66
83
|
if (!joined.joined)
|
|
67
84
|
return;
|
|
68
|
-
const square = await Square.at({ path: squarePath });
|
|
85
|
+
const square = await Square.at({ path: squarePath, hostLedger: hostLedgerForEnv(scopedEnv), env: scopedEnv });
|
|
69
86
|
try {
|
|
70
|
-
const participant = await square.join(binding.
|
|
87
|
+
const participant = await square.join(binding.participant);
|
|
71
88
|
await participant.done();
|
|
89
|
+
await square.reconcileBinding();
|
|
72
90
|
}
|
|
73
91
|
finally {
|
|
74
92
|
await square.close();
|
|
75
93
|
}
|
|
76
|
-
await recordSessionDone(sessionId, binding.name, squarePath, channel, env);
|
|
77
94
|
}
|
|
@@ -2,4 +2,4 @@ import type { InboxMembership } from './model.js';
|
|
|
2
2
|
/** A fresh blocking catch owns only the notifications admitted by its filter. */
|
|
3
3
|
export declare function pendingAtBoundary(inbox: InboxMembership[]): InboxMembership[];
|
|
4
4
|
export declare function renderPendingAtBoundary(inbox: InboxMembership[]): string;
|
|
5
|
-
export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
5
|
+
export declare function presentPendingAtBoundary<T>(sessionId: string, present: (context: string) => T | Promise<T>, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv, signal?: AbortSignal): Promise<T | undefined>;
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { leaseOwnsNotification } from './delivery.js';
|
|
2
|
-
import {
|
|
2
|
+
import { openSquare } from './square-file-adapter.js';
|
|
3
|
+
import { closeOpenSquare } from './open-square.js';
|
|
4
|
+
import { presentPending } from './presentation-operations.js';
|
|
3
5
|
import { sessionInbox } from './inbox.js';
|
|
4
6
|
import { ATTENTION_BODY_MAX, renderAttentionPreview } from './attention-presentation.js';
|
|
5
|
-
import { presentOnce } from './presented.js';
|
|
6
7
|
const CONTEXT_MAX = 1200;
|
|
8
|
+
const presentationLocks = new Map();
|
|
7
9
|
function pendingCount(inbox) {
|
|
8
10
|
return inbox.reduce((total, membership) => total + membership.notifications.length, 0);
|
|
9
11
|
}
|
|
@@ -60,9 +62,11 @@ function renderBoundary(inbox) {
|
|
|
60
62
|
continue;
|
|
61
63
|
}
|
|
62
64
|
blocks.push(block);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
65
|
+
complete.push({
|
|
66
|
+
membership,
|
|
67
|
+
actIndexes: [notification.actIndex],
|
|
68
|
+
markSeen: notification.body.replace(/\r\n/g, '\n').length <= ATTENTION_BODY_MAX,
|
|
69
|
+
});
|
|
66
70
|
}
|
|
67
71
|
return {
|
|
68
72
|
context: [
|
|
@@ -77,15 +81,55 @@ function renderBoundary(inbox) {
|
|
|
77
81
|
};
|
|
78
82
|
}
|
|
79
83
|
export async function presentPendingAtBoundary(sessionId, present, lookup = sessionInbox, env = process.env, signal) {
|
|
80
|
-
const inbox = await lookup(sessionId);
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
84
|
+
const inbox = await lookup(sessionId, env);
|
|
85
|
+
if (signal?.aborted)
|
|
86
|
+
return undefined;
|
|
87
|
+
const delivered = renderBoundary(pendingAtBoundary(inbox));
|
|
88
|
+
if (delivered.context === '')
|
|
89
|
+
return undefined;
|
|
90
|
+
let result;
|
|
91
|
+
let rendered = false;
|
|
92
|
+
const renderOnce = async () => { if (!rendered) {
|
|
93
|
+
rendered = true;
|
|
94
|
+
result = await present(delivered.context);
|
|
95
|
+
} };
|
|
96
|
+
for (const entry of delivered.complete) {
|
|
97
|
+
let square;
|
|
98
|
+
try {
|
|
99
|
+
try {
|
|
100
|
+
square = await openSquare(entry.membership.squarePath, { env });
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
for (const index of entry.actIndexes) {
|
|
106
|
+
const key = `${entry.membership.squarePath}\u0000${entry.membership.name.toLocaleLowerCase()}\u0000${index}`;
|
|
107
|
+
const prior = presentationLocks.get(key);
|
|
108
|
+
if (prior !== undefined) {
|
|
109
|
+
await prior;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
const work = presentPending({ artifact: square.artifact, location: entry.membership.squarePath, participant: entry.membership.name, activity: index, hostLedger: square.hostLedger, session: sessionId, sink: { present: renderOnce }, markSeen: entry.markSeen, now: Date.now() }).then(async (outcome) => {
|
|
113
|
+
// A stale projection can outlive its activity; still surface the bounded preview,
|
|
114
|
+
// but there is no artifact observation to commit.
|
|
115
|
+
if (!outcome.presented) {
|
|
116
|
+
const snapshot = await square.artifact.read().catch(() => undefined);
|
|
117
|
+
if (snapshot?.state.acts.every((activity) => activity.index !== index))
|
|
118
|
+
await renderOnce();
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
presentationLocks.set(key, work);
|
|
122
|
+
try {
|
|
123
|
+
await work;
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
presentationLocks.delete(key);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
finally {
|
|
131
|
+
if (square !== undefined)
|
|
132
|
+
await closeOpenSquare(square);
|
|
89
133
|
}
|
|
90
134
|
}
|
|
91
135
|
return result;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type Perception } from './square-core.js';
|
|
2
|
+
import { type SquareState, type StoredAct } from './model.js';
|
|
3
|
+
import type { CatchOptions } from './square-facade.js';
|
|
4
|
+
export interface CatchDecision {
|
|
5
|
+
readonly viewer: string;
|
|
6
|
+
readonly delivered: readonly StoredAct[];
|
|
7
|
+
readonly perceptions: ReadonlyMap<number, Perception>;
|
|
8
|
+
readonly consumedThrough: string | null;
|
|
9
|
+
readonly changed: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface CatchProjection {
|
|
12
|
+
cursorFor(name: string): number;
|
|
13
|
+
directedTo(activity: StoredAct, name: string): boolean;
|
|
14
|
+
perceive(activity: StoredAct, name: string): Perception;
|
|
15
|
+
}
|
|
16
|
+
/** Pure catch selection and acknowledgement over one SquareState snapshot. */
|
|
17
|
+
export declare function decideCatch(state: SquareState, requestedName: string, options: CatchOptions, at: number, project?: (state: SquareState) => CatchProjection): CatchDecision;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { audienceOf, formatActivityId, replayLandedAudiences } from './square-core.js';
|
|
2
|
+
import { matchesMentionTarget, readCursor, recordObservation } from './runtime.js';
|
|
3
|
+
import { participantIdentity } from './participant-identity.js';
|
|
4
|
+
import { SquareError, sameName, validateName } from './model.js';
|
|
5
|
+
import { resolveRosterName, rosterNames } from './runtime.js';
|
|
6
|
+
function defaultProjection(state) {
|
|
7
|
+
const landed = replayLandedAudiences(state.acts);
|
|
8
|
+
return {
|
|
9
|
+
cursorFor: (name) => readCursor(state, name, landed),
|
|
10
|
+
directedTo: (activity, name) => activity.kind === 'say' && landed.includes(activity, name),
|
|
11
|
+
perceive: (activity, name) => activity.kind !== 'say' || sameName(activity.actor, name) || landed.includes(activity, name) ? 'full' : 'presence',
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function resolveCatchName(state, requestedName) {
|
|
15
|
+
validateName(requestedName);
|
|
16
|
+
const known = resolveRosterName(state, requestedName);
|
|
17
|
+
if (known === undefined) {
|
|
18
|
+
throw new SquareError('invalid_args', `Unknown participant "${participantIdentity(requestedName)}". Expected one of: ${rosterNames(state).map(participantIdentity).join(', ')}.`);
|
|
19
|
+
}
|
|
20
|
+
return known;
|
|
21
|
+
}
|
|
22
|
+
/** Pure catch selection and acknowledgement over one SquareState snapshot. */
|
|
23
|
+
export function decideCatch(state, requestedName, options, at, project = defaultProjection) {
|
|
24
|
+
const viewer = resolveCatchName(state, requestedName);
|
|
25
|
+
const delivery = project(state);
|
|
26
|
+
const cursor = delivery.cursorFor(viewer);
|
|
27
|
+
const from = options.from;
|
|
28
|
+
const mentionOnly = options.mention === true;
|
|
29
|
+
const delivered = [];
|
|
30
|
+
const perceptions = new Map();
|
|
31
|
+
for (const activity of state.acts) {
|
|
32
|
+
if (activity.index <= cursor || activity.kind !== 'say' || !delivery.directedTo(activity, viewer))
|
|
33
|
+
continue;
|
|
34
|
+
if (from !== undefined && !from.some((participant) => sameName(participant, activity.actor)))
|
|
35
|
+
continue;
|
|
36
|
+
if (mentionOnly && audienceOf(activity).kind !== 'bell' && !matchesMentionTarget(activity, viewer))
|
|
37
|
+
continue;
|
|
38
|
+
delivered.push(activity);
|
|
39
|
+
perceptions.set(activity.index, delivery.perceive(activity, viewer));
|
|
40
|
+
}
|
|
41
|
+
let changed = false;
|
|
42
|
+
for (const activity of delivered) {
|
|
43
|
+
changed = recordObservation(state, viewer, activity.index, 'seen', at) || changed;
|
|
44
|
+
}
|
|
45
|
+
const consumed = delivery.cursorFor(viewer);
|
|
46
|
+
return {
|
|
47
|
+
viewer,
|
|
48
|
+
delivered,
|
|
49
|
+
perceptions,
|
|
50
|
+
consumedThrough: consumed < 0 ? null : formatActivityId(consumed),
|
|
51
|
+
changed,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/dist/claude-hook.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export interface NativeHookInput {
|
|
|
5
5
|
cwd?: unknown;
|
|
6
6
|
}
|
|
7
7
|
export declare function runClaudeHookAsync(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
|
8
|
-
export declare function claudeHookResponse(input: NativeHookInput, lookup?: (sessionId: string) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
|
|
8
|
+
export declare function claudeHookResponse(input: NativeHookInput, lookup?: (sessionId: string, env?: NodeJS.ProcessEnv) => Promise<InboxMembership[]> | InboxMembership[], env?: NodeJS.ProcessEnv): Promise<object | undefined>;
|
|
9
9
|
export declare function runClaudeHook(inputText: string, env?: NodeJS.ProcessEnv): Promise<string>;
|
package/dist/cli/context.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import os from 'node:os';
|
|
2
2
|
import { commandUsageHint } from '../help.js';
|
|
3
3
|
import { parseParticipantList, validateName } from '../model.js';
|
|
4
|
-
import {
|
|
4
|
+
import { createHostLedgerPort } from '../host-ledger-file-adapter.js';
|
|
5
|
+
import { projectSessionBindings, sessionIdsFromEnvironment } from '../square-projections.js';
|
|
5
6
|
export async function readStdin() {
|
|
6
7
|
try {
|
|
7
8
|
let content = '';
|
|
@@ -144,7 +145,14 @@ export async function parseGlobalArgs(rawArgs) {
|
|
|
144
145
|
}
|
|
145
146
|
const squarePath = requestedPath ?? configured;
|
|
146
147
|
if (name === undefined && squarePath !== undefined && command !== undefined && locationIsRequired(command)) {
|
|
147
|
-
|
|
148
|
+
const bindings = (await Promise.all(sessionIdsFromEnvironment().map((sessionId) => projectSessionBindings({
|
|
149
|
+
hostLedger: createHostLedgerPort(),
|
|
150
|
+
location: squarePath,
|
|
151
|
+
sessionId,
|
|
152
|
+
scopes: ['user', 'local'],
|
|
153
|
+
})))).flat();
|
|
154
|
+
const names = new Set(bindings.map((binding) => binding.participant));
|
|
155
|
+
name = names.size === 1 ? [...names][0] : undefined;
|
|
148
156
|
}
|
|
149
157
|
return { squarePath, explicitSquarePath: explicitSquarePath || configured !== undefined, multipleSquares: false, name, args };
|
|
150
158
|
}
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { type ActivitiesOptions, type WatchOptions } from '../model.js';
|
|
2
2
|
import { type CommandSpec } from './context.js';
|
|
3
|
+
interface HistoryCommandOptions extends ActivitiesOptions {
|
|
4
|
+
noTruncate: boolean;
|
|
5
|
+
continuationArgs: string[];
|
|
6
|
+
}
|
|
3
7
|
export declare const listCommand: CommandSpec<string[]>;
|
|
4
8
|
interface StreamIntent {
|
|
5
9
|
ndjson: boolean;
|
|
@@ -7,7 +11,7 @@ interface StreamIntent {
|
|
|
7
11
|
}
|
|
8
12
|
export declare const streamCommand: CommandSpec<StreamIntent>;
|
|
9
13
|
export declare const catchCommand: CommandSpec<WatchOptions>;
|
|
10
|
-
export declare const historyCommand: CommandSpec<
|
|
14
|
+
export declare const historyCommand: CommandSpec<HistoryCommandOptions, string>;
|
|
11
15
|
export declare const participantsCommand: CommandSpec<undefined, string>;
|
|
12
16
|
export declare const statusCommand: CommandSpec<undefined, string>;
|
|
13
17
|
interface InboxIntent {
|