@astrosheep/square 0.3.10 → 0.3.12
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/codex-plugin/.codex-plugin/plugin.json +1 -1
- package/dist/activity.js +6 -7
- package/dist/artifact.js +337 -618
- package/dist/boundary-presentation.js +1 -1
- package/dist/cli/context.js +3 -3
- package/dist/cli/harness-command.js +1 -1
- package/dist/cli/maintenance-commands.js +12 -58
- package/dist/cli/observation-commands.js +14 -34
- package/dist/cli/program.js +3 -6
- package/dist/cli/registry.js +1 -2
- package/dist/cli/square-commands.js +39 -20
- package/dist/cmd/notify-once.js +5 -15
- package/dist/compact.js +4 -4
- package/dist/decisions.js +21 -7
- package/dist/delivery-health.js +56 -136
- package/dist/delivery.js +11 -47
- package/dist/file-lock.js +112 -0
- package/dist/harness-codex.js +35 -29
- package/dist/harness-links.js +0 -3
- package/dist/harness-pi.js +57 -0
- package/dist/harness.js +10 -15
- package/dist/help.js +16 -18
- package/dist/index.js +11 -5
- package/dist/list.js +3 -47
- package/dist/model.js +4 -6
- package/dist/notifications.js +217 -32
- package/dist/paseo-connection.js +135 -0
- package/dist/paseo-delivery.js +73 -144
- package/dist/paseo-state.js +1 -1
- package/dist/paseo-timeline.js +32 -42
- package/dist/presentation.js +24 -39
- package/dist/presented.js +10 -72
- package/dist/registry.js +23 -24
- package/dist/routes.js +153 -0
- package/dist/runtime.js +6 -21
- package/dist/square-application.js +56 -127
- package/dist/square-core.js +56 -9
- package/dist/stream.js +1 -1
- package/dist/wake-attempts.js +175 -0
- package/dist/wake-evidence.js +35 -0
- package/dist/wake-port.js +22 -0
- package/dist/wake-sink.js +45 -6
- package/dist/watch.js +1 -2
- package/guides/participant.md +7 -174
- package/package.json +6 -3
- package/skills/brainstorm/SKILL.md +28 -28
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +23 -14
- package/skills/square-feedback/SKILL.md +7 -7
- package/dist/doctor.js +0 -35
- package/dist/notification-failures.js +0 -54
- package/template.md +0 -4
- package/templates/architect.md +0 -4
- package/templates/brainstorm.md +0 -4
package/dist/harness.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { doctorDeliveryHealth } from './delivery-health.js';
|
|
3
|
+
import { wakeGraceMs } from './notifications.js';
|
|
3
4
|
import { doctorClaudePlugin, installClaudePlugin, uninstallClaudePlugin, } from './harness-claude.js';
|
|
4
5
|
import { doctorCodexPlugin, installCodexPlugin, uninstallCodexPlugin, } from './harness-codex.js';
|
|
5
|
-
import {
|
|
6
|
+
import { doctorPiPackage, installPiPackage, uninstallPiPackage, } from './harness-pi.js';
|
|
7
|
+
import { doctorHarnessLinks, installHarnessLinks, opencodeExtensionLink, skillLinks, uninstallHarnessLinks, verifyOpenCodeRuntime, } from './harness-links.js';
|
|
6
8
|
function result(lines, notes = []) {
|
|
7
9
|
return { lines, notes };
|
|
8
10
|
}
|
|
@@ -28,13 +30,6 @@ function readableSquarePath(squarePath) {
|
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
const TARGETS = [
|
|
31
|
-
{
|
|
32
|
-
name: 'skills',
|
|
33
|
-
capabilities: ['install', 'uninstall', 'doctor'],
|
|
34
|
-
install: ({ homeDir, force }) => result(installHarnessLinks(skillLinks(homeDir), force)),
|
|
35
|
-
uninstall: ({ homeDir }) => result(uninstallHarnessLinks(skillLinks(homeDir))),
|
|
36
|
-
doctor: ({ homeDir }) => result(doctorHarnessLinks(skillLinks(homeDir))),
|
|
37
|
-
},
|
|
38
33
|
{
|
|
39
34
|
name: 'claude',
|
|
40
35
|
capabilities: ['install', 'uninstall', 'doctor'],
|
|
@@ -52,7 +47,7 @@ const TARGETS = [
|
|
|
52
47
|
name: 'codex',
|
|
53
48
|
capabilities: ['install', 'uninstall', 'doctor'],
|
|
54
49
|
async install({ homeDir }) {
|
|
55
|
-
const installed = await installCodexPlugin(homeDir);
|
|
50
|
+
const installed = await installCodexPlugin(homeDir, undefined, process.env.CODEX_HOME);
|
|
56
51
|
const lines = [
|
|
57
52
|
installed.configPath,
|
|
58
53
|
installed.marketplaceRoot,
|
|
@@ -62,10 +57,10 @@ const TARGETS = [
|
|
|
62
57
|
return result(lines, installed.notes);
|
|
63
58
|
},
|
|
64
59
|
async uninstall({ homeDir }) {
|
|
65
|
-
const removed = await uninstallCodexPlugin(homeDir);
|
|
60
|
+
const removed = await uninstallCodexPlugin(homeDir, undefined, process.env.CODEX_HOME);
|
|
66
61
|
return result(removed.paths, removed.notes);
|
|
67
62
|
},
|
|
68
|
-
async doctor({ homeDir }) { return doctorHost('Codex', () => doctorCodexPlugin(homeDir)); },
|
|
63
|
+
async doctor({ homeDir }) { return doctorHost('Codex', () => doctorCodexPlugin(homeDir, undefined, process.env.CODEX_HOME)); },
|
|
69
64
|
},
|
|
70
65
|
{
|
|
71
66
|
name: 'opencode',
|
|
@@ -77,15 +72,15 @@ const TARGETS = [
|
|
|
77
72
|
{
|
|
78
73
|
name: 'pi',
|
|
79
74
|
capabilities: ['install', 'uninstall', 'doctor'],
|
|
80
|
-
install: ({ homeDir
|
|
81
|
-
uninstall: ({ homeDir }) => result(
|
|
82
|
-
doctor: ({ homeDir }) => result(
|
|
75
|
+
install: ({ homeDir }) => result(installPiPackage(homeDir)),
|
|
76
|
+
uninstall: ({ homeDir }) => result(uninstallPiPackage(homeDir)),
|
|
77
|
+
doctor: ({ homeDir }) => result(doctorPiPackage(homeDir)),
|
|
83
78
|
},
|
|
84
79
|
{
|
|
85
80
|
name: 'delivery',
|
|
86
81
|
capabilities: ['doctor'],
|
|
87
82
|
doctor: ({ squarePath }) => result(readableSquarePath(squarePath)
|
|
88
|
-
? doctorDeliveryHealth(squarePath)
|
|
83
|
+
? doctorDeliveryHealth(squarePath, wakeGraceMs())
|
|
89
84
|
: ['○ delivery health skipped (no readable square path)']),
|
|
90
85
|
},
|
|
91
86
|
];
|
package/dist/help.js
CHANGED
|
@@ -11,14 +11,14 @@ const COMMANDS = [
|
|
|
11
11
|
details: ['Options:', ' --depth <N> Descend through at most N directory levels (default 4; 0 scans only the current directory).'],
|
|
12
12
|
},
|
|
13
13
|
{
|
|
14
|
-
names: ['join'], usage: '--as <name> join [--last N | --all]', usesSquare: true, group: 'participant',
|
|
14
|
+
names: ['join'], usage: '--as <name> join [--last N | --all] [--kick]', usesSquare: true, group: 'participant',
|
|
15
15
|
summary: 'Step into the square and read its current context.',
|
|
16
16
|
details: ['Options:', ' --last <N> Show the last N public activities (default 10).', ' --all Show the complete history.'],
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
|
-
names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--
|
|
19
|
+
names: ['express'], usage: '--as <name> express [-f|--force] [--no-wait] [--bell] [--reply <activity-id>] <activity | ->', usesSquare: true, group: 'participant',
|
|
20
20
|
summary: 'Speak, gesture, or do both.',
|
|
21
|
-
details: ['Options:', ' -f, --force Express without first catching unread activity.', ' --no-wait If held or throttled, save a draft and return.', ' --
|
|
21
|
+
details: ['Reach:', ' @name Address someone in the square. They hear the body; everyone else sees you walk over.', " --bell Call every participant's attention to this activity without a mention.", '', 'Options:', ' -f, --force Express without first catching unread activity.', ' --no-wait If held or throttled, save a draft and return.', ' --reply <activity-id> Mark this activity as a reply to an earlier activity (for example act/12).'],
|
|
22
22
|
},
|
|
23
23
|
{
|
|
24
24
|
names: ['catch'], usage: '--as <name> catch (--now | --idle <duration>) [--from <names>] [--mention [name]] [--replace]', usesSquare: true, group: 'participant',
|
|
@@ -39,10 +39,9 @@ const COMMANDS = [
|
|
|
39
39
|
{ names: ['claude-hook', 'codex-hook'], usage: '{command}', summary: 'Present pending attention at one native agent boundary.', hiddenFromIndex: true },
|
|
40
40
|
{
|
|
41
41
|
names: ['history'], usage: '[--as <name>] history [filters] [output]', usesSquare: true, group: 'participant',
|
|
42
|
-
summary: 'Read or search
|
|
43
|
-
details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time>, --until <time> Match a time window.', ' --grep <regex> | --fixed <s> Search ids, participant names, and bodies.', ' --mention <name> Match mentions.', ' --pending Match attention waiting for --as <name>.', ' --ids <ids> | --at <id> Match stable activity ids.', ' -B, -A, -C <N> Set non-negative context around --at.', ' --after <id> Match activities after an id.', '', 'Results:', ' --limit <N> | --all Bound the newest matches (default 10).', ' --order <asc|desc> Set display order.', '', 'Output:', ' --full --json --format <fields> --count'],
|
|
42
|
+
summary: 'Read or search the archive without changing what you have caught.',
|
|
43
|
+
details: ['Filters:', ' --from <names> Match activities from participants.', ' --since <time>, --until <time> Match a time window.', ' --grep <regex> | --fixed <s> Search ids, participant names, and original bodies.', ' --mention <name> Match mentions.', ' --pending Match attention waiting for --as <name>.', ' --ids <ids> | --at <id> Match stable activity ids and show original bodies.', ' -B, -A, -C <N> Set non-negative context around --at.', ' --after <id> Match activities after an id.', '', 'Results:', ' --limit <N> | --all Bound the newest matches (default 10).', ' --order <asc|desc> Set display order.', '', 'Output:', ' --full --json --format <fields> --count'],
|
|
44
44
|
},
|
|
45
|
-
{ names: ['warmup'], usage: 'warmup', usesSquare: true, group: 'host', summary: 'Print the complete embedded participant warmup.' },
|
|
46
45
|
{ names: ['status'], usage: '[--as <name>] status', usesSquare: true, group: 'participant', summary: 'Show who is present and what happened most recently.' },
|
|
47
46
|
{ names: ['participants'], usage: 'participants', usesSquare: true, group: 'host', summary: 'Show the full participant roster and current states.' },
|
|
48
47
|
{ names: ['hold'], usage: '--as <name> hold [reason | -]', usesSquare: true, group: 'participant', summary: 'Raise a hand and pause participant activity.' },
|
|
@@ -50,23 +49,22 @@ const COMMANDS = [
|
|
|
50
49
|
{
|
|
51
50
|
names: ['install'], usage: 'install (--all | <target>...) [-f]', group: 'maintenance',
|
|
52
51
|
summary: 'Install Square support for one or more agent hosts.',
|
|
53
|
-
details: ['Targets:', '
|
|
52
|
+
details: ['Targets:', ' claude, codex, opencode, pi', '', 'Options:', ' --all Install every supported target.', ' -f, --force Replace existing managed links.'],
|
|
54
53
|
},
|
|
55
54
|
{
|
|
56
55
|
names: ['uninstall'], usage: 'uninstall (--all | <target>...)', group: 'maintenance',
|
|
57
56
|
summary: 'Remove Square support from one or more agent hosts.',
|
|
58
|
-
details: ['Targets:', '
|
|
57
|
+
details: ['Targets:', ' claude, codex, opencode, pi', '', 'Options:', ' --all Remove every supported target.'],
|
|
59
58
|
},
|
|
60
59
|
{
|
|
61
|
-
names: ['harness'], usage: 'harness doctor [
|
|
60
|
+
names: ['harness'], usage: 'harness doctor [claude|codex|opencode|pi|delivery]', usesSquare: true, group: 'maintenance', hiddenFromIndex: true,
|
|
62
61
|
summary: 'Diagnose installed agent-host support.',
|
|
63
|
-
details: ['Targets:', '
|
|
62
|
+
details: ['Targets:', ' claude, codex, opencode, pi Diagnose one installed adapter.', ' delivery Diagnose delivery for the selected square.'],
|
|
64
63
|
},
|
|
65
64
|
{ names: ['compact'], usage: 'compact [--keep N]', usesSquare: true, group: 'host', summary: 'Move older activity out of the working artifact while keeping the latest N.' },
|
|
66
65
|
{
|
|
67
|
-
names: ['doctor'], usage: 'doctor
|
|
68
|
-
summary: '
|
|
69
|
-
details: ['Options:', ' --fix Repair recoverable artifact problems.'],
|
|
66
|
+
names: ['doctor'], usage: 'doctor', usesSquare: true, group: 'maintenance',
|
|
67
|
+
summary: 'Validate binary artifact integrity.',
|
|
70
68
|
},
|
|
71
69
|
];
|
|
72
70
|
function definitionFor(command) {
|
|
@@ -78,8 +76,8 @@ function isHelpFlag(value) {
|
|
|
78
76
|
export function renderGlobalHelp() {
|
|
79
77
|
const groups = [
|
|
80
78
|
{ key: 'participant', title: 'In the square:', order: ['join', 'express', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
|
|
81
|
-
{ key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants', '
|
|
82
|
-
{ key: 'maintenance', title: 'Setup
|
|
79
|
+
{ key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants', 'compact'] },
|
|
80
|
+
{ key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor'] },
|
|
83
81
|
];
|
|
84
82
|
const commandLines = groups.flatMap(({ key, title, order }) => [
|
|
85
83
|
title,
|
|
@@ -90,7 +88,7 @@ export function renderGlobalHelp() {
|
|
|
90
88
|
'',
|
|
91
89
|
]);
|
|
92
90
|
return [
|
|
93
|
-
'Usage: square [--
|
|
91
|
+
'Usage: square [--location <path>] [--as <name>] <command> [args...]',
|
|
94
92
|
'',
|
|
95
93
|
...commandLines,
|
|
96
94
|
"Run 'square <command> --help' for command options.",
|
|
@@ -104,7 +102,7 @@ export function renderSubcommandHelp(command) {
|
|
|
104
102
|
const aliases = definition.names.filter((name) => name !== command);
|
|
105
103
|
const usage = definition.usage.replace('{command}', command);
|
|
106
104
|
return [
|
|
107
|
-
`Usage: square ${definition.usesSquare ? '[--
|
|
105
|
+
`Usage: square ${definition.usesSquare ? '[--location <path>] ' : ''}${usage}`,
|
|
108
106
|
...(aliases.length > 0 ? [`Aliases: ${aliases.join(', ')}`] : []),
|
|
109
107
|
'',
|
|
110
108
|
definition.summary,
|
|
@@ -121,7 +119,7 @@ export function helpRequest(rawArgs) {
|
|
|
121
119
|
const args = [];
|
|
122
120
|
for (let index = 0; index < rawArgs.length; index++) {
|
|
123
121
|
const arg = rawArgs[index];
|
|
124
|
-
if (arg === '--
|
|
122
|
+
if (arg === '--location' || arg === '--as') {
|
|
125
123
|
const value = rawArgs[index + 1];
|
|
126
124
|
if (value === undefined || value.startsWith('--'))
|
|
127
125
|
return undefined;
|
package/dist/index.js
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
export { SquareError } from './model.js';
|
|
2
|
+
export { extractMentions, formatActivityId, parseActivityId } from './square-core.js';
|
|
2
3
|
export { loadSquare } from './artifact.js';
|
|
3
|
-
export {
|
|
4
|
+
export { countSays, joinedNames, doneNames, isCurrentlyJoined, publicActs, readCursor, } from './runtime.js';
|
|
4
5
|
import { loadSquare } from './artifact.js';
|
|
5
|
-
import { hasDeliveredNotification as hasDeliveredNotificationImpl, waitForDeliveredNotification as waitForDeliveredNotificationImpl, } from './notifications.js';
|
|
6
|
+
import { dispatchActNotifications, hasDeliveredNotification as hasDeliveredNotificationImpl, sweepPendingNotifications, waitForDeliveredNotification as waitForDeliveredNotificationImpl, } from './notifications.js';
|
|
6
7
|
import { WATCH_STALE_MS, freshWatchLease, getReadState as getDocReadState, } from './runtime.js';
|
|
7
8
|
import { resolveKnownName } from './decisions.js';
|
|
8
9
|
import { execute } from './square-application.js';
|
|
10
|
+
import { parseActivityId } from './square-core.js';
|
|
9
11
|
export { WATCH_STALE_MS };
|
|
10
12
|
function actRefIndex(ref) {
|
|
11
13
|
if (typeof ref === 'number')
|
|
12
14
|
return ref;
|
|
13
|
-
const
|
|
14
|
-
if (
|
|
15
|
+
const index = parseActivityId(ref);
|
|
16
|
+
if (index === undefined)
|
|
15
17
|
throw new Error(`Invalid act ref: ${ref}`);
|
|
16
|
-
return
|
|
18
|
+
return index;
|
|
17
19
|
}
|
|
18
20
|
export function getReadState(squarePath, name) {
|
|
19
21
|
const doc = loadSquare(squarePath);
|
|
@@ -52,6 +54,7 @@ export async function resume(squarePath, actor) {
|
|
|
52
54
|
await execute(squarePath, { type: 'resume', actor, now: Date.now() });
|
|
53
55
|
}
|
|
54
56
|
export async function express(squarePath, name, body, opts = {}) {
|
|
57
|
+
await sweepPendingNotifications(squarePath);
|
|
55
58
|
const committed = await execute(squarePath, {
|
|
56
59
|
type: 'say',
|
|
57
60
|
name,
|
|
@@ -60,6 +63,9 @@ export async function express(squarePath, name, body, opts = {}) {
|
|
|
60
63
|
now: Date.now(),
|
|
61
64
|
...(opts.reply === undefined ? {} : { reply: actRefIndex(opts.reply) }),
|
|
62
65
|
});
|
|
66
|
+
const sayAct = committed.acts.find((act) => act.kind === 'say');
|
|
67
|
+
if (sayAct !== undefined)
|
|
68
|
+
await dispatchActNotifications(squarePath, sayAct);
|
|
63
69
|
if (committed.result.type !== 'sent')
|
|
64
70
|
throw new Error(`Activity rejected: ${committed.result.type}`);
|
|
65
71
|
}
|
package/dist/list.js
CHANGED
|
@@ -1,37 +1,11 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { probeSquare } from './artifact.js';
|
|
4
4
|
import { inSquareCount, publicActs } from './runtime.js';
|
|
5
5
|
import { formatRelativeTime } from './time.js';
|
|
6
6
|
const DEFAULT_LIST_DEPTH = 4;
|
|
7
7
|
const LIST_SKIP_DIRS = new Set(['.git', 'node_modules', 'dist']);
|
|
8
|
-
function frontmatterOf(text) {
|
|
9
|
-
const match = text.match(/^---\n([\s\S]*?)\n---(?:\n|$)/);
|
|
10
|
-
return match ? match[1] : null;
|
|
11
|
-
}
|
|
12
|
-
function candidateFrontmatter(filePath) {
|
|
13
|
-
let fd;
|
|
14
|
-
try {
|
|
15
|
-
fd = fs.openSync(filePath, 'r');
|
|
16
|
-
const buffer = Buffer.allocUnsafe(4096);
|
|
17
|
-
const bytesRead = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
18
|
-
const prefix = buffer.toString('utf8', 0, bytesRead);
|
|
19
|
-
if (!prefix.startsWith('---\n'))
|
|
20
|
-
return null;
|
|
21
|
-
const frontmatter = frontmatterOf(prefix);
|
|
22
|
-
return frontmatter ?? frontmatterOf(fs.readFileSync(filePath, 'utf8'));
|
|
23
|
-
}
|
|
24
|
-
catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
finally {
|
|
28
|
-
if (fd !== undefined)
|
|
29
|
-
fs.closeSync(fd);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
8
|
function readSquareListItem(filePath, root) {
|
|
33
|
-
let text;
|
|
34
|
-
let doc;
|
|
35
9
|
let stat;
|
|
36
10
|
try {
|
|
37
11
|
stat = fs.statSync(filePath);
|
|
@@ -39,27 +13,9 @@ function readSquareListItem(filePath, root) {
|
|
|
39
13
|
catch {
|
|
40
14
|
return null;
|
|
41
15
|
}
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
44
|
-
return null;
|
|
45
|
-
if (!/^hard_cap:\s*(-1|\d+)\s*$/m.test(frontmatter))
|
|
16
|
+
const doc = probeSquare(filePath);
|
|
17
|
+
if (doc === undefined)
|
|
46
18
|
return null;
|
|
47
|
-
if (!/^format_version:\s*3\s*$/m.test(frontmatter))
|
|
48
|
-
return null;
|
|
49
|
-
try {
|
|
50
|
-
text = fs.readFileSync(filePath, 'utf8');
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return null;
|
|
54
|
-
}
|
|
55
|
-
if (!text.includes('<!-- square:warmup -->') || !text.includes('<!-- square:activities -->'))
|
|
56
|
-
return null;
|
|
57
|
-
try {
|
|
58
|
-
doc = parseSquare(text);
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
19
|
const relative = path.relative(root, filePath) || path.basename(filePath);
|
|
64
20
|
return {
|
|
65
21
|
path: relative,
|
package/dist/model.js
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
// Shared model and constants for Square.
|
|
2
|
+
export const WAKE_ROUTE_KINDS = ['opencode-server', 'codex-app-server', 'claude-native', 'pi-extension', 'paseo'];
|
|
3
|
+
export function isWakeRouteKind(value) {
|
|
4
|
+
return typeof value === 'string' && WAKE_ROUTE_KINDS.includes(value);
|
|
5
|
+
}
|
|
2
6
|
export class SquareError extends Error {
|
|
3
7
|
code;
|
|
4
8
|
constructor(code, message) {
|
|
@@ -7,12 +11,6 @@ export class SquareError extends Error {
|
|
|
7
11
|
this.name = 'SquareError';
|
|
8
12
|
}
|
|
9
13
|
}
|
|
10
|
-
export const WARMUP_HEADING = '## Warmup';
|
|
11
|
-
export const WARMUP_MARKER = '<!-- square:warmup -->';
|
|
12
|
-
export const ACTIVITIES_HEADING = '## Activities';
|
|
13
|
-
export const ACTIVITIES_MARKER = '<!-- square:activities -->';
|
|
14
|
-
export const ACT_MARKER_PREFIX = '<!-- square:act';
|
|
15
|
-
export const CURRENT_FORMAT_VERSION = 3;
|
|
16
14
|
export function formatHardCap(hardCap) {
|
|
17
15
|
return hardCap === null ? '-1' : String(hardCap);
|
|
18
16
|
}
|
package/dist/notifications.js
CHANGED
|
@@ -1,13 +1,24 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
2
4
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
5
|
import { fileURLToPath } from 'node:url';
|
|
4
6
|
import { loadSquare } from './artifact.js';
|
|
5
|
-
import { isDeliveryDelivered,
|
|
6
|
-
import {
|
|
7
|
+
import { deriveDeliveryModel, isDeliveryDelivered, leaseOwnsNotification, planActNotifications, } from './delivery.js';
|
|
8
|
+
import { sessionInbox } from './inbox.js';
|
|
7
9
|
import { hasPresentedAttention } from './presented.js';
|
|
8
|
-
import { SquareError } from './model.js';
|
|
10
|
+
import { nameKey, SquareError } from './model.js';
|
|
9
11
|
import { SLEEP_MS, matchesMentionTarget, resolveRosterName, rosterNames } from './runtime.js';
|
|
10
|
-
import {
|
|
12
|
+
import { formatActivityId, parseActivityId } from './square-core.js';
|
|
13
|
+
import { PaseoAdapter } from './paseo-delivery.js';
|
|
14
|
+
import { quoteShell } from './presentation.js';
|
|
15
|
+
import { lookupParticipant } from './registry.js';
|
|
16
|
+
import { isCurrentlyJoined } from './runtime.js';
|
|
17
|
+
import { execute } from './square-application.js';
|
|
18
|
+
import { nextWakeAttemptNumber, recordRecoveredUnknown, recordWakeAttempt, } from './wake-attempts.js';
|
|
19
|
+
import { joinedRecipients, wakeEvidence, wakeIsEligible } from './wake-evidence.js';
|
|
20
|
+
import { WakePort } from './wake-port.js';
|
|
21
|
+
const NOTIFY_LEASE_MS = 5 * 60 * 1000;
|
|
11
22
|
export { planActNotifications, matchesMentionTarget };
|
|
12
23
|
function known(doc, name) {
|
|
13
24
|
const value = resolveRosterName(doc, name);
|
|
@@ -16,20 +27,70 @@ function known(doc, name) {
|
|
|
16
27
|
return value;
|
|
17
28
|
}
|
|
18
29
|
export { notificationMessageId } from './delivery.js';
|
|
19
|
-
export function
|
|
20
|
-
const value = Number.parseInt(
|
|
21
|
-
if (!Number.isFinite(value) || value <= 0)
|
|
30
|
+
export function wakeGraceMs(env = process.env) {
|
|
31
|
+
const value = Number.parseInt(env.SQUARE_NOTIFY_DELIVERY_WAIT_MS ?? '5000', 10);
|
|
32
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
22
33
|
throw new SquareError('invalid_args', 'Invalid SQUARE_NOTIFY_DELIVERY_WAIT_MS: expected a positive integer.');
|
|
34
|
+
}
|
|
23
35
|
return value;
|
|
24
36
|
}
|
|
37
|
+
function catchCommand(squarePath, recipient) {
|
|
38
|
+
return `square --as ${quoteShell(recipient)} --location ${quoteShell(squarePath)} catch --now`;
|
|
39
|
+
}
|
|
40
|
+
function renderWakePayload(request) {
|
|
41
|
+
const display = request.squarePath.startsWith(homedir())
|
|
42
|
+
? `~${request.squarePath.slice(homedir().length)}`
|
|
43
|
+
: request.squarePath;
|
|
44
|
+
return [
|
|
45
|
+
'<system-reminder source="square">',
|
|
46
|
+
`${request.route === 'bell' ? 'Bell' : 'Mention'} from @${request.actor} in \`${display}\``,
|
|
47
|
+
'The native adapter will present it at the next boundary. If no native wake is available, pull from the square yourself.',
|
|
48
|
+
`\`${catchCommand(request.squarePath, request.recipient)}\``,
|
|
49
|
+
'</system-reminder>',
|
|
50
|
+
].join('\n');
|
|
51
|
+
}
|
|
52
|
+
async function waitForCatch(route, request, body) {
|
|
53
|
+
const binding = lookupParticipant(request.squarePath, request.recipient)
|
|
54
|
+
.find((item) => item.ownerId === route.ownerId);
|
|
55
|
+
const activeCatch = binding && sessionInbox(binding.sessionId)
|
|
56
|
+
.find((item) => item.name === request.recipient)?.catchLease;
|
|
57
|
+
if (!activeCatch || !leaseOwnsNotification(activeCatch, {
|
|
58
|
+
actor: request.actor,
|
|
59
|
+
body,
|
|
60
|
+
route: request.route,
|
|
61
|
+
}))
|
|
62
|
+
return false;
|
|
63
|
+
const deadline = Date.now() + 180_000;
|
|
64
|
+
while (Date.now() < deadline) {
|
|
65
|
+
const doc = loadSquare(request.squarePath);
|
|
66
|
+
if (isDeliveryDelivered(doc, request.recipient, request.actIndex))
|
|
67
|
+
return true;
|
|
68
|
+
const currentBinding = lookupParticipant(request.squarePath, request.recipient)
|
|
69
|
+
.find((item) => item.ownerId === route.ownerId);
|
|
70
|
+
const lease = currentBinding && sessionInbox(currentBinding.sessionId)
|
|
71
|
+
.find((item) => item.name === request.recipient)?.catchLease;
|
|
72
|
+
if (!lease || lease.expiresAt <= Date.now())
|
|
73
|
+
return false;
|
|
74
|
+
await sleep(Math.min(250, lease.expiresAt - Date.now()));
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
function notificationIndex(ref) {
|
|
79
|
+
if (typeof ref === 'number')
|
|
80
|
+
return ref;
|
|
81
|
+
const index = parseActivityId(ref);
|
|
82
|
+
if (index === undefined)
|
|
83
|
+
throw new Error(`Invalid act ref: ${ref}`);
|
|
84
|
+
return index;
|
|
85
|
+
}
|
|
25
86
|
export function hasDeliveredNotification(squarePath, name, ref) {
|
|
26
87
|
const doc = loadSquare(squarePath);
|
|
27
|
-
return isDeliveryDelivered(doc, known(doc, name),
|
|
88
|
+
return isDeliveryDelivered(doc, known(doc, name), notificationIndex(ref));
|
|
28
89
|
}
|
|
29
90
|
export function hasAttentionNotification(squarePath, name, ref, env = process.env) {
|
|
30
91
|
const doc = loadSquare(squarePath);
|
|
31
92
|
const recipient = known(doc, name);
|
|
32
|
-
const index =
|
|
93
|
+
const index = notificationIndex(ref);
|
|
33
94
|
return isDeliveryDelivered(doc, recipient, index) || hasPresentedAttention(squarePath, recipient, index, env);
|
|
34
95
|
}
|
|
35
96
|
export async function waitForDeliveredNotification(squarePath, name, ref, opts = {}) {
|
|
@@ -41,31 +102,128 @@ export async function waitForDeliveredNotification(squarePath, name, ref, opts =
|
|
|
41
102
|
}
|
|
42
103
|
return false;
|
|
43
104
|
}
|
|
105
|
+
function notifyLeaseKey(recipient, actIndex) {
|
|
106
|
+
return JSON.stringify([formatActivityId(actIndex), nameKey(recipient)]);
|
|
107
|
+
}
|
|
108
|
+
async function claimNotifyLease(squarePath, recipient, actIndex) {
|
|
109
|
+
const at = Date.now();
|
|
110
|
+
const committed = await execute(squarePath, {
|
|
111
|
+
type: 'claim-notify',
|
|
112
|
+
key: notifyLeaseKey(recipient, actIndex),
|
|
113
|
+
leaseId: randomUUID(),
|
|
114
|
+
at,
|
|
115
|
+
expiresAt: at + NOTIFY_LEASE_MS,
|
|
116
|
+
});
|
|
117
|
+
return committed.result;
|
|
118
|
+
}
|
|
119
|
+
async function transitionNotifyLease(squarePath, recipient, actIndex, leaseId, phase, routeKind, attemptN) {
|
|
120
|
+
const at = Date.now();
|
|
121
|
+
const committed = await execute(squarePath, {
|
|
122
|
+
type: 'transition-notify',
|
|
123
|
+
key: notifyLeaseKey(recipient, actIndex),
|
|
124
|
+
leaseId,
|
|
125
|
+
expiresAt: at + NOTIFY_LEASE_MS,
|
|
126
|
+
phase,
|
|
127
|
+
...(routeKind === undefined ? {} : { routeKind }),
|
|
128
|
+
...(attemptN === undefined ? {} : { attemptN }),
|
|
129
|
+
});
|
|
130
|
+
return committed.result.updated;
|
|
131
|
+
}
|
|
132
|
+
function releaseNotifyLease(squarePath, recipient, actIndex, leaseId) {
|
|
133
|
+
return execute(squarePath, {
|
|
134
|
+
type: 'release-notify',
|
|
135
|
+
key: notifyLeaseKey(recipient, actIndex),
|
|
136
|
+
leaseId,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
async function processNotification(squarePath, notification, opts) {
|
|
140
|
+
const env = opts.env ?? process.env;
|
|
141
|
+
const now = opts.now ?? Date.now;
|
|
142
|
+
const attention = {
|
|
143
|
+
squarePath,
|
|
144
|
+
actIndex: notification.item.index,
|
|
145
|
+
recipient: notification.recipient,
|
|
146
|
+
};
|
|
147
|
+
const initialAt = now();
|
|
148
|
+
if (!wakeIsEligible(wakeEvidence(squarePath, notification.recipient, notification.item.index, initialAt, env)))
|
|
149
|
+
return;
|
|
150
|
+
const claim = await claimNotifyLease(squarePath, notification.recipient, notification.item.index);
|
|
151
|
+
if (claim.type === 'busy')
|
|
152
|
+
return;
|
|
153
|
+
if (claim.type === 'ambiguous') {
|
|
154
|
+
const recovered = recordRecoveredUnknown(attention, claim.lease, env);
|
|
155
|
+
if (recovered !== undefined) {
|
|
156
|
+
await releaseNotifyLease(squarePath, notification.recipient, notification.item.index, claim.lease.leaseId);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const { leaseId } = claim;
|
|
161
|
+
let releaseLease = true;
|
|
162
|
+
try {
|
|
163
|
+
const dispatchAt = now();
|
|
164
|
+
const evidence = wakeEvidence(squarePath, notification.recipient, notification.item.index, dispatchAt, env);
|
|
165
|
+
if (!wakeIsEligible(evidence))
|
|
166
|
+
return;
|
|
167
|
+
const port = new WakePort(opts.adapters ?? [new PaseoAdapter()]);
|
|
168
|
+
const request = {
|
|
169
|
+
squarePath,
|
|
170
|
+
actIndex: notification.item.index,
|
|
171
|
+
recipient: notification.recipient,
|
|
172
|
+
actor: notification.item.actor,
|
|
173
|
+
route: notification.route,
|
|
174
|
+
};
|
|
175
|
+
await port.dispatch(evidence.attemptableRoutes, renderWakePayload(request), {
|
|
176
|
+
nextAttemptN: () => nextWakeAttemptNumber(attention, { env, now: now() }),
|
|
177
|
+
beforeSend: async (route, attemptN) => {
|
|
178
|
+
if (await waitForCatch(route, request, notification.item.body))
|
|
179
|
+
return false;
|
|
180
|
+
const currentAt = now();
|
|
181
|
+
const latest = loadSquare(squarePath);
|
|
182
|
+
if (!isCurrentlyJoined(latest.acts, notification.recipient))
|
|
183
|
+
return false;
|
|
184
|
+
const current = wakeEvidence(squarePath, notification.recipient, notification.item.index, currentAt, env);
|
|
185
|
+
if (!wakeIsEligible(current))
|
|
186
|
+
return false;
|
|
187
|
+
if (!current.attemptableRoutes.some((candidate) => candidate.ownerId === route.ownerId && candidate.kind === route.kind && candidate.sessionId === route.sessionId))
|
|
188
|
+
return false;
|
|
189
|
+
const dispatching = await transitionNotifyLease(squarePath, notification.recipient, notification.item.index, leaseId, 'dispatching', route.kind, attemptN);
|
|
190
|
+
if (dispatching)
|
|
191
|
+
releaseLease = false;
|
|
192
|
+
return dispatching;
|
|
193
|
+
},
|
|
194
|
+
record: async (route, attemptN, outcome) => {
|
|
195
|
+
if (outcome.outcome === 'failed') {
|
|
196
|
+
await transitionNotifyLease(squarePath, notification.recipient, notification.item.index, leaseId, 'claimed');
|
|
197
|
+
releaseLease = true;
|
|
198
|
+
}
|
|
199
|
+
recordWakeAttempt({
|
|
200
|
+
attention,
|
|
201
|
+
routeKind: route.kind,
|
|
202
|
+
outcome: outcome.outcome,
|
|
203
|
+
attemptN,
|
|
204
|
+
at: now(),
|
|
205
|
+
...('signature' in outcome ? { signature: outcome.signature } : {}),
|
|
206
|
+
...('message' in outcome ? { message: outcome.message } : {}),
|
|
207
|
+
...('diagnostic' in outcome && outcome.diagnostic !== undefined ? { diagnostic: outcome.diagnostic } : {}),
|
|
208
|
+
}, env);
|
|
209
|
+
if (outcome.outcome !== 'failed')
|
|
210
|
+
releaseLease = true;
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
finally {
|
|
215
|
+
if (releaseLease) {
|
|
216
|
+
await releaseNotifyLease(squarePath, notification.recipient, notification.item.index, leaseId);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
44
220
|
export async function processActNotificationsOnce(squarePath, actIndex, opts = {}) {
|
|
45
221
|
const doc = loadSquare(squarePath);
|
|
46
222
|
const item = doc.acts.find((candidate) => candidate.index === actIndex);
|
|
47
223
|
if (item === undefined)
|
|
48
224
|
return;
|
|
49
|
-
const notifications = planActNotifications(doc, item)
|
|
50
|
-
|
|
51
|
-
if (hasAttentionNotification(squarePath, notification.recipient, notification.item.index))
|
|
52
|
-
continue;
|
|
53
|
-
for (const sink of opts.sinks ?? defaultWakeSinks()) {
|
|
54
|
-
try {
|
|
55
|
-
await sink.dispatch(notification, { squarePath });
|
|
56
|
-
}
|
|
57
|
-
catch (error) {
|
|
58
|
-
recordNotificationFailure(squarePath, {
|
|
59
|
-
actIndex: notification.item.index,
|
|
60
|
-
recipient: notification.recipient,
|
|
61
|
-
route: notification.route,
|
|
62
|
-
sink: sink.name,
|
|
63
|
-
message: error instanceof Error ? error.message : String(error),
|
|
64
|
-
...(error instanceof Error && 'diagnostic' in error ? { diagnostic: error.diagnostic } : {}),
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
}
|
|
225
|
+
const notifications = planActNotifications(doc, item);
|
|
226
|
+
await Promise.all(notifications.map((notification) => processNotification(squarePath, notification, opts)));
|
|
69
227
|
}
|
|
70
228
|
function launchWorker(workerPath, args) {
|
|
71
229
|
const child = spawn(process.execPath, [workerPath, ...args], { detached: true, stdio: 'ignore', env: process.env });
|
|
@@ -73,10 +231,37 @@ function launchWorker(workerPath, args) {
|
|
|
73
231
|
}
|
|
74
232
|
/** Start one detached worker only when this act contains directed attention. */
|
|
75
233
|
export async function dispatchActNotifications(squarePath, item, opts = {}) {
|
|
76
|
-
|
|
234
|
+
const env = opts.env ?? process.env;
|
|
235
|
+
if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
77
236
|
return;
|
|
78
237
|
const doc = loadSquare(squarePath);
|
|
79
|
-
if (
|
|
238
|
+
if (planActNotifications(doc, item).length === 0)
|
|
80
239
|
return;
|
|
81
|
-
(opts.launchWorker ?? launchWorker)(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--
|
|
240
|
+
(opts.launchWorker ?? launchWorker)(fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url)), ['--location', squarePath, '--act-index', String(item.index)]);
|
|
241
|
+
}
|
|
242
|
+
/** Reconsider old pending attention at a bounded action boundary using the existing worker. */
|
|
243
|
+
export function sweepPendingNotifications(squarePath, opts = {}) {
|
|
244
|
+
const env = opts.env ?? process.env;
|
|
245
|
+
if (env.SQUARE_DISABLE_PASEO_WAKE === '1')
|
|
246
|
+
return [];
|
|
247
|
+
const now = opts.now ?? Date.now();
|
|
248
|
+
const limit = opts.limit ?? 8;
|
|
249
|
+
const doc = loadSquare(squarePath);
|
|
250
|
+
const model = deriveDeliveryModel(doc);
|
|
251
|
+
const indexes = new Set();
|
|
252
|
+
for (const recipient of joinedRecipients(doc)) {
|
|
253
|
+
for (const note of model.pendingFor(recipient)) {
|
|
254
|
+
if (now - note.item.at <= wakeGraceMs(env))
|
|
255
|
+
continue;
|
|
256
|
+
if (!wakeIsEligible(wakeEvidence(squarePath, recipient, note.item.index, now, env)))
|
|
257
|
+
continue;
|
|
258
|
+
indexes.add(note.item.index);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const selected = [...indexes].sort((a, b) => a - b).slice(0, Math.max(0, limit));
|
|
262
|
+
const workerPath = fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url));
|
|
263
|
+
for (const actIndex of selected) {
|
|
264
|
+
(opts.launchWorker ?? launchWorker)(workerPath, ['--location', squarePath, '--act-index', String(actIndex)]);
|
|
265
|
+
}
|
|
266
|
+
return selected;
|
|
82
267
|
}
|