@astrosheep/square 0.3.27 → 0.3.29

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.
Files changed (49) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/codex-plugin/.codex-plugin/plugin.json +1 -1
  3. package/dist/artifact.d.ts +4 -4
  4. package/dist/artifact.js +24 -19
  5. package/dist/automatic-session.js +18 -7
  6. package/dist/boundary-presentation.d.ts +1 -1
  7. package/dist/boundary-presentation.js +2 -2
  8. package/dist/cli/context.d.ts +4 -4
  9. package/dist/cli/context.js +11 -9
  10. package/dist/cli/maintenance-commands.js +2 -2
  11. package/dist/cli/observation-commands.js +2 -2
  12. package/dist/cli/program.js +1 -1
  13. package/dist/cli/square-commands.js +15 -16
  14. package/dist/codex-boundary-state.d.ts +4 -4
  15. package/dist/codex-boundary-state.js +19 -19
  16. package/dist/codex-hook.js +3 -3
  17. package/dist/codex-queue.js +2 -2
  18. package/dist/file-lock.d.ts +1 -1
  19. package/dist/file-lock.js +22 -50
  20. package/dist/harness-links.d.ts +2 -1
  21. package/dist/harness-links.js +41 -12
  22. package/dist/harness.js +11 -5
  23. package/dist/inbox.js +2 -2
  24. package/dist/list.js +3 -3
  25. package/dist/notifications.d.ts +1 -1
  26. package/dist/notifications.js +10 -10
  27. package/dist/opencode.d.ts +19 -0
  28. package/dist/opencode.js +49 -0
  29. package/dist/presented.d.ts +5 -13
  30. package/dist/presented.js +48 -158
  31. package/dist/registry.d.ts +18 -30
  32. package/dist/registry.js +89 -320
  33. package/dist/routes.d.ts +6 -6
  34. package/dist/routes.js +20 -20
  35. package/dist/square-file-adapter.d.ts +1 -1
  36. package/dist/square-file-adapter.js +5 -9
  37. package/dist/square-storage.d.ts +3 -3
  38. package/dist/square-storage.js +23 -20
  39. package/dist/square-wiring.js +1 -1
  40. package/dist/wake-attempts.d.ts +6 -6
  41. package/dist/wake-attempts.js +39 -31
  42. package/dist/wake-evidence.d.ts +1 -1
  43. package/dist/wake-evidence.js +11 -11
  44. package/dist/wake-port.d.ts +1 -1
  45. package/dist/wake-port.js +1 -1
  46. package/dist/watch.js +1 -1
  47. package/extensions/square-pi.js +30 -3
  48. package/package.json +5 -1
  49. package/extensions/square-opencode.js +0 -48
