@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
@@ -7,7 +7,7 @@ export type ArchiveRecord = Readonly<StoredAct>;
7
7
  export interface Activity {
8
8
  readonly id: ActivityId;
9
9
  readonly at: number;
10
- readonly kind: 'join' | 'say' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore';
10
+ readonly kind: 'join' | 'say' | 'done' | 'hold' | 'resume' | 'listen' | 'ignore' | 'unlisten';
11
11
  readonly actor: string;
12
12
  readonly body?: string;
13
13
  readonly mentions: readonly string[];
@@ -40,6 +40,8 @@ export interface ExpressResult extends ActivityResult {
40
40
  readonly replyAuthorNotHere?: string;
41
41
  /** Body @name tokens with no standing match at landing; never stored on the activity. */
42
42
  readonly unmatchedMentions?: readonly string[];
43
+ /** Occasional participant-facing nudge; not part of the activity. */
44
+ readonly hint?: string;
43
45
  readonly observation?: ExpressObservation;
44
46
  }
45
47
  export interface ListenerChangeResult {
@@ -67,6 +69,8 @@ export interface CatchResult {
67
69
  readonly idleExpired: boolean;
68
70
  readonly remaining: number;
69
71
  readonly status?: CatchTerminalStatus;
72
+ /** Occasional participant-facing nudge; not part of any activity. */
73
+ readonly hint?: string;
70
74
  readonly observation?: CatchObservation;
71
75
  }
72
76
  export interface CatchWaitStart {
@@ -184,6 +188,7 @@ export interface Participant {
184
188
  express(body: string, options?: ExpressOptions, control?: OperationControl): Promise<ExpressResult>;
185
189
  listen(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
186
190
  ignore(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
191
+ unlisten(target: string, options?: ObservationOptions, control?: OperationControl): Promise<ListenerChangeResult>;
187
192
  listening(options?: ObservationOptions, control?: OperationControl): Promise<ListeningResult>;
188
193
  catch(options?: CatchOptions, control?: OperationControl): Promise<CatchResult>;
189
194
  history(query?: HistoryQuery, control?: OperationControl): Promise<HistoryResult>;
@@ -8,17 +8,15 @@ export interface MigrationCounts {
8
8
  readonly routes: number;
9
9
  }
10
10
  export interface MigrationOutcome {
11
- readonly status: 'migrated' | 'already-format-2';
11
+ readonly status: 'migrated' | 'already-current';
12
12
  /** Participants standing after the conversion, for the CLI header. */
13
13
  readonly standing: number;
14
14
  readonly counts?: MigrationCounts;
15
15
  }
16
16
  /**
17
- * Explicit in-place conversion of a normally persisted version 1 artifact.
18
- *
19
- * The legacy snapshot is read, validated and written here and nowhere else in
20
- * production; the row schema, codec and landing transition come from the driver.
21
- * One transaction: preflight framing, the same recheck under the lock, the
22
- * preserved authority, the derived facts, the format flip and a single COMMIT.
17
+ * Explicit in-place conversion of a version 1 snapshot or version 2 row
18
+ * artifact. Each source is validated before and under the write lock; v1 is
19
+ * imported through the normal current row initializer, while v2 rebuilds only
20
+ * activities and its indexes before publishing the new format version.
23
21
  */
24
22
  export declare function migrateSquareFile(location: string, signal?: AbortSignal): Promise<MigrationOutcome>;
@@ -3,7 +3,7 @@ import { inSqliteTransaction } from './sqlite-transaction.js';
3
3
  import { canonicalPath } from './canonical-path.js';
4
4
  import { isWakeRouteKind } from './model.js';
5
5
  import { parseActivityId } from './square-core.js';
6
- import { APPLICATION_ID, V1_SNAPSHOT_TABLE, createSqliteDriver, initializeSqlite, invalidArtifact, validateLegacyFraming, validateSqliteHeader, validateStoredActivity, validateWatchLease, } from './square-sqlite.js';
6
+ import { APPLICATION_ID, ACTIVITIES_TABLE_SQL, ACTIVITY_INDEXES_SQL, CURRENT_USER_VERSION, V2_USER_VERSION, V1_SNAPSHOT_TABLE, createSqliteDriver, initializeSqlite, invalidArtifact, validateLegacyFraming, validateRowFraming, validateSqliteHeader, validateStoredActivity, validateWatchLease, } from './square-sqlite.js';
7
7
  function isObject(value) {
8
8
  return typeof value === 'object' && value !== null && !Array.isArray(value);
9
9
  }
@@ -57,7 +57,7 @@ function validateActs(value) {
57
57
  try {
58
58
  for (const candidate of value) {
59
59
  const act = validateStoredActivity(candidate);
60
- if (act.index <= previous)
60
+ if (act.kind === 'unlisten' || act.index <= previous)
61
61
  return false;
62
62
  previous = act.index;
63
63
  }
@@ -153,58 +153,78 @@ function counts(state) {
153
153
  routes: state.routes?.length ?? 0,
154
154
  };
155
155
  }
156
+ function rowCounts(database) {
157
+ const count = (table) => Number(database.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get().n);
158
+ return { activities: count('activities'), receipts: count('receipts'), leases: count('catch_leases'), routes: count('routes') };
159
+ }
160
+ const ACTIVITY_INDEX_NAMES = ['activity_kind_index', 'activity_actor_index', 'activity_say_actor_index', 'activity_say_time', 'activity_bell_actor_time'];
161
+ function rebuildV2Activities(database) {
162
+ database.exec([
163
+ ...ACTIVITY_INDEX_NAMES.map((name) => `DROP INDEX ${name};`),
164
+ 'ALTER TABLE activities RENAME TO activities_v2;',
165
+ ACTIVITIES_TABLE_SQL,
166
+ 'INSERT INTO activities(act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json) SELECT act_index,at,kind,actor_json,actor_key_json,reach,reply_index,payload_json FROM activities_v2 ORDER BY act_index;',
167
+ 'DROP TABLE activities_v2;',
168
+ ...ACTIVITY_INDEXES_SQL.map((sql) => `${sql};`),
169
+ ].join('\n'));
170
+ }
171
+ function validateMigrationSource(database, location) {
172
+ const before = identity(database);
173
+ if (before.applicationId !== APPLICATION_ID)
174
+ throw invalidArtifact('unrelated SQLite database.');
175
+ if (before.userVersion === 1) {
176
+ validateLegacyFraming(database, APPLICATION_ID, location);
177
+ return 1;
178
+ }
179
+ if (before.userVersion === V2_USER_VERSION) {
180
+ validateRowFraming(database, V2_USER_VERSION, location);
181
+ createSqliteDriver(database).header();
182
+ return 2;
183
+ }
184
+ if (before.userVersion === CURRENT_USER_VERSION) {
185
+ validateSqliteHeader(database, location);
186
+ return undefined;
187
+ }
188
+ throw invalidArtifact('unsupported SQLite format version.');
189
+ }
156
190
  /**
157
- * Explicit in-place conversion of a normally persisted version 1 artifact.
158
- *
159
- * The legacy snapshot is read, validated and written here and nowhere else in
160
- * production; the row schema, codec and landing transition come from the driver.
161
- * One transaction: preflight framing, the same recheck under the lock, the
162
- * preserved authority, the derived facts, the format flip and a single COMMIT.
191
+ * Explicit in-place conversion of a version 1 snapshot or version 2 row
192
+ * artifact. Each source is validated before and under the write lock; v1 is
193
+ * imported through the normal current row initializer, while v2 rebuilds only
194
+ * activities and its indexes before publishing the new format version.
163
195
  */
164
196
  export async function migrateSquareFile(location, signal) {
165
197
  const squarePath = await canonicalPath(location);
166
198
  const database = openExistingSquareDatabase(squarePath);
167
199
  try {
168
- // Owned framing, read-only, before any pragma or DDL touches the artifact.
169
- // Every preflight statement reads one snapshot taken in the helper's
170
- // retryable stage: a transient writer lock is waited out like any other
171
- // acquisition, and a migrator that commits while we look cannot move the
172
- // artifact between the identity read and the schema reads.
173
- let alreadyConverted;
200
+ let alreadyCurrent;
174
201
  const preflight = await inSqliteTransaction(database, false, () => {
175
- const before = identity(database);
176
- if (before.applicationId === APPLICATION_ID && before.userVersion === 1) {
177
- validateLegacyFraming(database, APPLICATION_ID, squarePath);
178
- return;
179
- }
180
- // A row artifact means another migrator finished here; anything else is
181
- // refused by the row header itself.
182
- validateSqliteHeader(database, squarePath);
183
- alreadyConverted = { status: 'already-format-2', standing: standing(database) };
184
- }, () => alreadyConverted, signal);
185
- if (preflight !== undefined)
186
- return preflight;
202
+ const source = validateMigrationSource(database, squarePath);
203
+ if (source === undefined)
204
+ alreadyCurrent = { status: 'already-current', standing: standing(database) };
205
+ return source;
206
+ }, (source) => source, signal);
207
+ if (preflight === undefined)
208
+ return alreadyCurrent;
187
209
  database.exec('PRAGMA busy_timeout = 0; PRAGMA synchronous = FULL');
188
210
  const result = {};
189
211
  await inSqliteTransaction(database, true, () => {
190
- // Another migration may have finished between preflight and this lock.
191
- const locked = identity(database);
192
- if (locked.applicationId !== APPLICATION_ID || locked.userVersion !== 1) {
193
- // A valid row artifact is done already; anything else is refused here.
194
- validateSqliteHeader(database, squarePath);
195
- return;
196
- }
197
- validateLegacyFraming(database, APPLICATION_ID, squarePath);
198
- result.converted = true;
212
+ const source = validateMigrationSource(database, squarePath);
213
+ result.source = source;
199
214
  }, () => {
200
- if (result.converted !== true)
201
- return;
202
- const legacy = decodeLegacySnapshot(database);
203
- initializeSqlite(database, legacy.state, legacy.revision);
204
- database.exec(`DROP TABLE ${V1_SNAPSHOT_TABLE}; PRAGMA user_version = 2`);
205
- result.outcome = { status: 'migrated', standing: standing(database), counts: counts(legacy.state) };
215
+ if (result.source === 1) {
216
+ const legacy = decodeLegacySnapshot(database);
217
+ initializeSqlite(database, legacy.state, legacy.revision);
218
+ database.exec(`DROP TABLE ${V1_SNAPSHOT_TABLE}; PRAGMA user_version = ${CURRENT_USER_VERSION}`);
219
+ result.outcome = { status: 'migrated', standing: standing(database), counts: counts(legacy.state) };
220
+ }
221
+ else if (result.source === 2) {
222
+ rebuildV2Activities(database);
223
+ database.exec(`PRAGMA user_version = ${CURRENT_USER_VERSION}`);
224
+ result.outcome = { status: 'migrated', standing: standing(database), counts: rowCounts(database) };
225
+ }
206
226
  }, signal);
207
- return result.outcome ?? { status: 'already-format-2', standing: standing(database) };
227
+ return result.outcome ?? { status: 'already-current', standing: standing(database) };
208
228
  }
209
229
  finally {
210
230
  database.close();
@@ -19,6 +19,8 @@ export interface SquareState {
19
19
  }
20
20
  export declare function emptyRuntimeState(nextActIndex?: number): SquareRuntimeState;
21
21
  export declare const APPLICATION_ID = 1397838405;
22
+ export declare const V2_USER_VERSION = 2;
23
+ export declare const CURRENT_USER_VERSION = 3;
22
24
  export declare const V1_SNAPSHOT_TABLE = "square_snapshot";
23
25
  export interface LegacyFraming {
24
26
  readonly applicationId: number;
@@ -32,11 +34,15 @@ export interface LegacyFraming {
32
34
  * migration. Anything else is unrelated or unsupported, not repaired.
33
35
  */
34
36
  export declare function validateLegacyFraming(database: DatabaseSync, applicationId: number, location?: string): LegacyFraming;
35
- export declare const SCHEMA_SQL = "CREATE TABLE square_meta (\n id INTEGER PRIMARY KEY CHECK (id = 1),\n revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),\n next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),\n hard_cap INTEGER CHECK (hard_cap BETWEEN 1 AND 9007199254740991),\n throttle_per_minute INTEGER CHECK (throttle_per_minute BETWEEN 1 AND 9007199254740991),\n preamble_json TEXT NOT NULL CHECK (json_valid(preamble_json)),\n warmup_json TEXT NOT NULL CHECK (json_valid(warmup_json))\n);\nCREATE TABLE activities (\n act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),\n at REAL NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('join','done','say','hold','resume','read','listen','ignore')),\n actor_json TEXT COLLATE BINARY,\n actor_key_json TEXT COLLATE BINARY,\n reach TEXT CHECK (reach = 'bell'),\n reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),\n CHECK ((actor_json IS NULL) = (actor_key_json IS NULL)),\n CHECK (kind IN ('hold','resume') OR actor_json IS NOT NULL),\n CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))\n);\nCREATE INDEX activity_kind_index ON activities(kind, act_index);\nCREATE INDEX activity_actor_index ON activities(actor_key_json, act_index);\nCREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say';\nCREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say';\nCREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell';\n\nCREATE TABLE receipts (\n owner_json TEXT COLLATE BINARY NOT NULL,\n act_index INTEGER NOT NULL CHECK (act_index BETWEEN 0 AND 9007199254740991),\n at REAL NOT NULL,\n PRIMARY KEY (owner_json, act_index)\n) WITHOUT ROWID;\nCREATE INDEX receipt_owner_time ON receipts(owner_json, at);\nCREATE TABLE catch_leases (\n owner_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,\n lease_id_json TEXT NOT NULL,\n heartbeat_at REAL NOT NULL,\n expires_at REAL NOT NULL CHECK (expires_at >= heartbeat_at),\n filter_json TEXT CHECK (filter_json IS NULL OR json_valid(filter_json))\n) WITHOUT ROWID;\n\nCREATE TABLE routes (\n position INTEGER PRIMARY KEY CHECK (position >= 0),\n participant_key_json TEXT COLLATE BINARY NOT NULL,\n session_json TEXT COLLATE BINARY NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('opencode-server','codex-queue','claude-native','paseo')),\n updated_at REAL NOT NULL,\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json))\n);\nCREATE INDEX route_participant ON routes(participant_key_json, position);\nCREATE INDEX route_session ON routes(session_json, position);\n\nCREATE TABLE participants (\n name_key_json TEXT COLLATE BINARY PRIMARY KEY NOT NULL,\n name_json TEXT NOT NULL,\n first_index INTEGER NOT NULL UNIQUE,\n joined INTEGER NOT NULL CHECK (joined IN (0,1)),\n done INTEGER NOT NULL CHECK (done IN (0,1)),\n say_count INTEGER NOT NULL CHECK (say_count >= 0),\n last_active_at REAL,\n last_join_index INTEGER,\n CHECK (NOT (joined=1 AND done=1))\n) WITHOUT ROWID;\nCREATE TABLE attention (\n listener_key_json TEXT COLLATE BINARY NOT NULL,\n target_key_json TEXT COLLATE BINARY NOT NULL,\n target_json TEXT NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('listen','ignore')),\n since_index INTEGER NOT NULL,\n PRIMARY KEY (listener_key_json, target_key_json)\n) WITHOUT ROWID;\nCREATE INDEX attention_target ON attention(target_key_json, kind, listener_key_json);\nCREATE TABLE landed_audience (\n act_index INTEGER NOT NULL,\n recipient_key_json TEXT COLLATE BINARY NOT NULL,\n recipient_order INTEGER NOT NULL CHECK (recipient_order >= 0),\n route TEXT NOT NULL CHECK (route IN ('mention','reply','attention','bell')),\n PRIMARY KEY (act_index, recipient_key_json),\n UNIQUE (act_index, recipient_order)\n) WITHOUT ROWID;\nCREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);";
37
+ export declare const ACTIVITIES_TABLE_SQL = "CREATE TABLE activities (\n act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),\n at REAL NOT NULL,\n kind TEXT NOT NULL CHECK (kind IN ('join','done','say','hold','resume','read','listen','ignore','unlisten')),\n actor_json TEXT COLLATE BINARY,\n actor_key_json TEXT COLLATE BINARY,\n reach TEXT CHECK (reach = 'bell'),\n reply_index INTEGER CHECK (reply_index >= 0 AND reply_index < act_index),\n payload_json TEXT NOT NULL CHECK (json_valid(payload_json)),\n CHECK ((actor_json IS NULL) = (actor_key_json IS NULL)),\n CHECK (kind IN ('hold','resume') OR actor_json IS NOT NULL),\n CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))\n);";
38
+ export declare const ACTIVITY_INDEXES_SQL: readonly ["CREATE INDEX activity_kind_index ON activities(kind, act_index)", "CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index)", "CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say'", "CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say'", "CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell'"];
39
+ export declare const SCHEMA_SQL: string;
40
+ export declare const SCHEMA_SQL_V2: string;
36
41
  export declare function invalidArtifact(detail: string): SquareError;
37
42
  export declare function validateStoredActivity(value: unknown): StoredAct;
38
43
  export declare function validateWatchLease(value: unknown): WatchLease;
39
44
  export declare function validateRoute(value: unknown): RouteFact;
45
+ export declare function validateRowFraming(db: DatabaseSync, version: 2 | 3, location?: string): void;
40
46
  export declare function validateSqliteHeader(db: DatabaseSync, location?: string): SquareHeader;
41
47
  export interface SqliteDriver extends SquareRead, LandingWriter {
42
48
  readonly changed: boolean;
@@ -6,6 +6,8 @@ export function emptyRuntimeState(nextActIndex = 0) {
6
6
  }
7
7
  export const APPLICATION_ID = 0x53515245;
8
8
  const V1_USER_VERSION = 1;
9
+ export const V2_USER_VERSION = 2;
10
+ export const CURRENT_USER_VERSION = 3;
9
11
  export const V1_SNAPSHOT_TABLE = 'square_snapshot';
10
12
  const V1_SNAPSHOT_DDL = `CREATE TABLE ${V1_SNAPSHOT_TABLE} (id INTEGER PRIMARY KEY CHECK(id=1), revision INTEGER NOT NULL CHECK(revision>=0), state TEXT NOT NULL)`;
11
13
  const V1_SNAPSHOT_COLUMNS = [
@@ -50,19 +52,12 @@ export function validateLegacyFraming(database, applicationId, location = '') {
50
52
  function normalizedLegacySql(sql) {
51
53
  return sql.trim().replace(/\s+/g, ' ').replace(/\s*(>=|[(),=])\s*/g, '$1');
52
54
  }
53
- export const SCHEMA_SQL = `CREATE TABLE square_meta (
54
- id INTEGER PRIMARY KEY CHECK (id = 1),
55
- revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),
56
- next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),
57
- hard_cap INTEGER CHECK (hard_cap BETWEEN 1 AND 9007199254740991),
58
- throttle_per_minute INTEGER CHECK (throttle_per_minute BETWEEN 1 AND 9007199254740991),
59
- preamble_json TEXT NOT NULL CHECK (json_valid(preamble_json)),
60
- warmup_json TEXT NOT NULL CHECK (json_valid(warmup_json))
61
- );
62
- CREATE TABLE activities (
55
+ const V2_ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','hold','resume','read','listen','ignore')";
56
+ const CURRENT_ACTIVITY_KIND_CHECK = "kind IN ('join','done','say','hold','resume','read','listen','ignore','unlisten')";
57
+ export const ACTIVITIES_TABLE_SQL = `CREATE TABLE activities (
63
58
  act_index INTEGER PRIMARY KEY CHECK (act_index BETWEEN 0 AND 9007199254740991),
64
59
  at REAL NOT NULL,
65
- kind TEXT NOT NULL CHECK (kind IN ('join','done','say','hold','resume','read','listen','ignore')),
60
+ kind TEXT NOT NULL CHECK (${CURRENT_ACTIVITY_KIND_CHECK}),
66
61
  actor_json TEXT COLLATE BINARY,
67
62
  actor_key_json TEXT COLLATE BINARY,
68
63
  reach TEXT CHECK (reach = 'bell'),
@@ -71,12 +66,25 @@ CREATE TABLE activities (
71
66
  CHECK ((actor_json IS NULL) = (actor_key_json IS NULL)),
72
67
  CHECK (kind IN ('hold','resume') OR actor_json IS NOT NULL),
73
68
  CHECK (kind = 'say' OR (reach IS NULL AND reply_index IS NULL))
69
+ );`;
70
+ export const ACTIVITY_INDEXES_SQL = [
71
+ 'CREATE INDEX activity_kind_index ON activities(kind, act_index)',
72
+ 'CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index)',
73
+ "CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say'",
74
+ "CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say'",
75
+ "CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell'",
76
+ ];
77
+ export const SCHEMA_SQL = `CREATE TABLE square_meta (
78
+ id INTEGER PRIMARY KEY CHECK (id = 1),
79
+ revision INTEGER NOT NULL CHECK (revision BETWEEN 0 AND 9007199254740991),
80
+ next_act_index INTEGER NOT NULL CHECK (next_act_index BETWEEN 0 AND 9007199254740991),
81
+ hard_cap INTEGER CHECK (hard_cap BETWEEN 1 AND 9007199254740991),
82
+ throttle_per_minute INTEGER CHECK (throttle_per_minute BETWEEN 1 AND 9007199254740991),
83
+ preamble_json TEXT NOT NULL CHECK (json_valid(preamble_json)),
84
+ warmup_json TEXT NOT NULL CHECK (json_valid(warmup_json))
74
85
  );
75
- CREATE INDEX activity_kind_index ON activities(kind, act_index);
76
- CREATE INDEX activity_actor_index ON activities(actor_key_json, act_index);
77
- CREATE INDEX activity_say_actor_index ON activities(actor_key_json, act_index) WHERE kind='say';
78
- CREATE INDEX activity_say_time ON activities(at, act_index) WHERE kind='say';
79
- CREATE INDEX activity_bell_actor_time ON activities(actor_key_json, at, act_index) WHERE kind='say' AND reach='bell';
86
+ ${ACTIVITIES_TABLE_SQL}
87
+ ${ACTIVITY_INDEXES_SQL.join(';\n')};
80
88
 
81
89
  CREATE TABLE receipts (
82
90
  owner_json TEXT COLLATE BINARY NOT NULL,
@@ -134,6 +142,8 @@ CREATE TABLE landed_audience (
134
142
  ) WITHOUT ROWID;
135
143
  CREATE INDEX audience_recipient ON landed_audience(recipient_key_json, act_index);`;
136
144
  const EXPECTED_SCHEMA_SQL = SCHEMA_SQL.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
145
+ export const SCHEMA_SQL_V2 = SCHEMA_SQL.replace(CURRENT_ACTIVITY_KIND_CHECK, V2_ACTIVITY_KIND_CHECK);
146
+ const EXPECTED_SCHEMA_SQL_V2 = SCHEMA_SQL_V2.split(';').map(s => s.trim()).filter(Boolean).map(normalizedSql).sort();
137
147
  const encode = JSON.stringify;
138
148
  const key = (name) => encode(nameKey(name));
139
149
  export function invalidArtifact(detail) {
@@ -189,7 +199,7 @@ function immutable(value) {
189
199
  }
190
200
  return value;
191
201
  }
192
- const kinds = ['join', 'done', 'say', 'hold', 'resume', 'read', 'listen', 'ignore'];
202
+ const kinds = ['join', 'done', 'say', 'hold', 'resume', 'read', 'listen', 'ignore', 'unlisten'];
193
203
  function head(row) {
194
204
  const index = integer(row.act_index), at = finite(row.at);
195
205
  requireFact(kinds.includes(string(row.kind)));
@@ -237,6 +247,7 @@ export function validateStoredActivity(value) {
237
247
  break;
238
248
  case 'listen':
239
249
  case 'ignore':
250
+ case 'unlisten':
240
251
  shape(a, [...common, 'target']);
241
252
  string(a.target, true);
242
253
  break;
@@ -247,7 +258,7 @@ export function validateStoredActivity(value) {
247
258
  function activity(row) {
248
259
  const h = head(row), payload = object(json(row.payload_json));
249
260
  // Payload cannot shadow indexed structural columns.
250
- const allowed = h.kind === 'say' ? ['body', 'mentions'] : h.kind === 'done' || h.kind === 'hold' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' ? ['target'] : [];
261
+ const allowed = h.kind === 'say' ? ['body', 'mentions'] : h.kind === 'done' || h.kind === 'hold' ? ['body'] : h.kind === 'read' ? ['through'] : h.kind === 'listen' || h.kind === 'ignore' || h.kind === 'unlisten' ? ['target'] : [];
251
262
  requireFact(Object.keys(payload).every(k => allowed.includes(k)));
252
263
  return immutable(validateStoredActivity({ ...h, ...payload }));
253
264
  }
@@ -315,6 +326,17 @@ function route(row) {
315
326
  }));
316
327
  }
317
328
  function normalizedSql(sql) { return sql.trim().replace(/;$/, '').replace(/\s+/g, ' ').replace(/\s*([(),=])\s*/g, '$1'); }
329
+ function validateOwnedSchema(db, expected, location) {
330
+ const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all().map(r => normalizedSql(string(r.sql))).sort();
331
+ requireFact(actual.length === expected.length && actual.every((sql, i) => sql === expected[i]), 'unsupported row schema.');
332
+ requireFact(db.prepare('PRAGMA journal_mode').get().journal_mode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
333
+ }
334
+ export function validateRowFraming(db, version, location = '') {
335
+ const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
336
+ requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
337
+ requireFact(identity.user_version === version, 'unsupported SQLite format version.');
338
+ validateOwnedSchema(db, version === V2_USER_VERSION ? EXPECTED_SCHEMA_SQL_V2 : EXPECTED_SCHEMA_SQL, location);
339
+ }
318
340
  export function validateSqliteHeader(db, location = '') {
319
341
  const identity = db.prepare('SELECT application_id,user_version FROM pragma_application_id,pragma_user_version').get();
320
342
  requireFact(identity.application_id === APPLICATION_ID, 'unrelated SQLite database.');
@@ -322,10 +344,12 @@ export function validateSqliteHeader(db, location = '') {
322
344
  validateLegacyFraming(db, APPLICATION_ID, location);
323
345
  throw new SquareError('invalid_args', `Square artifact at ${location} needs migration.\nStop sessions using it, back it up, then migrate and verify before restarting.`, { reason: 'migration_required' });
324
346
  }
325
- requireFact(identity.user_version === 2, 'unsupported SQLite format version.');
326
- const actual = db.prepare("SELECT sql FROM sqlite_schema WHERE name NOT GLOB 'sqlite_*' AND sql IS NOT NULL").all().map(r => normalizedSql(string(r.sql))).sort();
327
- requireFact(actual.length === EXPECTED_SCHEMA_SQL.length && actual.every((sql, i) => sql === EXPECTED_SCHEMA_SQL[i]), 'unsupported row schema.');
328
- requireFact(db.prepare('PRAGMA journal_mode').get().journal_mode === (location === ':memory:' ? 'memory' : 'delete'), 'unsupported journal mode.');
347
+ if (identity.user_version === V2_USER_VERSION) {
348
+ validateRowFraming(db, V2_USER_VERSION, location);
349
+ throw new SquareError('invalid_args', `Square artifact at ${location} needs migration.\nStop sessions using it, back it up, then migrate and verify before restarting.`, { reason: 'migration_required' });
350
+ }
351
+ requireFact(identity.user_version === CURRENT_USER_VERSION, 'unsupported SQLite format version.');
352
+ validateRowFraming(db, CURRENT_USER_VERSION, location);
329
353
  return createSqliteDriver(db).header();
330
354
  }
331
355
  function activitySelection(filter, alias = 'a') {
@@ -550,6 +574,12 @@ export function createSqliteDriver(db) {
550
574
  }
551
575
  if (effect.removeListeningOf !== undefined)
552
576
  run("DELETE FROM attention WHERE listener_key_json=? AND kind='listen'", [key(effect.removeListeningOf)]);
577
+ if (effect.removeListening !== undefined) {
578
+ const target = effect.removeListening;
579
+ string(target.listener, true);
580
+ string(target.target, true);
581
+ run("DELETE FROM attention WHERE listener_key_json=? AND target_key_json=? AND kind='listen'", [key(target.listener), key(target.target)]);
582
+ }
553
583
  if (effect.replaceAttention) {
554
584
  const t = effect.replaceAttention;
555
585
  string(t.listener, true);
@@ -34,8 +34,9 @@ export const SQUARE_TOOLS = [
34
34
  limit: z.number().int().min(1).optional(), order: z.enum(['asc', 'desc']).optional(), before: activityId.optional(), after: activityId.optional(),
35
35
  grep: z.string().optional(), fixed: z.string().optional(), from: stringArray.optional(), mention: z.string().optional(),
36
36
  }, (square, args, control) => square.history(args, control)),
37
- tool('listen', 'Listen for future bare activity from a participant, clearing any ignore of them.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).listen(target, {}, control)),
37
+ tool('listen', 'Listen for future activity from a participant, clearing any ignore of them.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).listen(target, {}, control)),
38
38
  tool('ignore', 'Block a participant’s future mentions and bare activity. Listening again clears the ignore; already landed activity is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).ignore(target, {}, control)),
39
+ tool('unlisten', 'Stop listening to a participant’s future activity; mentions and replies still reach you. An existing ignore is unchanged.', { as: participant, target: z.string() }, async (square, { as, target }, control) => (await square.participant(as)).unlisten(target, {}, control)),
39
40
  tool('listening', 'List participants this participant is listening to.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).listening({}, control)),
40
41
  tool('hold', 'Raise a hand to hold the square.', { as: participant, reason: z.string().optional() }, async (square, { as, reason }, control) => (await square.participant(as)).hold(reason, {}, control)),
41
42
  tool('resume', 'Release the current hold.', { as: participant }, async (square, { as }, control) => (await square.participant(as)).resume({}, control)),
package/dist/views.d.ts CHANGED
@@ -6,7 +6,7 @@ import type { Activity, ArchiveRecord, HistoryQuery, HistoryContinuation, Operat
6
6
  export type { EntryPresentation, ListPresentation, CatchPresence, StreamProjection } from './square-facade.js';
7
7
  export declare const HISTORY_DEFAULT_LIMIT = 10;
8
8
  export declare const HISTORY_MAX_LIMIT = 100;
9
- export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume", "listen", "ignore"];
9
+ export declare const NON_READ: readonly ["join", "done", "say", "hold", "resume", "listen", "ignore", "unlisten"];
10
10
  /** The one StoredAct-to-public-Activity field mapping. Rejects read and actorless acts. */
11
11
  export declare function toPublicActivity(stored: StoredAct): Activity;
12
12
  export declare function parseRequiredActivityId(id: ActivityId): number;
package/dist/views.js CHANGED
@@ -9,7 +9,7 @@ export const HISTORY_DEFAULT_LIMIT = 10;
9
9
  export const HISTORY_MAX_LIMIT = 100;
10
10
  const STATUS_HERE_WINDOW_MS = 30 * 60_000;
11
11
  const PRESENCE_WINDOW_MS = 8 * 60 * 60_000;
12
- export const NON_READ = ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore'];
12
+ export const NON_READ = ['join', 'done', 'say', 'hold', 'resume', 'listen', 'ignore', 'unlisten'];
13
13
  const ARCHIVE = new Set(['join', 'done', 'say', 'hold', 'resume']);
14
14
  /** The one StoredAct-to-public-Activity field mapping. Rejects read and actorless acts. */
15
15
  export function toPublicActivity(stored) {
@@ -50,9 +50,9 @@ protocol, resident service or ledger schema migration is introduced.
50
50
 
51
51
  ## Membership and lifecycle
52
52
 
53
- If `.square/PUBLIC.square` already exists, the mod joins the automatic participant.
54
- It never creates PUBLIC. Existing manual memberships also reconcile independently
55
- of PUBLIC. Main-conversation tool completion (including failed/aborted tools) and
53
+ Participants join explicitly. The mod never discovers or joins a square from its
54
+ filename; `PUBLIC.square` has no special behavior. Existing memberships reconcile
55
+ on startup. Main-conversation tool completion (including failed/aborted tools) and
56
56
  conversation start reconcile late memberships: the receiver itself publishes
57
57
  routes; the CLI cannot infer mod capability from a socket or session environment.
58
58
  This reconciliation injects no activity and does not poll. Its recovery sweep
@@ -1,78 +1,75 @@
1
- # Migrating an existing square to row storage
1
+ # Migrating an existing square
2
2
 
3
3
  Square stores authority in one versioned SQLite artifact. Version 1 kept the
4
- whole history as one JSON snapshot; version 2 keeps typed rows — activities,
5
- sparse receipts, catch leases, ordered routes and the small configuration
6
- facts — in one in-place database.
4
+ whole history as one JSON snapshot. Version 2 introduced typed rows. Current
5
+ format 3 adds the `unlisten` history event without changing the other tables.
7
6
 
8
- Existing artifacts are **not** converted automatically. A normal read of a
9
- version 1 artifact refuses with a descriptive offline-migration requirement.
10
- The CLI adds the concrete recovery command; native tools retain the description:
7
+ Existing artifacts are **not** converted automatically. Ordinary reads and
8
+ writes refuse versions 1 and 2 with an offline-migration requirement. The CLI
9
+ adds the concrete recovery command; native tools retain the description:
11
10
 
12
11
  ```sh
13
12
  square --location /path/to/SQUARE.square migrate
14
13
  ```
15
14
 
16
- That is the only migration command. There is no `--check`, `--backup`,
17
- `--force`, bulk mode, sidecar, daemon or dual reader, and no operator
18
- configuration to set.
15
+ This is the only artifact migration command. There is no `--check`, `--backup`,
16
+ `--force`, bulk mode, sidecar, daemon or dual reader. The host ledger is a
17
+ separate resource; unlisten does not change its format.
19
18
 
20
19
  ## What the command does
21
20
 
22
- It converts the artifact **in place**, in **one transaction**:
23
-
24
- 1. Reads the owned legacy framing before touching anything (application id,
25
- format version, `DELETE` journal mode, the exact snapshot schema) and
26
- refuses anything that is not an owned version 1 square.
27
- 2. Takes `BEGIN IMMEDIATE`, re-checks that framing under the lock, and decodes
28
- and validates the complete snapshot inside the lock.
29
- 3. Creates the row schema and writes the preserved authority: activity indices
30
- and their gaps, optional-field presence, raw receipt and lease owner keys,
31
- route order and duplicates, string content and finite fractional timestamps.
32
- 4. Rebuilds the derived participant, hold, attention and landed-audience facts
33
- through the same single landing transition the runtime uses.
34
- 5. Drops the snapshot table, sets the format version, and commits once.
35
-
36
- The revision is preserved; migration never invents or advances it. Nothing is
37
- published piecemeal, and the file keeps its path, inode and registry identity.
38
-
39
- Two shapes are already fine and change nothing:
40
-
41
- - an artifact that is already version 2 (reported as already format 2);
42
- - an artifact converted by another process while this one waited for the lock.
21
+ Both supported conversions happen **in place**, in **one transaction**:
22
+
23
+ 1. Read the owned framing before any persistent pragma or DDL: application id,
24
+ source format, `DELETE` journal mode, and exact known schema.
25
+ 2. Take `BEGIN IMMEDIATE` and re-check that framing under the lock.
26
+ 3. Convert the source using its explicit path:
27
+ - **Version 1:** decode and validate the complete snapshot, create current
28
+ rows, and preserve activity indices/gaps, optional fields, raw receipt and
29
+ lease owners, ordered routes, strings and finite fractional timestamps.
30
+ Rebuild derived participant, hold, attention and landed-audience facts
31
+ with the runtime's shared landing transition, then drop the snapshot.
32
+ - **Version 2:** rebuild only `activities` and its indexes to accept
33
+ `unlisten`, copying every existing activity row unchanged. All other
34
+ tables, including attention, landed audience, receipts, leases, routes
35
+ and configuration, remain untouched. No history is reinterpreted.
36
+ 4. Set format 3 and commit once.
37
+
38
+ Revision and assigned activity indices are preserved. Migration adds no domain
39
+ activity. Nothing is published piecemeal, and path, inode and registry identity
40
+ remain unchanged. A format-3 artifact is reported as already current without
41
+ writing, including when another migrator finished while this one waited.
43
42
 
44
43
  ## What it refuses
45
44
 
46
- Nothing is written when the input is not a normally persisted version 1 square:
47
- a missing file, an unrelated SQLite database, an unsupported format version, a
48
- different journal mode, an unknown snapshot schema, unreadable snapshot JSON, or
49
- a route carrying a non-finite number (an exponent-overflow value such as
50
- `1e999`, which the previous permissive check accepted and an ordinary JSON round
51
- trip cannot carry) are all refused before any write. The refusal names the
52
- offending route property.
53
-
54
- Journal mode is never rewritten to make a migration possible.
45
+ Missing files, unrelated databases, unsupported versions, different journal
46
+ modes, unknown schemas and malformed headers are refused without a write.
47
+ Version-1 snapshot JSON and activity vocabulary must be valid for that source
48
+ format. Non-finite numbers anywhere in a legacy route, including nested extra
49
+ properties, are refused with the offending property named; they are not
50
+ silently coerced to null. Journal mode is never rewritten to make conversion
51
+ possible, and migration is not a corruption-repair command.
55
52
 
56
53
  ## Before you migrate
57
54
 
58
- - Stop attached runtimes, or upgrade them together with the migration. An
59
- in-flight remote delivery is not revoked by migration, and a version 1 writer
60
- either commits before the conversion or is refused afterwards — it can never
61
- overwrite converted rows.
62
- - An offline backup is optional and, if you want one, ordinary: copy the file
63
- while no writer is attached. The command does not manage backups.
55
+ Stop every session and service using that artifact. Make an offline backup, or
56
+ use SQLite's backup facilities rather than copying a live database. The command
57
+ does not manage backups, stop processes, revoke in-flight remote deliveries,
58
+ or update installed runtimes.
64
59
 
65
- ## Crash safety
60
+ Upgrade all attached runtimes before reopening the migrated artifact. Both
61
+ version-1 and version-2 runtimes refuse format 3; do not mix old and new
62
+ processes against the converted file. An older writer either commits before
63
+ conversion or meets the new version gate afterwards.
66
64
 
67
- Before `COMMIT`, SQLite's own journal recovers the complete version 1 artifact;
68
- after it, the complete version 2 rows are there. A process killed mid-migration
69
- therefore leaves one or the other, never a partial artifact, and re-running the
70
- command finishes the job. A busy `COMMIT` retries the same transaction and never
71
- re-runs the conversion.
65
+ ## Crash safety and checking
72
66
 
73
- ## Checking an artifact
67
+ Before `COMMIT`, SQLite's journal recovers the complete source version; after
68
+ it, complete format-3 rows are present. A failure or process death cannot leave
69
+ a partial conversion. A busy `COMMIT` retries the same transaction, never the
70
+ copy or domain callback.
74
71
 
75
- `square --location <path> doctor` is read-only. On a legacy artifact it
76
- recognizes version 1 and prints the migration command; it does not call the
77
- artifact damaged. On version 2 it validates the rows and recomputes the derived
78
- indices to compare them against the stored ones.
72
+ `square --location <path> doctor` is read-only. On versions 1 and 2 it prints
73
+ the migration requirement without calling the artifact damaged. On current
74
+ format 3 it validates rows and recomputes derived indices for comparison.
75
+ Run it after migration and before restarting upgraded clients.
@@ -1,6 +1,9 @@
1
1
  # Row-oriented artifact design
2
2
 
3
- **Status: design closed; row runtime candidate implemented and measured, awaiting independent review/acceptance.**
3
+ **Historical accepted design for artifact format 2.** The current runtime uses format 3 for the
4
+ `unlisten` event; see [Square API](square-api.md#artifact-format-and-explicit-migration) for current
5
+ format/migration behavior. The schema and delivery-stage terms below record the original row-storage
6
+ replacement, not a pending implementation or a second current-format definition.
4
7
  Source baseline: `99679eb` (0.3.70). This document specifies the design; [runtime delivery evidence](row-runtime-delivery-evidence.md) records implementation verification and actual capacity measurements. Explicit migration remains a separately accepted delivery.
5
8
 
6
9
  ## Decision in brief