@@ -39,7 +39,7 @@ SPDX-License-Identifier: CC0-1.0
#### Archived sessions (queryable by time and by dir)
- idx/archived/{ts_be}/{sid} -> {dir_hash} // for global archive lists in chronological order
-- dir/{dir_hash}/archived/{ts_be} -> {sid} // for "archive history" per working directory
+- dir/{dir_hash}/archived/{ts_be}/{sid} -> "" // for "archive history" per working directory
- {ts_be} = 8-byte big-endian Unix nanos, hex-encoded lowercase, zero-padded to 16 hex chars; sorts chronologically
#### Per-session namespace
@@ -56,7 +56,13 @@ SPDX-License-Identifier: CC0-1.0
- s/{sid}/evt/{seq_be} -> JSON event record
- {seq_be} = 8-byte big-endian u64 counter, hex-encoded lowercase, zero-padded to 16 hex chars; assures correct chronological iteration
- { seq, at, type, reason: null|string, cmd: "np ...", payload: {...} }
- - Types you'll likely use: session_started, goal_set, goal_updated, task_added, task_updated, task_status_changed, session_archived, note
+ - Event payloads capture immutable snapshots so subscribers can render changes without extra lookups:
+ - type=goal_set → `{"goal":{"title":"…","description":"…","updated_at":"RFC3339"}}`
+ - type=goal_updated → `{"goal_before":{"title":"…","description":"…","updated_at":"RFC3339"},"goal_after":{"title":"…","description":"…","updated_at":"RFC3339"}}`
+ - type=task_added → `{"task":{"id":"a1b2c3","title":"…","description":"…","status":"pending","created_at":"RFC3339","updated_at":"RFC3339","created_seq":1}}`
+ - type=task_updated → `{"task_id":"a1b2c3","before":{"title":"…","description":"…","updated_at":"RFC3339"},"after":{"title":"…","description":"…","updated_at":"RFC3339"}}`
+ - type=task_status_changed → `{"task_id":"a1b2c3","title":"…","status_before":"pending","status_after":"in_progress","updated_at":"RFC3339"}`
+ - Supported event types: goal_set, goal_updated, task_added, task_updated, task_status_changed
### Core operations (transactional)
@@ -67,11 +73,10 @@ SPDX-License-Identifier: CC0-1.0
- If it exists, read the active {sid} and print: "Session {sid} is already active for this directory. There's already an active session for this directory; ask your operator whether they want to resume or archive it."
- Return 0 (idempotent operation).
4) If no active session exists, begin txn:
- - Generate new sid = ULID (time-ordered) or blake3(rand) hex; keep as short as practical (e.g., 26 char ULID).
+ - Generate new sid = ULID (canonical 26-character string for uniqueness and natural sort order).
- Put s/{sid}/meta (state=active), s/{sid}/meta/evt_seq=0 (do NOT create goal yet; goal is created when first set).
- Put dir/{dir_hash}/active -> {sid}.
- Put idx/active/{sid} -> {dir_hash}.
- - Append event s/{sid}/evt/{0000000000000001} type=session_started.
5) Commit.
#### Set goal (np g s …)
@@ -82,7 +87,7 @@ SPDX-License-Identifier: CC0-1.0
3) txn:
- Create s/{sid}/goal JSON.
- Update s/{sid}/meta.last_updated_at.
- - Increment s/{sid}/meta/evt_seq and write s/{sid}/evt/{seq} with type=goal_set (no reason required).
+ - Increment s/{sid}/meta/evt_seq (read current value, add 1, persist) before writing the corresponding event, then write s/{sid}/evt/{seq} with type=goal_set (no reason required).
#### Update goal (np g u …)
1) Lookup sid via dir/{dir_hash}/active.
@@ -93,7 +98,7 @@ SPDX-License-Identifier: CC0-1.0
4) txn:
- Update s/{sid}/goal JSON.
- Update s/{sid}/meta.last_updated_at.
- - Increment s/{sid}/meta/evt_seq and write s/{sid}/evt/{seq} with type=goal_updated, reason in payload.
+ - Increment s/{sid}/meta/evt_seq (read, add 1, persist) before writing the event, then write s/{sid}/evt/{seq} with type=goal_updated, reason in payload.
#### Add tasks (np t a …)
1) For each task:
@@ -103,11 +108,11 @@ SPDX-License-Identifier: CC0-1.0
- Treat adds as idempotent: if the same task (same title+description) is re-added, it will resolve to the same id and be a no-op.
- If a user wants to retry a cancelled task with the exact same title and description, they should update the existing task's status rather than adding a new one, or modify the title/description slightly to differentiate it.
- txn:
- - If s/{sid}/task/{id} absent, increment evt_seq and create task with status=pending, created_at=now, created_seq=evt_seq.
+ - If s/{sid}/task/{id} absent, read and increment evt_seq (persisting the new value) and create the task with status=pending, created_at=now, created_seq=evt_seq.
- Put s/{sid}/idx/status/pending/{id}.
- Update meta.last_updated_at.
- Append event task_added with task payload.
- - Note: When adding multiple tasks in a single np t a invocation, use a single transaction and increment evt_seq for each task to preserve the order they were provided on the command line. Tasks should be displayed sorted by created_seq (not created_at) to maintain stable, predictable ordering.
+ - Note: When adding multiple tasks in a single np t a invocation, use a single transaction (one task, or batch of sequenced tasks, per transaction) and increment evt_seq for each task to preserve the order they were provided on the command line. Tasks should be displayed sorted by created_seq (not created_at) to maintain stable, predictable ordering.
#### Update task status/title/description (np t u …)
1) Determine if reason is required:
@@ -119,7 +124,7 @@ SPDX-License-Identifier: CC0-1.0
- If status changes: delete old s/{sid}/idx/status/{old}/{id}, put new s/{sid}/idx/status/{new}/{id}.
- Update task JSON.
- Update meta.last_updated_at.
- - Increment evt_seq, append event task_updated or task_status_changed with reason (if provided).
+ - Increment evt_seq (read, add 1, persist) and append event task_updated or task_status_changed with reason (if provided).
#### Archive session (np a)
1) Read s/{sid}/meta:
@@ -129,8 +134,7 @@ SPDX-License-Identifier: CC0-1.0
- Delete idx/active/{sid}.
- Update s/{sid}/meta.state="archived", archived_at=now, last_updated_at.
- Put idx/archived/{ts_be}/{sid} -> {dir_hash}.
- - Put dir/{dir_hash}/archived/{ts_be} -> {sid}.
- - Increment evt_seq, append session_archived event.
+ - Put dir/{dir_hash}/archived/{ts_be}/{sid} -> "".
### Query patterns
@@ -245,7 +249,7 @@ Note on `reason` field:
- Enforce one active session per dir:
- np s checks if dir/{dir_hash}/active exists before creating a new session (idempotent behavior documented above).
- Deterministic task IDs:
- - Use blake3(normalized(title)+"|"+normalized(description)+"|"+sid), take first 8 hex.
+- Use blake3(normalized(title)+"|"+normalized(description)+"|"+sid), take first 6 hex.
- Never recompute on updates; the id is "creation id," not a content hash of current state.
- Re-adding the same task (same title+description) resolves to the same id and is treated as a no-op.