@astrosheep/square 0.3.2
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 +25 -0
- package/codex-plugin/hooks/hooks.json +28 -0
- package/dist/activity-feed.js +36 -0
- package/dist/activity.js +151 -0
- package/dist/artifact.js +739 -0
- package/dist/claude-hook.js +112 -0
- package/dist/cmd/notify-once.js +37 -0
- package/dist/compact.js +39 -0
- package/dist/decisions.js +286 -0
- package/dist/delivery-health.js +249 -0
- package/dist/delivery.js +93 -0
- package/dist/doctor.js +34 -0
- package/dist/harness.js +584 -0
- package/dist/help.js +131 -0
- package/dist/inbox.js +33 -0
- package/dist/index.js +163 -0
- package/dist/list.js +126 -0
- package/dist/model.js +44 -0
- package/dist/notifications.js +97 -0
- package/dist/paseo-timeline.js +206 -0
- package/dist/presentation.js +468 -0
- package/dist/presented.js +211 -0
- package/dist/registry.js +299 -0
- package/dist/runtime.js +304 -0
- package/dist/search.js +54 -0
- package/dist/square-core.js +183 -0
- package/dist/square.js +1366 -0
- package/dist/stream.js +149 -0
- package/dist/terminal.js +125 -0
- package/dist/time.js +81 -0
- package/dist/wake-sink.js +219 -0
- package/dist/watch.js +386 -0
- package/extensions/square-opencode.js +87 -0
- package/extensions/square-pi.js +167 -0
- package/guides/architect.md +165 -0
- package/guides/brainstorm.md +404 -0
- package/guides/participant.md +171 -0
- package/package.json +57 -0
- package/skills/brainstorm/SKILL.md +136 -0
- package/skills/square/.claude-plugin/plugin.json +8 -0
- package/skills/square/SKILL.md +154 -0
- package/skills/square/hooks/hooks.json +27 -0
- package/skills/square-feedback/SKILL.md +55 -0
- package/skills/square-feedback/agents/openai.yaml +4 -0
- package/template.md +4 -0
- package/templates/architect.md +4 -0
- package/templates/brainstorm.md +4 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { canonicalSquarePath, lookupParticipant, lookupSessionBindings } from './registry.js';
|
|
6
|
+
import { sameName } from './model.js';
|
|
7
|
+
const RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
8
|
+
const LOCK_STALE_MS = 5 * 60_000;
|
|
9
|
+
const LOCK_RETRY_MS = 10;
|
|
10
|
+
const lockWait = new Int32Array(new SharedArrayBuffer(4));
|
|
11
|
+
const heldLocks = new Set();
|
|
12
|
+
export function presentedPath(env = process.env) {
|
|
13
|
+
return env.SQUARE_PRESENTED || path.join(os.homedir(), '.square', 'presented.ndjsonl');
|
|
14
|
+
}
|
|
15
|
+
function rowKey(row) {
|
|
16
|
+
return `${row.owner_id}\u0000${canonicalSquarePath(row.square_path)}\u0000${row.name.toLocaleLowerCase()}\u0000${row.act_index}`;
|
|
17
|
+
}
|
|
18
|
+
function readRows(filePath, now = Date.now()) {
|
|
19
|
+
let text;
|
|
20
|
+
try {
|
|
21
|
+
text = fs.readFileSync(filePath, 'utf8');
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code === 'ENOENT')
|
|
25
|
+
return [];
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
const cutoff = now - RETENTION_MS;
|
|
29
|
+
const rows = [];
|
|
30
|
+
for (const line of text.split('\n')) {
|
|
31
|
+
if (!line.trim())
|
|
32
|
+
continue;
|
|
33
|
+
try {
|
|
34
|
+
const parsed = JSON.parse(line);
|
|
35
|
+
if (parsed.v !== 2 ||
|
|
36
|
+
typeof parsed.ts !== 'number' ||
|
|
37
|
+
typeof parsed.owner_id !== 'string' ||
|
|
38
|
+
typeof parsed.presenter_session_id !== 'string' ||
|
|
39
|
+
typeof parsed.square_path !== 'string' ||
|
|
40
|
+
typeof parsed.name !== 'string' ||
|
|
41
|
+
typeof parsed.act_index !== 'number' ||
|
|
42
|
+
parsed.ts < cutoff) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
rows.push(parsed);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// The presented ledger is a disposable cache; malformed rows are ignored.
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return rows;
|
|
52
|
+
}
|
|
53
|
+
function writeRows(filePath, rows) {
|
|
54
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
55
|
+
const temp = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
56
|
+
fs.writeFileSync(temp, rows.map((row) => JSON.stringify(row)).join('\n') + (rows.length ? '\n' : ''), {
|
|
57
|
+
mode: 0o600,
|
|
58
|
+
});
|
|
59
|
+
fs.renameSync(temp, filePath);
|
|
60
|
+
}
|
|
61
|
+
function lockOwnerState(lockPath) {
|
|
62
|
+
let pid;
|
|
63
|
+
try {
|
|
64
|
+
pid = Number.parseInt(fs.readFileSync(lockPath, 'utf8').split('\n')[0], 10);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return 'unknown';
|
|
68
|
+
}
|
|
69
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
70
|
+
return 'unknown';
|
|
71
|
+
try {
|
|
72
|
+
process.kill(pid, 0);
|
|
73
|
+
return 'alive';
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
return error.code === 'ESRCH' ? 'dead' : 'alive';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function withFileLock(lockPath, fn) {
|
|
80
|
+
if (heldLocks.has(lockPath))
|
|
81
|
+
throw new Error(`Reentrant presented lock: ${lockPath}`);
|
|
82
|
+
fs.mkdirSync(path.dirname(lockPath), { recursive: true });
|
|
83
|
+
while (true) {
|
|
84
|
+
let acquired = false;
|
|
85
|
+
try {
|
|
86
|
+
const fd = fs.openSync(lockPath, 'wx', 0o600);
|
|
87
|
+
try {
|
|
88
|
+
fs.writeFileSync(fd, `${process.pid}\n${Date.now()}\n`, 'utf8');
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
fs.closeSync(fd);
|
|
92
|
+
}
|
|
93
|
+
acquired = true;
|
|
94
|
+
heldLocks.add(lockPath);
|
|
95
|
+
return fn();
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
const errno = error;
|
|
99
|
+
if (acquired || errno.code !== 'EEXIST')
|
|
100
|
+
throw error;
|
|
101
|
+
try {
|
|
102
|
+
const stat = fs.statSync(lockPath);
|
|
103
|
+
if (lockOwnerState(lockPath) === 'dead' || Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
104
|
+
fs.unlinkSync(lockPath);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
catch { }
|
|
109
|
+
Atomics.wait(lockWait, 0, 0, LOCK_RETRY_MS);
|
|
110
|
+
}
|
|
111
|
+
finally {
|
|
112
|
+
if (acquired) {
|
|
113
|
+
heldLocks.delete(lockPath);
|
|
114
|
+
try {
|
|
115
|
+
fs.unlinkSync(lockPath);
|
|
116
|
+
}
|
|
117
|
+
catch { }
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function membershipKey(membership) {
|
|
123
|
+
return `${canonicalSquarePath(membership.squarePath)}\u0000${membership.name.toLocaleLowerCase()}`;
|
|
124
|
+
}
|
|
125
|
+
function attentionLockPath(filePath, membership) {
|
|
126
|
+
const digest = createHash('sha256').update(membershipKey(membership)).digest('hex');
|
|
127
|
+
return `${filePath}.${digest}.lock`;
|
|
128
|
+
}
|
|
129
|
+
function withAttentionLocks(filePath, inbox, fn) {
|
|
130
|
+
const lockPaths = [...new Set(inbox.map((membership) => attentionLockPath(filePath, membership)))].sort();
|
|
131
|
+
function acquire(index) {
|
|
132
|
+
if (index >= lockPaths.length)
|
|
133
|
+
return fn();
|
|
134
|
+
return withFileLock(lockPaths[index], () => acquire(index + 1));
|
|
135
|
+
}
|
|
136
|
+
return acquire(0);
|
|
137
|
+
}
|
|
138
|
+
function ownerFor(sessionId, membership) {
|
|
139
|
+
const squarePath = canonicalSquarePath(membership.squarePath);
|
|
140
|
+
const binding = lookupSessionBindings(sessionId).find((candidate) => candidate.squarePath === squarePath && sameName(candidate.name, membership.name));
|
|
141
|
+
return binding?.ownerId ?? `session:${sessionId}`;
|
|
142
|
+
}
|
|
143
|
+
function selectUnpresented(sessionId, inbox, rows) {
|
|
144
|
+
const known = new Set(rows.map(rowKey));
|
|
145
|
+
return inbox.flatMap((membership) => {
|
|
146
|
+
const ownerId = ownerFor(sessionId, membership);
|
|
147
|
+
const notifications = membership.notifications.filter((notification) => !known.has(rowKey({
|
|
148
|
+
owner_id: ownerId,
|
|
149
|
+
square_path: membership.squarePath,
|
|
150
|
+
name: membership.name,
|
|
151
|
+
act_index: notification.actIndex,
|
|
152
|
+
})));
|
|
153
|
+
return notifications.length === 0
|
|
154
|
+
? []
|
|
155
|
+
: [{ membership: { ...membership, notifications }, ownerId }];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
/** True when the current participant owner has already received this attention. */
|
|
159
|
+
export function hasPresentedAttention(squarePath, name, actIndex, env = process.env) {
|
|
160
|
+
const ownerIds = new Set(lookupParticipant(squarePath, name).map((binding) => binding.ownerId));
|
|
161
|
+
if (ownerIds.size === 0)
|
|
162
|
+
return false;
|
|
163
|
+
const resolved = canonicalSquarePath(squarePath);
|
|
164
|
+
return readRows(presentedPath(env)).some((row) => ownerIds.has(row.owner_id) &&
|
|
165
|
+
canonicalSquarePath(row.square_path) === resolved &&
|
|
166
|
+
sameName(row.name, name) &&
|
|
167
|
+
row.act_index === actIndex);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Serialize presentation only for the affected participants. Delivery runs
|
|
171
|
+
* outside the short ledger-write lock, so unrelated owners never wait on an
|
|
172
|
+
* adapter. A throwing callback leaves no row and remains retryable.
|
|
173
|
+
*/
|
|
174
|
+
export function presentOnce(sessionId, lookup, deliver, env = process.env, at = Date.now()) {
|
|
175
|
+
const filePath = presentedPath(env);
|
|
176
|
+
const initial = lookup(sessionId).filter((membership) => membership.notifications.length > 0);
|
|
177
|
+
if (initial.length === 0)
|
|
178
|
+
return undefined;
|
|
179
|
+
const lockedMemberships = new Set(initial.map(membershipKey));
|
|
180
|
+
return withAttentionLocks(filePath, initial, () => {
|
|
181
|
+
const current = lookup(sessionId).filter((membership) => lockedMemberships.has(membershipKey(membership)));
|
|
182
|
+
const selected = selectUnpresented(sessionId, current, readRows(filePath, at));
|
|
183
|
+
if (selected.length === 0)
|
|
184
|
+
return undefined;
|
|
185
|
+
const result = deliver(selected.map(({ membership }) => membership));
|
|
186
|
+
withFileLock(`${filePath}.lock`, () => {
|
|
187
|
+
const rows = readRows(filePath, at);
|
|
188
|
+
const known = new Set(rows.map(rowKey));
|
|
189
|
+
for (const { membership, ownerId } of selected) {
|
|
190
|
+
for (const notification of membership.notifications) {
|
|
191
|
+
const row = {
|
|
192
|
+
v: 2,
|
|
193
|
+
ts: at,
|
|
194
|
+
owner_id: ownerId,
|
|
195
|
+
presenter_session_id: sessionId,
|
|
196
|
+
square_path: canonicalSquarePath(membership.squarePath),
|
|
197
|
+
name: membership.name,
|
|
198
|
+
act_index: notification.actIndex,
|
|
199
|
+
};
|
|
200
|
+
const key = rowKey(row);
|
|
201
|
+
if (known.has(key))
|
|
202
|
+
continue;
|
|
203
|
+
rows.push(row);
|
|
204
|
+
known.add(key);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
writeRows(filePath, rows);
|
|
208
|
+
});
|
|
209
|
+
return result;
|
|
210
|
+
});
|
|
211
|
+
}
|
package/dist/registry.js
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Machine-local participant discovery cache.
|
|
3
|
+
*
|
|
4
|
+
* The Square artifact remains authoritative for membership. This append-only
|
|
5
|
+
* cache only maps native harness sessions and optional Paseo agent ids back to
|
|
6
|
+
* active (square path, participant name) pairs.
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { randomUUID } from 'node:crypto';
|
|
12
|
+
import { nameKey, sameName } from './model.js';
|
|
13
|
+
const MAX_AGE_MS = 7 * 24 * 60 * 60 * 1000;
|
|
14
|
+
const COMPACT_BYTES = 64 * 1024;
|
|
15
|
+
const COMPACT_LINES = 1000;
|
|
16
|
+
const VALID_CHANNELS = new Set(['claude-code', 'codex', 'opencode', 'pi', 'paseo', 'unknown']);
|
|
17
|
+
export function registryPath() {
|
|
18
|
+
if (process.env['SQUARE_REGISTRY'])
|
|
19
|
+
return process.env['SQUARE_REGISTRY'];
|
|
20
|
+
return path.join(homedir(), '.square', 'sessions.ndjsonl');
|
|
21
|
+
}
|
|
22
|
+
export function canonicalSquarePath(squarePath) {
|
|
23
|
+
const absolute = path.resolve(squarePath);
|
|
24
|
+
try {
|
|
25
|
+
return fs.realpathSync.native(absolute);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
return absolute;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
function bindingKey(sessionId, squarePath, name) {
|
|
32
|
+
return JSON.stringify([sessionId, canonicalSquarePath(squarePath), nameKey(name)]);
|
|
33
|
+
}
|
|
34
|
+
function participantKey(squarePath, name) {
|
|
35
|
+
return JSON.stringify([canonicalSquarePath(squarePath), nameKey(name)]);
|
|
36
|
+
}
|
|
37
|
+
function nextOwnerId() {
|
|
38
|
+
return randomUUID();
|
|
39
|
+
}
|
|
40
|
+
function parseLine(raw, now) {
|
|
41
|
+
let value;
|
|
42
|
+
try {
|
|
43
|
+
value = JSON.parse(raw);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (value === null || typeof value !== 'object')
|
|
49
|
+
return undefined;
|
|
50
|
+
const entry = value;
|
|
51
|
+
if ((entry.v !== undefined && entry.v !== 1) ||
|
|
52
|
+
(entry.op !== 'join' && entry.op !== 'done') ||
|
|
53
|
+
typeof entry.session_id !== 'string' ||
|
|
54
|
+
entry.session_id === '' ||
|
|
55
|
+
typeof entry.name !== 'string' ||
|
|
56
|
+
entry.name === '' ||
|
|
57
|
+
typeof entry.square_path !== 'string' ||
|
|
58
|
+
entry.square_path === '' ||
|
|
59
|
+
typeof entry.ts !== 'string') {
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
62
|
+
const updatedAt = Date.parse(entry.ts);
|
|
63
|
+
if (!Number.isFinite(updatedAt) || updatedAt > now || now - updatedAt > MAX_AGE_MS)
|
|
64
|
+
return undefined;
|
|
65
|
+
const channel = entry.channel ?? 'unknown';
|
|
66
|
+
if (!VALID_CHANNELS.has(channel))
|
|
67
|
+
return undefined;
|
|
68
|
+
if (entry.child !== undefined && entry.child !== true)
|
|
69
|
+
return undefined;
|
|
70
|
+
if (entry.paseo_agent_id !== undefined && typeof entry.paseo_agent_id !== 'string')
|
|
71
|
+
return undefined;
|
|
72
|
+
if (entry.owner_id !== undefined && typeof entry.owner_id !== 'string')
|
|
73
|
+
return undefined;
|
|
74
|
+
return { ...entry, v: 1, channel };
|
|
75
|
+
}
|
|
76
|
+
function foldRegistry(raw, now) {
|
|
77
|
+
const state = new Map();
|
|
78
|
+
// A later claim replaces the participant's prior agent, while one claim may retain multiple adapter identities.
|
|
79
|
+
const owners = new Map();
|
|
80
|
+
let order = 0;
|
|
81
|
+
for (const line of raw.split('\n')) {
|
|
82
|
+
if (line.trim() === '')
|
|
83
|
+
continue;
|
|
84
|
+
const entry = parseLine(line, now);
|
|
85
|
+
if (!entry)
|
|
86
|
+
continue;
|
|
87
|
+
order++;
|
|
88
|
+
const ownerId = entry.owner_id ?? `legacy:${order}`;
|
|
89
|
+
state.set(bindingKey(entry.session_id, entry.square_path, entry.name), {
|
|
90
|
+
entry,
|
|
91
|
+
updatedAt: Date.parse(entry.ts),
|
|
92
|
+
ownerId,
|
|
93
|
+
});
|
|
94
|
+
if (entry.op === 'join')
|
|
95
|
+
owners.set(participantKey(entry.square_path, entry.name), ownerId);
|
|
96
|
+
}
|
|
97
|
+
const active = [];
|
|
98
|
+
for (const { entry, updatedAt, ownerId } of state.values()) {
|
|
99
|
+
if (entry.op !== 'join')
|
|
100
|
+
continue;
|
|
101
|
+
if (owners.get(participantKey(entry.square_path, entry.name)) !== ownerId)
|
|
102
|
+
continue;
|
|
103
|
+
active.push({
|
|
104
|
+
sessionId: entry.session_id,
|
|
105
|
+
name: entry.name,
|
|
106
|
+
squarePath: canonicalSquarePath(entry.square_path),
|
|
107
|
+
channel: entry.channel,
|
|
108
|
+
child: entry.child === true,
|
|
109
|
+
...(entry.paseo_agent_id ? { paseoAgentId: entry.paseo_agent_id } : {}),
|
|
110
|
+
ownerId,
|
|
111
|
+
updatedAt,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
return active.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
115
|
+
}
|
|
116
|
+
function compactRegistry(filePath, raw, now) {
|
|
117
|
+
const active = foldRegistry(raw, now);
|
|
118
|
+
const compacted = active
|
|
119
|
+
.slice()
|
|
120
|
+
.reverse()
|
|
121
|
+
.map((binding) => JSON.stringify({
|
|
122
|
+
v: 1,
|
|
123
|
+
ts: new Date(binding.updatedAt).toISOString(),
|
|
124
|
+
op: 'join',
|
|
125
|
+
channel: binding.channel,
|
|
126
|
+
session_id: binding.sessionId,
|
|
127
|
+
name: binding.name,
|
|
128
|
+
square_path: binding.squarePath,
|
|
129
|
+
...(binding.child ? { child: true } : {}),
|
|
130
|
+
...(binding.paseoAgentId ? { paseo_agent_id: binding.paseoAgentId } : {}),
|
|
131
|
+
owner_id: binding.ownerId,
|
|
132
|
+
}))
|
|
133
|
+
.join('\n');
|
|
134
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
135
|
+
fs.writeFileSync(temporary, compacted === '' ? '' : `${compacted}\n`, { mode: 0o600 });
|
|
136
|
+
fs.renameSync(temporary, filePath);
|
|
137
|
+
}
|
|
138
|
+
function maybeCompactRegistry(filePath, now) {
|
|
139
|
+
let stat;
|
|
140
|
+
try {
|
|
141
|
+
stat = fs.statSync(filePath);
|
|
142
|
+
}
|
|
143
|
+
catch (error) {
|
|
144
|
+
if (error.code === 'ENOENT')
|
|
145
|
+
return;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
if (stat.size <= COMPACT_BYTES)
|
|
149
|
+
return;
|
|
150
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
151
|
+
const lines = raw.split('\n').filter(Boolean).length;
|
|
152
|
+
if (stat.size > COMPACT_BYTES || lines > COMPACT_LINES)
|
|
153
|
+
compactRegistry(filePath, raw, now);
|
|
154
|
+
}
|
|
155
|
+
function appendRegistryLine(entry, now) {
|
|
156
|
+
const filePath = registryPath();
|
|
157
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
158
|
+
maybeCompactRegistry(filePath, now);
|
|
159
|
+
const fd = fs.openSync(filePath, 'a', 0o600);
|
|
160
|
+
try {
|
|
161
|
+
fs.writeSync(fd, `${JSON.stringify(entry)}\n`);
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
fs.closeSync(fd);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
function writeLifecycle(op, sessionId, name, squarePath, options) {
|
|
168
|
+
if (!sessionId || !name || !squarePath)
|
|
169
|
+
return;
|
|
170
|
+
const at = options.at ?? Date.now();
|
|
171
|
+
if (!Number.isFinite(at))
|
|
172
|
+
return;
|
|
173
|
+
try {
|
|
174
|
+
appendRegistryLine({
|
|
175
|
+
v: 1,
|
|
176
|
+
ts: new Date(at).toISOString(),
|
|
177
|
+
op,
|
|
178
|
+
channel: options.channel ?? 'unknown',
|
|
179
|
+
session_id: sessionId,
|
|
180
|
+
name,
|
|
181
|
+
square_path: canonicalSquarePath(squarePath),
|
|
182
|
+
...(options.child ? { child: true } : {}),
|
|
183
|
+
...(options.paseoAgentId ? { paseo_agent_id: options.paseoAgentId } : {}),
|
|
184
|
+
...(op === 'join' ? { owner_id: options.ownerId ?? nextOwnerId() } : {}),
|
|
185
|
+
}, at);
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
process.stderr.write(`! square registry write failed: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export function recordJoin(sessionId, name, squarePath, options = {}) {
|
|
192
|
+
writeLifecycle('join', sessionId, name, squarePath, options);
|
|
193
|
+
}
|
|
194
|
+
export function recordDone(sessionId, name, squarePath, options = {}) {
|
|
195
|
+
writeLifecycle('done', sessionId, name, squarePath, options);
|
|
196
|
+
}
|
|
197
|
+
function readActiveBindings(now = Date.now()) {
|
|
198
|
+
try {
|
|
199
|
+
return foldRegistry(fs.readFileSync(registryPath(), 'utf8'), now);
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error.code === 'ENOENT')
|
|
203
|
+
return [];
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export function lookupSessionBindings(sessionId, now = Date.now()) {
|
|
208
|
+
return readActiveBindings(now).filter((binding) => binding.sessionId === sessionId);
|
|
209
|
+
}
|
|
210
|
+
export function lookupSession(sessionId, now = Date.now()) {
|
|
211
|
+
return lookupSessionBindings(sessionId, now).map(({ name, squarePath }) => ({ name, squarePath }));
|
|
212
|
+
}
|
|
213
|
+
export function lookupParticipant(squarePath, name, now = Date.now()) {
|
|
214
|
+
const canonicalPath = canonicalSquarePath(squarePath);
|
|
215
|
+
return readActiveBindings(now).filter((binding) => binding.squarePath === canonicalPath && sameName(binding.name, name));
|
|
216
|
+
}
|
|
217
|
+
export function localSessionIdentities(env = process.env) {
|
|
218
|
+
const paseoAgentId = env['PASEO_AGENT_ID']?.trim() || undefined;
|
|
219
|
+
const identities = [];
|
|
220
|
+
const claudeSessionId = env['CLAUDE_CODE_SESSION_ID']?.trim();
|
|
221
|
+
if (claudeSessionId) {
|
|
222
|
+
identities.push({
|
|
223
|
+
sessionId: claudeSessionId,
|
|
224
|
+
channel: 'claude-code',
|
|
225
|
+
child: env['CLAUDE_CODE_CHILD_SESSION'] === '1',
|
|
226
|
+
...(paseoAgentId ? { paseoAgentId } : {}),
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
const codexThreadId = env['CODEX_THREAD_ID']?.trim();
|
|
230
|
+
if (codexThreadId && !identities.some((identity) => identity.sessionId === codexThreadId)) {
|
|
231
|
+
identities.push({
|
|
232
|
+
sessionId: codexThreadId,
|
|
233
|
+
channel: 'codex',
|
|
234
|
+
child: false,
|
|
235
|
+
...(paseoAgentId ? { paseoAgentId } : {}),
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
const openCodeSessionId = env['OPENCODE_SESSION_ID']?.trim();
|
|
239
|
+
if (openCodeSessionId && !identities.some((identity) => identity.sessionId === openCodeSessionId)) {
|
|
240
|
+
identities.push({
|
|
241
|
+
sessionId: openCodeSessionId,
|
|
242
|
+
channel: 'opencode',
|
|
243
|
+
child: false,
|
|
244
|
+
...(paseoAgentId ? { paseoAgentId } : {}),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
const piSessionId = env['SQUARE_PI_SESSION_ID']?.trim();
|
|
248
|
+
if (piSessionId && !identities.some((identity) => identity.sessionId === piSessionId)) {
|
|
249
|
+
identities.push({
|
|
250
|
+
sessionId: piSessionId,
|
|
251
|
+
channel: 'pi',
|
|
252
|
+
child: false,
|
|
253
|
+
...(paseoAgentId ? { paseoAgentId } : {}),
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
if (paseoAgentId && !identities.some((identity) => identity.sessionId === paseoAgentId)) {
|
|
257
|
+
identities.push({
|
|
258
|
+
sessionId: paseoAgentId,
|
|
259
|
+
channel: 'paseo',
|
|
260
|
+
child: false,
|
|
261
|
+
paseoAgentId,
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
return identities;
|
|
265
|
+
}
|
|
266
|
+
/** True when this process belongs to a harness that can deliver Square attention without a foreground catch. */
|
|
267
|
+
export function hasAutomaticDeliveryIdentity(env = process.env) {
|
|
268
|
+
return localSessionIdentities(env).length > 0;
|
|
269
|
+
}
|
|
270
|
+
export function recordLocalJoin(name, squarePath, env = process.env) {
|
|
271
|
+
const at = Date.now();
|
|
272
|
+
const identities = localSessionIdentities(env);
|
|
273
|
+
const identityIds = new Set(identities.map((identity) => identity.sessionId));
|
|
274
|
+
const current = lookupParticipant(squarePath, name, at);
|
|
275
|
+
const ownerId = current.find((binding) => identityIds.has(binding.sessionId))?.ownerId ?? nextOwnerId();
|
|
276
|
+
for (const identity of identities) {
|
|
277
|
+
recordJoin(identity.sessionId, name, squarePath, { ...identity, at, ownerId });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
export function recordLocalDone(name, squarePath, env = process.env) {
|
|
281
|
+
const at = Date.now();
|
|
282
|
+
const identities = localSessionIdentities(env);
|
|
283
|
+
const identityIds = new Set(identities.map((identity) => identity.sessionId));
|
|
284
|
+
const current = lookupParticipant(squarePath, name, at);
|
|
285
|
+
const ownerId = current.find((binding) => identityIds.has(binding.sessionId))?.ownerId;
|
|
286
|
+
if (ownerId === undefined) {
|
|
287
|
+
for (const identity of identities)
|
|
288
|
+
recordDone(identity.sessionId, name, squarePath, { ...identity, at });
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
for (const binding of current.filter((candidate) => candidate.ownerId === ownerId)) {
|
|
292
|
+
recordDone(binding.sessionId, binding.name, binding.squarePath, {
|
|
293
|
+
channel: binding.channel,
|
|
294
|
+
child: binding.child,
|
|
295
|
+
...(binding.paseoAgentId ? { paseoAgentId: binding.paseoAgentId } : {}),
|
|
296
|
+
at,
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
}
|