@astrosheep/square 0.3.5 → 0.3.6
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 +3 -2
- package/dist/activity-feed.js +26 -18
- package/dist/activity.js +9 -10
- package/dist/artifact.js +126 -202
- package/dist/claude-hook.js +45 -21
- package/dist/cli/context.js +7 -7
- package/dist/cli/maintenance-commands.js +10 -26
- package/dist/cli/meta-commands.js +3 -6
- package/dist/cli/observation-commands.js +44 -52
- package/dist/cli/program.js +4 -4
- package/dist/cli/registry.js +5 -5
- package/dist/cli/square-commands.js +16 -18
- package/dist/cmd/notify-once.js +23 -21
- package/dist/compact.js +1 -1
- package/dist/decisions.js +53 -86
- package/dist/delivery-health.js +104 -210
- package/dist/delivery.js +68 -18
- package/dist/doctor.js +9 -8
- package/dist/harness-claude.js +38 -245
- package/dist/harness-codex.js +82 -616
- package/dist/harness-stage.js +36 -0
- package/dist/harness.js +3 -5
- package/dist/help.js +43 -35
- package/dist/inbox.js +12 -11
- package/dist/index.js +9 -121
- package/dist/list.js +1 -1
- package/dist/model.js +0 -6
- package/dist/notification-failures.js +54 -0
- package/dist/notifications.js +47 -62
- package/dist/paseo-timeline.js +58 -188
- package/dist/presentation.js +55 -63
- package/dist/presented.js +9 -8
- package/dist/registry.js +55 -45
- package/dist/runtime.js +27 -84
- package/dist/square-application.js +135 -130
- package/dist/square-core.js +3 -11
- package/dist/stream.js +27 -126
- package/dist/wake-sink.js +134 -188
- package/dist/watch.js +65 -122
- package/extensions/square-opencode.js +1 -1
- package/extensions/square-pi.js +8 -130
- package/guides/architect.md +3 -3
- package/guides/participant.md +25 -16
- package/package.json +2 -2
- package/skills/brainstorm/SKILL.md +25 -32
- package/skills/square/.claude-plugin/plugin.json +1 -1
- package/skills/square/SKILL.md +39 -107
- package/skills/square-feedback/SKILL.md +4 -4
- package/dist/harness-lifecycle.js +0 -102
- package/dist/square-store.js +0 -111
- package/dist/terminal.js +0 -125
|
@@ -1,107 +1,139 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
3
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
4
|
+
import { emptyRuntimeState, loadRuntimeSidecar, loadSquare, mergeRuntimeState, renderArtifactAct, renderSquare, renderSquareDoc, saveRuntimeSidecar } from './artifact.js';
|
|
4
5
|
import { coreCompact, coreDone, coreHold, coreResume, decideAct, decideJoin, resolveKnownName } from './decisions.js';
|
|
5
|
-
import { partitionPendingDeliveries } from './delivery-health.js';
|
|
6
|
-
import { planRepair } from './doctor.js';
|
|
7
6
|
import { dispatchActNotifications } from './notifications.js';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const backup = path.join(parent, `.${path.basename(filePath)}.${token}.previous`);
|
|
19
|
-
let replaced = false;
|
|
20
|
-
let hadOriginal = false;
|
|
21
|
-
try {
|
|
22
|
-
fs.writeFileSync(stage, text);
|
|
23
|
-
hadOriginal = fs.existsSync(filePath);
|
|
24
|
-
if (hadOriginal)
|
|
25
|
-
fs.renameSync(filePath, backup);
|
|
26
|
-
fs.renameSync(stage, filePath);
|
|
27
|
-
replaced = true;
|
|
28
|
-
}
|
|
29
|
-
catch (error) {
|
|
7
|
+
import { planRepair } from './doctor.js';
|
|
8
|
+
import { stageReplacement } from './harness-stage.js';
|
|
9
|
+
import { SquareError } from './model.js';
|
|
10
|
+
import { advanceCursor, freshWatchLease, LOCK_RETRY_MS, LOCK_STALE_MS, removeWatchLease, touchPresenceCursor, watchLease, writeWatchLease } from './runtime.js';
|
|
11
|
+
/** The only persistence primitive: one per-square lock, one Markdown write, one sidecar write. */
|
|
12
|
+
export async function withSquareLock(squarePath, fn) {
|
|
13
|
+
const lockPath = `${squarePath}.lock`;
|
|
14
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
15
|
+
while (true) {
|
|
16
|
+
let fd;
|
|
30
17
|
try {
|
|
31
|
-
fs.
|
|
18
|
+
fd = fs.openSync(lockPath, 'wx');
|
|
19
|
+
fs.writeFileSync(fd, `${process.pid}\n${Date.now()}\n`, 'utf8');
|
|
32
20
|
}
|
|
33
|
-
catch {
|
|
34
|
-
|
|
21
|
+
catch (error) {
|
|
22
|
+
if (fd !== undefined) {
|
|
23
|
+
try {
|
|
24
|
+
fs.closeSync(fd);
|
|
25
|
+
}
|
|
26
|
+
catch { }
|
|
27
|
+
try {
|
|
28
|
+
fs.unlinkSync(lockPath);
|
|
29
|
+
}
|
|
30
|
+
catch { }
|
|
31
|
+
}
|
|
32
|
+
const errno = error;
|
|
33
|
+
if (errno.code !== 'EEXIST')
|
|
34
|
+
throw error;
|
|
35
35
|
try {
|
|
36
|
-
fs.
|
|
36
|
+
if (Date.now() - fs.statSync(lockPath).mtimeMs > LOCK_STALE_MS)
|
|
37
|
+
fs.unlinkSync(lockPath);
|
|
38
|
+
}
|
|
39
|
+
catch { }
|
|
40
|
+
await sleep(LOCK_RETRY_MS);
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
fs.closeSync(fd);
|
|
44
|
+
try {
|
|
45
|
+
return await fn();
|
|
46
|
+
}
|
|
47
|
+
finally {
|
|
48
|
+
try {
|
|
49
|
+
fs.unlinkSync(lockPath);
|
|
37
50
|
}
|
|
38
51
|
catch { }
|
|
39
52
|
}
|
|
40
|
-
throw error;
|
|
41
53
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
}
|
|
55
|
+
export function writeSquareDoc(squarePath, doc) {
|
|
56
|
+
const temporary = path.join(path.dirname(squarePath), `.${path.basename(squarePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
57
|
+
fs.writeFileSync(temporary, renderSquareDoc(doc));
|
|
58
|
+
fs.renameSync(temporary, squarePath);
|
|
59
|
+
saveRuntimeSidecar(squarePath, doc.runtime);
|
|
60
|
+
}
|
|
61
|
+
export function appendAct(squarePath, doc, act) {
|
|
62
|
+
const stored = applyActs(doc, [act])[0];
|
|
63
|
+
writeSquareDoc(squarePath, doc);
|
|
64
|
+
return stored;
|
|
65
|
+
}
|
|
66
|
+
function applyActs(doc, acts, mutateRuntime) {
|
|
67
|
+
const stored = [];
|
|
68
|
+
for (const act of acts) {
|
|
69
|
+
const item = { ...act, index: doc.runtime.nextActIndex };
|
|
70
|
+
doc.runtime.nextActIndex++;
|
|
71
|
+
doc.acts.push(item);
|
|
72
|
+
if (item.actor !== undefined)
|
|
73
|
+
touchPresenceCursor(doc, item.actor, item.at, item.index);
|
|
74
|
+
stored.push(item);
|
|
75
|
+
}
|
|
76
|
+
mutateRuntime?.(doc);
|
|
77
|
+
return stored;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Publish a dependent persistence file before the Square document. A retained
|
|
81
|
+
* backup lets a failed document commit restore the prior file exactly.
|
|
82
|
+
*/
|
|
83
|
+
function prepareAppend(filePath, block, existing = fs.existsSync(filePath) ? fs.readFileSync(filePath, 'utf8') : '') {
|
|
84
|
+
return stageReplacement(filePath, (stage) => {
|
|
85
|
+
fs.writeFileSync(stage, `${existing}${existing === '' ? '' : '\n'}${block}\n`);
|
|
86
|
+
});
|
|
53
87
|
}
|
|
54
88
|
function plan(doc, intent) {
|
|
55
89
|
switch (intent.type) {
|
|
56
90
|
case 'join': {
|
|
57
91
|
const decision = decideJoin(doc, intent.name, intent.now);
|
|
58
|
-
return { result: decision, acts: [decision.joinAct]
|
|
92
|
+
return { result: decision, acts: [decision.joinAct] };
|
|
59
93
|
}
|
|
60
94
|
case 'say': {
|
|
61
95
|
const decision = decideAct(doc, intent);
|
|
62
96
|
return {
|
|
63
97
|
result: decision,
|
|
64
98
|
acts: decision.type === 'sent' ? [decision.act] : [],
|
|
65
|
-
afterCommit: [],
|
|
66
99
|
};
|
|
67
100
|
}
|
|
68
101
|
case 'hold':
|
|
69
|
-
return { result: undefined, acts: [coreHold(doc, intent.actor, intent.body, intent.now)]
|
|
102
|
+
return { result: undefined, acts: [coreHold(doc, intent.actor, intent.body, intent.now)] };
|
|
70
103
|
case 'resume':
|
|
71
|
-
return { result: undefined, acts: [coreResume(doc, intent.actor, intent.now)]
|
|
104
|
+
return { result: undefined, acts: [coreResume(doc, intent.actor, intent.now)] };
|
|
72
105
|
case 'done':
|
|
73
|
-
return { result: undefined, acts: [coreDone(doc, intent.name, intent.body, intent.now)]
|
|
106
|
+
return { result: undefined, acts: [coreDone(doc, intent.name, intent.body, intent.now)] };
|
|
74
107
|
case 'lease': {
|
|
75
108
|
const name = resolveKnownName(doc, intent.name);
|
|
76
109
|
const existing = freshWatchLease(doc, name, intent.at);
|
|
77
110
|
if (existing !== undefined && !intent.force) {
|
|
78
|
-
return { result: { type: 'active', lease: existing }, acts: []
|
|
111
|
+
return { result: { type: 'active', lease: existing }, acts: [] };
|
|
79
112
|
}
|
|
80
113
|
return {
|
|
81
114
|
result: { type: 'started', name, replaced: existing !== undefined },
|
|
82
115
|
acts: [],
|
|
83
|
-
mutateRuntime: (
|
|
84
|
-
|
|
116
|
+
mutateRuntime: (nextDoc) => {
|
|
117
|
+
writeWatchLease(nextDoc, name, {
|
|
85
118
|
leaseId: intent.leaseId,
|
|
119
|
+
...(intent.ownerId === undefined ? {} : { ownerId: intent.ownerId }),
|
|
86
120
|
heartbeatAt: intent.at,
|
|
87
121
|
expiresAt: intent.expiresAt,
|
|
88
122
|
...(intent.filter === undefined ? {} : { filter: intent.filter }),
|
|
89
|
-
};
|
|
90
|
-
touchPresenceCursor(
|
|
123
|
+
});
|
|
124
|
+
touchPresenceCursor(nextDoc, name, intent.at);
|
|
91
125
|
},
|
|
92
|
-
afterCommit: [],
|
|
93
126
|
};
|
|
94
127
|
}
|
|
95
128
|
case 'release-lease': {
|
|
96
129
|
const name = resolveKnownName(doc, intent.name);
|
|
97
|
-
if (doc
|
|
98
|
-
return { result: { released: false }, acts: []
|
|
130
|
+
if (watchLease(doc, name)?.leaseId !== intent.leaseId) {
|
|
131
|
+
return { result: { released: false }, acts: [] };
|
|
99
132
|
}
|
|
100
133
|
return {
|
|
101
134
|
result: { released: true },
|
|
102
135
|
acts: [],
|
|
103
|
-
mutateRuntime: (
|
|
104
|
-
afterCommit: [],
|
|
136
|
+
mutateRuntime: (nextDoc) => { removeWatchLease(nextDoc, name, intent.leaseId); },
|
|
105
137
|
};
|
|
106
138
|
}
|
|
107
139
|
case 'consume': {
|
|
@@ -109,15 +141,7 @@ function plan(doc, intent) {
|
|
|
109
141
|
return {
|
|
110
142
|
result: { name },
|
|
111
143
|
acts: [],
|
|
112
|
-
mutateRuntime: (
|
|
113
|
-
const current = runtime.cursors[name];
|
|
114
|
-
runtime.cursors[name] = {
|
|
115
|
-
consumedThroughIndex: Math.max(current?.consumedThroughIndex ?? -1, intent.throughIndex),
|
|
116
|
-
updatedAt: Math.max(current?.updatedAt ?? 0, intent.at),
|
|
117
|
-
source: intent.source ?? 'watch',
|
|
118
|
-
};
|
|
119
|
-
},
|
|
120
|
-
afterCommit: [],
|
|
144
|
+
mutateRuntime: (nextDoc) => { advanceCursor(nextDoc, name, intent.throughIndex, intent.at); },
|
|
121
145
|
};
|
|
122
146
|
}
|
|
123
147
|
case 'compact': {
|
|
@@ -134,36 +158,8 @@ function plan(doc, intent) {
|
|
|
134
158
|
const block = archive
|
|
135
159
|
.map((act, index) => renderArtifactAct(act, { first: existing === '' && index === 0 }))
|
|
136
160
|
.join('\n');
|
|
137
|
-
return
|
|
161
|
+
return prepareAppend(intent.archivePath, block, existing);
|
|
138
162
|
},
|
|
139
|
-
afterCommit: [],
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
case 'reconcile-delivery-backlog': {
|
|
143
|
-
const { recent, historical } = partitionPendingDeliveries('', { now: intent.now, doc });
|
|
144
|
-
const actor = intent.actor ?? 'doctor --fix reconcile-backlog';
|
|
145
|
-
const receipts = historical.filter((item) => doc.runtime.mentionReceipts[item.recipient]?.[actId(item.actIndex)]?.status !== 'delivered');
|
|
146
|
-
const result = { reconciled: receipts.length, skippedRecent: recent.length, items: historical };
|
|
147
|
-
return {
|
|
148
|
-
result,
|
|
149
|
-
acts: [],
|
|
150
|
-
...(receipts.length === 0
|
|
151
|
-
? {}
|
|
152
|
-
: {
|
|
153
|
-
mutateRuntime: (runtime) => {
|
|
154
|
-
for (const item of receipts) {
|
|
155
|
-
const recipientReceipts = runtime.mentionReceipts[item.recipient] ?? {};
|
|
156
|
-
recipientReceipts[actId(item.actIndex)] = {
|
|
157
|
-
status: 'delivered',
|
|
158
|
-
at: intent.now,
|
|
159
|
-
reason: 'reconciled',
|
|
160
|
-
actor,
|
|
161
|
-
};
|
|
162
|
-
runtime.mentionReceipts[item.recipient] = recipientReceipts;
|
|
163
|
-
}
|
|
164
|
-
},
|
|
165
|
-
}),
|
|
166
|
-
afterCommit: [],
|
|
167
163
|
};
|
|
168
164
|
}
|
|
169
165
|
case 'repair':
|
|
@@ -174,36 +170,22 @@ function plan(doc, intent) {
|
|
|
174
170
|
preparePersistence: intent.quarantine === undefined || intent.quarantine.blocks.length === 0
|
|
175
171
|
? undefined
|
|
176
172
|
: () => {
|
|
177
|
-
const existing = fs.existsSync(intent.quarantine.path) ? fs.readFileSync(intent.quarantine.path, 'utf8') : '';
|
|
178
173
|
const block = intent.quarantine.blocks.join('\n\n');
|
|
179
|
-
return
|
|
174
|
+
return prepareAppend(intent.quarantine.path, block);
|
|
180
175
|
},
|
|
181
|
-
afterCommit: [],
|
|
182
176
|
};
|
|
183
177
|
}
|
|
184
178
|
}
|
|
185
|
-
async function runEffects(squarePath, effects) {
|
|
186
|
-
for (const effect of effects) {
|
|
187
|
-
if (effect.type === 'dispatch-act-notifications') {
|
|
188
|
-
await dispatchActNotifications(squarePath, effect.item);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
179
|
function commitPlan(squarePath, doc, planned) {
|
|
193
180
|
const nextDoc = planned.replaceDoc ?? doc;
|
|
194
|
-
const
|
|
195
|
-
const afterCommit = [
|
|
196
|
-
...planned.afterCommit,
|
|
197
|
-
...applied.acts.map((item) => ({ type: 'dispatch-act-notifications', item })),
|
|
198
|
-
];
|
|
199
|
-
const committed = { result: planned.result, acts: applied.acts, afterCommit };
|
|
181
|
+
const committed = { result: planned.result, acts: applyActs(nextDoc, planned.acts, planned.mutateRuntime) };
|
|
200
182
|
if (planned.acts.length === 0 && planned.mutateRuntime === undefined && planned.replaceDoc === undefined) {
|
|
201
183
|
return committed;
|
|
202
184
|
}
|
|
203
185
|
let persistence;
|
|
204
186
|
try {
|
|
205
187
|
persistence = planned.preparePersistence?.();
|
|
206
|
-
|
|
188
|
+
writeSquareDoc(squarePath, nextDoc);
|
|
207
189
|
persistence?.finalize();
|
|
208
190
|
return committed;
|
|
209
191
|
}
|
|
@@ -217,31 +199,56 @@ function commitPlan(squarePath, doc, planned) {
|
|
|
217
199
|
}
|
|
218
200
|
/** The one mutation pipeline shared by package and CLI adapters. */
|
|
219
201
|
export async function execute(squarePath, intent) {
|
|
220
|
-
const committed = await
|
|
221
|
-
|
|
202
|
+
const committed = await withSquareLock(squarePath, () => {
|
|
203
|
+
const doc = loadSquare(squarePath);
|
|
204
|
+
return commitPlan(squarePath, doc, plan(doc, intent));
|
|
205
|
+
});
|
|
206
|
+
for (const act of committed.acts) {
|
|
207
|
+
if (act.kind === 'say')
|
|
208
|
+
await dispatchActNotifications(squarePath, act);
|
|
209
|
+
}
|
|
222
210
|
return committed;
|
|
223
211
|
}
|
|
224
212
|
/** Application-owned artifact creation; adapters provide validated options and stdin text only. */
|
|
225
213
|
export async function createSquare(squarePath, options, snippet) {
|
|
226
|
-
await
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
now,
|
|
214
|
+
await withSquareLock(squarePath, () => {
|
|
215
|
+
if (fs.existsSync(squarePath) && !options.force) {
|
|
216
|
+
throw new SquareError('conflict', `Refusing to overwrite existing square: ${squarePath}\nPass -f to overwrite.`);
|
|
217
|
+
}
|
|
218
|
+
const temporary = path.join(path.dirname(squarePath), `.${path.basename(squarePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
219
|
+
fs.mkdirSync(path.dirname(squarePath), { recursive: true });
|
|
220
|
+
fs.writeFileSync(temporary, renderSquare(options, snippet));
|
|
221
|
+
fs.renameSync(temporary, squarePath);
|
|
222
|
+
saveRuntimeSidecar(squarePath, emptyRuntimeState(0));
|
|
236
223
|
});
|
|
237
|
-
return committed.result;
|
|
238
224
|
}
|
|
239
225
|
/** Keep artifact repair planning and dependent quarantine persistence inside the application boundary. */
|
|
240
226
|
export async function repairSquare(squarePath) {
|
|
241
|
-
const result = await
|
|
227
|
+
const result = await withSquareLock(squarePath, () => {
|
|
228
|
+
let text;
|
|
229
|
+
try {
|
|
230
|
+
text = fs.readFileSync(squarePath, 'utf8');
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
if (error.code === 'ENOENT')
|
|
234
|
+
throw new SquareError('not_found', `square file not found: ${squarePath}`);
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
242
237
|
const repair = planRepair(text);
|
|
243
238
|
if (repair.diagnosis.unfixable || repair.repaired === undefined)
|
|
244
|
-
return { repair
|
|
239
|
+
return { repair };
|
|
240
|
+
// Repair changes Markdown only. Keep the sidecar's runtime metadata and
|
|
241
|
+
// merge history boundaries so a doctor run cannot erase delivery state or
|
|
242
|
+
// reuse a stable activity index.
|
|
243
|
+
const sidecarRuntime = loadRuntimeSidecar(squarePath, repair.repaired.doc.runtime);
|
|
244
|
+
const indexesPreserved = repair.diagnosis.acts.every(({ act }, index) => repair.repaired.doc.acts[index]?.index === act.index);
|
|
245
|
+
if (indexesPreserved) {
|
|
246
|
+
repair.repaired.doc.runtime = mergeRuntimeState(repair.repaired.doc.runtime, sidecarRuntime);
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
repair.repaired.doc.runtime = emptyRuntimeState(Math.max(repair.repaired.doc.runtime.nextActIndex, sidecarRuntime.nextActIndex));
|
|
250
|
+
repair.repaired.actions.push({ message: 'reset runtime delivery metadata because act indexes changed' });
|
|
251
|
+
}
|
|
245
252
|
const quarantinePath = squarePath.replace(/\.md$/, '') + '.quarantine.md';
|
|
246
253
|
const intent = {
|
|
247
254
|
type: 'repair',
|
|
@@ -250,10 +257,8 @@ export async function repairSquare(squarePath) {
|
|
|
250
257
|
? {}
|
|
251
258
|
: { quarantine: { path: quarantinePath, blocks: repair.repaired.quarantinedBlocks } }),
|
|
252
259
|
};
|
|
253
|
-
|
|
254
|
-
return { repair
|
|
260
|
+
commitPlan(squarePath, repair.repaired.doc, plan(repair.repaired.doc, intent));
|
|
261
|
+
return { repair };
|
|
255
262
|
});
|
|
256
|
-
await runEffects(squarePath, result.afterCommit);
|
|
257
263
|
return result.repair;
|
|
258
264
|
}
|
|
259
|
-
export const application = { execute, plan };
|
package/dist/square-core.js
CHANGED
|
@@ -26,14 +26,6 @@ function touchParticipant(byKey, ordered, actor) {
|
|
|
26
26
|
ordered.push(created);
|
|
27
27
|
return created;
|
|
28
28
|
}
|
|
29
|
-
export function isWarm(lastSeen, now, threshold) {
|
|
30
|
-
if (lastSeen === undefined)
|
|
31
|
-
return false;
|
|
32
|
-
if (!Number.isFinite(lastSeen) || !Number.isFinite(now) || !Number.isFinite(threshold) || threshold <= 0)
|
|
33
|
-
return false;
|
|
34
|
-
const delta = now - lastSeen;
|
|
35
|
-
return delta >= 0 && delta <= threshold;
|
|
36
|
-
}
|
|
37
29
|
function pushThrottleAt(state, at) {
|
|
38
30
|
if (typeof at === 'number' && Number.isFinite(at))
|
|
39
31
|
state.throttleActivityAts.push(at);
|
|
@@ -59,7 +51,7 @@ function bellRecentAt(state, actor, at, windowMs) {
|
|
|
59
51
|
}
|
|
60
52
|
return latest;
|
|
61
53
|
}
|
|
62
|
-
export function fold(acts
|
|
54
|
+
export function fold(acts) {
|
|
63
55
|
const ordered = [];
|
|
64
56
|
const byKey = new Map();
|
|
65
57
|
const hold = { active: false };
|
|
@@ -168,12 +160,12 @@ export function validate(state, act, options = {}) {
|
|
|
168
160
|
return { ok: true };
|
|
169
161
|
}
|
|
170
162
|
}
|
|
171
|
-
export function perceive(state, act, viewer
|
|
163
|
+
export function perceive(state, act, viewer) {
|
|
172
164
|
void state;
|
|
173
165
|
if (act.kind !== 'say')
|
|
174
166
|
return 'full';
|
|
175
167
|
const actor = act.actor;
|
|
176
|
-
if (
|
|
168
|
+
if (sameName(actor, viewer))
|
|
177
169
|
return 'full';
|
|
178
170
|
if (act.reach === undefined || act.reach === 'bell')
|
|
179
171
|
return 'full';
|
package/dist/stream.js
CHANGED
|
@@ -1,149 +1,50 @@
|
|
|
1
|
-
// stream.ts — live activity feed
|
|
2
1
|
import fs from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
4
3
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
5
4
|
import { loadSquare } from './artifact.js';
|
|
6
|
-
import { planActNotifications } from './
|
|
5
|
+
import { planActNotifications } from './delivery.js';
|
|
7
6
|
import { sameName } from './model.js';
|
|
8
|
-
import { indexedDelta } from './activity-feed.js';
|
|
9
|
-
import { SLEEP_MS, actStableIndex, inSquareCount, latestIndexedActIndex, nowMs, rosterNames, sayNumberFor } from './runtime.js';
|
|
10
7
|
import { quoteShell } from './presentation.js';
|
|
11
|
-
import {
|
|
12
|
-
const INITIAL_DUMP = 20;
|
|
8
|
+
import { SLEEP_MS } from './runtime.js';
|
|
13
9
|
export function streamNotificationFor(doc, item, recipient) {
|
|
14
10
|
return planActNotifications(doc, item).find((notification) => sameName(notification.recipient, recipient));
|
|
15
11
|
}
|
|
16
|
-
|
|
17
|
-
return
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
12
|
+
function streamRows(squarePath, doc, cursor, recipient) {
|
|
13
|
+
return doc.acts.filter((act) => act.index > cursor).flatMap((act) => {
|
|
14
|
+
const notification = recipient === undefined ? undefined : streamNotificationFor(doc, act, recipient);
|
|
15
|
+
if (recipient !== undefined && notification === undefined)
|
|
16
|
+
return [];
|
|
17
|
+
return [JSON.stringify({
|
|
18
|
+
seq: act.index,
|
|
19
|
+
square: squarePath,
|
|
20
|
+
...act,
|
|
21
|
+
...(notification === undefined ? {} : { route: notification.route }),
|
|
22
|
+
})];
|
|
23
|
+
});
|
|
27
24
|
}
|
|
28
|
-
|
|
25
|
+
/** Machine-readable tailing stays available; interactive terminal rendering was retired. */
|
|
26
|
+
export async function cmdStreamNdjson(squarePath, recipient) {
|
|
29
27
|
if (!fs.existsSync(squarePath)) {
|
|
30
28
|
process.stderr.write(`square not found: ${squarePath}\n`);
|
|
31
|
-
process.
|
|
29
|
+
process.exitCode = 2;
|
|
30
|
+
return;
|
|
32
31
|
}
|
|
33
|
-
let doc = loadSquare(squarePath);
|
|
34
32
|
let cursor = -1;
|
|
35
|
-
const emit = (events) => {
|
|
36
|
-
for (const { act, index } of events) {
|
|
37
|
-
const item = { act, index };
|
|
38
|
-
const notification = forName ? streamNotificationFor(doc, item, forName) : undefined;
|
|
39
|
-
if (forName && !notification)
|
|
40
|
-
continue;
|
|
41
|
-
process.stdout.write(`${JSON.stringify({
|
|
42
|
-
seq: index,
|
|
43
|
-
square: squarePath,
|
|
44
|
-
...act,
|
|
45
|
-
...(notification ? { via: notification.via } : {}),
|
|
46
|
-
})}\n`);
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
const backlog = indexedDelta(doc.acts, cursor);
|
|
50
|
-
emit(backlog);
|
|
51
|
-
cursor = latestIndexedActIndex(doc.acts.map((act) => ({ act, index: actStableIndex(act) })));
|
|
52
33
|
while (true) {
|
|
53
|
-
await sleep(SLEEP_MS);
|
|
54
34
|
try {
|
|
55
|
-
doc = loadSquare(squarePath);
|
|
35
|
+
const doc = loadSquare(squarePath);
|
|
36
|
+
for (const row of streamRows(squarePath, doc, cursor, recipient))
|
|
37
|
+
process.stdout.write(`${row}\n`);
|
|
38
|
+
cursor = Math.max(cursor, ...doc.acts.map((act) => act.index));
|
|
56
39
|
}
|
|
57
40
|
catch {
|
|
58
|
-
//
|
|
59
|
-
continue;
|
|
41
|
+
// A concurrent artifact replacement is retried on the next poll.
|
|
60
42
|
}
|
|
61
|
-
|
|
62
|
-
if (delta.length === 0)
|
|
63
|
-
continue;
|
|
64
|
-
emit(delta);
|
|
65
|
-
cursor = latestIndexedActIndex(doc.acts.map((act) => ({ act, index: actStableIndex(act) })));
|
|
43
|
+
await sleep(SLEEP_MS);
|
|
66
44
|
}
|
|
67
45
|
}
|
|
68
|
-
async function readKey() {
|
|
69
|
-
return new Promise((resolve) => {
|
|
70
|
-
const finish = (value) => {
|
|
71
|
-
clearTimeout(timer);
|
|
72
|
-
process.stdin.removeListener('data', onData);
|
|
73
|
-
resolve(value);
|
|
74
|
-
};
|
|
75
|
-
const onData = (chunk) => finish(chunk.toString());
|
|
76
|
-
const timer = setTimeout(() => finish(null), 100);
|
|
77
|
-
timer.unref();
|
|
78
|
-
process.stdin.once('data', onData);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
46
|
export async function cmdStream(squarePath) {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
87
|
-
process.stderr.write('✕ interactive stream requires a TTY\n');
|
|
88
|
-
process.stderr.write(`» square --square-path ${quoteShell(path.resolve(squarePath))} stream --ndjson\n`);
|
|
89
|
-
process.exit(2);
|
|
90
|
-
}
|
|
91
|
-
let doc = loadSquare(squarePath);
|
|
92
|
-
let cursor = latestIndexedActIndex(doc.acts.map((act) => ({ act, index: actStableIndex(act) })));
|
|
93
|
-
let stoppedBy;
|
|
94
|
-
const stop = (signal) => {
|
|
95
|
-
stoppedBy = signal;
|
|
96
|
-
};
|
|
97
|
-
const onSigint = () => stop('SIGINT');
|
|
98
|
-
const onSigterm = () => stop('SIGTERM');
|
|
99
|
-
process.once('SIGINT', onSigint);
|
|
100
|
-
process.once('SIGTERM', onSigterm);
|
|
101
|
-
enterAlternateScreen();
|
|
102
|
-
enableRawMode();
|
|
103
|
-
hideCursor();
|
|
104
|
-
clearScreen();
|
|
105
|
-
try {
|
|
106
|
-
const allIndexed = doc.acts.map((act) => ({ act, index: actStableIndex(act) }));
|
|
107
|
-
const initial = allIndexed.filter((item) => item.act.kind !== 'read').slice(-INITIAL_DUMP);
|
|
108
|
-
cursor = Math.max(cursor, latestIndexedActIndex(initial));
|
|
109
|
-
process.stdout.write(renderDump(squarePath, doc, initial, nowMs()));
|
|
110
|
-
process.stdout.write(renderWaiting());
|
|
111
|
-
while (stoppedBy === undefined) {
|
|
112
|
-
const key = await readKey();
|
|
113
|
-
if (key === 'q' || key === '\x1b' || key === '\x03')
|
|
114
|
-
break;
|
|
115
|
-
try {
|
|
116
|
-
doc = loadSquare(squarePath);
|
|
117
|
-
}
|
|
118
|
-
catch {
|
|
119
|
-
await sleep(SLEEP_MS);
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
const delta = indexedDelta(doc.acts, cursor);
|
|
123
|
-
if (delta.length === 0) {
|
|
124
|
-
await sleep(SLEEP_MS);
|
|
125
|
-
continue;
|
|
126
|
-
}
|
|
127
|
-
cursor = latestIndexedActIndex(doc.acts.map((act) => ({ act, index: actStableIndex(act) })));
|
|
128
|
-
cursorUp(1);
|
|
129
|
-
clearLine();
|
|
130
|
-
const fresh = nowMs();
|
|
131
|
-
for (const item of delta) {
|
|
132
|
-
if (item.act.kind !== 'read') {
|
|
133
|
-
process.stdout.write(renderStreamEvent(item.act, fresh, item.act.kind === 'say' ? sayNumberFor(doc.acts, item.act) : undefined));
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
process.stdout.write(renderWaiting());
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
finally {
|
|
140
|
-
process.off('SIGINT', onSigint);
|
|
141
|
-
process.off('SIGTERM', onSigterm);
|
|
142
|
-
disableRawMode();
|
|
143
|
-
showCursor();
|
|
144
|
-
leaveAlternateScreen();
|
|
145
|
-
showCursor();
|
|
146
|
-
}
|
|
147
|
-
if (stoppedBy !== undefined)
|
|
148
|
-
process.exitCode = stoppedBy === 'SIGINT' ? 130 : 143;
|
|
47
|
+
process.stderr.write('✕ interactive stream was removed\n');
|
|
48
|
+
process.stderr.write(`» square --square-path ${quoteShell(path.resolve(squarePath))} stream --ndjson\n`);
|
|
49
|
+
process.exitCode = 2;
|
|
149
50
|
}
|