@astrosheep/square 0.3.80 → 0.3.82

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 (54) hide show
  1. package/dist/artifact.js +3 -3
  2. package/dist/catch-decisions.d.ts +1 -0
  3. package/dist/catch-decisions.js +8 -2
  4. package/dist/claude-mod.js +1 -2
  5. package/dist/cli/catch-presentation.js +3 -1
  6. package/dist/cli/context.js +1 -0
  7. package/dist/cli/express-presentation.js +2 -3
  8. package/dist/cli/maintenance-commands.js +2 -2
  9. package/dist/cli/registry.js +2 -1
  10. package/dist/cli/square-commands.d.ts +1 -0
  11. package/dist/cli/square-commands.js +7 -2
  12. package/dist/codex-hook.js +3 -11
  13. package/dist/decisions.d.ts +3 -2
  14. package/dist/decisions.js +3 -0
  15. package/dist/help.js +6 -5
  16. package/dist/hints.d.ts +4 -0
  17. package/dist/hints.js +25 -0
  18. package/dist/mcp.js +1 -1
  19. package/dist/model.d.ts +1 -1
  20. package/dist/participant-identity.d.ts +2 -0
  21. package/dist/participant-identity.js +13 -0
  22. package/dist/plugins/opencode/index.js +1 -3
  23. package/dist/presentation.d.ts +0 -1
  24. package/dist/presentation.js +3 -11
  25. package/dist/runtime.js +1 -1
  26. package/dist/session-lifecycle.d.ts +3 -0
  27. package/dist/session-lifecycle.js +21 -0
  28. package/dist/square-actions.d.ts +1 -0
  29. package/dist/square-actions.js +3 -2
  30. package/dist/square-api.js +7 -4
  31. package/dist/square-core.d.ts +5 -0
  32. package/dist/square-domain.d.ts +4 -0
  33. package/dist/square-domain.js +5 -1
  34. package/dist/square-facade.d.ts +6 -1
  35. package/dist/square-migration.d.ts +5 -7
  36. package/dist/square-migration.js +62 -42
  37. package/dist/square-sqlite.d.ts +7 -1
  38. package/dist/square-sqlite.js +52 -22
  39. package/dist/square-tools.js +2 -1
  40. package/dist/views.d.ts +1 -1
  41. package/dist/views.js +1 -1
  42. package/guides/claude-native.md +3 -3
  43. package/guides/migration.md +54 -57
  44. package/guides/row-storage-design.md +4 -1
  45. package/guides/square-api.md +26 -2
  46. package/package.json +2 -1
  47. package/plugins/claude/.claude-plugin/plugin.json +1 -1
  48. package/plugins/claude/skills/square/SKILL.md +6 -7
  49. package/plugins/codex/.codex-plugin/plugin.json +1 -1
  50. package/plugins/opencode/index.ts +1 -2
  51. package/plugins/pi/square-pi.js +4 -22
  52. package/skills/square/SKILL.md +6 -7
  53. package/dist/automatic-session.d.ts +0 -7
  54. package/dist/automatic-session.js +0 -137
package/dist/artifact.js CHANGED
@@ -9,7 +9,7 @@ import { observeFileVersion } from './file-changes.js';
9
9
  import { canonicalPath } from './canonical-path.js';
10
10
  import { InternalSquareError, SquareError } from './model.js';
11
11
  import { wrapSquareWrite } from './square-domain.js';
12
- import { APPLICATION_ID, createSqliteDriver, initializeSqlite, exportSqliteState, inspectSqlite, invalidArtifact, validateSqliteHeader, emptyRuntimeState } from './square-sqlite.js';
12
+ import { APPLICATION_ID, CURRENT_USER_VERSION, createSqliteDriver, initializeSqlite, exportSqliteState, inspectSqlite, invalidArtifact, validateSqliteHeader, emptyRuntimeState } from './square-sqlite.js';
13
13
  const guideContents = new Map();