package/dist/file-lock.js CHANGED
@@ -1,13 +1,11 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import fs from 'node:fs';
2
+ import { promises as fs } from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { setTimeout as sleep } from 'node:timers/promises';
5
- const lockWait = new Int32Array(new SharedArrayBuffer(4));
6
- const heldSyncLocks = new Set();
7
- function ownerState(lockPath) {
5
+ async function ownerState(lockPath) {
8
6
  let pid;
9
7
  try {
10
- pid = Number.parseInt(fs.readFileSync(lockPath, 'utf8').split('\n')[0], 10);
8
+ pid = Number.parseInt((await fs.readFile(lockPath, 'utf8')).split('\n')[0], 10);
11
9
  }
12
10
  catch {
13
11
  return 'unknown';
@@ -22,10 +20,10 @@ function ownerState(lockPath) {
22
20
  return error.code === 'ESRCH' ? 'dead' : 'alive';
23
21
  }
24
22
  }
25
- function createLock(lockPath) {
23
+ async function createLock(lockPath) {
26
24
  let fd;
27
25
  try {
28
- fd = fs.openSync(lockPath, 'wx', 0o600);
26
+ fd = await fs.open(lockPath, 'wx', 0o600);
29
27
  }
30
28
  catch (error) {
31
29
  if (error.code === 'EEXIST')
@@ -34,79 +32,53 @@ function createLock(lockPath) {
34
32
  }
35
33
  const token = `${process.pid}\n${Date.now()}\n${randomUUID()}\n`;
36
34
  try {
37
- fs.writeFileSync(fd, token, 'utf8');
35
+ await fd.writeFile(token, 'utf8');
38
36
  return token;
39
37
  }
40
38
  catch (error) {
41
- try {
42
- fs.unlinkSync(lockPath);
43
- }
44
- catch { }
39
+ await fs.unlink(lockPath).catch(() => undefined);
45
40
  throw error;
46
41
  }
47
42
  finally {
48
- fs.closeSync(fd);
43
+ await fd.close();
49
44
  }
50
45
  }
51
- function reclaimLock(lockPath, staleMs) {
46
+ async function reclaimLock(lockPath, staleMs) {
52
47
  try {
53
- const stale = Date.now() - fs.statSync(lockPath).mtimeMs > staleMs;
54
- if (ownerState(lockPath) !== 'dead' && !stale)
48
+ const stale = Date.now() - (await fs.stat(lockPath)).mtimeMs > staleMs;
49
+ if (await ownerState(lockPath) !== 'dead' && !stale)
55
50
  return false;
56
- fs.unlinkSync(lockPath);
51
+ await fs.unlink(lockPath);
57
52
  return true;
58
53
  }
59
54
  catch (error) {
60
55
  return error.code === 'ENOENT';
61
56
  }
62
57
  }
63
- function releaseLock(lockPath, token) {
58
+ async function releaseLock(lockPath, token) {
64
59
  try {
65
- if (fs.readFileSync(lockPath, 'utf8') === token)
66
- fs.unlinkSync(lockPath);
60
+ if (await fs.readFile(lockPath, 'utf8') === token)
61
+ await fs.unlink(lockPath);
67
62
  }
68
63
  catch { }
69
64
  }
70
- function prepare(lockPath) {
71
- fs.mkdirSync(path.dirname(lockPath), { recursive: true });
72
- }
73
- export function withFileLockSync(lockPath, options, fn) {
74
- if (heldSyncLocks.has(lockPath))
75
- throw new Error(`Reentrant file lock: ${lockPath}`);
76
- prepare(lockPath);
77
- let token;
78
- while (token === undefined) {
79
- token = createLock(lockPath);
80
- if (token !== undefined)
81
- break;
82
- if (reclaimLock(lockPath, options.staleMs))
83
- continue;
84
- Atomics.wait(lockWait, 0, 0, options.retryMs);
85
- }
86
- heldSyncLocks.add(lockPath);
87
- try {
88
- return fn();
89
- }
90
- finally {
91
- heldSyncLocks.delete(lockPath);
92
- releaseLock(lockPath, token);
93
- }
94
- }
95
65
  export async function withFileLock(lockPath, options, fn) {
96
- prepare(lockPath);
66
+ await fs.mkdir(path.dirname(lockPath), { recursive: true });
97
67
  let token;
98
68
  while (token === undefined) {
99
- token = createLock(lockPath);
69
+ if (options.signal?.aborted)
70
+ throw options.signal.reason ?? new Error('File lock acquisition aborted');
71
+ token = await createLock(lockPath);
100
72
  if (token !== undefined)
101
73
  break;
102
- if (reclaimLock(lockPath, options.staleMs))
74
+ if (await reclaimLock(lockPath, options.staleMs))
103
75
  continue;
104
- await sleep(options.retryMs);
76
+ await sleep(options.retryMs, undefined, { signal: options.signal });
105
77
  }
106
78
  try {
107
79
  return await fn();
108
80
  }
109
81
  finally {
110
- releaseLock(lockPath, token);
82
+ await releaseLock(lockPath, token);
111
83
  }
112
84
  }
@@ -11,7 +11,8 @@ export type OpenCodeCommandRunner = (homeDir: string, args: string[]) => {
11
11
  stdout: string;
12
12
  stderr: string;
13
13
  };
14
+ export declare function installOpenCodePlugin(homeDir: string, force?: boolean, run?: OpenCodeCommandRunner): string[];
15
+ export declare function uninstallOpenCodePlugin(homeDir: string): string[];
14
16
  /** Verify that OpenCode accepts its resolved runtime configuration after links are installed. */
15
17
  export declare function verifyOpenCodeRuntime(homeDir: string, run?: OpenCodeCommandRunner): string;
16
18
  export declare function skillLinks(homeDir?: string, parents?: Array<'.claude' | '.agents'>): HarnessLink[];
17
- export declare function opencodeExtensionLink(homeDir?: string): HarnessLink;
@@ -1,8 +1,9 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
- import { fileURLToPath, pathToFileURL } from 'node:url';
4
+ import { fileURLToPath } from 'node:url';
5
5
  import crossSpawn from 'cross-spawn';
6
+ import { SQUARE_IDENTITY } from './identity.js';
6
7
  function packageRoot() {
7
8
  // Emitted modules live in dist; package assets are one level above them.
8
9
  return fileURLToPath(new URL('../', import.meta.url));
@@ -83,6 +84,42 @@ function runOpenCode(homeDir, args) {
83
84
  throw result.error;
84
85
  return { status: result.status ?? 1, stdout: result.stdout || '', stderr: result.stderr || '' };
85
86
  }
87
+ function requireOpenCodeSuccess(result, action) {
88
+ if (result.status === 0)
89
+ return;
90
+ throw new Error(`OpenCode ${action} failed: ${result.stderr.trim() || result.stdout.trim() || `exit ${result.status}`}`);
91
+ }
92
+ export function installOpenCodePlugin(homeDir, force = false, run = runOpenCode) {
93
+ const args = ['plugin', SQUARE_IDENTITY.packageName, '--global'];
94
+ if (force)
95
+ args.push('--force');
96
+ requireOpenCodeSuccess(run(homeDir, args), 'plugin install');
97
+ return [SQUARE_IDENTITY.packageName];
98
+ }
99
+ function configPath(homeDir) {
100
+ const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config');
101
+ return path.join(configHome, 'opencode', 'opencode.jsonc');
102
+ }
103
+ function removeConfiguredPlugin(homeDir) {
104
+ const target = configPath(homeDir);
105
+ let source;
106
+ try {
107
+ source = fs.readFileSync(target, 'utf8');
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ const escaped = SQUARE_IDENTITY.packageName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
+ const packageLine = new RegExp(`^\\s*"${escaped}(?:@[^"\\n]+)?"\\s*,?\\s*$`, 'm');
114
+ const next = source.replace(packageLine, '');
115
+ if (next === source)
116
+ return false;
117
+ fs.writeFileSync(target, next);
118
+ return true;
119
+ }
120
+ export function uninstallOpenCodePlugin(homeDir) {
121
+ return removeConfiguredPlugin(homeDir) ? [SQUARE_IDENTITY.packageName] : [];
122
+ }
86
123
  /** Verify that OpenCode accepts its resolved runtime configuration after links are installed. */
87
124
  export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
88
125
  try {
@@ -98,10 +135,10 @@ export function verifyOpenCodeRuntime(homeDir, run = runOpenCode) {
98
135
  return '✕ OpenCode debug config returned invalid JSON';
99
136
  }
100
137
  const plugin = config.config?.plugin;
101
- const expected = pathToFileURL(opencodeExtensionLink(homeDir).target).href;
138
+ const expected = SQUARE_IDENTITY.packageName;
102
139
  if (Array.isArray(plugin) && plugin.includes(expected))
103
- return '✓ OpenCode debug config loaded';
104
- return `○ OpenCode plugin not loaded: ${expected}`;
140
+ return '✓ OpenCode npm plugin loaded';
141
+ return `○ OpenCode npm plugin not loaded: ${expected}`;
105
142
  }
106
143
  catch (error) {
107
144
  return `○ OpenCode runtime unavailable (${error instanceof Error ? error.message : String(error)})`;
@@ -114,11 +151,3 @@ export function skillLinks(homeDir = os.homedir(), parents = ['.claude', '.agent
114
151
  kind: 'skill',
115
152
  })));
116
153
  }
117
- export function opencodeExtensionLink(homeDir = os.homedir()) {
118
- const configHome = process.env.XDG_CONFIG_HOME ?? path.join(homeDir, '.config');
119
- return {
120
- source: path.join(packageRoot(), 'extensions', 'square-opencode.js'),
121
- target: path.join(configHome, 'opencode', 'plugins', 'square.js'),
122
- kind: 'extension',
123
- };
124
- }
package/dist/harness.js CHANGED
@@ -4,7 +4,7 @@ import { wakeGraceMs } from './notifications.js';
4
4
  import { doctorClaudePlugin, installClaudePlugin, uninstallClaudePlugin, } from './harness-claude.js';
5
5
  import { doctorCodexPlugin, installCodexPlugin, uninstallCodexPlugin, } from './harness-codex.js';
6
6
  import { doctorPiPackage, installPiPackage, uninstallPiPackage, } from './harness-pi.js';
7
- import { doctorHarnessLinks, installHarnessLinks, opencodeExtensionLink, skillLinks, uninstallHarnessLinks, verifyOpenCodeRuntime, } from './harness-links.js';
7
+ import { doctorHarnessLinks, installHarnessLinks, installOpenCodePlugin, skillLinks, uninstallOpenCodePlugin, uninstallHarnessLinks, verifyOpenCodeRuntime, } from './harness-links.js';
8
8
  function result(lines, notes = []) {
9
9
  return { lines, notes };
10
10
  }
@@ -17,7 +17,7 @@ async function doctorHost(label, inspect) {
17
17
  }
18
18
  }
19
19
  function openCodeLinks(homeDir) {
20
- return [opencodeExtensionLink(homeDir), ...skillLinks(homeDir, ['.agents'])];
20
+ return skillLinks(homeDir, ['.agents']);
21
21
  }
22
22
  function readableSquarePath(squarePath) {
23
23
  if (squarePath === undefined)
@@ -65,9 +65,15 @@ const TARGETS = [
65
65
  {
66
66
  name: 'opencode',
67
67
  capabilities: ['install', 'uninstall', 'doctor'],
68
- install: ({ homeDir, force }) => result(installHarnessLinks(openCodeLinks(homeDir), force)),
69
- uninstall: ({ homeDir }) => result(uninstallHarnessLinks(openCodeLinks(homeDir))),
70
- doctor: ({ homeDir }) => result([...doctorHarnessLinks(openCodeLinks(homeDir)), verifyOpenCodeRuntime(homeDir)]),
68
+ install: async ({ homeDir, force }) => result([
69
+ ...installOpenCodePlugin(homeDir, force),
70
+ ...installHarnessLinks(openCodeLinks(homeDir), force),
71
+ ]),
72
+ uninstall: ({ homeDir }) => result([
73
+ ...uninstallOpenCodePlugin(homeDir),
74
+ ...uninstallHarnessLinks(openCodeLinks(homeDir)),
75
+ ]),
76
+ doctor: ({ homeDir }) => result([verifyOpenCodeRuntime(homeDir), ...doctorHarnessLinks(openCodeLinks(homeDir))]),
71
77
  },
72
78
  {
73
79
  name: 'pi',
package/dist/inbox.js CHANGED
@@ -18,7 +18,7 @@ function withoutExcluded(inbox, excludeKeys) {
18
18
  }
19
19
  export async function sessionInbox(sessionId) {
20
20
  const inbox = [];
21
- for (const binding of lookupSessionBindings(sessionId)) {
21
+ for (const binding of await lookupSessionBindings(sessionId)) {
22
22
  let square;
23
23
  try {
24
24
  square = await openSquare(binding.squarePath);
@@ -53,7 +53,7 @@ export async function waitForSessionPending(sessionId, timeoutMs, options = {})
53
53
  }
54
54
  if (timeoutMs <= 0 || options.signal?.aborted)
55
55
  return [];
56
- const bindings = lookupSessionBindings(sessionId);
56
+ const bindings = await lookupSessionBindings(sessionId);
57
57
  const paths = [...new Set(bindings.map((binding) => binding.squarePath))];
58
58
  let aborted = false;
59
59
  let projectAfterReady = !options.skipImmediate;
package/dist/list.js CHANGED
@@ -15,12 +15,12 @@ function contextLines(lines) {
15
15
  async function readSquareListItem(filePath, root) {
16
16
  let stat;
17
17
  try {
18
- stat = fs.statSync(filePath);
18
+ stat = await fs.promises.stat(filePath);
19
19
  }
20
20
  catch {
21
21
  return null;
22
22
  }
23
- const square = probeSquare(filePath);
23
+ const square = await probeSquare(filePath);
24
24
  if (square === undefined)
25
25
  return null;
26
26
  const projection = await listPresentation(square).finally(() => closeOpenSquare(square));
@@ -38,7 +38,7 @@ async function collectSquareList(root, maxDepth) {
38
38
  async function walk(dir, depth) {
39
39
  let entries;
40
40
  try {
41
- entries = fs.readdirSync(dir, { withFileTypes: true });
41
+ entries = await fs.promises.readdir(dir, { withFileTypes: true });
42
42
  }
43
43
  catch {
44
44
  // Directory vanished or became unreadable mid-walk — skip it, don't abort the scan.
@@ -28,6 +28,6 @@ export interface SweepPendingNotificationsOptions extends WorkerLaunchOptions {
28
28
  limit?: number;
29
29
  }
30
30
  /** Select sweep candidates from one frozen snapshot and one delivery replay. */
31
- export declare function pendingNotificationSweepFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, limit: number, deriveDelivery?: (snapshot: SquareState) => DeliveryModel): number[];
31
+ export declare function pendingNotificationSweepFromState(squarePath: string, state: SquareState, now: number, env: NodeJS.ProcessEnv, limit: number, deriveDelivery?: (snapshot: SquareState) => DeliveryModel): Promise<number[]>;
32
32
  /** Reconsider old pending attention at a bounded action boundary using the existing worker. */
33
33
  export declare function sweepPendingNotifications(squarePath: string, opts?: SweepPendingNotificationsOptions): Promise<number[]>;
@@ -53,7 +53,7 @@ function renderWakePayload(request, body, kind) {
53
53
  ].join('\n');
54
54
  }
55
55
  async function waitForCatch(route, request, body) {
56
- const binding = lookupParticipant(request.squarePath, request.recipient)
56
+ const binding = (await lookupParticipant(request.squarePath, request.recipient))
57
57
  .find((item) => item.ownerId === route.ownerId);
58
58
  const activeCatch = binding && (await sessionInbox(binding.sessionId))
59
59
  .find((item) => item.name === request.recipient)?.catchLease;
@@ -68,7 +68,7 @@ async function waitForCatch(route, request, body) {
68
68
  while (Date.now() < deadline) {
69
69
  if (await hasDeliveredNotification(request.squarePath, request.recipient, request.actIndex))
70
70
  return true;
71
- const currentBinding = lookupParticipant(request.squarePath, request.recipient)
71
+ const currentBinding = (await lookupParticipant(request.squarePath, request.recipient))
72
72
  .find((item) => item.ownerId === route.ownerId);
73
73
  const lease = currentBinding && (await sessionInbox(currentBinding.sessionId))
74
74
  .find((item) => item.name === request.recipient)?.catchLease;
@@ -101,7 +101,7 @@ export async function hasAttentionNotification(squarePath, name, ref, env = proc
101
101
  try {
102
102
  const recipient = (await resolveParticipant(square, name)).name;
103
103
  const index = notificationIndex(ref);
104
- return await notificationDelivered(square, recipient, index) || hasPresentedAttention(squarePath, recipient, index, env);
104
+ return await notificationDelivered(square, recipient, index) || await hasPresentedAttention(squarePath, recipient, index, env);
105
105
  }
106
106
  finally {
107
107
  await closeOpenSquare(square);
@@ -162,7 +162,7 @@ async function processNotification(squarePath, notification, opts) {
162
162
  return;
163
163
  }
164
164
  if (claim.type === 'ambiguous') {
165
- const recovered = recordRecoveredUnknown(attention, claim.lease, env);
165
+ const recovered = await recordRecoveredUnknown(attention, claim.lease, env);
166
166
  if (recovered !== undefined) {
167
167
  await releaseNotifyLease(square, notification.recipient, notification.item.index, claim.lease.leaseId);
168
168
  }
@@ -185,7 +185,7 @@ async function processNotification(squarePath, notification, opts) {
185
185
  route: notification.route,
186
186
  };
187
187
  await port.dispatch(evidence.attemptableRoutes, (route) => renderWakePayload(request, notification.item.body, route.kind), {
188
- nextAttemptN: () => nextWakeAttemptNumber(attention, { env, now: now() }),
188
+ nextAttemptN: async () => nextWakeAttemptNumber(attention, { env, now: now() }),
189
189
  beforeSend: async (route, attemptN) => {
190
190
  if (await waitForCatch(route, request, notification.item.body))
191
191
  return false;
@@ -207,7 +207,7 @@ async function processNotification(squarePath, notification, opts) {
207
207
  await transitionNotifyLease(square, notification.recipient, notification.item.index, leaseId, 'claimed');
208
208
  releaseLease = true;
209
209
  }
210
- recordWakeAttempt({
210
+ await recordWakeAttempt({
211
211
  attention,
212
212
  routeKind: route.kind,
213
213
  outcome: outcome.outcome,
@@ -224,7 +224,7 @@ async function processNotification(squarePath, notification, opts) {
224
224
  releaseLease = true;
225
225
  },
226
226
  invalidate: async (route) => {
227
- retireWakeRoute(route, { env, at: now() });
227
+ await retireWakeRoute(route, { env, at: now() });
228
228
  },
229
229
  });
230
230
  }
@@ -257,10 +257,10 @@ export function wakeNotifierForSquare(squarePath, env = process.env) {
257
257
  };
258
258
  }
259
259
  /** Select sweep candidates from one frozen snapshot and one delivery replay. */
260
- export function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery = deriveDeliveryModel) {
260
+ export async function pendingNotificationSweepFromState(squarePath, state, now, env, limit, deriveDelivery = deriveDeliveryModel) {
261
261
  const delivery = deriveDelivery(state);
262
262
  const pending = pendingDeliveriesFromState(state, delivery);
263
- const evidence = wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery);
263
+ const evidence = await wakeEvidenceProjectionFromState(squarePath, state, now, env, delivery);
264
264
  const indexes = new Set();
265
265
  for (const recipient of pending) {
266
266
  for (const note of recipient.notifications) {
@@ -288,7 +288,7 @@ export async function sweepPendingNotifications(squarePath, opts = {}) {
288
288
  finally {
289
289
  await closeOpenSquare(square);
290
290
  }
291
- const selected = pendingNotificationSweepFromState(squarePath, state, now, env, limit);
291
+ const selected = await pendingNotificationSweepFromState(squarePath, state, now, env, limit);
292
292
  const workerPath = fileURLToPath(new URL('./cmd/notify-once.js', import.meta.url));
293
293
  for (const actIndex of selected) {
294
294
  (opts.launchWorker ?? launchWorker)(workerPath, ['--location', squarePath, '--act-index', String(actIndex)]);
@@ -0,0 +1,19 @@
1
+ /** OpenCode's server plugin entrypoint for the published Square package. */
2
+ export default function squareOpenCodePlugin(): Promise<{
3
+ event: ({ event }: {
4
+ event: {
5
+ type?: string;
6
+ properties?: Record<string, any>;
7
+ };
8
+ }) => Promise<void>;
9
+ 'shell.env': (input: {
10
+ sessionID?: string;
11
+ }, output: {
12
+ env: Record<string, string>;
13
+ }) => Promise<void>;
14
+ 'tool.execute.after': (input: {
15
+ sessionID: string;
16
+ }, output: {
17
+ output: string;
18
+ }) => Promise<void>;
19
+ }>;
@@ -0,0 +1,49 @@
1
+ import { presentPendingAtBoundary } from './boundary-presentation.js';
2
+ import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
3
+ /** OpenCode's server plugin entrypoint for the published Square package. */
4
+ export default async function squareOpenCodePlugin() {
5
+ const joining = new Map();
6
+ return {
7
+ event: async ({ event }) => {
8
+ if (event.type === 'session.created' || event.type === 'session.updated') {
9
+ const sessionID = event.properties?.sessionID;
10
+ const cwd = event.properties?.info?.directory || process.cwd();
11
+ if (sessionID) {
12
+ try {
13
+ const context = await automaticSessionStart('opencode', sessionID, cwd);
14
+ if (context)
15
+ joining.set(sessionID, context);
16
+ }
17
+ catch { /* startup remains bounded */ }
18
+ }
19
+ }
20
+ else if (event.type === 'session.deleted') {
21
+ const sessionID = event.properties?.sessionID;
22
+ const cwd = event.properties?.info?.directory || process.cwd();
23
+ if (sessionID) {
24
+ joining.delete(sessionID);
25
+ await automaticSessionEnd('opencode', sessionID, cwd);
26
+ }
27
+ }
28
+ },
29
+ 'shell.env': async (input, output) => {
30
+ if (input.sessionID)
31
+ output.env.OPENCODE_SESSION_ID = input.sessionID;
32
+ },
33
+ 'tool.execute.after': async (input, output) => {
34
+ try {
35
+ const joined = joining.get(input.sessionID);
36
+ if (joined) {
37
+ joining.delete(input.sessionID);
38
+ output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${joined}`;
39
+ }
40
+ await presentPendingAtBoundary(input.sessionID, (context) => {
41
+ output.output = `${output.output}${output.output === '' ? '' : '\n\n'}${context}`;
42
+ });
43
+ }
44
+ catch {
45
+ // A failed admission remains available at a later boundary.
46
+ }
47
+ },
48
+ };
49
+ }
@@ -6,16 +6,8 @@ export interface PresentedAttention {
6
6
  actIndex: number;
7
7
  }
8
8
  export declare function presentedPath(env?: NodeJS.ProcessEnv): string;
9
- /** Read the current presentation facts once for a derived evidence projection. */
10
- export declare function readPresentedAttentions(env?: NodeJS.ProcessEnv, now?: number): PresentedAttention[];
11
- export declare function hasPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
12
- /** True when any current participant owner has already received this attention. */
13
- export declare function hasPresentedAttention(squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): boolean;
14
- /** Record presentation by a transport that delivered the bounded attention body. */
15
- export declare function recordPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, at?: number): void;
16
- /**
17
- * Serialize presentation only for the affected participants. Delivery runs
18
- * outside the short ledger-write lock, so unrelated owners never wait on an
19
- * adapter. A throwing or rejecting callback leaves no row and remains unpresented.
20
- */
21
- export declare function presentOnce<T>(sessionId: string, lookup: (sessionId: string) => InboxMembership[] | Promise<InboxMembership[]>, deliver: (inbox: InboxMembership[]) => T | Promise<T>, env?: NodeJS.ProcessEnv, at?: number): Promise<T | undefined>;
9
+ export declare function readPresentedAttentions(env?: NodeJS.ProcessEnv, now?: number): Promise<PresentedAttention[]>;
10
+ export declare function hasPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): Promise<boolean>;
11
+ export declare function hasPresentedAttention(squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, now?: number): Promise<boolean>;
12
+ export declare function recordPresentedForOwner(ownerId: string, squarePath: string, name: string, actIndex: number, env?: NodeJS.ProcessEnv, at?: number): Promise<void>;
13
+ export declare function presentOnce<T>(sessionId: string, lookup: (sessionId: string) => InboxMembership[] | Promise<InboxMembership[]>, deliver: (inbox: InboxMembership[]) => T | Promise<T>, env?: NodeJS.ProcessEnv, at?: number, signal?: AbortSignal): Promise<T | undefined>;