From feb8a6268c3cf4d3b984e5d15171d9c0e608869d Mon Sep 17 00:00:00 2001 From: Jordan Santell Date: Mon, 10 Nov 2025 10:07:53 -0800 Subject: [PATCH 1/3] chore: Align creation of sessions across the workspace (#2037) chore: Align creation of sessions across the workspace as a precursor to identity signing. --- .../background-charm-service/cast-admin.ts | 6 ++-- .../background-charm-service/src/worker.ts | 6 ++-- packages/charm/src/manager.ts | 2 +- packages/charm/src/ops/charms-controller.ts | 14 ++------ packages/cli/lib/charm.ts | 15 ++------ packages/identity/src/index.ts | 2 +- packages/identity/src/session.ts | 36 ++++++++++--------- .../runner/integration/array_push.test.ts | 4 +-- .../integration/derive_array_leak.test.ts | 4 +-- packages/seeder/cli.ts | 2 +- packages/shell/src/lib/runtime.ts | 23 ++---------- scripts/main.ts | 6 ++-- 12 files changed, 43 insertions(+), 77 deletions(-) diff --git a/packages/background-charm-service/cast-admin.ts b/packages/background-charm-service/cast-admin.ts index cc91221486..d83c9a45da 100644 --- a/packages/background-charm-service/cast-admin.ts +++ b/packages/background-charm-service/cast-admin.ts @@ -3,7 +3,7 @@ import { CharmManager, compileRecipe } from "@commontools/charm"; import { Runtime } from "@commontools/runner"; import { StorageManager } from "@commontools/runner/storage/cache.deno"; import { type DID } from "@commontools/identity"; -import { createAdminSession } from "@commontools/identity"; +import { createSessionFromDid } from "@commontools/identity"; import { BG_CELL_CAUSE, BG_SYSTEM_SPACE_ID, @@ -87,9 +87,9 @@ async function castRecipe() { console.log("Casting recipe..."); // Create session and charm manager (matching main.ts pattern) - const session = await createAdminSession({ + const session = await createSessionFromDid({ identity, - name: "recipe-caster", + spaceName: "recipe-caster", space: spaceId as DID, }); diff --git a/packages/background-charm-service/src/worker.ts b/packages/background-charm-service/src/worker.ts index 093b6d4553..d47a7f3d98 100644 --- a/packages/background-charm-service/src/worker.ts +++ b/packages/background-charm-service/src/worker.ts @@ -12,7 +12,7 @@ import { import { StorageManager } from "@commontools/runner/storage/cache.deno"; import { - createAdminSession, + createSessionFromDid, type DID, Identity, Session, @@ -93,9 +93,9 @@ async function initialize( // Initialize session spaceId = did as DID; - currentSession = await createAdminSession({ + currentSession = await createSessionFromDid({ identity, - name: "~background-service-worker", + spaceName: "~background-service-worker", space: spaceId, }); diff --git a/packages/charm/src/manager.ts b/packages/charm/src/manager.ts index 43549210d2..40561ca3a0 100644 --- a/packages/charm/src/manager.ts +++ b/packages/charm/src/manager.ts @@ -226,7 +226,7 @@ export class CharmManager { } getSpaceName(): string { - return this.session.name; + return this.session.spaceName; } async synced(): Promise { diff --git a/packages/charm/src/ops/charms-controller.ts b/packages/charm/src/ops/charms-controller.ts index f9ce7c473c..1bffb51d53 100644 --- a/packages/charm/src/ops/charms-controller.ts +++ b/packages/charm/src/ops/charms-controller.ts @@ -8,7 +8,7 @@ import { StorageManager } from "@commontools/runner/storage/cache"; import { CharmManager } from "../index.ts"; import { CharmController } from "./charm-controller.ts"; import { compileProgram } from "./utils.ts"; -import { ANYONE, Identity } from "@commontools/identity"; +import { createSession, Identity } from "@commontools/identity"; export interface CreateCharmOptions { input?: object; @@ -114,17 +114,7 @@ export class CharmsController { identity: Identity; spaceName: string; }): Promise { - const account = spaceName.startsWith("~") - ? identity - : await Identity.fromPassphrase(ANYONE); - const user = await account.derive(spaceName); - const session = { - private: account.did() === identity.did(), - name: spaceName, - space: user.did(), - as: user, - }; - + const session = await createSession({ identity, spaceName }); const runtime = new Runtime({ apiUrl: new URL(apiUrl), storageManager: StorageManager.open({ diff --git a/packages/cli/lib/charm.ts b/packages/cli/lib/charm.ts index 7481c98a3f..da25c1f11c 100644 --- a/packages/cli/lib/charm.ts +++ b/packages/cli/lib/charm.ts @@ -1,4 +1,4 @@ -import { ANYONE, Identity, Session } from "@commontools/identity"; +import { createSession, Session } from "@commontools/identity"; import { ensureDir } from "@std/fs"; import { loadIdentity } from "./identity.ts"; import { @@ -35,17 +35,8 @@ async function makeSession(config: SpaceConfig): Promise { if (config.space.startsWith("did:key")) { throw new Error("DID key spaces not yet supported."); } - const root = await loadIdentity(config.identity); - const account = config.space.startsWith("~") - ? root - : await Identity.fromPassphrase(ANYONE); - const user = await account.derive(config.space); - return { - private: account.did() === root.did(), - name: config.space, - space: user.did(), - as: user, - }; + const identity = await loadIdentity(config.identity); + return createSession({ identity, spaceName: config.space }); } export async function loadManager(config: SpaceConfig): Promise { diff --git a/packages/identity/src/index.ts b/packages/identity/src/index.ts index dda6a90464..59016b57b9 100644 --- a/packages/identity/src/index.ts +++ b/packages/identity/src/index.ts @@ -8,7 +8,7 @@ export { KeyStore } from "./key-store.ts"; export * from "./interface.ts"; export { ANYONE, - createAdminSession, createSession, + createSessionFromDid, type Session, } from "./session.ts"; diff --git a/packages/identity/src/session.ts b/packages/identity/src/session.ts index fec994e5b3..6948364480 100644 --- a/packages/identity/src/session.ts +++ b/packages/identity/src/session.ts @@ -3,37 +3,41 @@ import { type DID } from "./interface.ts"; export const ANYONE = "common user"; export type Session = { - private: boolean; - name: string; + isPrivate: boolean; + spaceName: string; space: DID; as: Identity; }; // Create a session where `Identity` is used directly and not derived. -export const createAdminSession = async ( - { identity, space, name }: { +export const createSessionFromDid = ( + { identity, space, spaceName }: { identity: Identity; space: DID; - name: string; + spaceName: string; }, -) => - await ({ - private: name.startsWith("~"), - name, +): Promise => { + const isPrivate = spaceName.startsWith("~"); + return Promise.resolve({ + isPrivate, + spaceName, space, as: identity, }); +}; // Create a session where `Identity` is used to derive a space key. export const createSession = async ( - { identity, name }: { identity: Identity; name: string }, -) => { - const space = await identity.derive(name); + { identity, spaceName }: { identity: Identity; spaceName: string }, +): Promise => { + const isPrivate = spaceName.startsWith("~"); + const account = isPrivate ? identity : await Identity.fromPassphrase(ANYONE); + const user = await account.derive(spaceName); return { - private: name.startsWith("~"), - name, - space: space.did(), - as: space, + isPrivate, + spaceName, + space: user.did(), + as: user, }; }; diff --git a/packages/runner/integration/array_push.test.ts b/packages/runner/integration/array_push.test.ts index 8700280e09..9a4e605dbc 100644 --- a/packages/runner/integration/array_push.test.ts +++ b/packages/runner/integration/array_push.test.ts @@ -33,8 +33,8 @@ async function runTest() { const space_thingy = await account.derive(SPACE_NAME); const space_thingy_space = space_thingy.did(); const session = { - private: false, - name: SPACE_NAME, + isPrivate: false, + spaceName: SPACE_NAME, space: space_thingy_space, as: space_thingy, } as Session; diff --git a/packages/runner/integration/derive_array_leak.test.ts b/packages/runner/integration/derive_array_leak.test.ts index a2b4cd66d2..2156b556c5 100644 --- a/packages/runner/integration/derive_array_leak.test.ts +++ b/packages/runner/integration/derive_array_leak.test.ts @@ -85,8 +85,8 @@ async function runTest() { const space_thingy = await account.derive(SPACE_NAME); const space_thingy_space = space_thingy.did(); const session = { - private: false, - name: SPACE_NAME, + isPrivate: false, + spaceName: SPACE_NAME, space: space_thingy_space, as: space_thingy, } as Session; diff --git a/packages/seeder/cli.ts b/packages/seeder/cli.ts index eb235d73e1..3875303b15 100644 --- a/packages/seeder/cli.ts +++ b/packages/seeder/cli.ts @@ -46,7 +46,7 @@ setLLMUrl(apiUrl); const identity = await Identity.fromPassphrase("common user"); const session = await createSession({ identity, - name, + spaceName: name, }); const runtime = new Runtime({ diff --git a/packages/shell/src/lib/runtime.ts b/packages/shell/src/lib/runtime.ts index 2b0b63ce56..6228f54839 100644 --- a/packages/shell/src/lib/runtime.ts +++ b/packages/shell/src/lib/runtime.ts @@ -1,4 +1,4 @@ -import { ANYONE, Identity, Session } from "@commontools/identity"; +import { createSession, Identity } from "@commontools/identity"; import { Runtime, RuntimeTelemetry, @@ -18,25 +18,6 @@ const logger = getLogger("shell.telemetry", { level: "debug", }); -async function createSession( - root: Identity, - spaceName: string, -): Promise { - const account = spaceName.startsWith("~") - ? root - : await Identity.fromPassphrase(ANYONE); - - const user = await account.derive(spaceName); - const session = { - private: account.did() === root.did(), - name: spaceName, - space: user.did(), - as: user, - }; - - return session; -} - // RuntimeInternals bundles all of the lifetimes // of resources bound to an identity,host,space triplet, // containing runtime, inspector, and charm references. @@ -103,7 +84,7 @@ export class RuntimeInternals extends EventTarget { apiUrl: URL; }, ): Promise { - const session = await createSession(identity, spaceName); + const session = await createSession({ identity, spaceName }); // We're hoisting CharmManager so that // we can create it after the runtime, but still reference diff --git a/scripts/main.ts b/scripts/main.ts index 7b3444b688..9dc421c768 100755 --- a/scripts/main.ts +++ b/scripts/main.ts @@ -10,7 +10,7 @@ import { } from "@commontools/runner"; import { StorageManager } from "@commontools/runner/storage/cache"; import { - createAdminSession, + createSessionFromDid, type DID, Identity, type Session, @@ -85,10 +85,10 @@ async function main() { const space: DID = spaceDID as DID ?? identity.did(); - const session = await createAdminSession({ + const session = await createSessionFromDid({ identity, space, - name: spaceName ?? "unknown", + spaceName: spaceName ?? "unknown", }) satisfies Session; // TODO(seefeld): It only wants the space, so maybe we simplify the above and just space the space did? From 643da0508041a7e45aba62bce078ed6e5e521e03 Mon Sep 17 00:00:00 2001 From: Bernhard Seefeld Date: Mon, 10 Nov 2025 10:50:47 -0800 Subject: [PATCH 2/3] Add getArgumentCell method to access recipe instance arguments (#2039) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This change introduces a new method `getArgumentCell()` that provides convenient access to the argument cell of an instantiated recipe. This is useful when you need to update the arguments of a running recipe instance and have the changes automatically propagate through the reactive system. Changes: - Add `getArgumentCell()` method to IResolvable interface in packages/api/index.ts - Supports both generic type parameter and JSONSchema validation - Returns Cell | undefined (undefined if no source cell exists) - Implement `getArgumentCell()` in CellImpl (packages/runner/src/cell.ts) - Retrieves the source cell using existing getSourceCell() method - Kicks off sync to ensure latest data is available - Returns the "argument" key of the source cell with optional schema validation - Added to cellMethods set for proper proxy support - Add comprehensive test in packages/runner/test/cell.test.ts - Tests reactive behavior: instantiates a doubling recipe with an initial argument - Updates the argument via getArgumentCell() and verifies output changes - Tests multiple updates to ensure continued reactivity Use case: When working with recipe instances, you can now easily access and modify their arguments without manually navigating to the source cell's argument field. The method handles the complexity of retrieving the source cell and extracting the argument field while ensuring proper synchronization. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude --- packages/api/index.ts | 4 +++ packages/runner/src/cell.ts | 13 +++++++ packages/runner/test/cell.test.ts | 59 ++++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 1 deletion(-) diff --git a/packages/api/index.ts b/packages/api/index.ts index 3cf5b44594..bc7466fb91 100644 --- a/packages/api/index.ts +++ b/packages/api/index.ts @@ -218,6 +218,10 @@ export interface ICreatable> { */ export interface IResolvable> { resolveAsCell(): C; + getArgumentCell( + schema?: S, + ): Cell> | undefined; + getArgumentCell(): Cell | undefined; } /** diff --git a/packages/runner/src/cell.ts b/packages/runner/src/cell.ts index 792d6b77c3..2cdde07c52 100644 --- a/packages/runner/src/cell.ts +++ b/packages/runner/src/cell.ts @@ -225,6 +225,7 @@ const cellMethods = new Set>([ "setRaw", "getSourceCell", "setSourceCell", + "getArgumentCell", "freeze", "isFrozen", "setSchema", @@ -916,6 +917,18 @@ export class CellImpl implements ICell, IStreamable { ); } + getArgumentCell(schema?: JSONSchema): Cell | undefined { + const sourceCell = this.getSourceCell(); + if (!sourceCell) return undefined; + // Kick off sync, since when used in a pattern, this wasn't automatically + // subscribed to yet. So we might still get a conflict on first write, but will + // get the correct version on retry. + sourceCell.sync(); + // TODO(seefeld): Ideally we intersect this schema with the actual argument + // schema, so that get isn't for any. + return sourceCell.key("argument").asSchema(schema); + } + freeze(reason: string): void { this.readOnlyReason = reason; } diff --git a/packages/runner/test/cell.test.ts b/packages/runner/test/cell.test.ts index fb1e96cf70..d66d32736e 100644 --- a/packages/runner/test/cell.test.ts +++ b/packages/runner/test/cell.test.ts @@ -8,7 +8,7 @@ import { isCell } from "../src/cell.ts"; import { LINK_V1_TAG } from "../src/sigil-types.ts"; import { isCellResult } from "../src/query-result-proxy.ts"; import { toCell } from "../src/back-to-cell.ts"; -import { ID, JSONSchema } from "../src/builder/types.ts"; +import { ID, JSONSchema, type Recipe } from "../src/builder/types.ts"; import { popFrame, pushFrame } from "../src/builder/recipe.ts"; import { Runtime } from "../src/runtime.ts"; import { txToReactivityLog } from "../src/scheduler.ts"; @@ -225,6 +225,63 @@ describe("Cell", () => { expect(retrievedSource?.get()).toEqual({ foo: 456 }); }); + it("should update recipe output when argument is changed via getArgumentCell", async () => { + // Create a simple doubling recipe + const doubleRecipe: Recipe = { + argumentSchema: { + type: "object", + properties: { input: { type: "number" } }, + required: ["input"], + }, + resultSchema: { + type: "object", + properties: { output: { type: "number" } }, + }, + result: { output: { $alias: { path: ["internal", "doubled"] } } }, + nodes: [ + { + module: { + type: "javascript", + implementation: (args: { input: number }) => (args.input * 2), + }, + inputs: { input: { $alias: { path: ["argument", "input"] } } }, + outputs: { $alias: { path: ["internal", "doubled"] } }, + }, + ], + }; + + // Instantiate the recipe with initial argument + const resultCell = runtime.getCell(space, "doubling recipe instance"); + runtime.setup(undefined, doubleRecipe, { input: 5 }, resultCell); + runtime.start(resultCell); + await runtime.idle(); + + // Verify initial output + expect(resultCell.getAsQueryResult().output).toEqual(10); + + // Get the argument cell and update it + const argumentCell = resultCell.getArgumentCell<{ input: number }>(); + expect(argumentCell).toBeDefined(); + expect(argumentCell?.get()).toEqual({ input: 5 }); + + // Update the argument via the argument cell + const updateTx = runtime.edit(); + argumentCell!.withTx(updateTx).set({ input: 7 }); + updateTx.commit(); + await runtime.idle(); + + // Verify the output has changed + expect(resultCell.getAsQueryResult()).toEqual({ output: 14 }); + + // Update again to verify reactivity + const updateTx2 = runtime.edit(); + argumentCell!.withTx(updateTx2).set({ input: 100 }); + updateTx2.commit(); + await runtime.idle(); + + expect(resultCell.getAsQueryResult()).toEqual({ output: 200 }); + }); + it("should translate circular references into links", () => { const c = runtime.getCell( space, From 091ec3da732b3aa5f7b5c9e9fe1468a958009c69 Mon Sep 17 00:00:00 2001 From: Ellyse <141240083+ellyxir@users.noreply.github.com> Date: Mon, 10 Nov 2025 20:12:30 +0100 Subject: [PATCH 3/3] restart server script (#2040) --- scripts/restart-local-dev.sh | 41 ++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100755 scripts/restart-local-dev.sh diff --git a/scripts/restart-local-dev.sh b/scripts/restart-local-dev.sh new file mode 100755 index 0000000000..c60390793d --- /dev/null +++ b/scripts/restart-local-dev.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Change to repository root (parent of scripts directory) +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +cd "$SCRIPT_DIR/.." + +# Parse command line arguments +CLEAR_CACHE=false +FORCE=false +while [[ $# -gt 0 ]]; do + case $1 in + --clear-cache) + CLEAR_CACHE=true + shift + ;; + --force) + FORCE=true + shift + ;; + *) + echo "Unknown option: $1" + echo "Usage: $0 [--clear-cache] [--force]" + exit 1 + ;; + esac +done + +echo "Stopping local dev servers..." +./scripts/stop-local-dev.sh + +if [[ "$CLEAR_CACHE" == "true" ]]; then + echo "Clearing cache..." + rm -rf packages/toolshed/cache/* + echo "Cache cleared." +fi + +echo "Starting local dev servers..." +if [[ "$FORCE" == "true" ]]; then + ./scripts/start-local-dev.sh --force +else + ./scripts/start-local-dev.sh +fi