14
14
  function readGuide(name) {
15
15
  if (!['participant', 'architect', 'brainstorm'].includes(name))
@@ -107,7 +107,7 @@ export function createArtifactPort(location, initial, externalSignal) {
107
107
  database = new DatabaseSync(':memory:');
108
108
  try {
109
109
  initializeSqlite(database, initial);
110
- database.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version=2`);
110
+ database.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version=${CURRENT_USER_VERSION}`);
111
111
  }
112
112
  catch (error) {
113
113
  database.close();
@@ -270,7 +270,7 @@ async function initializeFile(location, state, signal) {
270
270
  db.exec('PRAGMA journal_mode=DELETE; PRAGMA synchronous=FULL');
271
271
  await inSqliteTransaction(db, true, () => { }, () => {
272
272
  initializeSqlite(db, state);
273
- db.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version=2`);
273
+ db.exec(`PRAGMA application_id=${APPLICATION_ID}; PRAGMA user_version=${CURRENT_USER_VERSION}`);
274
274
  }, signal);
275
275
  }
276
276
  finally {
@@ -14,6 +14,7 @@ export interface CatchSelection {
14
14
  readonly ownActivityCount: number;
15
15
  readonly hardCap: number | null;
16
16
  readonly terminalStatus: 'capped' | 'quorum' | undefined;
17
+ readonly hint?: string;
17
18
  }
18
19
  /** Capture a selected page (or an empty completion) without opening another resource. */
19
20
  export declare function observeCatchSelection(facts: SquareRead, participant: string, activities: readonly StoredAct[], filter: DirectedFilter, at: number): CatchSelection;
@@ -2,6 +2,7 @@ import { formatActivityId, parseActivityId } from './square-core.js';
2
2
  import { readCursor, terminalStatusFor } from './runtime.js';
3
3
  import { SquareError } from './model.js';
4
4
  import { resolveKnownName } from './decisions.js';
5
+ import { catchHint } from './hints.js';
5
6
  /** Capture a selected page (or an empty completion) without opening another resource. */
6
7
  export function observeCatchSelection(facts, participant, activities, filter, at) {
7
8
  if (typeof at !== 'number' || !Number.isFinite(at))
@@ -60,6 +61,11 @@ export function decideCatch(facts, requestedName, options, at) {
60
61
  filter = page.filter;
61
62
  delivered = facts.activities(page.indexes);
62
63
  }
63
- facts.markSeen(viewer, delivered.map((item) => item.index), at);
64
- return observeCatchSelection(facts, viewer, delivered, filter, at);
64
+ const marked = facts.markSeen(viewer, delivered.map((item) => item.index), at);
65
+ const selection = observeCatchSelection(facts, viewer, delivered, filter, at);
66
+ // Receipts are the durable catch count: everything directed here minus what is still unseen.
67
+ const caught = facts.directedCount(viewer, {}) - facts.directedCount(viewer, { unseenOnly: true });
68
+ // An exact id catch is a targeted fetch and stays repeatable, so it carries no nudge.
69
+ const hint = options.id === undefined ? catchHint(caught - marked, caught) : undefined;
70
+ return hint === undefined ? selection : { ...selection, hint };
65
71
  }
@@ -1,4 +1,4 @@
1
- import { automaticSessionStart, operationEnv } from './automatic-session.js';
1
+ import { operationEnv } from './participant-identity.js';
2
2
  import { claimSessionParticipant, hostLedgerForEnv } from './registry.js';
3
3
  import { openSquare } from './open-square.js';
4
4
  import { closeOpenSquare } from './open-square.js';
@@ -93,7 +93,6 @@ export async function runClaudeMod(inputText, env = process.env) {
93
93
  await square.close();
94
94
  }
95
95
  }
96
- await automaticSessionStart('claude', input.sessionId, input.cwd, env);
97
96
  }
98
97
  if (input.operation === 'start' || input.operation === 'reconcile' || input.operation === 'cancel' || input.cancelAt !== undefined) {
99
98
  const memberships = await ledger.listPresence({ session: input.sessionId });
@@ -1,5 +1,6 @@
1
1
  import { parseActivityId } from '../square-api.js';
2
2
  import { participantCommandPrefix, renderAmbientEvent, renderWatchAlreadyActive, renderWatchForceTakeover, renderWatchReplaceMissing, renderWatchReplaced, renderWatchStatus, withPathOutput, } from '../presentation.js';
3
+ import { style } from '../tty-style.js';
3
4
  export function renderCatchProgress(squarePath, progress) {
4
5
  if (progress.kind !== 'catch-started' || !progress.replacementRequested)
5
6
  return undefined;
@@ -48,8 +49,9 @@ export function renderCatchOutcome(squarePath, data, options = {}) {
48
49
  });
49
50
  const fallback = stop === undefined && !projection.automaticDeliveryAvailable && data.remaining === 0
50
51
  ? `${participantCommandPrefix(location, name)} catch --idle 30m\n stay available for new activity` : '';
52
+ const hint = data.hint === undefined ? '' : style('dim', `· ${data.hint}`);
51
53
  return {
52
- stdout: withPathOutput(location, [terminal, rendered, remaining, fallback].filter(Boolean).join('\n\n'), header),
54
+ stdout: withPathOutput(location, [terminal, rendered, hint, remaining, fallback].filter(Boolean).join('\n\n'), header),
53
55
  exitCode: stop === 'capped' ? 1 : 0,
54
56
  };
55
57
  }
@@ -93,6 +93,7 @@ const LOCATION_REQUIRED_COMMANDS = new Set([
93
93
  'express',
94
94
  'listen',
95
95
  'ignore',
96
+ 'unlisten',
96
97
  'listening',
97
98
  'done',
98
99
  'hold',
@@ -3,7 +3,7 @@ import fs from 'node:fs';
3
3
  import path from 'node:path';
4
4
  import { isSquareError } from '../model.js';
5
5
  import { formatActivityId, parseActivityId } from '../square-api.js';
6
- import { expressHintLine, formatRefusal, refusalText, renderActivityBlocked, renderActivityLimit, renderActivityUncertain, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, joinRecoveryCommand, participantIdentity, participantsRecoveryCommand, participantCommandPrefix, commandPrefix, quoteShell, takeoverRecoveryLines, withPathOutput, } from '../presentation.js';
6
+ import { formatRefusal, refusalText, renderActivityBlocked, renderActivityLimit, renderActivityUncertain, renderExpressNoWait, renderExpressWaiting, renderPendingFeed, joinRecoveryCommand, participantIdentity, participantsRecoveryCommand, participantCommandPrefix, commandPrefix, quoteShell, takeoverRecoveryLines, withPathOutput, } from '../presentation.js';
7
7
  import { formatDuration } from '../time.js';
8
8
  import { style } from '../tty-style.js';
9
9
  export function saveActivityDraft(squarePath, name, body, now = Date.now()) {
@@ -57,13 +57,12 @@ export function renderExpression(squarePath, data) {
57
57
  : `● ${activity.mentions.length > 0
58
58
  ? `you walk over to ${activity.mentions.map((target) => participantIdentity(target)).join(', ')}`
59
59
  : isGestureOnly(activity.body ?? '') ? 'your gesture lands' : 'your words land'} — ${activity.id}${style('dim', capCount)}`;
60
- const hint = expressHintLine(ownCount);
61
60
  const replyNotice = data.replyAuthorNotHere === undefined ? ''
62
61
  : `\n○ ${participantIdentity(data.replyAuthorNotHere)} isn't in the square right now — the reply landed without reaching them`;
63
62
  const unmatchedNotice = data.unmatchedMentions?.length
64
63
  ? `\n · ${data.unmatchedMentions.map((token) => `@${token} isn't standing here`).join('; ')} — ${activity.mentions.length === 0 && activity.reply === undefined ? 'activity landed bare' : 'left as literal text'}` : '';
65
64
  return withPathOutput(projection.location ?? squarePath, (projection.reentry === undefined ? '' : '● you stepped back into the square\n')
66
- + confirmation + (hint ? `\n${style('dim', hint)}` : '') + replyNotice + unmatchedNotice + pending, { participantCount: projection.standingCount, held: projection.held });
65
+ + confirmation + (data.hint ? `\n${style('dim', `· ${data.hint}`)}` : '') + replyNotice + unmatchedNotice + pending, { participantCount: projection.standingCount, held: projection.held });
67
66
  }
68
67
  /** Body adaptation, terminal IO and drafts only; Participant owns every attempt and wait. */
69
68
  export async function executeExpression(open, squarePath, requestedName, rawInput, opts) {
@@ -12,10 +12,10 @@ export const migrateCommand = {
12
12
  const outcome = await migrateSquare(squareInput(context));
13
13
  const body = outcome.status === 'migrated' && outcome.counts !== undefined
14
14
  ? [
15
- '✓ migrated to row storage',
15
+ '✓ migrated to current row storage',
16
16
  ` · ${outcome.counts.activities} activities · ${outcome.counts.receipts} receipts · ${outcome.counts.leases} leases · ${outcome.counts.routes} routes`,
17
17
  ].join('\n')
18
- : '· already format 2 — nothing changed';
18
+ : '· already current format — nothing changed';
19
19
  return { output: withPathOutput(squarePath, body, { participantCount: outcome.standing }) };
20
20
  },
21
21
  present(result) {
@@ -1,4 +1,4 @@
1
- import { buildCommand, doneCommand, expressCommand, holdCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, resumeCommand } from './square-commands.js';
1
+ import { buildCommand, doneCommand, expressCommand, holdCommand, ignoreCommand, joinCommand, listenCommand, listeningCommand, resumeCommand, unlistenCommand } from './square-commands.js';
2
2
  import { doctorCommand, migrateCommand } from './maintenance-commands.js';
3
3
  import { ledgerCommand } from './ledger-command.js';
4
4
  import { harnessCommand, installCommand, uninstallCommand } from './harness-command.js';
@@ -18,6 +18,7 @@ export const commandRegistry = [
18
18
  { names: ['express'], spec: expressCommand },
19
19
  { names: ['listen'], spec: listenCommand },
20
20
  { names: ['ignore'], spec: ignoreCommand },
21
+ { names: ['unlisten'], spec: unlistenCommand },
21
22
  { names: ['listening'], spec: listeningCommand },
22
23
  { names: ['done'], spec: doneCommand },
23
24
  { names: ['hold'], spec: holdCommand },
@@ -30,6 +30,7 @@ export declare const joinCommand: CommandSpec<JoinIntent, string>;
30
30
  export declare const expressCommand: CommandSpec<ActivityIntent>;
31
31
  export declare const listenCommand: CommandSpec<ListenerIntent, string>;
32
32
  export declare const ignoreCommand: CommandSpec<ListenerIntent, string>;
33
+ export declare const unlistenCommand: CommandSpec<ListenerIntent, string>;
33
34
  export declare const listeningCommand: CommandSpec<ListenerIntent, string>;
34
35
  export declare const doneCommand: CommandSpec<BodyIntent, string>;
35
36
  export declare const holdCommand: CommandSpec<BodyIntent, string>;
@@ -171,8 +171,12 @@ function listenerCommand(verb) {
171
171
  const actor = participantIdentity(projection.participant);
172
172
  const target = participantIdentity(projection.target);
173
173
  const action = data.activity !== null
174
- ? verb === 'listen' ? `${actor} turns an ear toward ${target}` : `${actor} turns away from ${target}`
175
- : verb === 'listen' ? `${actor} already turns an ear toward ${target}` : `${actor} is not turned toward ${target}`;
174
+ ? verb === 'listen' ? `${actor} turns an ear toward ${target}`
175
+ : verb === 'ignore' ? `${actor} turns away from ${target}`
176
+ : `${actor} stops listening to ${target}`
177
+ : verb === 'listen' ? `${actor} already turns an ear toward ${target}`
178
+ : verb === 'ignore' ? `${actor} is not turned toward ${target}`
179
+ : `${actor} is not listening to ${target}`;
176
180
  return withPathOutput(projection.location, `${data.activity === null ? '○' : '·'} ${action}`, {
177
181
  participantCount: projection.standingCount, held: projection.held,
178
182
  });
@@ -182,6 +186,7 @@ function listenerCommand(verb) {
182
186
  }
183
187
  export const listenCommand = listenerCommand('listen');
184
188
  export const ignoreCommand = listenerCommand('ignore');
189
+ export const unlistenCommand = listenerCommand('unlisten');
185
190
  export const listeningCommand = {
186
191
  parse(argv, context) { return parseListener(argv, context, false); },
187
192
  async execute(_intent, context) {
@@ -1,6 +1,6 @@
1
1
  import { presentPendingAtBoundary } from './boundary-presentation.js';
2
2
  import { sessionInbox } from './inbox.js';
3
- import { automaticSessionEnd, automaticSessionStart } from './automatic-session.js';
3
+ import { endSessionMemberships } from './session-lifecycle.js';
4
4
  import { clearCodexBoundary, recordCodexBoundary } from './codex-boundary-state.js';
5
5
  import { PRIVILEGED_HOOK_BUDGET_MS, sweepPrivilegedPending } from './notifications.js';
6
6
  /** Overall hook budget: Codex kills the hook at its configured 5s timeout, so the hook must
@@ -83,20 +83,12 @@ export async function runCodexHookAsync(inputText, env = process.env) {
83
83
  return runCodexHook(inputText, env);
84
84
  if (value.hook_event_name === 'SessionStart' || value.hook_event_name === 'SessionResume') {
85
85
  await recordCodexBoundary(value.session_id, 'non-stop', env, AbortSignal.timeout(hookBudgetMs(env)));
86
- const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
87
- try {
88
- const context = await automaticSessionStart('codex', value.session_id, cwd, env);
89
- return context === undefined ? '' : `${JSON.stringify({ hookSpecificOutput: { hookEventName: value.hook_event_name, additionalContext: context } })}\n`;
90
- }
91
- catch {
92
- return '';
93
- }
86
+ return '';
94
87
  }
95
88
  if (value.hook_event_name === 'SessionEnd') {
96
89
  await clearCodexBoundary(value.session_id, env, AbortSignal.timeout(hookBudgetMs(env)));
97
- const cwd = typeof value.cwd === 'string' ? value.cwd : process.cwd();
98
90
  try {
99
- await automaticSessionEnd('codex', value.session_id, cwd, env);
91
+ await endSessionMemberships('codex', value.session_id, env);
100
92
  }
101
93
  catch { /* end remains bounded */ }
102
94
  return '';
@@ -54,8 +54,9 @@ export declare function decideAct(facts: SquareRead, input: {
54
54
  }): ActDecision;
55
55
  export declare function validateDoneBody(body: unknown): void;
56
56
  export declare function coreDone(facts: SquareRead, name: string, body: string, now: number): TimedAct<'done'>;
57
- export declare function coreListen(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore"> | undefined;
58
- export declare function coreIgnore(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore"> | undefined;
57
+ export declare function coreListen(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
58
+ export declare function coreIgnore(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
59
+ export declare function coreUnlisten(facts: SquareRead, actor: string, target: string, now: number): TimedAct<"listen" | "ignore" | "unlisten"> | undefined;
59
60
  export declare function coreListening(facts: SquareRead, actor: string): readonly string[];
60
61
  export declare function coreHold(facts: SquareRead, actor: string, body: string, now: number): TimedAct<'hold'>;
61
62
  export declare function coreResume(facts: SquareRead, actor: string, now: number): TimedAct<'resume'>;
package/dist/decisions.js CHANGED
@@ -171,6 +171,8 @@ function attentionChange(facts, kind, actor, target, now) {
171
171
  requireStanding(current);
172
172
  const outgoing = facts.attention(current.name);
173
173
  const existing = outgoing.find((item) => sameName(item.target, target));
174
+ if (kind === 'unlisten')
175
+ return existing?.kind === 'listen' ? { kind, actor: current.name, target, at: now } : undefined;
174
176
  if (kind === 'listen' && existing?.kind !== 'listen' && outgoing.filter((item) => item.kind === 'listen').length >= MAX_IDENTITY_SET_SIZE) {
175
177
  throw new SquareError('invalid_args', `A participant can listen to at most ${MAX_IDENTITY_SET_SIZE} others`);
176
178
  }
@@ -178,6 +180,7 @@ function attentionChange(facts, kind, actor, target, now) {
178
180
  }
179
181
  export function coreListen(facts, actor, target, now) { return attentionChange(facts, 'listen', actor, target, now); }
180
182
  export function coreIgnore(facts, actor, target, now) { return attentionChange(facts, 'ignore', actor, target, now); }
183
+ export function coreUnlisten(facts, actor, target, now) { return attentionChange(facts, 'unlisten', actor, target, now); }
181
184
  export function coreListening(facts, actor) {
182
185
  const current = resolveStanding(facts, actor);
183
186
  return facts.attention(current.name).filter((item) => item.kind === 'listen').map((item) => item.target);
package/dist/help.js CHANGED
@@ -34,7 +34,8 @@ const COMMANDS = [
34
34
  ' --no-wait If held or throttled, save a draft and return instead of waiting. A held wait gives up on its own after 10 minutes.',
35
35
  ],
36
36
  },
37
- { names: ['listen'], usage: '--as <name> listen <participant>', usesSquare: true, group: 'participant', summary: 'Turn an ear toward one participant\'s future bare says.' },
37
+ { names: ['listen'], usage: '--as <name> listen <participant>', usesSquare: true, group: 'participant', summary: 'Turn an ear toward one participant\'s future activity.' },
38
+ { names: ['unlisten'], usage: '--as <name> unlisten <participant>', usesSquare: true, group: 'participant', summary: 'Stop listening without blocking that participant\'s mentions or replies.' },
38
39
  { names: ['ignore'], usage: '--as <name> ignore <participant>', usesSquare: true, group: 'participant', summary: 'Ignore one participant\'s future mentions and bare says.' },
39
40
  { names: ['listening'], usage: '--as <name> listening', usesSquare: true, group: 'participant', summary: 'Show who you are turned toward.', details: ['Listening is future-only: the edge is fixed when a say lands; it never rewrites history.'] },
40
41
  {
@@ -89,8 +90,8 @@ const COMMANDS = [
89
90
  },
90
91
  {
91
92
  names: ['migrate'], usage: 'migrate', usesSquare: true, group: 'maintenance',
92
- summary: 'Convert an existing version 1 square to row storage in place.',
93
- details: ['For the selected .square artifact only. Global identity and delivery records use square ledger migrate instead.', 'Stop sessions and services using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'A version 2 square is left untouched; an unrelated or unsupported artifact is refused without a write. There is no --check, --backup or --force.'],
93
+ summary: 'Convert an existing version 1 or 2 square to current format 3 in place.',
94
+ details: ['For the selected .square artifact only. Global identity and delivery records use square ledger migrate instead.', 'Stop sessions and services using this artifact before conversion. Back up the stopped artifact, migrate it, then run square --location <path> doctor before starting updated clients.', 'A version 3 square is left untouched; an unrelated or unsupported artifact is refused without a write. There is no --check, --backup or --force.'],
94
95
  },
95
96
  {
96
97
  names: ['ledger'], usage: 'ledger <verify|migrate|gc>', usesSquare: false, group: 'maintenance',
@@ -106,7 +107,7 @@ function isHelpFlag(value) {
106
107
  }
107
108
  export function renderGlobalHelp() {
108
109
  const groups = [
109
- { key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'ignore', 'listening', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
110
+ { key: 'participant', title: 'In the square:', order: ['join', 'express', 'listen', 'unlisten', 'ignore', 'listening', 'catch', 'history', 'status', 'hold', 'resume', 'done'] },
110
111
  { key: 'host', title: 'Prepare and manage:', order: ['build', 'list', 'participants'] },
111
112
  { key: 'maintenance', title: 'Setup:', order: ['install', 'uninstall', 'doctor', 'migrate', 'ledger'] },
112
113
  ];
@@ -121,7 +122,7 @@ export function renderGlobalHelp() {
121
122
  return [
122
123
  'Usage: square [--location <square>] [--as <name>] <command> [args...]',
123
124
  'Environment: SQUARE_LOCATION sets the CLI location; square-accessing commands require it or --location. SQUARE_PARTICIPANT_NAME sets the participant name.',
124
- 'CLI flags override environment values. Automatic provider sessions use only .square/PUBLIC.square.',
125
+ 'CLI flags override environment values. Participants join explicitly; no square is joined automatically.',
125
126
  '',
126
127
  ...commandLines,
127
128
  "Run 'square <command> --help' for command options.",
@@ -0,0 +1,4 @@
1
+ /** First activity, then every fifth. */
2
+ export declare function expressHint(ownActivityCount: number): string | undefined;
3
+ /** First caught activity, then each time the caught total crosses another ten. */
4
+ export declare function catchHint(caughtBefore: number, caughtAfter: number): string | undefined;
package/dist/hints.js ADDED
@@ -0,0 +1,25 @@
1
+ /** Participant-facing nudges shared by every transport; CLI only decides how they look. */
2
+ const EXPRESS_HINTS = [
3
+ '*asterisks* are your body — slam a table, shrug, sketch in the air',
4
+ "answer someone's actual words — they're standing right there",
5
+ "say the half-shaped thing — that's what the square is for",
6
+ ];
7
+ const CATCH_HINTS = [
8
+ "someone @'d you with something you don't need? reply and tell them to stop reaching you",
9
+ 'someone keeps flooding you? ignore them, then tell your human',
10
+ "nothing to add? quiet is fine — catching is listening too",
11
+ 'want to follow one voice? listen to them; unlisten once you have heard enough',
12
+ ];
13
+ /** First activity, then every fifth. */
14
+ export function expressHint(ownActivityCount) {
15
+ if (ownActivityCount !== 1 && ownActivityCount % 5 !== 0)
16
+ return undefined;
17
+ return EXPRESS_HINTS[Math.floor(ownActivityCount / 5) % EXPRESS_HINTS.length];
18
+ }
19
+ /** First caught activity, then each time the caught total crosses another ten. */
20
+ export function catchHint(caughtBefore, caughtAfter) {
21
+ if (caughtAfter <= caughtBefore)
22
+ return undefined;
23
+ const crossed = caughtBefore === 0 || Math.floor(caughtAfter / 10) > Math.floor(caughtBefore / 10);
24
+ return crossed ? CATCH_HINTS[Math.floor(caughtAfter / 10) % CATCH_HINTS.length] : undefined;
25
+ }
package/dist/mcp.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
2
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
3
  import { SQUARE_TOOLS, SQUARE_TOOL_INSTRUCTIONS, squareToolFailure } from './square-tools.js';
4
- import { operationEnv } from './automatic-session.js';
4
+ import { operationEnv } from './participant-identity.js';
5
5
  import { readClaudeMcpCall } from './claude-mcp-context.js';
6
6
  import { SQUARE_IDENTITY } from './identity.js';
7
7
  import { Square, createSquareResourceOwner } from './square-api.js';
package/dist/model.d.ts CHANGED
@@ -99,7 +99,7 @@ export type PublicAct = Extract<StoredAct, {
99
99
  kind: 'say' | 'done';
100
100
  }>;
101
101
  export type RoomChangeAct = Extract<StoredAct, {
102
- kind: 'join' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore';
102
+ kind: 'join' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore' | 'unlisten';
103
103
  }>;
104
104
  export declare function formatHardCap(hardCap: HardCap): string;
105
105
  export declare function parseParticipantList(value: string): string[];
@@ -14,6 +14,8 @@ export interface HarnessSessionSource {
14
14
  readonly childVariable?: 'CLAUDE_CODE_CHILD_SESSION';
15
15
  }
16
16
  export declare const harnessSessionSources: readonly HarnessSessionSource[];
17
+ /** Scope a native call without changing the harness-owned process environment. */
18
+ export declare function operationEnv(provider: AutomaticProvider, sessionId: string, env: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
17
19
  /** Raw nonempty harness session ids in table order. Duplicates are preserved, never collapsed. */
18
20
  export declare function sessionIdsFromEnvironment(env?: NodeJS.ProcessEnv): readonly string[];
19
21
  /** The first nonempty harness coordinate in table order, or undefined when none is set. */
@@ -10,6 +10,19 @@ export const harnessSessionSources = [
10
10
  { variable: 'PI_SESSION_ID', channel: 'pi', provider: 'pi' },
11
11
  { variable: 'PASEO_AGENT_ID', channel: 'paseo' },
12
12
  ];
13
+ /** Scope a native call without changing the harness-owned process environment. */
14
+ export function operationEnv(provider, sessionId, env) {
15
+ const scoped = { ...env };
16
+ // Clear other native coordinates (and the Claude child flag); PASEO_AGENT_ID stays as it is.
17
+ for (const source of harnessSessionSources) {
18
+ if (source.provider === undefined)
19
+ continue;
20
+ scoped[source.variable] = source.provider === provider ? sessionId : '';
21
+ if (source.childVariable !== undefined)
22
+ scoped[source.childVariable] = '';
23
+ }
24
+ return scoped;
25
+ }
13
26
  /** Raw nonempty harness session ids in table order. Duplicates are preserved, never collapsed. */
14
27
  export function sessionIdsFromEnvironment(env = process.env) {
15
28
  return harnessSessionSources
@@ -1,4 +1,4 @@
1
- import { automaticSessionStart, operationEnv } from '../../automatic-session.js';
1
+ import { operationEnv } from '../../participant-identity.js';
2
2
  import { hostLedgerForEnv } from '../../registry.js';
3
3
  import { canonicalPath } from '../../canonical-path.js';
4
4
  import { openSquare } from '../../open-square.js';
@@ -55,8 +55,6 @@ export default {
55
55
  continue;
56
56
  }
57
57
  const scopedEnv = operationEnv('opencode', sessionId, env);
58
- if (sessionId === activeSession)
59
- await automaticSessionStart('opencode', sessionId, directory, scopedEnv);
60
58
  if (!await publishOpenCodeRoutes(sessionId, scopedEnv, lifetime.signal)) {
61
59
  stop(sessionId);
62
60
  continue;
@@ -68,7 +68,6 @@ export declare function joinRecoveryCommand(squarePath: string, name: string): s
68
68
  export declare function takeoverRecoveryLines(squarePath: string, name: string): string[];
69
69
  export declare function participantsRecoveryCommand(squarePath: string): string;
70
70
  export declare function renderPresenceLines(recent: readonly ParticipantSummary[], now: number, limit?: number): string[];
71
- export declare function expressHintLine(ownActivityCount: number): string | undefined;
72
71
  export declare function truncateChars(body: string, maxChars: number): {
73
72
  text: string;
74
73
  remaining: number;
@@ -91,17 +91,6 @@ export function renderPresenceLines(recent, now, limit = 5) {
91
91
  lines.push(style('dim', ` ○ … ${remaining} more nearby`));
92
92
  return lines;
93
93
  }
94
- const EXPRESS_HINTS = [
95
- '*asterisks* are your body — slam a table, shrug, sketch in the air',
96
- "answer someone's actual words — they're standing right there",
97
- "say the half-shaped thing — that's what the square is for",
98
- ];
99
- export function expressHintLine(ownActivityCount) {
100
- if (ownActivityCount !== 1 && ownActivityCount % 5 !== 0)
101
- return undefined;
102
- const hint = EXPRESS_HINTS[Math.floor(ownActivityCount / 5) % EXPRESS_HINTS.length];
103
- return `· ${hint}`;
104
- }
105
94
  const BODY_PREVIEW_LENGTH = 200;
106
95
  export function truncateChars(body, maxChars) {
107
96
  const chars = [...body];
@@ -139,6 +128,8 @@ export function renderRoomChangeText(event) {
139
128
  return `${identity} turned an ear toward ${participantIdentity(event.target ?? 'someone')}`;
140
129
  case 'ignore':
141
130
  return `${identity} turned away from ${participantIdentity(event.target ?? 'someone')}`;
131
+ case 'unlisten':
132
+ return `${identity} stopped listening to ${participantIdentity(event.target ?? 'someone')}`;
142
133
  default:
143
134
  throw new Error('Expected a lifecycle activity');
144
135
  }
@@ -171,6 +162,7 @@ function renderEventCliFacts(event, opts = {}) {
171
162
  return { text: `${style('release', '✓')} ${renderRoomChangeText(event)}`, bodyClipped: false };
172
163
  case 'listen':
173
164
  case 'ignore':
165
+ case 'unlisten':
174
166
  return { text: `· ${renderRoomChangeText(event)}`, bodyClipped: false };
175
167
  case 'say': {
176
168
  const body = renderedBody(event.body, maxBody);
package/dist/runtime.js CHANGED
@@ -48,7 +48,7 @@ export function readCursor(facts, name) {
48
48
  const block = facts.directed(name, { afterIndex: boundary, unseenOnly: true }, { edge: 'oldest', limit: 1 }).items[0]?.index;
49
49
  const last = facts.activityHeads({
50
50
  afterIndex: boundary, ...(block === undefined ? {} : { beforeIndex: block }), actorRequired: true,
51
- kinds: ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore'],
51
+ kinds: ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore', 'unlisten'],
52
52
  }, { edge: 'newest', limit: 1 }).items[0]?.index;
53
53
  return Math.max(boundary, last ?? boundary);
54
54
  }
@@ -0,0 +1,3 @@
1
+ import { type AutomaticProvider } from './participant-identity.js';
2
+ /** Retire only memberships owned at this session's end, regardless of square filename or cwd. */
3
+ export declare function endSessionMemberships(provider: AutomaticProvider, sessionId: string, env?: NodeJS.ProcessEnv): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { Square } from './square-api.js';
2
+ import { operationEnv } from './participant-identity.js';
3
+ import { hostLedgerForEnv } from './registry.js';
4
+ import { projectSessionBindings } from './square-projections.js';
5
+ /** Retire only memberships owned at this session's end, regardless of square filename or cwd. */
6
+ export async function endSessionMemberships(provider, sessionId, env = process.env) {
7
+ const scoped = operationEnv(provider, sessionId, env);
8
+ const hostLedger = hostLedgerForEnv(scoped);
9
+ const bindings = await projectSessionBindings({ hostLedger, sessionId });
10
+ for (const binding of bindings) {
11
+ const square = await Square.at({ path: binding.location, hostLedger, env: scoped });
12
+ try {
13
+ // The captured epoch fences a same-session rejoin as well as a foreign takeover.
14
+ // endOwnedSession also settles routes and presence under the ownership claim lock.
15
+ await square.endOwnedSession(binding.participant, sessionId, binding.epoch ?? 0);
16
+ }
17
+ finally {
18
+ await square.close();
19
+ }
20
+ }
21
+ }
@@ -42,6 +42,7 @@ export interface ListenerCommittedResult {
42
42
  }
43
43
  export declare function listen(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerCommittedResult>;
44
44
  export declare function ignore(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerCommittedResult>;
45
+ export declare function unlisten(square: OperationContext, actor: string, target: string, control?: OperationControl): Promise<ListenerCommittedResult>;
45
46
  export declare function listening(square: OperationContext, actor: string, control?: OperationControl): Promise<readonly string[]>;
46
47
  export interface LifecycleCommittedResult extends ActivityResult {
47
48
  readonly observation: LandingContext & {
@@ -1,4 +1,4 @@
1
- import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, decideAct, decideImplicitJoin, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
1
+ import { coreDone, coreHold, coreIgnore, coreListen, coreListening, coreResume, coreUnlisten, decideAct, decideImplicitJoin, decideJoin, resolveKnownName, validateDoneBody } from './decisions.js';
2
2
  import { isSquareError, nameKey, SquareError, validateName } from './model.js';
3
3
  import { participantIdentity } from './participant-identity.js';
4
4
  import { deliverPending } from './delivery-operations.js';
@@ -238,7 +238,7 @@ async function landListenerChange(square, verb, actor, target, control) {
238
238
  throwIfAborted(control);
239
239
  const now = square.clock();
240
240
  const committed = (await square.artifact.transact((facts) => {
241
- const act = verb === 'listen' ? coreListen(facts, actor, target, now) : coreIgnore(facts, actor, target, now);
241
+ const act = verb === 'listen' ? coreListen(facts, actor, target, now) : verb === 'ignore' ? coreIgnore(facts, actor, target, now) : coreUnlisten(facts, actor, target, now);
242
242
  const stored = act === undefined ? null : facts.land(act);
243
243
  return { stored, observation: { standingCount: inSquareCount(facts), held: facts.hold().active, now } };
244
244
  }, control?.signal)).value;
@@ -246,6 +246,7 @@ async function landListenerChange(square, verb, actor, target, control) {
246
246
  }
247
247
  export function listen(square, actor, target, control) { return landListenerChange(square, 'listen', actor, target, control); }
248
248
  export function ignore(square, actor, target, control) { return landListenerChange(square, 'ignore', actor, target, control); }
249
+ export function unlisten(square, actor, target, control) { return landListenerChange(square, 'unlisten', actor, target, control); }
249
250
  export async function listening(square, actor, control) { return (await square.artifact.read((facts) => coreListening(facts, actor), control?.signal)).value; }
250
251
  async function landCore(square, verb, actor, body = '', fence = {}, control) {
251
252
  if (verb === 'done')
@@ -6,9 +6,10 @@ import { resolveKnownName, validateDoneBody } from './decisions.js';
6
6
  import { HELD_WAIT_BUDGET_MS, WATCH_HEARTBEAT_MS, inSquareCount, presenceAnchor } from './runtime.js';
7
7
  import { catchLimit, observeCatchSelection } from './catch-decisions.js';
8
8
  import { directedSuffix } from './activity-feed.js';
9
+ import { expressHint } from './hints.js';
9
10
  import { assertLiveOwner } from './participant-host.js';
10
11
  import { buildMemorySquare, openSquare, closeOpenSquare } from './open-square.js';
11
- import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, resume, retireEndedSessionRoutes } from './square-actions.js';
12
+ import { catchUp, done, endOwnedSession, express, hold, ignore, implicitJoin, join, takeover, listen, listening, unlisten, resume, retireEndedSessionRoutes } from './square-actions.js';
12
13
  import { recoveryObservation, currentParticipant, entryPresentation, resolveParticipant, snapshot, queryStatus, queryParticipants, catchPresence, toPublicActivity, publicActivities, selectHistory, NON_READ, HISTORY_DEFAULT_LIMIT, HISTORY_MAX_LIMIT } from './views.js';
13
14
  import { hasAutomaticDeliveryIdentity, hostLedgerForEnv, localParticipantName, localSessionIdentities, presenceEpoch, readParticipantOwner } from './registry.js';
14
15
  import { projectSessionBindings } from './square-projections.js';
@@ -232,7 +233,8 @@ class ParticipantHandle {
232
233
  }, control?.signal)).value;
233
234
  result = await landExpression(this.square, name, body, options, this.transport, control);
234
235
  const { observation: committed, ...data } = result;
235
- return { ...data, ...(feed === undefined ? {} : { observation: {
236
+ const hint = expressHint(committed.ownActivityCount);
237
+ return { ...data, ...(hint === undefined ? {} : { hint }), ...(feed === undefined ? {} : { observation: {
236
238
  ...observedContext(this.square, committed.standingCount, committed.held, name, committed.now), ...committed, ...feed, participant: name,
237
239
  ...(entry.activity === null ? {} : { reentry: entry.activity }),
238
240
  } }) };
@@ -246,7 +248,7 @@ class ParticipantHandle {
246
248
  }
247
249
  async listener(verb, target, options, control) {
248
250
  const name = (await resolveParticipant(this.square, this.name, control)).name;
249
- const { observation: committed, ...result } = await (verb === 'listen' ? listen : ignore)(this.square, name, target, control);
251
+ const { observation: committed, ...result } = await (verb === 'listen' ? listen : verb === 'ignore' ? ignore : unlisten)(this.square, name, target, control);
250
252
  return { ...result, ...(!options.observe ? {} : { observation: {
251
253
  ...observedContext(this.square, committed.standingCount, committed.held, name, committed.now),
252
254
  participant: name, requestedTarget: target, target: result.activity?.target ?? target,
@@ -254,6 +256,7 @@ class ParticipantHandle {
254
256
  }
255
257
  listen(target, options = {}, control) { return this.listener('listen', target, options, control); }
256
258
  ignore(target, options = {}, control) { return this.listener('ignore', target, options, control); }
259
+ unlisten(target, options = {}, control) { return this.listener('unlisten', target, options, control); }
257
260
  async listening(options = {}, control) {
258
261
  const name = (await resolveParticipant(this.square, this.name, control)).name;
259
262
  return { listening: await listening(this.square, name, control),
@@ -265,7 +268,7 @@ class ParticipantHandle {
265
268
  const status = stop === 'already-active' || stop === 'replaced' || stop === 'capped' || stop === 'quorum' ? stop : undefined;
266
269
  return { activities: view.activities.map((activity) => ({ ...toPublicActivity(activity), perception: 'full' })),
267
270
  consumedThrough: view.consumedThrough, remaining: view.remaining, idleExpired: stop === 'stale',
268
- ...(status === undefined ? {} : { status }), ...(!options.observe ? {} : { observation: {
271
+ ...(status === undefined ? {} : { status }), ...(view.hint === undefined ? {} : { hint: view.hint }), ...(!options.observe ? {} : { observation: {
269
272
  ...observedContext(this.square, view.standingCount, view.held, view.participant, view.now), participant: view.participant,
270
273
  sayNumbers: view.sayNumbers, ownActivityCount: view.ownActivityCount, hardCap: view.hardCap, stop,
271
274
  ...(presence === undefined ? { enrichmentUnavailable: true } : { presence }), waitStarted: result.waitStarted, replaced: result.replaced,
@@ -52,6 +52,11 @@ export type Act = {
52
52
  actor: Participant;
53
53
  target: Participant;
54
54
  at?: number;
55
+ } | {
56
+ kind: 'unlisten';
57
+ actor: Participant;
58
+ target: Participant;
59
+ at?: number;
55
60
  };
56
61
  export declare function audienceOf(say: {
57
62
  body: string;
@@ -4,6 +4,10 @@ export interface LandingEffect {
4
4
  readonly activity: StoredAct;
5
5
  readonly participant?: ParticipantFact;
6
6
  readonly removeListeningOf?: string;
7
+ readonly removeListening?: {
8
+ readonly listener: string;
9
+ readonly target: string;
10
+ };
7
11
  readonly replaceAttention?: AttentionFact;
8
12
  readonly audience: readonly LandedRecipient[];
9
13
  }
@@ -45,9 +45,13 @@ export function deriveLanding(activity, facts) {
45
45
  replaceAttention = { listener: participant.name, target: activity.target, kind: activity.kind, sinceIndex: activity.index };
46
46
  }
47
47
  }
48
+ const removeListening = activity.kind === 'unlisten'
49
+ && facts.outgoing.some((item) => item.kind === 'listen' && sameName(item.target, activity.target))
50
+ ? { listener: participant.name, target: activity.target } : undefined;
48
51
  return {
49
52
  activity, participant, audience,
50
53
  ...(activity.kind === 'done' ? { removeListeningOf: participant.name } : {}),
54
+ ...(removeListening === undefined ? {} : { removeListening }),
51
55
  ...(replaceAttention === undefined ? {} : { replaceAttention }),
52
56
  };
53
57
  }
@@ -56,7 +60,7 @@ export function landingFacts(read, activity) {
56
60
  actor: activity.actor === undefined ? undefined : read.participant(activity.actor),
57
61
  standing: activity.kind === 'say' ? read.roster().filter((item) => item.joined) : [],
58
62
  incoming: activity.kind === 'say' ? read.incomingAttention(activity.actor) : [],
59
- outgoing: activity.kind === 'listen' || activity.kind === 'ignore' ? read.attention(activity.actor) : [],
63
+ outgoing: activity.kind === 'listen' || activity.kind === 'ignore' || activity.kind === 'unlisten' ? read.attention(activity.actor) : [],
60
64
  replyAuthor: activity.kind === 'say' && activity.reply !== undefined ? read.activity(activity.reply)?.actor : undefined,
61
65
  };
62
66
  }