mirror of
https://github.com/NeuralNomadsAI/CodeNomad.git
synced 2026-11-05 21:50:36 +00:00
feat(missions): resume recurring Missions after OpenCode restarts with native schedules (#1259)
Closes #926. ## What changes Recurring Missions now survive OpenCode restarts (update, crash and relaunch, reboot) without a manual **Resume**, and the scheduler offers more than "once a day at a fixed time". - **Native schedules.** OpenCode's own scheduler (`experimental.schedules`, OpenCode 2.2.3+) owns the calendar; Missions decides whether a passage may run (CAS admission, no duplicates, Pause/Stop, errors and uncertain states stay manual). A dedicated technical conversation with a local no-op model carries the wake, so wakes, titles and compaction make no provider HTTP requests. - **Richer scheduling**, in one collapsed **Scheduling** section of the Create/Edit editor (default: once, immediately, no condition): date, delay, interval (≥15 min), daily, chosen weekdays, up to eight times a day, day of month, native cron; optional end date/duration and a maximum number of launched passages; five event conditions (another schedule succeeds, a one-time Mission completes, new local commits, a new message in a followed conversation, explicit files change). Up to five upcoming dates are previewed in the saved IANA zone. - **Created paused, started with Play.** Everything is editable before the first launch; after it, edits follow the approved matrix (future parameters between passages, objective/notes journaled during a passage). - **Keep OpenCode running.** A Preferences setting supervises the shared OpenCode service through the OS (Windows Task Scheduler with a periodic recovery task, macOS LaunchAgent, Linux systemd user unit with lingering, WSL from Windows). ON by default at the first Play unless OFF was saved; OFF removes supervision without stopping OpenCode; a live service is never killed or duplicated. - **Existing daily schedules** keep their path and controls; no migration. - Help (English and nine translations) and dev-docs are updated. ## Validation - Typechecks (4 workspaces); server Missions/host-lifetime suites; UI unit tests 1945/1945; help 31/31 and build. - Real isolated OpenCode services (2.2.5, minimum 2.2.3): resting schedule resumes after service restart with no client and no Resume; 32 real passages with history/quota/phase preserved; concurrent wakes admit once; DST and catch-up for every frequency type; forced local auto-compaction and restart during maintenance with zero provider HTTP; real upgrade 2.2.3 → 2.2.5. - OS supervision on Windows, WSL, Ubuntu VM (two real reboots, healthy before login; OFF without a session) and macOS VM (Aqua session, crashes, OFF/adoption). On the final bundle: two real periodic Windows recovery relaunches; WSL OFF/Retry on a live HTTP 500 service keeps the same process. - Windows Electron and Tauri packages run in private profiles: paused creation, nine frequencies, preview. - Full browser suite: 956/957 in the last tier; the failure is `directory-browser.test.ts` (folder picker timeout), unchanged by this branch, and that file passes 20/20 on rerun. Earlier dev-baseline runs on the same machine also lost module loads to `net::ERR_NO_BUFFER_SPACE`. ## Known limits (not qualified in this PR) - macOS reboot followed by a real login, and a Windows host reboot/logon (needs a disposable bench). - Real screen-reader output (keyboard, ARIA and RTL checks pass). - Linux/macOS desktop packages. - OpenCode's background *pre*-compaction never applies to the technical wake conversations: their local model reports zero tokens, below the native minimum. Their automatic local compaction is qualified. ## Follow-up Integration with native `/goal` comes in a separate PR.
This commit is contained in:
parent
c250db6c4f
commit
a5efdefc8d
514 changed files with 38887 additions and 2043 deletions
|
|
@ -19,7 +19,7 @@ description: |
|
|||
|
||||
- Runtime requirements live in `opencode/runtime-support.ts`: OpenCode 2.2.3 is the minimum, required by the `experimental_image_generate` image tool contract (upstream #60258) that the Images gallery, Design agent and Images settings consume; it also provides Code Mode's JavaScript job ordering (`scripts/test-codemode-jobs-native.mjs`) and the `native`/`metadata.keepID` replay contract consumed by the pinned client reducer; 2.2.5 is the recommendation. Each blocking requirement needs demonstrated API/behavior evidence; distinguish technically incompatible, recommended/tested and unverified versions. Shared-daemon restart is explicit and uses the native-parent launch bridge. See `dev-docs/OPENCODE_COMPATIBILITY.md` for the version boundaries and `dev-docs/OPENCODE_RUNTIME_SETUP.md` for setup, installation and admission.
|
||||
|
||||
- Server and UI pin the official `@opencode/client@2.1.0` together; native plugins pin `@opencode/plugin@2.1.0` for authoring and use `packages/server/scripts/native-plugin-build.mjs` to keep Effect/public SDK modules external. The daemon supplies their runtime instances. The canonical server-info client is `server.info()`, adapted to the authenticated discovered route. Review official V2 docs, installed declarations, generated wire paths and native integration tests when upgrading. Never infer a minimum from the latest publication or dependency pin alone. Update `dev-docs/OPENCODE_COMPATIBILITY.md` in place and retain detailed evidence in PR/CI logs. The runtime CLI is managed independently.
|
||||
- Server and UI pin the official `@opencode/client@2.2.3` together; native plugins pin `@opencode/plugin@2.2.3` for authoring and use `packages/server/scripts/native-plugin-build.mjs` to keep Effect/public SDK modules external. The daemon supplies their runtime instances. The canonical server-info client is `server.info()`, adapted to the authenticated discovered route. Review official V2 docs, installed declarations, generated wire paths and native integration tests when upgrading. Never infer a minimum from the latest publication or dependency pin alone. Update `dev-docs/OPENCODE_COMPATIBILITY.md` in place and retain detailed evidence in PR/CI logs. The runtime CLI is managed independently.
|
||||
- Do not use `@opencode-ai/sdk`, `@opencode-ai/sdk/v2/client`, or `createOpencodeClient()`; follow installed `@opencode/client` declarations.
|
||||
- Never add general plugin extension points. The narrow native integrations are the bundled `codenomad.automation` plugin, project-local native session coordination (`codenomad.missions`) and the bundled session-pruning RPC; see `dev-docs/BROWSER_AUTOMATION.md`, `dev-docs/MISSIONS.md` and `dev-docs/SESSION_PRUNING_RPC.md`. All automation tools follow backend presence, sharing the authenticated native transport and execution-time session/window fences.
|
||||
- The server uses the selected host or WSL CLI's official `service status`, `service start`, and `service get password` lifecycle to connect to one externally owned global OpenCode daemon. It owns no private port/database/registration/PID and never stops the daemon on backend shutdown. WSL requires Windows localhost forwarding and uses no cross-namespace PID operations.
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ Client-state V3 is a per-window envelope over the V2 content-addressed partition
|
|||
|
||||
| Owner | Responsibilities | Main paths |
|
||||
|---|---|---|
|
||||
| OpenCode V2 | Sessions, messages, permissions, Forms, files, session Shell/instructions, background Shells, interactive PTYs | pinned `@opencode/client@2.1.0` contract across server and UI; host-owned native plugin runtime |
|
||||
| OpenCode V2 | Sessions, messages, permissions, Forms, files, session Shell/instructions, background Shells, interactive PTYs | pinned `@opencode/client@2.2.3` contract across server and UI; host-owned native plugin runtime |
|
||||
| CodeNomad server | Shared service lifecycle, locations, proxy authorization, Git mutations, Yolo, auth, storage, speech, SSE multiplexing, desktop automation bridge | `packages/server/src/` |
|
||||
| CodeNomad UI | Generated Promise clients, state reconciliation, interaction and rendering | `packages/ui/src/` |
|
||||
| Desktop hosts | Start CodeNomad and provide native OS integration | `packages/electron-app/`, `packages/tauri-app/` |
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Package
|
||||
|
||||
CodeNomad server and UI pin `@opencode/client@2.1.0`. The runtime CLI is managed independently; startup validates authenticated loopback `/api/info`, then (only to identify an outdated daemon) `/api/status`, then `/api/health`, advancing only on HTTP 404 with the same endpoint, credentials and deadline. Each response has its own validated schema and a 64 KiB bound. The shared transport forwards canonical `server.info()` unchanged. Review official V2 docs, installed declarations, generated routes and native regression tests together when upgrading.
|
||||
CodeNomad server and UI pin `@opencode/client@2.2.3`. The runtime CLI is managed independently; startup validates authenticated loopback `/api/info`, then (only to identify an outdated daemon) `/api/status`, then `/api/health`, advancing only on HTTP 404 with the same endpoint, credentials and deadline. Each response has its own validated schema and a 64 KiB bound. The shared transport forwards canonical `server.info()` unchanged. Review official V2 docs, installed declarations, generated routes and native regression tests together when upgrading.
|
||||
|
||||
Runtime requirements live in `packages/server/src/opencode/runtime-support.ts`: minimum 2.2.3 (the `experimental_image_generate` image tool contract, Code Mode JavaScript job ordering and the `native`/`metadata.keepID` replay contract), with the recommendation (2.2.5) tracked separately. Native builds share the daemon's Effect/public SDK instead of bundling a second runtime. The shared connection binds authenticated runtime identity, the canonical client and forwarding transport. Admission precedes functional requests/plugin provisioning; unlisted custom/prerelease/future labels require authenticated bounded API recognition. Never add operation-specific retry fallbacks. See `dev-docs/OPENCODE_COMPATIBILITY.md` for the version boundaries and `dev-docs/OPENCODE_RUNTIME_SETUP.md` for admission.
|
||||
|
||||
|
|
@ -36,7 +36,7 @@ Native Forms own pending interruption state. Global Forms use `sessionID: "globa
|
|||
|
||||
Mutations use `permission.reply({ decision })`, `session.command({ name })`, `session.interrupt({ resume })`, `session.fork({ before? })`, `session.inbox.update({ delivery })` and `session.message.get(...)`. Credential removal is global and takes only `credentialID`. Plugin activation state comes from catalog reads and `plugin.updated`.
|
||||
|
||||
Wait, instructions, import/export, stats and log use `/api/experimental/session/...` paths. Cancellation is `DELETE /api/session/:sessionID/form/:formID`. Preserve generated response envelopes in the proxy: `session.active` consumes `{ data }`, while `project.list` consumes an array. Native `cursor.next` remains the sole continuation authority.
|
||||
Wait, instructions, import/export, stats and log use `/api/experimental/session/...` paths. Cancellation is `DELETE /api/session/:sessionID/form/:formID`. Preserve HTTP response envelopes in the proxy: the native active-session wire response carries `{ data }`; the public Promise client's `session.active()` returns the decoded session-ID record. `project.list` returns an array. Native `cursor.next` remains the sole continuation authority.
|
||||
|
||||
## Routing
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
## Contract
|
||||
|
||||
- Server and UI pin `@opencode/client@2.1.0`. Manage the runtime CLI independently: startup checks authenticated loopback `/api/info`, then (only to identify an outdated daemon) `/api/status`, then `/api/health`, advancing only on HTTP 404. All probes share the endpoint, credentials, 64 KiB response bound and absolute deadline; authentication, transport and malformed response failures end discovery without fallback. The shared transport forwards canonical `server.info()` unchanged. Review documentation, installed declarations and proxy/API parity whenever the client contract changes.
|
||||
- Server and UI pin `@opencode/client@2.2.3`. Manage the runtime CLI independently: startup checks authenticated loopback `/api/info`, then (only to identify an outdated daemon) `/api/status`, then `/api/health`, advancing only on HTTP 404. All probes share the endpoint, credentials, 64 KiB response bound and absolute deadline; authentication, transport and malformed response failures end discovery without fallback. The shared transport forwards canonical `server.info()` unchanged. Review documentation, installed declarations and proxy/API parity whenever the client contract changes.
|
||||
- The package root is the generated zero-Effect Promise client. Use installed declarations; never follow `@opencode-ai/sdk` examples.
|
||||
- Native routes are `/api/*`; CodeNomad exposes them only through the authorized `/workspaces/:id/instance` proxy.
|
||||
- That proxy is an explicit method/path allowlist: expose each new upstream API only after review.
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ Then open a pull request on GitHub targeting the `dev` branch.
|
|||
|
||||
### OpenCode V2 Boundaries
|
||||
|
||||
- Server and UI pin `@opencode/client@2.1.0`; native plugins pin `@opencode/plugin@2.1.0` for authoring but resolve Effect and the public SDK from the daemon at runtime. Upgrade pins together with the lockfile and isolated native validation. The runtime CLI is managed independently; its demonstrated minimum and recommended/tested versions remain separate constants in `packages/server/src/opencode/runtime-support.ts` (minimum 2.2.3 for the `experimental_image_generate` image tool contract of upstream #60258, consumed by the Images gallery, Design agent and Images settings; recommended 2.2.5). A build pin alone is never a runtime requirement, and unfamiliar newer/custom labels retain authenticated contract recognition. Review current OpenCode documentation, installed declarations, and proxy/API parity whenever the client contract changes.
|
||||
- Server and UI pin `@opencode/client@2.2.3`; native plugins pin `@opencode/plugin@2.2.3` for authoring but resolve Effect and the public SDK from the daemon at runtime. Upgrade pins together with the lockfile and isolated native validation. The runtime CLI is managed independently; its demonstrated minimum and recommended/tested versions remain separate constants in `packages/server/src/opencode/runtime-support.ts` (minimum 2.2.3 for the `experimental_image_generate` image tool contract of upstream #60258, consumed by the Images gallery, Design agent and Images settings; recommended 2.2.5). A build pin alone is never a runtime requirement, and unfamiliar newer/custom labels retain authenticated contract recognition. Review current OpenCode documentation, installed declarations, and proxy/API parity whenever the client contract changes.
|
||||
- Upgrade references: [OpenCode V2 changelog](https://opencode.ai/changelog) (GitHub Releases list V1), the exact release tags (`vX.Y.Z`) or published npm packages for source/declaration deltas, [OpenCode V2 documentation](https://opencode.ai/v2/docs/), and `node_modules/@opencode/client/dist/promise/`.
|
||||
- `packages/server/src/workspaces/opencode-service.ts` uses the selected host or WSL CLI's official `service status`, `service start`, and `service get password` lifecycle to connect to one externally owned global daemon. CodeNomad owns no private port, database, registration, or daemon PID and never stops the daemon on backend shutdown.
|
||||
- WSL requires Windows localhost forwarding and runs the Linux CLI lifecycle inside the distribution; never inspect or signal Linux PIDs from Windows.
|
||||
|
|
|
|||
|
|
@ -103,3 +103,11 @@ synthetic provider.
|
|||
cancellations. Same-URL network aborts and missing, unknown or reused identities
|
||||
remain failures. Hold a controller-owned read across a native SSE open to
|
||||
exercise supersession with the navigation ownership assertions intact.
|
||||
- Fixtures consuming EventSource serve a persistent `text/event-stream` response
|
||||
and let the real connection close with its page. Never answer that path with
|
||||
the generic JSON fallback. Composer bootstrap awaits DOM readiness and its
|
||||
explicit rendered-fixture signal; stream headers are observed before navigation.
|
||||
- A fixture entry with top-level asynchronous restoration exposes its ready signal
|
||||
after instance registration. Tests await that signal before connecting its native
|
||||
client: the document load event can precede module continuation, and a later
|
||||
`addInstance` publishes the fixture's initial client value.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,8 @@ Three independent notions:
|
|||
|
||||
OpenCode sessions and messages live in the shared OpenCode daemon and are the same in every profile. Server configuration (`CLI_CONFIG`, default `~/.config/codenomad/config.yaml`) is also independent of the profile; a non-default config file gets its own scope.
|
||||
|
||||
**Keep OpenCode running** is shared by the native service, with its manifest and artifacts outside data profiles; changing or deleting a profile preserves this preference and its supervision ([runtime setup](OPENCODE_RUNTIME_SETUP.md#service-supervision-primitives-and-native-adapters)).
|
||||
|
||||
## Selecting a profile
|
||||
|
||||
Precedence, evaluated by both Electron (`electron/main/data-profile.ts`) and Tauri (`src-tauri/src/data_profile.rs`) before any profile path, singleton or WebView storage is fixed:
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ Start with [README](../README.md) for installation and usage, then
|
|||
|
||||
- [Missions](MISSIONS.md): one-time Mission maps, native admission, Wayfinder/Debugging/Product delivery playbooks and validation.
|
||||
- [Recurring Missions](MISSIONS_RECURRING.md): daily native schedule contract; the [qualification fixture](MISSIONS_RECURRING_QUALIFICATION.md) runs its journeys against an isolated runtime.
|
||||
- [Missions native contracts](MISSIONS_NATIVE_CONTRACTS.md): public/private boundaries, per-invocation lifetimes, bounded capabilities and experimental document authors.
|
||||
- [Native schedule transport](MISSIONS_NATIVE_SCHEDULE_TRANSPORT.md): exact read-only discovery, incarnation-fenced RPC and local carrier primitives.
|
||||
- [Autonomy requirement](MISSIONS_AUTONOMY.md): Missions run in the OpenCode service with the CodeNomad UI and backend closed.
|
||||
|
||||
## Desktop automation and validation
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -9,3 +9,24 @@
|
|||
- Admission, autorisations, environnement/profil, stockage et programmation
|
||||
s'exécutent côté OpenCode pendant que le service fonctionne.
|
||||
- Ne jamais satisfaire cette cible par un serveur CodeNomad persistant en arrière-plan.
|
||||
|
||||
## Responsabilités
|
||||
|
||||
- Les calendriers natifs OpenCode persistent les échéances et réveillent leur
|
||||
Location après redémarrage. Une programmation saine en marche récupère son
|
||||
calendrier automatiquement ; Pause, Stop, erreur et incertitude gardent leurs
|
||||
barrières d'admission.
|
||||
- Missions admet les passages, fige leurs entrées, empêche les chevauchements et
|
||||
clôture après corrélation terminale et repos de toute la famille. La récupération
|
||||
conserve les identités originales du passage et de son premier message.
|
||||
- **Keep OpenCode running** supervise la vie du service natif au niveau du système
|
||||
d'exploitation. Cette préférence partagée par le service reste indépendante du
|
||||
nombre de Missions et des profils CodeNomad ; le premier Play active un choix
|
||||
encore absent, et un OFF explicite persiste. OFF laisse OpenCode fonctionner.
|
||||
- Windows, macOS et WSL démarrent à la connexion utilisateur. Linux natif utilise
|
||||
linger avec les permissions et gardes de restauration du compte. Veille,
|
||||
hibernation, durée de vie WSL et contrôle de l'hôte restent des limites propres.
|
||||
|
||||
Contrats : [programmations et récupération](MISSIONS_RECURRING.md),
|
||||
[transport natif](MISSIONS_NATIVE_SCHEDULE_TRANSPORT.md) et
|
||||
[supervision du service](OPENCODE_RUNTIME_SETUP.md#service-supervision-primitives-and-native-adapters).
|
||||
|
|
|
|||
553
dev-docs/MISSIONS_NATIVE_CONTRACTS.md
Normal file
553
dev-docs/MISSIONS_NATIVE_CONTRACTS.md
Normal file
|
|
@ -0,0 +1,553 @@
|
|||
# Missions native contract boundaries
|
||||
|
||||
## Scheduled HTTP request retention
|
||||
|
||||
`server/routes/mission-scheduled.ts` transfers its original physical permit through
|
||||
`prepareMissionMapMutation.retain` to the fence-scoped scheduled-request hold.
|
||||
The registry retains at most 128 Missions, with the original action/request/revision,
|
||||
payload digest, workspace object, native project/canonical path, physical checkout,
|
||||
profile, directory and connection. Unknown and partial outcomes keep that permit.
|
||||
Only a positive exact original receipt with both result and control pending false
|
||||
releases it. Missing/error/expired-proof reads retain uncertainty.
|
||||
|
||||
GET and repeated original requests read the original request ID. A new human
|
||||
Check first requires its positive exact partial receipt, uses the retained
|
||||
permit's busy guard and original connection, signs the human Check's fresh request
|
||||
and revision, then rereads the original receipt before releasing that permit.
|
||||
The held action, request, revision and payload digest stay fixed. Explicit partial
|
||||
retry also requires a positive exact partial receipt and the original connection.
|
||||
A retired connection's permit can settle through a fresh connection after the
|
||||
same workspace, native project and physical-root proof. Reads can use that retained
|
||||
permit while deletion waits, and remain observation-only. Revalidate live human
|
||||
auth, request cancellation and ownership after asynchronous reads and responses.
|
||||
Never infer an exact DELETE acknowledgement from an unqualified tombstone.
|
||||
|
||||
New scheduled CREATE rejects a pure immediate policy before physical admission;
|
||||
standard one-time creation owns that policy. Configure retains same-object
|
||||
conversion to immediate with its native armless-owner contract.
|
||||
|
||||
Calendar controls require a calendar-owned document. A journal-bound preparation
|
||||
keeps its one-time binding and immutable creation receipt. Its immediate structural
|
||||
edits use `MissionControl.configurePreparation`: ordinary project writer, then
|
||||
non-waiting document exclusion. Contention refuses before effects, preserving the
|
||||
existing lock order while calendar business calls can hold document exclusion.
|
||||
The document saves the exact request, both revisions, configuration and deterministic
|
||||
replacement coordinator before native creation. An attempted missing replacement
|
||||
retains uncertainty; an existing exact empty replacement is adopted. Public session
|
||||
creation resolves omitted selections to native defaults. The original empty conversation
|
||||
is retained; `mission.prepared` publishes the new coordinator and preparation fields.
|
||||
Final document CAS rebinds that coordinator and completes the exact receipt. Native
|
||||
execution admits its exact pending request immediately after durable publication,
|
||||
retaining the invocation budget across native creation and readback. Attached external
|
||||
coordinators keep the journal structural lock and its existing text-edit contract.
|
||||
Native
|
||||
message/inbox/child/claim absence, placement, title, metadata and selection are pinned
|
||||
and reread inside the document transaction through `native-source-fence.ts`; activity
|
||||
and pending requests are checked in the current invocation. Play, text edits and
|
||||
deletion refuse an unresolved preparation. Completed replay stays observation-only.
|
||||
Before first Play, configuration can transfer execution ownership to the calendar
|
||||
with followed context retained for scheduled passages. Returning a journal-bound
|
||||
object to pure immediate retains followed conversations. First Play reads their
|
||||
stable complete prefix through `prepareNativeDocumentSources` in initial-context
|
||||
mode, with the shared source exclusions, native byte/placement fence and launch
|
||||
envelope. Document exclusion covers source I/O through the original first-Play CAS.
|
||||
The scoped native adapter lives through publication/readback. The saved first-Play
|
||||
text is immutable; transport carries those exact bytes and the backend verifies its
|
||||
digest against the authoritative snapshot before native admission. Unknown source
|
||||
coverage blocks with an explanation before intent. Replay uses the frozen text,
|
||||
original operation/message identity and existing saved native ACK recovery.
|
||||
Configuration transfers ownership
|
||||
while retaining `oneTime` and its immutable receipt. `dormantJournal` pins the exact
|
||||
prepared journal revision. A return to immediate uses the same preparation owner and
|
||||
restores ordinary journal controls. Calendar admission uses separate passage roots;
|
||||
the dormant creation coordinator remains unprompted. Partial preparation fences both
|
||||
owners, settings activation, text edits and deletion. Panel rows and point reads hide
|
||||
the dormant map while full authority snapshots retain its membership evidence.
|
||||
Calendar deletion holds document exclusion then the project writer, retires only the
|
||||
exact empty prepared journal, rereads its original tombstone or permanent deletion
|
||||
record after journal retention, and records that receipt
|
||||
before document compaction. Both conversations are retained. Lost journal/CAS replies
|
||||
keep the original deletion for replay; compaction requires the confirmed retirement.
|
||||
Native qualification: `scripts/test-native-mission-preparation.mjs`; the product
|
||||
schedule driver accepts `--convert-preparation` for conversion before native first Play.
|
||||
Never clear a journal binding from a calendar-only configuration request.
|
||||
|
||||
Public plugin/client contracts and private Core services have separate authority
|
||||
and lifetime. Acquire private services inside the current RPC/Location Effect
|
||||
graph; `native-invocation.ts` joins child reads and finalizers before releasing
|
||||
that graph. A local `ctx.rpc` call keeps its originating graph. Every consumed
|
||||
private shape and result requires a bounded check in the installed bundle.
|
||||
Missing required coverage keeps the operation blocked with its original identity.
|
||||
|
||||
### Invocation graph availability
|
||||
|
||||
| Entry | Session | Database | Location | Schedule |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| Plugin setup | absent | absent | absent | absent |
|
||||
| Prompt hook | present | present | present | present |
|
||||
| Context hook | absent | present | present | absent |
|
||||
| Tool execution | absent | present | present | absent |
|
||||
| Fresh receiving HTTP RPC | present | present | present | present |
|
||||
|
||||
`nativeInvocation.capture` preserves and joins the originating graph. Acquire the
|
||||
scheduled passage runtime in a fresh receiving HTTP RPC when called from context
|
||||
or tool execution. The public Session/KV prefilter selects the exact pending
|
||||
family; the internal bridge accepts only fixed business-control methods and ENV
|
||||
preparation, with each mutation entering `executeBusiness` under document exclusion.
|
||||
Human lifecycle controls use their separate authenticated control path.
|
||||
|
||||
Internal RPC authentication carries the service's full privileges. Its trusted
|
||||
adapter owns the exact service registration, endpoint, incarnation and native
|
||||
pending-family checks. Never accept endpoint/path selectors from model input,
|
||||
expose lifecycle through the business bridge, fabricate Core tags, or acquire an
|
||||
unrelated root graph to complete a partial invocation.
|
||||
|
||||
One-time first Play persists its original validated native acknowledgement in the
|
||||
same document CAS as admission time and launch count. The proof is internal;
|
||||
public document summaries expose only its IDs/timestamps. A missing journal
|
||||
receipt is reconstructed from that exact saved acknowledgement without invoking
|
||||
transport, including when native history has been pruned. Shared running guards
|
||||
combine the journal state and document desired state, so a paused/stopped
|
||||
document blocks new work while a journal control publication catches up.
|
||||
Journal-only maps retain their existing immediate controls and business execution
|
||||
path. Their reads and mutations preserve the journal representation. New scheduling
|
||||
uses explicitly configured unified documents; native conversation controls remain
|
||||
available independently.
|
||||
|
||||
Paths below are relative to `packages/server/src/opencode/missions/`.
|
||||
|
||||
## Public surfaces
|
||||
|
||||
`scheduleList` uses the public plugin storage scan over the exact project document
|
||||
prefix, with at most eight entries and one page per request. Cursor IDs are bounded,
|
||||
keys strictly increase, and the returned continuation must match the last scanned
|
||||
key. Tombstones retain their identity checks and advance pagination. Full documents
|
||||
are parsed under the per-document byte ceiling before projection. The display route
|
||||
rechecks its acquired owner after the RPC and validates the returned native project.
|
||||
Exact request receipts remain point reads through `scheduleRead`.
|
||||
|
||||
| Surface | Consumed shape | Boundary |
|
||||
| --- | --- | --- |
|
||||
| `@opencode/client/service` | `discover({file})` returns Endpoint/undefined; `headers(endpoint)` builds authentication; public `Info` describes registration fields | Exact absolute registration, bounded non-secret id/pid/url/version projection, current PID/incarnation before dispatch and inside receiving RPC. Keep credentials in operation memory. For wake transport, forbid ensure/start/stop/terminate, CLI children, standalone and alternate namespaces. |
|
||||
| Client HTTP/RPC | `OpenCode.make(...).rpc(definition).method(input,{location,signal})` | Acquired authenticated connection, exact native directory and cancellation; preserve uncertainty after dispatch. |
|
||||
| Schedule HTTP contract | `experimental.schedule.create/list/get/pause/resume/delete`, `{data:Info}`, session-filtered list, delete 204 | `opencode/native-schedule-capability.ts` recognizes the consumed request/response shapes read-only. Read `experimental.schedules` independently from the highest-priority whole native block. Never create probe schedules. |
|
||||
| Plugin Session facade | get/create/prompt/synthetic/interrupt/context | Prefer facade methods when they expose the required operation. Preserve exact session/message IDs and fixed profiles at admission. Use bounded point/page reads for source coverage. |
|
||||
| Session/AISDK hooks | prompt/context/title/compaction/language | Context admission requires the exact durable user message and schedule correlation. Title/compaction/language carry only local technical work. Treat metadata as correlation within service privileges. |
|
||||
| Native context message classification | Exact `session_message` point reads of IDs referenced by the current model context; synthetic `metadata.notice:"restart"` | Classification and deterministic wake proofs share 128 reads/1 MiB per invocation; SQL caps each body at 256 KiB and the remaining budget before transfer. Schedule-bearing or deterministic wake IDs retain strict validation; notice prose supplies no authority. |
|
||||
|
||||
The HTTP create payload accepts optional `when` and condition keys `if`/`until`.
|
||||
Missions supplies its validated timing and fixed prompt; source conditions stay in
|
||||
the business reader. The generated Promise client unwraps the HTTP `{data:Info}`
|
||||
envelope for create/get/pause/resume and `{data:Info[]}` for list. Callers consume
|
||||
those decoded values directly. Capability recognition validates the complete
|
||||
`when` shape independently of its required/optional marker; the read-only
|
||||
`scripts/test-native-schedule-openapi.mjs` check exercises versioned native OpenAPI
|
||||
documents without imposing a runtime version list.
|
||||
|
||||
## Private service inventory
|
||||
|
||||
| Context key / state | Consumed contract | Callers and required checks |
|
||||
| --- | --- | --- |
|
||||
| `@opencode/Session` | get/create → Session.Info; prompt/synthetic → SessionInbox.Info; environment → exact string map; inbox → array; cancelInbox → void | `native-service-adapter.ts`: method/Effect checks, public result codecs, pinned placement/parent/profiles/metadata, synchronous fence adjacent to dispatch, ≤1024 inbox entries. Environment is process-local and reapplied before admission. |
|
||||
| Session message readers | messages({sessionID,limit,order,cursor:{id,direction}}) → array; message({sessionID,messageID}) → Info/undefined | Same adapter: ≤32 messages/source, ≤256 KiB native message, 16,384-character launch envelope; point-read anchor and each message, stable complete prefix, source placement/bytes rechecked after I/O. Deleted/moved/changed anchors block. |
|
||||
| Session active | `active` is an Effect returning a ReadonlySet | `native-passage-observation.ts`: actual Set, ≤4096 sessions. Treat activity as a process-local observation. Require correlated durable terminal evidence and complete family quiescence for restart settlement. Keep an old execution claim uncertain during asynchronous native recovery. |
|
||||
| `@opencode/Location` | directory/workspaceID/project{id,directory,canonical} | `native-service-adapter.ts`, `native-service-clock.ts`, recurrence readers: structural Location.Info fields, exact native/physical placement and current graph checks after I/O and before effects. |
|
||||
| `@opencode/storage/Database` | db.$client.unsafe(sql,params).withoutTransform → Effect rows; db.transaction(callback,{behavior:"immediate"}) | `native-recurrence-storage.ts`, `native-passage-observation.ts`: current graph, actual SQL client/transaction, safe row types and synchronous query guard. Preserve the driver's transaction Context when composing effects. Asynchronous-only reads cannot implement the synchronous admission fence. |
|
||||
| KV/CAS SQL | kv(key,value,time_created,time_updated); plugin namespace is plugin ID encoded as padded UTF-16 hex | `native-recurrence-storage.ts`: exact project key/bytes/revision, immediate transaction, conditional update and readback. Publish invalidation after commit; an invalidation failure preserves the committed result. |
|
||||
| Family SQL | session_v2 placement/parent/time_suspended, session_inbox, session_message id/type/seq/content/idle | `native-passage-observation.ts`: family ≤4096, exact message/inbox presence and nonnegative counts. Anchor the original input in durable seq order and require a later terminal idle as the latest projection. Count failures only between that input and terminal, then recheck family, claims and requests. Missing anchor/terminal or any remaining claim keeps uncertainty. |
|
||||
| Durable background delivery | kv keys in `job.background/`; JSON recovery kind and session/parent/child IDs | Same reader: ≤1024 records, IDs/kind only. A completed child with an undelivered native result still blocks its family. Unknown shape, table/column or coverage refuses settlement. |
|
||||
| `@opencode/Form` | list/get/state/reply Effects; public Form.Info/State result codecs | `native-passage-observation.ts`, `native-human-answer.ts`: ≤1024 requests, exact native IDs, confirmed durable UI-answer mark and invocation lifetime. Sessionless requests conservatively prevent quiescence. |
|
||||
| `@opencode/Permission`, `@opencode/Shell` | list Effects returning public Request/Info arrays | `native-passage-observation.ts`: ≤1024 each; unknown request ownership or running uncorrelated Shell blocks settlement. |
|
||||
| `@opencode/Job` | get/start/cancel; running/completed/error/cancelled | `native-service-clock.ts`: exact Job identity/metadata, process-local lifetime, current Location leases and cancellation. Qualify any use as an acceleration separately from durable schedule authority. |
|
||||
| `@opencode/example/LocationServiceMap` and Effect RcMap | contextEffect/contextEffectOption, rcMap state tag/map | Same clock: scoped leases, current loaded entry/placement checks. Required cross-Location family coverage stays blocking when an entry or contract is unavailable. |
|
||||
| `@opencode/Bus` | listen → unsubscribe Effect; publish(definition,data,{location}) | `native-service-clock.ts`, `native-recurrence-events.ts`: acquire/release listener lifetime, event as wake hint, post-commit invalidation. |
|
||||
| `@opencode/Global`, `@opencode/server/ServerInfo` | state/config roots; urls() → string array | `native-managed-owner.ts`: bounded ownership reads, exact executable/database/process namespace, ambiguous identity refusal. `native-schedule-installation.ts` acquires the own-process registration during arming: ≤256 state entries, ≤64 service records of ≤32 KiB, ≤16 advertised URLs, exact public discovery with a four-second deadline. Its guard rechecks native graph values, own PID/incarnation and immutable artifact bytes within the current scoped RPC. Discovery transport uses the public service facade. |
|
||||
|
||||
Assistant content follows the native storage projection: ordinal `session_part`
|
||||
rows take precedence when present, otherwise the message's inline content applies.
|
||||
`native-message-data.ts` supplies that SQL projection to tool quiescence, human
|
||||
answer verification/discovery and source digest fencing. Source proofs reread the
|
||||
rehydrated bytes inside their existing transaction graph. Storage qualification
|
||||
checks the consumed part columns before passage admission.
|
||||
|
||||
## Schedule integration qualification
|
||||
|
||||
Before consuming private `@opencode/Schedule`, qualify each actual method in its
|
||||
current RPC graph: create/list({sessionID})/get/pause/resume/remove/enabled. Core
|
||||
create takes tagged `when` and `action:{type:"prompt",prompt}`; HTTP create takes
|
||||
the public `when` input and `prompt`. Core list/get/pause/remove require explicit
|
||||
flag/session/mapping checks at the caller. Bound adoption to the exact carrier.
|
||||
|
||||
Semantic qualification covers deterministic `msg_<scheduleID>_<run>` and metadata
|
||||
propagation, native missed-slot/phase rules, durable turn ordering, local cached
|
||||
language/title/compaction, and continuation concurrent with schedule recovery.
|
||||
Keep a pending passage uncertain while its terminal correlation or family
|
||||
coverage is incomplete. Tests of method presence and typechecks accompany these
|
||||
native checks; qualification results belong in the delivery evidence.
|
||||
|
||||
### Durable calendar owner
|
||||
|
||||
`native-schedule-owner.ts` composes `NativeScheduleCalendarOwner` with the current
|
||||
invocation's `native-schedule-calendar-service.ts` facade and
|
||||
`nativeScheduleOwnerBindings`. The parent supplies authenticated human controls,
|
||||
ready schedule activation, immutable installation artifacts and the configured
|
||||
profile/YAML source. Each control holds `NativeMissionDocumentStore.withExclusion`
|
||||
across native I/O and uses its borrowed store for short individual CASes. The
|
||||
borrowed view and private facade expire with their exclusion and RPC Scope;
|
||||
`LocationLifecycle.isClosed()` also fences each operation.
|
||||
|
||||
Creation persists the exact slot intent, reverse ownership index and attempted
|
||||
barrier before its native effects. Adoption lists at most 16 rows for that carrier
|
||||
and requires one exact candidate; duplicate candidates are paused. The auxiliary
|
||||
is positively active before principals can arm. Pause retains native identities;
|
||||
temporal replacement retires exact old mappings before completing new coverage.
|
||||
A current auxiliary mapping is published immediately after its adoption, before
|
||||
principal effects. Complete synchronization preserves that mapping's exact
|
||||
high-water and pending/preparing references; newly added slots start at zero and
|
||||
retired IDs stay fenced.
|
||||
A paused temporal edit creates/adopts and pauses its replacement rows, completes
|
||||
its synchronization, and retains those identities for explicit Resume.
|
||||
|
||||
`readNativeScheduleAuxiliaryWakeBinding` performs read-only current-intent
|
||||
preflight inside a fresh receiving RPC: verified reverse ownership, current
|
||||
auxiliary descriptor, one exact native candidate and native run acknowledgement.
|
||||
The parent also verifies the exact durable user message. When an auxiliary create
|
||||
ACK was lost, the context-side claim helper may use its optional trusted
|
||||
`readAuxiliaryIntent` resolver to obtain that preflight binding from the read-only
|
||||
own-process RPC. Claim admission rereads the same complete document and current
|
||||
intent under exclusion. Ordinary principal guards remain applied. Only the exact
|
||||
current auxiliary can cross desired-state, synchronization and blocked/deletion
|
||||
barriers for technical reconciliation.
|
||||
|
||||
In the claimed wake RPC, `owner.adoptAuxiliary(wake)` records the existing
|
||||
auxiliary descriptor and mapping without creation, principal effects or business
|
||||
reservation. The parent then consumes its technical wake and closes its claim
|
||||
under document exclusion **before** calling `owner.reconcile`. Read-only preflight
|
||||
acquires no document exclusion and mutates no saved state; private services stay
|
||||
inside each receiving RPC Scope.
|
||||
|
||||
### Production technical runtime
|
||||
|
||||
`registerNativeMissionScheduleRuntime(ctx, artifacts?, invocation?)` in
|
||||
`native-mission-schedule-runtime.ts` contributes the local provider, broad exact
|
||||
technical ownership hooks, the wake/status and maintenance RPCs, native terminal
|
||||
hints and the scheduled business bridge.
|
||||
It returns `NativeDocumentBusiness`; the bridge registration is shared once with
|
||||
the persistent plugin's routing. `native-mission-schedule-owner.ts` supplies the
|
||||
shared scoped owner/native/store factory for controls and autonomous receivers.
|
||||
|
||||
`native-mission-schedule-wake-prepare.ts` persists the exact principal
|
||||
`wakePreparing` in the native prompt hook, under document exclusion and before
|
||||
returning to Session prompt admission. Prompt entry supplies the pre-inbox proof;
|
||||
Pause, temporal edits, wrong/currently unadopted mappings and unresolved control
|
||||
barriers refuse that admission. An exact preparation survives a pre-inbox crash
|
||||
and fences structural editing without consuming high-water or launch count.
|
||||
`native-mission-schedule-wake-context.ts` reads bounded durable user messages in
|
||||
the hook's Database/Location graph and retains recovery preparation for delivered
|
||||
originals. It sends read-only native HTTP
|
||||
preflight before claiming. `ready` includes complete fan-in coverage; a signed
|
||||
eight-second receipt freezes its cutoff. One outer deadline covers status and
|
||||
wake. An already dispatched exact identity or an unconsumed uncertain claim stays
|
||||
status-only. Native compaction hints are classified by exact referenced IDs with
|
||||
bounded SQL point reads; an absent candidate checks the next deterministic wake
|
||||
directly, including a checkpoint without a model-facing ID.
|
||||
|
||||
Each wake/status receiver has its own `nativeInvocation` and full HTTP graph.
|
||||
It verifies saved registration/own process/incarnation, physical placement,
|
||||
current ownership, native schedule ACK and the exact durable original user. A
|
||||
missing auxiliary mapping is adopted from its qualified existing row. Auxiliary
|
||||
CAS and claim closure precede owner reconciliation and pending-business
|
||||
observation; another owner reconciliation applies quota/end cleanup immediately
|
||||
after that observation, including when the last pending settles. Principal
|
||||
reservation closes its technical claim with its durable high-water while business
|
||||
remains pending. Fresh auxiliary turns recover only original next principal wake
|
||||
identities, respecting every prior dispatch hold.
|
||||
|
||||
Title/compaction remain local through paused, retiring and attempted ownership;
|
||||
owned corrupt sessions and reserved unknown technical IDs fail closed. Every owned
|
||||
provider HTTP request is refused. Public terminal-event subscription learns only
|
||||
pure installation/carrier data from live ownership hooks (128 records maximum).
|
||||
One bounded hint per Mission, after the native inactive-transition debounce,
|
||||
contacts the fixed `codenomad.missions.schedule-maintenance/reconcile` method.
|
||||
The fresh receiver rechecks exact current/retiring ownership and positive native
|
||||
active/claim/inbox/child quiescence before owner reconciliation. Setup event/KV
|
||||
failures preserve ordinary Missions; unavailable setup KV delegates the exact
|
||||
learned hint's lookup to the receiver. Scope disposal stops the subscription.
|
||||
Missed hints retain cleanup for explicit Check. Native auxiliary wakes provide
|
||||
restart reconciliation of saved work; installed native event/facade qualification
|
||||
accompanies shipment.
|
||||
|
||||
Each new descriptor carries `anchorAt`. The facade supplies an Effect Clock only
|
||||
to native `Schedule.create`; wall-clock milliseconds/nanoseconds use that logical
|
||||
anchor, while monotonic time and sleep delegate to the real Clock. Native
|
||||
`Info.time.created`, `nextDueAt` and absolute ends provide the readback. First Play
|
||||
anchors relative timings and the common end. An unchanged `in` keeps its original
|
||||
duration and absolute `oneShotAt` through bounds-only edits. An unchanged Every
|
||||
bounds edit reads native phase before retirement, chooses its next strictly future
|
||||
slot, and derives the replacement anchor as that slot minus the interval. A timing
|
||||
change may anchor Every at the validated edit. Native recorded due times remain
|
||||
authoritative for future slots and late-wake eligibility.
|
||||
|
||||
`nativeScheduleOwnerControlReceipt` reads the exact retained human operation,
|
||||
including Check aliases, across automatic reconciliation. Receipts retain the
|
||||
latest 16 completed controls. The internal `resumePending(locked, document)` port
|
||||
qualifies the exact already-admitted pending passage and its fresh guard before
|
||||
Resume, including its last-budget continuation. Original passage identities and
|
||||
admitted counts remain intact. Unknown admission retains the pending barrier.
|
||||
|
||||
Terminal cleanup keeps the auxiliary and mapping proofs while pending business,
|
||||
technical claims or incomplete fan-in remain. Session removal requires positive
|
||||
claim/inbox/child quiescence. A self-active auxiliary retains its native wake and
|
||||
reverse index; the parent schedules a fresh invocation after idle or an explicit
|
||||
Check to finish that same cleanup intent.
|
||||
|
||||
## Event source readers
|
||||
|
||||
`native-mission-condition-reader.ts`, `native-scheduled-passage-sources.ts` and
|
||||
`missions/mission-local-sources.ts` read one explicit condition in the service
|
||||
namespace. First Play prepares a fresh baseline under document exclusion; Resume
|
||||
retains it. Editing adds baselines only for new source keys. Retained keys win
|
||||
over newly observed proofs during configuration, preserving remove/re-add cursors.
|
||||
The pending stores the complete bounded checkpoint. Successful archive consumes
|
||||
that checkpoint, while failures preserve the prior one and retain the admission
|
||||
barrier. A conversation condition also contributes its frozen prefix to launch input.
|
||||
|
||||
Schedule success uses durable `successfulCount` and `lastSuccessfulPassageID`,
|
||||
independent of the 30 retained results. Every unseen success needs its retained
|
||||
receipt, exact completed journal and correlated whole-family terminal proof.
|
||||
Exceeding that receipt window blocks. One-time completion uses the exact original
|
||||
admission, completed journal and the same family proof. A positively active family
|
||||
waits with its prior checkpoint; unavailable original/terminal correlation blocks
|
||||
explicitly as unknown coverage. Journal-only sources derive placement from their
|
||||
coordinator's journal identity and exact native session, validate the configured
|
||||
authorized root and pin that placement across checks. Dependency chains are
|
||||
point-read with a 64-object ceiling and reject cycles, deletion and unknown edges.
|
||||
|
||||
Conversation reads use a latest stable baseline page of at most 32 messages,
|
||||
then the shared bounded stable-prefix reader. Exact native message digests and
|
||||
placement remain pinned. Technical wakes/restart notices contribute no event;
|
||||
technical carriers and the schedule's own coordinator families are excluded.
|
||||
The bounded, cycle-checked parent walk pins each ancestor's placement and metadata
|
||||
before awaiting its native read, then rechecks that same projection at publication;
|
||||
carrier recognition uses `codenomad.schedule-carrier` and the reserved ID prefix.
|
||||
Streaming, compaction, changed/deleted anchors and input capacity preserve unknown
|
||||
coverage. A full prefix containing only technical records requires a decision.
|
||||
`native-source-observation.ts` borrows only an explicitly selected authorized
|
||||
source Location for family requests, retaining its graph through the receiving RPC.
|
||||
|
||||
Local Git reads use a full local branch, physical worktree/common-directory identity,
|
||||
commit OIDs and ancestry. Asynchronous child commands have a shared ten-second deadline and
|
||||
1 MiB output ceiling, with lazy fetch, optional locks and prompts disabled. Files
|
||||
use explicit relative paths, absence or SHA-256 bytes, at most 128 paths,
|
||||
1 MiB/file and 16 MiB/check. Parent links, devices, directories, root escapes,
|
||||
unreadable files and changed reads fail closed. Promise-based filesystem calls run
|
||||
off the service event loop. One ten-second deadline covers initial reads and final
|
||||
revalidation; cancellation or expiry promptly rejects the caller, including while
|
||||
metadata, open, read or close is pending. Late kernel completions are observed and
|
||||
acquired handles close in `finally`; a blocked kernel operation releases its handle
|
||||
when it returns. Git cancellation kills its child. Byte and physical-placement
|
||||
rereads run under document exclusion after the target read and immediately before
|
||||
publication. A failed revalidation invalidates the proof. External filesystem
|
||||
writers can race the final reread and SQLite commit.
|
||||
|
||||
`native-source-fence.ts` pins bounded exact document/journal/message/family SQL
|
||||
projections before observation, then checks them synchronously at target CAS.
|
||||
Family proofs include the existence and full native bytes of each correlation
|
||||
input and latest terminal, with exact assignment IDs for managed actor roots.
|
||||
The document adapter supplies its current transaction Context to those reads;
|
||||
the guard borrows that transaction's SQL service. Local publication preparation
|
||||
awaits the byte/branch/placement reread outside SQL; its synchronous guard checks
|
||||
the proof's lifetime, cancellation, deadline and completed revalidation state.
|
||||
The source guard expires with its RPC.
|
||||
|
||||
Qualification: `node --import tsx scripts/test-mission-event-readers-native.mjs
|
||||
<absolute-isolated-cli> <fresh-approved-reader-scratch>` exercises all five actual
|
||||
readers, private SQL/CAS, service restart without a CodeNomad backend, frozen
|
||||
consumption, remove/re-add and a changed-file reservation race. Its explicit test
|
||||
controls seed/archive passage journals; product wake and actor-settlement journeys
|
||||
remain covered by the separate scheduled-product qualification.
|
||||
|
||||
## Experimental document authors
|
||||
|
||||
`native-experimental-settings.ts` shares the depth/schedules JSONC reader and
|
||||
editor. Preserve explicit false/zero and local companions; materialize inherited
|
||||
true schedules when writing a depth, and preserve depth from the target's own
|
||||
lower-priority block only when creating an experimental block for schedules.
|
||||
An existing block keeps its depth key or native default unchanged. Copy only
|
||||
those two known keys.
|
||||
`PluginControls` owns connection/Location/deletion fences, byte expectations and
|
||||
the physical file lock. Duplicate keys, invalid parents and simultaneous JSON/
|
||||
JSONC in one layer refuse settings edits.
|
||||
|
||||
Native regression: `node --import tsx
|
||||
scripts/test-mission-experimental-settings-native.mjs <absolute-isolated-cli>`.
|
||||
It exercises real discovery/whole-block precedence/watcher acknowledgement and
|
||||
both edit orders, using private roots and a provider request counter.
|
||||
|
||||
The product preparation endpoint is
|
||||
`/api/workspaces/:id/missions/:missionID/schedule-settings`: GET reads its document;
|
||||
POST accepts only `requestID` and `expectedDocumentRevision`. It holds the owned
|
||||
worktree admission, refreshes authority before every typed `documentActivation`
|
||||
phase and composes the two-file executor with native document CAS. Readiness keeps
|
||||
the Mission paused. Immediate objects refuse this path. A failed intent with no
|
||||
attempted write can be replaced only by a new explicit request at the fresh
|
||||
document revision; an attempted effect retains its original request fence.
|
||||
|
||||
## Integration conventions
|
||||
|
||||
`scheduleControl` accepts fixed human actions with an exact signed action/revision/
|
||||
payload tuple. Creation records its immutable request/digest; `scheduleRead` can
|
||||
select a retained original request receipt. Run now stores `manualExpectedRevision`
|
||||
with its pending and archive, and a changed revision cannot reuse that request.
|
||||
The response combines calendar and actor receipts. Document deletion remains
|
||||
pending through compaction; the tombstone exposes a bounded `deletionReceipt`
|
||||
containing only ID, original request/revision and the cleanup option.
|
||||
|
||||
The scheduled HTTP route retains its physical deletion permit after dispatch until
|
||||
a positive exact complete receipt. Holds are fence-scoped, bounded to 128 Missions
|
||||
and persist until that receipt. GET/reconnect stay read-only. An explicit Check of a positively
|
||||
recorded partial request runs under the original permit, then rereads that original
|
||||
receipt. A replaced connection can settle a positive receipt only after the same
|
||||
workspace, project, profile and physical placement are re-proven.
|
||||
|
||||
The authenticated parent completes this preparation before the calendar owner's
|
||||
Play/Resume and composes scheduled controls, immutable carrier mappings, the
|
||||
auxiliary barrier and transactional business admission/settlement. Product
|
||||
registration composes the owner with the ordinary-entry guards. Native regression
|
||||
of the product preparation route:
|
||||
`scripts/test-mission-schedule-activation-native.mjs`.
|
||||
|
||||
### Scheduled passage invocation
|
||||
|
||||
`native-scheduled-passage-runtime.ts` acquires the document store, native service,
|
||||
whole-family observation and calendar coverage inside each receiving invocation.
|
||||
The business port first reads public ancestry, then reacquires for each `execute`
|
||||
and `prepare`. Hold document exclusion before MissionControl's writer; SQL CAS
|
||||
guards check only synchronous invocation/incarnation/exclusion identity. Native
|
||||
effect-entry authority reads the exact unified document outside SQL publication.
|
||||
|
||||
Before creating a technical claim, require the read-only runtime `preflight`:
|
||||
exact native message and run acknowledgement first, then full principal coverage
|
||||
at the frozen cutoff for a running principal without pending work. It preserves
|
||||
an unfinished window's cutoff and publishes no document state. Positive contiguous
|
||||
auxiliary/overlap/paused wake proof stays on the non-reserving path. Exact prepared
|
||||
wakes participate only when covered by their contiguous native messages. The
|
||||
transport owns prior-claim admission and the short-lived process-local receipt;
|
||||
pass its validated cutoff unchanged to `wake`, under the shared request deadline.
|
||||
|
||||
Fan-in keeps its cutoff and overlap suppression in the document. Every principal
|
||||
needs an exact mapping, enabled flag, contiguous messages and a native firing
|
||||
barrier qualified for admission or suppressed observation below; coverage is
|
||||
bounded to 256 occurrences and rechecks the complete group. Source
|
||||
conditions require a qualified reader; watched inputs retain complete frozen
|
||||
prefixes and native anchor digests. Original admission increments the global count
|
||||
only after exact native ACK/message/inbox proof. Recorded admission stays
|
||||
observe-only through pruning, with whole-family H11 checks before settlement.
|
||||
The original's `pending.admissionAttempted` barrier is published under document
|
||||
CAS immediately before the first native admission call; a missing field reads
|
||||
false. A saved attempt with absent/pruned original proof keeps the same pending
|
||||
unknown. Positive exact message/inbox evidence reconstructs its admission count.
|
||||
A root created before the attempt barrier can still receive its first original
|
||||
input after positive absence. A lost attempt-write acknowledgement preserves
|
||||
uncertainty, and the bit stays monotone for the pending's complete lifetime.
|
||||
Paused manual dispatch requires the exact manual pending's `manualAllowed` bit;
|
||||
the authenticated reservation sets it and a subsequent Pause/Stop clears it.
|
||||
Run now reservation also stores optional `manualExpectedRevision` in its CAS and
|
||||
retains it in the passage receipt. It is counter-validated, exclusive to manual
|
||||
origins and immutable throughout that pending identity. Pending/archive replay
|
||||
requires the original request ID and exact saved revision; a missing saved
|
||||
revision preserves unknown identity. The 30-result retention bound applies to
|
||||
these receipts.
|
||||
The committed manual reservation invokes its original control's `admitControl`
|
||||
under document exclusion before the first reconciliation. This exact pending or
|
||||
archive proves the request/revision without manufacturing a calendar receipt;
|
||||
ownership checks and the original 95-second deadline stay active past admission's
|
||||
30-second freshness window.
|
||||
|
||||
### Scheduled actor lifecycle
|
||||
|
||||
`native-mission-schedule-actors.ts` acquires its lifecycle port inside the current
|
||||
RPC scope. `owner.prepareControl` publishes the exact guarded request before
|
||||
execution-side mutations; `prepareInterruption` restricts this stage to Pause/Stop/Delete.
|
||||
The authenticated proof's 30-second freshness window guards admission through
|
||||
that CAS. `admitControl` validates the committed request/action/revision/payload
|
||||
receipt, then its execution retains Location, physical-root and ownership checks
|
||||
under the RPC's shared 95-second deadline. The passage's MissionControl attempts interruption
|
||||
from that exact current intent, then calendar reconciliation proceeds independently.
|
||||
Native journal/document reads share the actor lifecycle's 85-second cancellation
|
||||
budget, including each checkpoint's narrower signal, with finalizers joined before
|
||||
interruption can advance to the root. Adopted calendars require the owned fixed
|
||||
prompt and absent native `if`/`until` conditions on every Info projection.
|
||||
Start/Resume require completed calendar controls. MissionControl supplies its
|
||||
existing request/operation, frozen targets and receipts under document exclusion
|
||||
followed by the project writer. Actor callbacks
|
||||
point-read that document and passage journal without reacquiring document exclusion.
|
||||
They preserve the human calendar request ID and frozen root profiles/metadata.
|
||||
The interruption barrier accepts the exact current explicit pending operation;
|
||||
completed receipts remain eligible while the desired state and latest intent agree.
|
||||
The captured explicit control identity is rechecked before effects alongside
|
||||
the desired-state and pending-passage guards.
|
||||
|
||||
The public Session facade owns root reads, interrupts and lifecycle synthetic
|
||||
admission. Family listing, activity, inbox cancellation and environment use an
|
||||
authenticated own-process client discovered from the saved absolute registration,
|
||||
with invocation/incarnation, placement and operation fences. Secrets stay in that
|
||||
invocation's memory. `missions/native-actor-lifecycle.ts` shares the bounded
|
||||
deepest-first interruption, post-root checks, Stop drain and Pause parking with
|
||||
the desktop backend. Resume uses its deterministic lifecycle message identity and
|
||||
actual queued acknowledgement; the original passage input and launch count stay
|
||||
unchanged. Partial family observations remain explicit in the existing receipt.
|
||||
Each Session call combines its caller's cancellation signal with the scoped
|
||||
lifecycle signal. The shared 50-second family walk and 10-second post-root recheck
|
||||
cancel and join a blocked descendant's Effect/finalizers within their own budgets;
|
||||
the root attempt continues under the lifecycle budget and its receipt uses the
|
||||
admitted execution guard.
|
||||
Dedicated `controlPassage` calls hold the same exclusion and supply the current
|
||||
invocation's actor lifecycle transport. They select the isolated journal's Mission
|
||||
identity and forward only the explicit request ID, action and journal revision.
|
||||
The runtime entry's document identity and execution callbacks stay at that boundary.
|
||||
Journal control retries recover the
|
||||
original journal revision and exact receipts through MissionControl. Observation
|
||||
authority permits control publication after the desired Pause/Stop is durable;
|
||||
initial dispatch and business effects retain their separate admission predicates.
|
||||
|
||||
Suppressed fan-in observation accepts positively paused principals as a frozen
|
||||
future-firing barrier. Their exact mappings, native flag, acknowledged contiguous
|
||||
messages, preparing references and stable whole-group reread remain required.
|
||||
Admission windows retain their active/future-or-done barrier. Terminal deletion
|
||||
consumes positively acknowledged current preparations and closes the corresponding
|
||||
technical claim before native removal; missing or changing proof retains the intent.
|
||||
|
||||
An armless first Play publishes its desired state and original reservation together.
|
||||
Public control completion follows that original's persisted admission or proved
|
||||
not-started archive. Origin/request identity distinguishes first admission from an
|
||||
actor Resume, including equal timestamps. The bounded control history pins this
|
||||
first receipt while the original remains unconfirmed.
|
||||
|
||||
`native-scheduled-passage-capability.ts` qualifies each new native mutation after
|
||||
lazy effect construction and pinned-root reads. Calendar-backed policies require the exact current
|
||||
provider/model contribution, immutable artifact bytes, the ready auxiliary's
|
||||
adopted mapping and active native row, its technical carrier identity, and a
|
||||
positive native schedule flag. The synchronous initial/business fence rechecks
|
||||
the fresh document's installation and artifact bytes. Observation authority uses
|
||||
the original passage identity and whole-family terminal proof, allowing controls
|
||||
and settlement while technical provider artifacts are unavailable. Human Check
|
||||
keeps an unadmitted original pending when mutation qualification fails.
|
||||
|
||||
A pure immediate policy uses its frozen `requiresMissionCalendar` result and an
|
||||
armless owner: empty current/owned/retired calendar slots and auxiliary readiness
|
||||
false. Its Session/Database/Location, profile, root, original admission and H11
|
||||
fences remain active; technical provider artifacts and Schedule services belong
|
||||
to calendar-backed policies. An unplayed scheduled object can keep its identity
|
||||
and explicit profiles when configured immediate, then first Play atomically
|
||||
records the armless owner and reserves origin `immediate`. The existing runtime
|
||||
`reconcile` admits that exact original pending once, under the one-launch limit.
|
||||
|
||||
The immediate configuration path performs only its guarded document CAS and
|
||||
physical-placement capture. `acquireNativeMissionServiceRegistration` supplies the
|
||||
own-process RPC registration proof independently of technical artifacts; calendar
|
||||
arming composes that proof with exact provider/runtime byte validation. The business
|
||||
bridge derives the calendar requirement from the pending's frozen launch policy
|
||||
and includes it in the captured source identity. Pure immediate selection and ENV
|
||||
use their native session/profile guards even when the technical provider file is
|
||||
unavailable.
|
||||
263
dev-docs/MISSIONS_NATIVE_SCHEDULE_TRANSPORT.md
Normal file
263
dev-docs/MISSIONS_NATIVE_SCHEDULE_TRANSPORT.md
Normal file
|
|
@ -0,0 +1,263 @@
|
|||
# Native schedule transport
|
||||
|
||||
`native-mission-schedule-runtime.ts` registers the product carrier hooks, local
|
||||
provider, receiving wake/status RPC, after-idle maintenance and fixed business
|
||||
bridge. Its durable calendar owner and passage engine share the project document
|
||||
exclusion. The daily document runtime keeps its own admission and control path.
|
||||
|
||||
`native-schedule-transport*` and `native-schedule-carrier*` supply the shared
|
||||
transport/local-language primitives. `registerNativeScheduleCarrierFoundation`
|
||||
provides a separate no-business-effect qualification composition.
|
||||
|
||||
The launch policy, document CAS and passage engine describe business scheduling
|
||||
independently of native transport. Only the adapter owns the fixed carrier prompt,
|
||||
private calendar facade and own-process HTTP hop. UI configuration projects the
|
||||
Missions policy rather than native scripts. The observation port separately
|
||||
requires original-input/terminal correlation and whole-family quiescence; a native
|
||||
idle or inactive process-local session alone leaves that business proof incomplete.
|
||||
|
||||
## Transport contract
|
||||
|
||||
The caller supplies the owned absolute registration path resolved at arming in
|
||||
the service's host namespace, the exact Location directory and wake identity.
|
||||
Discovery uses only `Service.discover({ file })`; authentication is constructed
|
||||
in memory with `Service.headers`. The public `Info` projection keeps
|
||||
`id/pid/url/version`, requires a nonempty incarnation and the current process PID,
|
||||
and reads at most 32 KiB. Versions participate in equality checks; availability
|
||||
follows the consumed contracts. Discovery and the registration projection run
|
||||
again before dispatch. The RPC handler uses `nativeScheduleIncarnationGuard`
|
||||
after I/O and synchronously before every CAS/native effect, alongside its fresh
|
||||
mapping/Location/generation/state guards. The receiver resolves its registration
|
||||
path from its owned durable mapping and requires the supplied path to match it.
|
||||
|
||||
The adapter calls only the internal `codenomad.missions.schedule.wake` mutation
|
||||
and `status` read through the official client. Register these methods in the
|
||||
current native RPC invocation graph, using the existing `nativeInvocation`
|
||||
bridge where Promise business code needs scoped Core services. `status` reads the
|
||||
exact saved outcome and can return `messagePresent` from a bounded point-read of
|
||||
that deterministic native user message and its acknowledged schedule run. This
|
||||
read hint preserves an `unknown` outcome and every prior dispatch hold. Returned
|
||||
replies correlate the full wake key; business outcomes and archive/cursor updates
|
||||
remain the receiver's CAS concern.
|
||||
|
||||
Every mutation requires a caller-owned durable `claim`: under the shared Mission
|
||||
mutation exclusion, persist the exact dispatch intent/incarnation and return true
|
||||
only for the first dispatch. Any unresolved dispatch in that Mission, and every
|
||||
prior dispatch of the exact identity, returns false, including after a reload.
|
||||
Release that exclusion before awaiting HTTP RPC. A claim
|
||||
that outlives cancellation must retain its intent. Persisted intents are reconciled
|
||||
through exact status reads; `unknown`, expiry or another occurrence must never
|
||||
authorize a mutation resend. A pre-dispatch refusal describes only the current
|
||||
attempt; preserve earlier uncertainty for that identity.
|
||||
|
||||
One process-wide lane per Mission spans bundle incarnations. The whole operation
|
||||
has an eight-second deadline and an AbortSignal passed to RPC. Read-only discovery
|
||||
has native probe timeouts and can finish after that deadline: its lane stays held
|
||||
until it finishes, and its cancelled continuation refuses dispatch. RPC responses
|
||||
are bounded to 1 MiB, both declared and streamed; redirects fail. Post-dispatch
|
||||
failure remains uncertain. Error results contain fixed codes, keeping credentials,
|
||||
native exception text and endpoint authentication out of logs and model messages.
|
||||
|
||||
Never add lifecycle calls, CLI children, default registration discovery, alternate
|
||||
namespace lookup or standalone fallback to the wake transport. A running service
|
||||
can be contacted independently of the supervision preference. Effective
|
||||
`experimental.schedules`, service ownership and autonomous admission remain
|
||||
distinct receiver guards.
|
||||
|
||||
## Business result bounds
|
||||
|
||||
The business receiver fits mutations with the ordinary tools' shared
|
||||
`missionMutationToolResult` before serializing its reply. Only its `toolResult`
|
||||
crosses the wire: revision/frontier, compact affected tasks and a native declaration's
|
||||
canonical assignment, with the same omission reads and revision task pages. Reports
|
||||
and briefings keep their compact revision result. Exact original request IDs retain
|
||||
the journal's committed replay semantics after a lost reply.
|
||||
|
||||
All map, task, list and content inspection uses `inspectTool` in the owning invocation.
|
||||
Its observed conversations and decision candidates are fitted with the rest of the
|
||||
task view. Direct `inspect`, `listMissions`, `observeTask` and `decisionCandidates`
|
||||
bridge calls are refused; internal observation stays inside the receiver. Each fitted
|
||||
result's two-space JSON holds at most 100,000 UTF-16 units, hence at most 300,000 UTF-8
|
||||
bytes (three bytes per unit; escaped controls and lone surrogates are already counted
|
||||
in the JSON text), plus fewer than 128 bytes of fixed RPC envelopes, below 1 MiB.
|
||||
Catalog ownership checks carry a null acknowledgement after validation. Replies use
|
||||
the byte-checked JSON codec so journal-valid artifact nesting fits inside its display
|
||||
and RPC envelopes; requests retain the authority codec's structural checks.
|
||||
|
||||
`native-scheduled-business-results.test.ts` compares scheduled and ordinary registered
|
||||
tools with 60 large tasks, three-byte BMP text, prompt/content reads and paging.
|
||||
`native-scheduled-business-rpc.test.ts` also drives the authenticated bridge with real
|
||||
MissionControl writes and a lost committed reply, verifying an exact replay leaves
|
||||
the journal revision and write count unchanged.
|
||||
|
||||
## Local carrier primitives
|
||||
|
||||
`nativeScheduleCarrierHooks` supplies public language/context/title/compaction and
|
||||
provider-HTTP hooks. Register them with the native plugin and an exact owned
|
||||
carrier lookup. Bundle `createNativeScheduleCarrierProvider` at the acquired
|
||||
`aisdk:file` path, create the carrier with its explicit technical title and the
|
||||
fixed local model, and reject missing provider/bundle/selection at admission.
|
||||
The language object is shared and stateless; title and compaction return local
|
||||
results even when auxiliary agent defaults point to remote models. Context
|
||||
validation clears technical tools; provider HTTP for an owned carrier throws.
|
||||
|
||||
`nativeScheduleCarrierWakes` extracts correlation hints from at most 128 context
|
||||
messages / 1 MiB, retaining at most 32 contiguous new runs after durable high-water.
|
||||
Exact schedule IDs, deterministic message IDs, run, dueAt and late fields are
|
||||
required. Gaps and incomplete metadata fail closed. RPC must point-read durable
|
||||
messages and revalidate authority before reservation. Summaries remain prose;
|
||||
same-carrier native compaction and exact reads own maintenance without changing
|
||||
the schedule identity or phase.
|
||||
|
||||
## Checks and integration gates
|
||||
|
||||
Run:
|
||||
|
||||
```sh
|
||||
node --import tsx --test packages/server/src/opencode/missions/native-schedule-transport.test.ts packages/server/src/opencode/missions/native-schedule-carrier.test.ts packages/server/src/opencode/missions/native-schedule-carrier-binding.test.ts
|
||||
```
|
||||
|
||||
The transport tests exercise official discovery/auth/RPC against a local HTTP
|
||||
fixture, same-URL incarnation replacement, an eight-second discovery overrun,
|
||||
response bounds, cancellation, shared lanes and durable-claim replay refusal.
|
||||
Carrier tests exercise cached local language, local auxiliary hooks, exact wake
|
||||
keys, replay high-water and incomplete-context refusal.
|
||||
|
||||
Native shipment qualification requires registered handlers with actual durable
|
||||
CAS, cold restart/backend absence, private Core graph/shape/lifetime checks,
|
||||
auxiliary reconciliation schedules, provider/network spies with forced native
|
||||
compaction, ordinary-entry carrier protection and installed bundle retention.
|
||||
The no-provider-HTTP guarantee belongs to that installed native composition;
|
||||
transport discovery and internal RPC themselves use loopback HTTP.
|
||||
|
||||
## Native document composition and packaging
|
||||
|
||||
The production `native-mission-schedule-runtime.ts` composes the provider and exact
|
||||
local hooks with its fresh wake/status receivers. Context-side dispatch first
|
||||
reads signed native readiness and freezes a fan-in cutoff; its outer eight-second
|
||||
deadline spans both preflight and wake. Incomplete coverage and prior uncertain
|
||||
dispatches retain their original durable preparations and stay read-only. The
|
||||
receiver consumes and closes auxiliary technical claims before owner and pending
|
||||
business reconciliation, then reapplies owner cleanup after settlement. Its
|
||||
business bridge is registered once by `registerNativeScheduledBusinessRpc`.
|
||||
|
||||
The principal preparation is written by the prompt hook before native inbox
|
||||
admission, serialized with Pause and temporal replacement. Context reads the
|
||||
persisted original and native ACK before transport. A separate fixed maintenance
|
||||
RPC receives bounded public terminal hints from learned pure carrier/installation
|
||||
data. It rechecks native quiescence in a new HTTP graph before cleaning a carrier
|
||||
that was active during its own wake. It never uses status reads as mutations or
|
||||
resends a prior dispatch claim. The subscription, its maximum 128 pending hints
|
||||
and temporary debounce fibers belong to the plugin Scope.
|
||||
|
||||
The registered runtime resolves the exact session marker into its point-read
|
||||
unified Mission document and current calendar, checks its project/Location/local
|
||||
model, and rejects stale generations. It keeps one native KV dispatch claim per
|
||||
Mission (32 KiB), serialized with document mutations across bundle incarnations.
|
||||
An unresolved claim holds newer wakes; a current high-water proof permits replacing
|
||||
the single slot. `closeNativeScheduleCarrierClaim` persists a monotone
|
||||
`settled: { revision, highWater }` receipt on the exact claim under the same
|
||||
project document-mutation exclusion. Its proof is the matching session/schedule/
|
||||
time-generation mapping in the current or retiring calendars, with high-water
|
||||
covering that run. Principal reservation consumes its technical occurrence in the
|
||||
same CAS: high-water advances to its run while `pendingWake` retains the exact
|
||||
business reference. This permits closing the principal claim while business stays
|
||||
pending and admits the auxiliary claim. Later ignored contiguous occurrences keep
|
||||
that pending reference and may advance high-water; settlement preserves its maximum.
|
||||
The runtime closes after the committed technical CAS; a calendar replacement owner
|
||||
closes before discarding retiring mappings. A closure receipt survives temporal
|
||||
replacement and lets the next generation use the single slot. Missing/pruned proof,
|
||||
an unreserved uncertain run, a lost dispatch acknowledgement or an elapsed deadline
|
||||
keep an unclosed claim held. Read-only status remains mutation-free.
|
||||
During incomplete arm/replacement, the current auxiliary has its own durable
|
||||
partial mapping. A lost auxiliary-create ACK uses read-only exact native-row
|
||||
preflight against its verified current intent; the optional trusted claim
|
||||
resolver carries that binding across the own-process RPC boundary. The claimed
|
||||
receiver adopts the existing auxiliary, consumes its technical occurrence and
|
||||
closes its claim before owner reconciliation. Complete synchronization preserves
|
||||
the partial mapping's progress. Principal admission keeps its control barriers.
|
||||
Native document publication uses
|
||||
`acquireNativeMissionDocumentStore` and its real SQL revision CAS.
|
||||
|
||||
`nativeScheduleCarrierLookupKey` point-reads the reverse ownership record under
|
||||
`documents-v1/carriers/<projectToken>/<sessionID>`, including current, retiring and
|
||||
pending-cleanup descriptors. The owner writes it before session creation and
|
||||
removes it after confirmed native session removal. Ordinary-route protection uses
|
||||
ownership even when marker/model verification fails; technical execution requires
|
||||
verified current ownership. New technical roots use
|
||||
`ses_cn_schedule_<40 lowercase hex>` and keep their indices during uncertain cleanup.
|
||||
|
||||
The receiver rereads exact native session/message/schedule rows around publication
|
||||
in its current RPC graph. Its transaction guard checks incarnation and installed
|
||||
artifact bytes alongside the document adapter's revision CAS. Cross-row native
|
||||
mutation fencing for business admission belongs to CORE's admission boundary.
|
||||
Never acquire another root SQL lease inside the document transaction's synchronous
|
||||
guard. Keep required Core work in the current invocation and join it before graph
|
||||
release.
|
||||
|
||||
Native compaction checkpoints and restart notices project as user-role model
|
||||
messages. The invocation reader point-reads only referenced non-wake IDs from
|
||||
the current model context to classify completed compactions and synthetics
|
||||
carrying native `metadata.notice:"restart"`. Classification and deterministic
|
||||
wake proofs share 128 reads and 1 MiB per invocation; SQL restricts each message
|
||||
to 256 KiB and the remaining byte budget before transferring its body.
|
||||
The model-context selector ignores their exact IDs only when schedule metadata is
|
||||
absent and the ID is outside the current deterministic wake namespace. Prose and
|
||||
description matching supply no classification. New scheduled users keep
|
||||
their exact metadata and are point-read again by the RPC receiver. Automatic
|
||||
compaction can also serialize the current wake into the checkpoint: when context
|
||||
has no candidate, the hook first asks read-only `status` for the next deterministic
|
||||
native message, including when the checkpoint has no model-facing ID. Only a positive exact-message hint permits attempting its original
|
||||
claim; an existing hold still refuses dispatch. The mutation rereads the message.
|
||||
|
||||
`buildNativeScheduleFoundation` in
|
||||
`packages/server/scripts/build-native-schedule-foundation.mjs` writes a standalone
|
||||
local provider and reusable runtime bundle into a caller-owned absolute directory.
|
||||
It returns SHA-256 manifests and enforces external native Effect/plugin modules.
|
||||
Every owned hook checks provider/runtime existence and exact bytes, including when
|
||||
the language is cached. Qualification and a hook-free observer/controlled-clock
|
||||
package are explicit opt-in artifacts.
|
||||
|
||||
The owned provider's `languageModel` throws when the language hook is absent.
|
||||
Create carriers with the fixed local model and an explicit technical title so
|
||||
native initial-title generation stays out of the wake path. Primary model
|
||||
resolution constructs that provider before automatic compaction selection; a
|
||||
missing runtime therefore fails at local language construction, and a missing
|
||||
provider fails at package loading. Qualify this consumed native ordering with
|
||||
remote auxiliary defaults and an over-ceiling history when updating integration
|
||||
contracts. Never replace it with a globally rewritten auxiliary agent selection.
|
||||
|
||||
`assertOrdinaryMissionScheduleInput` is the ordinary-input boundary primitive. Its
|
||||
caller supplies authoritative carrier mapping membership and the resolved session
|
||||
metadata; reserved schedule/carrier metadata are also refused on ordinary sends.
|
||||
The native prompt hook separately rejects malformed carrier inputs. Apply the
|
||||
ordinary guard to prompt/command/shell/compact/fork/import/move/update/delete at
|
||||
the authorized product routes before exposing programmed Missions.
|
||||
|
||||
Run the isolated composition with an absolute qualified CLI and fresh scratch
|
||||
directory:
|
||||
|
||||
```sh
|
||||
node scripts/test-native-schedule-foundation.mjs <absolute-cli> <fresh-scratch>
|
||||
```
|
||||
|
||||
`--cold-missing=runtime` and `--cold-missing=provider` retain an over-ceiling
|
||||
history and the original session/schedule, remove the artifact while stopped,
|
||||
and observe the natural native wake after cold startup with no project client.
|
||||
`--controlled-maintenance` uses a real every:90-minutes row and automatic
|
||||
compaction, holds the local compaction hook to compare row snapshots, then
|
||||
restarts and processes the next original slot. Only the tick's native Effect
|
||||
Clock is controlled; OS time is unchanged. That mode measures maintenance and
|
||||
phase/end preservation, while ordinary cold autonomy uses the natural-wake mode.
|
||||
|
||||
It installs the reusable registration/provider against the native unified document
|
||||
adapter in a private service. The fixture owns its paths, credentials, network spy,
|
||||
calendar and daemon processes.
|
||||
|
||||
Product qualification uses `scripts/test-native-mission-schedule-product.mjs` with
|
||||
an absolute isolated CLI and fresh approved scratch child. It drives the real
|
||||
authenticated creation/settings/control routes and retained product bundle, then
|
||||
closes the backend and restarts only its owned managed service. Native SQL reads
|
||||
observe the cold no-client window. `--controlled-settlement` supplies an Effect
|
||||
Clock only to the qualification tick; the natural wake/restart window uses wall
|
||||
time. Additional fault-injection and platform gates belong in delivery evidence.
|
||||
|
|
@ -1,9 +1,250 @@
|
|||
# Recurring Missions — simple native contract
|
||||
# Recurring Missions
|
||||
|
||||
The Wayfinder UI-answer mark below applies to both one-time and recurring Missions.
|
||||
Acceptance status: `MISSIONS_RECURRING_QUALIFICATION.md`.
|
||||
|
||||
## Product boundary
|
||||
## Unified native schedules
|
||||
|
||||
Unified documents use `launch-policy.ts` for immediate, absolute/relative one-shot,
|
||||
elapsed Every, cron and up to eight cron calendars. A source condition uses its
|
||||
qualified reader. Native schedule rows own future dates and civil-time behavior;
|
||||
Missions owns admission, non-overlap, frozen inputs, launch counts and settlement.
|
||||
|
||||
### Visible timing, limits and editing
|
||||
|
||||
Scheduling labels use focusable **i** tips for secondary explanations, available
|
||||
on hover, keyboard focus and touch. Frequency and condition controls retain their
|
||||
timing and admission semantics. The list shows condition and end details only
|
||||
when configured, and source checks only with a condition. The info tip beside
|
||||
the **Technical details** disclosure explains technical activity and costs.
|
||||
|
||||
The editor exposes nine frequencies: immediate once, once at a civil date, once
|
||||
after an elapsed delay, elapsed interval, daily, selected weekdays, several daily
|
||||
times, day of each month and advanced cron. First Play freezes relative origins
|
||||
and source baselines. Delays use positive whole minutes/hours; intervals are at
|
||||
least 15 minutes and first wake one interval after Play. Cron runs at most once
|
||||
per minute. Up to eight daily calendars share admission and non-overlap; duplicate
|
||||
times coalesce. Civil rules use the saved IANA zone and native DST behavior;
|
||||
months without the selected day are skipped. An unchanged interval retains its
|
||||
native phase; the preview uses its persisted next slot when available.
|
||||
|
||||
Catch-up uses the native row's recorded `dueAt`, including when later slots were
|
||||
missed. Multiple principal wakes fan in to at most one catch-up passage before
|
||||
future slots resume. Explicitly paused recurring calendars resume at a future slot;
|
||||
a paused one-shot may arrive once, late. End bounds apply to the recorded due slot.
|
||||
|
||||
Three combinable bounds are an absolute last slot, an elapsed end from first Play
|
||||
and a global maximum of admitted passages. The earliest time bound applies and
|
||||
continues through Pause; an eligible recorded slot may arrive after that end.
|
||||
The quota counts each passage once on admission of its coordinator's first input,
|
||||
including Run now and a passage that later fails. Checks, technical wakes and
|
||||
refused admissions consume zero; uncertain admission keeps its reservation until
|
||||
reconciliation. One-shot policies allow at most one launch. Counts and first-Play
|
||||
origins survive edits, restarts and the thirty-result history window.
|
||||
|
||||
The five source conditions and their consumption rules are below. H9 editing
|
||||
follows the [shared editor table](MISSIONS.md#user-journey-regression-checks): full
|
||||
eligible preparation before admission, future configuration between recurring
|
||||
passages, objective/notes during admitted work and read-only uncertain state.
|
||||
Template changes require that no task has ever been created. Reopen preserves the
|
||||
loaded draft; explicit conflict reload retains a comparison, and receipt reads
|
||||
keep the original request/revision. Configuration and temporal generations remain
|
||||
separate; unchanged timing and end groups preserve phase and absolute limits.
|
||||
|
||||
### Restart recovery
|
||||
|
||||
OpenCode reloads persisted calendars after service restart. A healthy schedule
|
||||
desired running wakes its owning Location with CodeNomad UI/backend closed and
|
||||
without prior project access or human Resume. At rest it returns to calendar work;
|
||||
with pending work it reconciles the original passage/session/message identities.
|
||||
The durable auxiliary reconciliation calendar remains until that passage settles.
|
||||
|
||||
Explicit Pause, terminal Stop, proven failure and a proven end without a report
|
||||
retain their admission barriers and human recovery controls. Incomplete native
|
||||
correlation or uncertain effects keep the original pending record and block a new
|
||||
passage. Native continuation is observed under its original identity. The technical
|
||||
wake joins the exact existing service; OS supervision owns service availability
|
||||
through its independent shared preference ([runtime setup](OPENCODE_RUNTIME_SETUP.md#service-supervision-primitives-and-native-adapters)).
|
||||
|
||||
The authenticated product seam is `POST /api/workspaces/:id/missions/scheduled`
|
||||
for paused creation, and `GET|POST /api/workspaces/:id/missions/:missionID/schedule`
|
||||
for status and explicit controls. Requests pin the native directory, human principal,
|
||||
profile source, request ID and document revision through the existing signed bridge.
|
||||
`scheduleRead({missionID,requestID})` reads an exact retained control receipt;
|
||||
missing or evicted proof stays unknown. Passive reads send no controls.
|
||||
|
||||
Serialized calendar validation returns an identity/payload-bound refusal for an
|
||||
expired first-Play date or a revision conflict before its first write. The initial
|
||||
dispatch's exact refusal releases its backend and UI holds with a conflict response;
|
||||
lost replies and applied partial controls retain uncertainty. An explicit Check or
|
||||
original-request retry of a positively read partial control may use a retired
|
||||
connection's replacement after revalidating the same owner, physical checkout,
|
||||
Location, profile and exact partial receipt. It acquires fresh physical admission
|
||||
before retiring the old permit and preserves the original request ID and revision.
|
||||
|
||||
`GET /api/workspaces/:id/missions/scheduled/requests/:requestID` reads an exact
|
||||
creation receipt when the HTTP response containing the document ID was lost.
|
||||
It uses the retained physical permit, or derives the document ID from the acquired
|
||||
native project owner after permit settlement/restart. A negative or incomplete
|
||||
receipt retains uncertainty; the read performs exact observation only.
|
||||
|
||||
`GET /api/workspaces/:id/missions/scheduled` brokers the read-only `scheduleList`
|
||||
RPC. It reads at most eight key-ordered unified documents per page, selected by an
|
||||
optional `after` Mission ID and `limit`. Tombstones consume page capacity and the
|
||||
cursor advances across them. Each page validates project identity and document
|
||||
bounds; malformed entries fail the page. Immediate unified documents carry
|
||||
`kind: one-time`, calendar-owned documents `kind: schedule`, allowing the panel
|
||||
to merge journal rows by Mission ID and retain their owning control path.
|
||||
List and detail projections include the full launch policy/configuration, frozen
|
||||
one-shot/end instants, admitted/settled/successful counts, structural/template locks,
|
||||
pending passage identity and the thirty retained passage receipts.
|
||||
|
||||
The panel publishes its loaded inventory and cursor independently of the selected
|
||||
document's point read. A partial selected receipt keeps uncertainty visible beside
|
||||
the valid inventory. Scoped retained controls keep their display and original
|
||||
request/revision for explicit read-only reconciliation. Only successfully read
|
||||
documents enable new controls or representation changes; retained display metadata
|
||||
and partial error-body receipts keep the original reconciliation available. Selection,
|
||||
scope, connection and disposal fence late selected reads, including ABA changes.
|
||||
Exact control settlement schedules the still-current parent's fresh read and deletion
|
||||
callback before releasing a selected hold's display owner. Late confirmations release
|
||||
only their matching hold; chained receipt reads and activation continuations retain
|
||||
their original view and connection fences.
|
||||
|
||||
List and detail also expose `observation`, `availableActions` and `blockReason`.
|
||||
Their `technical` projection exposes only the recorded current/retiring carrier
|
||||
session and calendar identifiers, role, slot, temporal generation and pending
|
||||
cleanup. It derives these from the same document read, including attempted
|
||||
creation and deletion still awaiting reconciliation. Installation paths, provider
|
||||
descriptors and profile environment stay private. The collapsed **Technical
|
||||
details** disclosure keeps these conversations read-only, with the principal-wake
|
||||
counter scoped to its current temporal generation. Source checks and business
|
||||
launches retain their separate counters.
|
||||
The native display adapter reads at most eight exact principal rows, validates
|
||||
their adopted mapping and checks the document revision again after I/O. Missing,
|
||||
foreign, partial or retired reads project `unknown`. `nextDueAt` is the earliest
|
||||
active native principal's saved date; a condition labels it as a check. Paused
|
||||
rows contribute their counter but have no active next wake. `nativeStatus`
|
||||
separately exposes active/paused/done/mixed principals beside the document's
|
||||
desired state. `wakeCount` sums
|
||||
native principal prompt admissions in the current `timeGeneration`; auxiliary
|
||||
wakes, source-verification counts and lifetime business launch/success totals
|
||||
have separate meanings. Available actions are durable-state candidates; the
|
||||
authenticated mutation rechecks capabilities, source/actor proofs and CAS.
|
||||
Prepared wakes lock structural editing through the same H9 projection used by
|
||||
the document mutator.
|
||||
|
||||
During an admitted passage, a text-only configure validates H9 and persists its
|
||||
original request, document/journal revisions, passage identity and payload in the
|
||||
calendar operation before publishing `mission.updated` in its isolated journal.
|
||||
An exact retained event confirms the future objective/notes under document exclusion,
|
||||
including after passage archive. Reconciliation only reads that event; explicit replay
|
||||
uses the saved original revision and payload. Missing proof retains the operation and
|
||||
the editor's original uncertain request. Frozen passage inputs, task contracts, profiles and
|
||||
session/message identities remain intact. Never send a prompt from this edit path.
|
||||
Create and Edit share empty-calendar validation; saved unknown interval phases and
|
||||
intentionally exhausted launch quotas retain their explicit display. Preview end
|
||||
bounds apply to both the persisted next slot and subsequent slots, including late slots.
|
||||
|
||||
`checks: {count,since} | null` counts completed, revalidated source observations
|
||||
committed with a principal fan-in's consumed high-water or passage reservation,
|
||||
and explicit Run now reservations. A group of up to eight principal wakes counts
|
||||
once. True and false conditions both count; baseline reads at Play, auxiliary
|
||||
wakes, replays, overlaps, suppressed wakes, failed/incomplete source reads and
|
||||
unconditional policies contribute zero. Admission or settlement retries preserve
|
||||
the count. Configuration changes and temporal generations retain it. New documents
|
||||
track from creation; documents without tracking begin at their first committed
|
||||
check. The UI displays `since` alongside the count and treats `null` as unavailable
|
||||
history, preserving the unknown earlier total.
|
||||
|
||||
`node scripts/test-native-mission-schedule-list.mjs <absolute-isolated-cli>`
|
||||
qualifies the production read handlers and public native `storage.scan` against
|
||||
fixture-owned documents: bounded pagination across a tombstone, cursor semantics,
|
||||
list/detail parity and unchanged revisions. One document has real adopted native
|
||||
calendars; its exact next date/counter is read through the invocation-scoped
|
||||
adapter, with zero model requests.
|
||||
|
||||
`GET /api/workspaces/:id/missions/scheduled/capabilities` reads the acquired
|
||||
connection's consumed API contract, effective schedules flag and origin. Its
|
||||
read-only activation plan exposes the target scopes/paths or a refusal category.
|
||||
The native document preparation endpoint owns authenticated configuration writes.
|
||||
Both display routes revalidate workspace, connection, physical checkout and request
|
||||
lifetime after asynchronous work. Missing methods or capabilities return an
|
||||
explicit unavailable response. UI reads use the shared secondary HTTP budget.
|
||||
|
||||
Five explicit source conditions share the native source preparation boundary:
|
||||
another schedule's successful passage, a completed one-time Mission, commits on
|
||||
a local branch, stable new messages in a followed conversation, and an explicit
|
||||
file set. Event-only policies check every 15 minutes; calendar-backed conditions
|
||||
are read only at those calendar slots. First Play establishes their baseline,
|
||||
Pause/Resume preserves it, and configuration retains removed source checkpoints.
|
||||
A pending freezes the observed proof; only successful settlement consumes it.
|
||||
Success counters outlive the 30-result window, and missing retained success proof,
|
||||
cyclic dependencies, moved/deleted sources or incomplete reads block admission.
|
||||
Missing native correlation is an explicit unknown block; positively active source
|
||||
families retain their prior checkpoint while waiting. Historical journal-only
|
||||
sources use their validated coordinator placement under an explicitly authorized
|
||||
root. Followed conversations exclude the Mission's own passage families through
|
||||
their bounded native parent chains.
|
||||
Local file and Git reads yield the service event loop and share a cancellable
|
||||
ten-second budget with their final pre-publication revalidation. Exact native
|
||||
input/terminal bytes remain fenced synchronously inside the document CAS.
|
||||
Bounds, transaction fences and qualification are in the native contracts' event
|
||||
source readers section.
|
||||
|
||||
The native calendar owner records create intents and reverse carrier ownership
|
||||
before native effects. It acknowledges the reconciliation calendar before arming
|
||||
principals. Temporal edits preserve retired proofs until cleanup, while relative
|
||||
origins, the first Play and admitted counts remain durable. A first Play on a
|
||||
calendar-backed policy requires ready settings activation. Creation remains paused.
|
||||
|
||||
Human Play also calls the shared service-supervision default-ON path on its exact
|
||||
acquired connection. An explicit OFF remains OFF; creating, configuring, pausing,
|
||||
stopping and deleting a Mission preserve that service-wide preference. Supervision
|
||||
preparation precedes the signed control proof and native schedule dispatch. Runtime
|
||||
namespace/storage failures remain explicit at the product boundary; independent
|
||||
OS protection and native calendar admission keep separate status contracts.
|
||||
|
||||
Each technical wake enters an exact-file, own-process HTTP RPC with an eight-second
|
||||
deadline. A read-only preflight freezes complete fan-in coverage before a dispatch
|
||||
claim. Technical high-water advances at durable reservation; the pending business
|
||||
reference remains separate, so auxiliary observation can reconcile the last passage.
|
||||
Native persisted schedules load their owning Location on wake after service startup.
|
||||
Pause/Stop, failures, unqualified observations and uncertain effects retain their
|
||||
admission barriers. Never resend an attempted original whose proof is missing.
|
||||
|
||||
Context hooks and tool execution use the fixed-method internal business bridge;
|
||||
its receiving RPC acquires fresh Session/Database/Location services. Each business
|
||||
mutation takes the document exclusion before the existing journal writer. Native
|
||||
permissions, original message identities and whole-family H11 settlement apply.
|
||||
Technical carriers use the reserved `ses_cn_schedule_` namespace, local provider,
|
||||
local title/compaction hooks and authoritative ordinary-input guards.
|
||||
Ordinary sidebar browsing/search and both Mission conversation-source pickers
|
||||
exclude this namespace through the shared carrier-identity predicate; native
|
||||
observations remain available to ownership and reconciliation readers.
|
||||
Technical turns keep native model-selection events/statistics while making zero
|
||||
provider HTTP requests. Their compaction preserves the carrier/calendar identity
|
||||
and bounds processed context. Persisted native messages and events follow native
|
||||
retention and may grow until explicit terminal cleanup; the thirty Mission result
|
||||
receipts have their own independent bound.
|
||||
Technical wake conversations use a zero-token local model, so OpenCode's background precompaction never applies to them; their automatic local compaction does.
|
||||
|
||||
Implementation contracts and isolated qualification commands:
|
||||
[native contracts](MISSIONS_NATIVE_CONTRACTS.md) and
|
||||
[schedule transport](MISSIONS_NATIVE_SCHEDULE_TRANSPORT.md).
|
||||
|
||||
## Daily document product boundary
|
||||
|
||||
Daily documents use `NativeMissionRecurrenceStore` and the daily controls below.
|
||||
Journal-only missions keep their existing execution path. Storage representation
|
||||
selects the owning adapter; reads and reconnects preserve it.
|
||||
|
||||
The document/revision, controls, process-local Job/clock, snapshot and daily
|
||||
acceptance sections below describe existing daily documents. Unified native
|
||||
documents use the persisted-calendar and restart contract above. Whole-family
|
||||
settlement, original identity/correlation and completed-only source consumption
|
||||
apply to both paths; `lastDaily` and daily watched-cursor fields belong to the
|
||||
daily format. The Wayfinder UI-answer mark is shared by both paths.
|
||||
|
||||
Creation stores a paused schedule, never starts work or arms a timer, and is never
|
||||
resent; Play requests carry the then-current revision so CAS refuses a duplicate.
|
||||
|
|
@ -11,7 +252,7 @@ New titles follow the 1–60 Mission title rule; stored schedules keep their
|
|||
120-character bound.
|
||||
Explicit user controls and native execution are separate responsibilities.
|
||||
|
||||
## Schedule document and revision
|
||||
## Daily schedule document and revision
|
||||
|
||||
Store one schedule document in project-scoped plugin kv, including:
|
||||
|
||||
|
|
@ -29,8 +270,9 @@ Ceilings (`missions/recurrence-contract.ts`): 64 schedules per project, a 64 KiB
|
|||
configuration, a 256 KiB document and 64 remembered conversations
|
||||
(watched plus cursor-retaining); beyond that ceiling the document is refused, so never
|
||||
reset deduplication silently. Never add signing keys, authority epochs, grants,
|
||||
standing/child authority, per-effect receipts, budgets or ledgers, per-tool effect
|
||||
accounting or exact event-log replay to schedules, settlement or snapshots; human
|
||||
standing/child authority, per-effect receipts, per-tool budgets or ledgers, per-tool effect
|
||||
accounting or exact event-log replay to schedules, settlement or snapshots; the
|
||||
unified document owns the user-visible admitted-passage limit and count. Human
|
||||
answers use the UI-answer marks below.
|
||||
|
||||
Native kv offers no compare-and-swap, so the plugin serializes document updates and
|
||||
|
|
@ -39,7 +281,7 @@ follow a read with an unguarded write. Controls, due admission and settlement us
|
|||
the same guarded document mutation path; concurrent attempts cannot replace each
|
||||
other's pending identity or lose an archive.
|
||||
|
||||
## User controls
|
||||
## Daily document controls
|
||||
|
||||
Create paused, Play, Pause, Stop, Resume, Run now and Check passage are admitted only through:
|
||||
|
||||
|
|
@ -176,23 +418,26 @@ permissions and OS isolation are the security boundary, so never describe marks
|
|||
tamper-proof. Ownership, authentication, permission and data-loss protections apply
|
||||
to every passage, and unknown effects stay unknown.
|
||||
|
||||
## Native guarantees and missing primitives
|
||||
## Daily runtime guarantees and shared settlement primitives
|
||||
|
||||
The implementation relies on existing native contracts, qualified at their callers:
|
||||
|
||||
- Session creation with a caller-supplied session ID is idempotent.
|
||||
- Prompt admission with a caller-supplied message ID is first-admission-wins.
|
||||
- Plugin tools receive native session, message and call identity.
|
||||
- Jobs are process-local and disappear when the service restarts.
|
||||
- Daily Jobs are process-local and disappear when the service restarts.
|
||||
- A turn holds a durable execution claim (`session_v2.time_suspended`) until its
|
||||
terminal event; a hard kill leaves the claim without one. Never rely on native
|
||||
continuation after a restart.
|
||||
terminal event; a hard kill preserves that claim. Managed native recovery can
|
||||
resume it asynchronously after a restart.
|
||||
|
||||
Resume never sends a continuation or replacement coordinator prompt. A claim
|
||||
written before the current service process started and not live now is a turn
|
||||
cut by a restart: once the rest of the family is quiescent, the passage settles
|
||||
`ended-without-report` with reason `interrupted` ("Interrupted by a restart").
|
||||
If native work later resumes such a claim anyway, its effects remain unknown.
|
||||
Resume observes the original coordinator input and native continuation. Every
|
||||
remaining claim blocks settlement, including an old claim absent from
|
||||
`Session.active`. Require the original input and a later durable idle in native
|
||||
`session_message.seq` order, then recheck the whole family, claims, inboxes,
|
||||
requests and outstanding background deliveries. Missing/pruned input, an
|
||||
uncorrelated terminal or a changed second observation keeps the same pending
|
||||
uncertain. Never infer a terminal from age, inactivity or a timeout; never send a
|
||||
second original prompt after admission has been recorded.
|
||||
|
||||
Plugin-side mechanisms cover three native gaps:
|
||||
|
||||
|
|
@ -200,9 +445,9 @@ Plugin-side mechanisms cover three native gaps:
|
|||
- Family settlement: observation of the whole passage family and queues.
|
||||
- Form answerer identity: the backend UI-answer mark below.
|
||||
|
||||
## Scheduler and civil time
|
||||
## Daily scheduler and civil time
|
||||
|
||||
Keep **one native Job per running schedule** in the live OpenCode process.
|
||||
Keep **one native Job per running daily document** in the live OpenCode process.
|
||||
It sleeps until `min(nextDueAt, now + 1h)`, then rereads the wall clock and document.
|
||||
This hourly ceiling bounds clock-change detection; never poll per minute.
|
||||
A native `session.execution.succeeded|failed|interrupted` publication (Bus
|
||||
|
|
@ -226,10 +471,11 @@ Desired running without a live native Job is displayed as **Interrupted** with
|
|||
an interruption reason. Service restart therefore leaves scheduling Interrupted.
|
||||
Do not rearm from plugin load, a read, UI reopen or backend reconnect.
|
||||
Only explicit Resume may restore scheduling; pending work is reconciled first.
|
||||
An Interrupted schedule keeps its passage pending; a passage cut by the restart
|
||||
settles as interrupted after Resume, without waiting for a native continuation.
|
||||
An Interrupted schedule keeps its passage pending. Resume reconciles its original
|
||||
identities and observes native continuation until terminal correlation and full
|
||||
family quiescence are available.
|
||||
|
||||
## Write-ahead passage start and recovery
|
||||
## Daily write-ahead passage start and recovery
|
||||
|
||||
Daily and manual passages use the same start/observation/settlement path.
|
||||
Before any native effect, CAS-write a pending record containing:
|
||||
|
|
@ -260,8 +506,8 @@ deterministic start message exists (delivered or inbox):
|
|||
- Absent for a transient cause: retry admission on a later wake with the **same**
|
||||
session and message identities (native first admission wins), never a new one.
|
||||
Retry applies only while no admission was ever recorded. Once recorded, a later
|
||||
native absence of the start message (pruned history) is observed history: the
|
||||
passage settles by family quiescence and the message is never sent again.
|
||||
native absence of the start message (pruned history) keeps terminal correlation
|
||||
uncertain and the original message is never sent again.
|
||||
Run now cannot overlap an unresolved pending passage or bypass terminal Stop.
|
||||
A manual run never counts as another scheduled civil day's completion.
|
||||
|
||||
|
|
@ -337,11 +583,18 @@ Supported outcomes are:
|
|||
terminal failure in the family without a completed final report. Never guessed
|
||||
from prose or an idle flag.
|
||||
- `ended-without-report`: settled native work ended without a final report and
|
||||
without native terminal failure; reason `interrupted` when a family claim was
|
||||
cut by a service restart.
|
||||
without native terminal failure; reason `interrupted` requires a correlated
|
||||
native idle whose outcome is interrupted.
|
||||
|
||||
Atomically archive the result, clear pending and update the scheduled `lastDaily`
|
||||
in the guarded schedule document. Keep history at ≤30; do not let a crash between
|
||||
The terminal reader uses native `session_message.seq`, anchored to the original
|
||||
coordinator input or exact fresh managed-root assignment. Historical failures
|
||||
before that input are excluded. Reused external roots retain uncertainty while
|
||||
their entry/family provenance is unqualified. The bounded `job.background/` KV
|
||||
projection (kind and session IDs only, ≤1024 records) blocks settlement while a
|
||||
completed child still owes its native parent notification.
|
||||
|
||||
For daily documents, atomically archive the result, clear pending and update the
|
||||
scheduled `lastDaily` in the guarded document. Keep history at ≤30; do not let a crash between
|
||||
separate document writes permit a second passage for the same settled day.
|
||||
Advance watched processed-message cursors **only on completed**.
|
||||
Failed and ended-without-report leave those cursors unchanged.
|
||||
|
|
@ -429,20 +682,21 @@ reported as uncertain.
|
|||
Missing evidence leaves the gate unmet and never replays an answer; the ordinary
|
||||
answer path keeps its ownership and uncertainty checks.
|
||||
|
||||
## Per-schedule snapshot
|
||||
## Daily document snapshot
|
||||
|
||||
Expose `id`, `title`, `clock`, display `nextDueAt`, revision and available actions.
|
||||
Expose state including Interrupted and `interruptionReason` when applicable.
|
||||
Expose pending status `starting | running | settling | uncertain`, exact identity,
|
||||
latestResult and history. `starting` means a live Job/observer is handling a not
|
||||
yet admitted start; `uncertain` (with reason `not-observed` or `admission-failing`)
|
||||
only when no observer is live or its latest admission attempt failed. History and pending
|
||||
when no observer is live, admission is unconfirmed, or terminal correlation stays
|
||||
unknown. Native activity remains a separate observation. History and pending
|
||||
items carry their `daily | manual` trigger; a non-blocking `lastError` is exposed
|
||||
while relevant.
|
||||
Actions reflect terminality, unresolved controls and pending passage state.
|
||||
Read-only projections and invalidations never start work or settle unknown sends.
|
||||
|
||||
## Acceptance journeys
|
||||
## Daily document acceptance journeys
|
||||
|
||||
1. **Daily useful work:** create paused → explicit Play → close UI/backend → due
|
||||
passage uses ordinary read/shell and Mission tools → final report → whole family
|
||||
|
|
@ -451,9 +705,10 @@ Read-only projections and invalidations never start work or settle unknown sends
|
|||
in both task modes.
|
||||
2. **Restart with pending:** restart the service during a pending passage → reopen
|
||||
shows Interrupted → explicit Resume reconciles existing session/message/inbox →
|
||||
no second coordinator message → settled archive (`ended-without-report` /
|
||||
`interrupted` when the restart cut the turn) → next day. Missing/uncertain evidence
|
||||
stays pending, and only Resume rearms scheduling.
|
||||
observe native recovery with no second coordinator message → archive only after
|
||||
correlated terminal and whole-family quiescence. An old claim or missing
|
||||
correlation stays pending and uncertain through repeated wakes; only Resume
|
||||
rearms the daily Job.
|
||||
3. **Run now:** explicit authenticated Run now uses the shared write-ahead path;
|
||||
duplicate request/status reads produce no second passage. An unresolved pending
|
||||
passage blocks overlap. A lost reply is reread, never automatically resent.
|
||||
|
|
@ -476,6 +731,51 @@ daemon or user Mission.
|
|||
|
||||
## Agent conventions
|
||||
|
||||
- Missions autonomy target is explicitly native: the OpenCode service/plugin runs scheduled passages with both CodeNomad UI and intermediary backend closed; neither a persistent CodeNomad backend nor a window timer is a substitute. One process-local native Job per running schedule sleeps until min(nextDueAt, now + 1h), then rereads clock/document and admits only the latest due civil day newer than lastDaily. A native session-execution terminal event (Bus `listen`, 3 s debounce) also wakes the Job so archives follow family quiescence promptly; the event is a wake hint, never settlement evidence. Pause/Stop cancel the Job; desired running without a live Job is Interrupted until explicit Resume, never automatic rearming. Do not rely on native continuation after a restart: Resume never sends one, and a family turn cut by the restart settles `ended-without-report` / `interrupted`.
|
||||
### Unified document foundation
|
||||
|
||||
`missions/launch-policy.ts` and `mission-document{,-state,-store}.ts` provide a
|
||||
strict internal configuration codec and conditional document store. Their
|
||||
`codenomad-missions/documents-v1` namespace owns paused creation, immediate/at/in/
|
||||
every/cron/multi launch policies, one optional source condition, combined end
|
||||
bounds, and a global admitted-passage count. Relative origins are frozen at first
|
||||
Play; the absolute slot end applies to recorded due times, including late arrivals.
|
||||
Each passage references its execution journal by immutable mission ID. The existing
|
||||
journal ranges, deletion reserve and activation index own execution history.
|
||||
|
||||
Configuration generations advance separately from temporal generations. Per-calendar
|
||||
high-water, exact pending/preparing wake references, the monotone task-created mark,
|
||||
and first-Play/budget counters survive the 30-receipt window. Checkpoint payloads are
|
||||
bounded JSON snapshots; successful settlement consumes the pending snapshot.
|
||||
Deletion retains the full stopped document through reconciliation, then reserves a
|
||||
per-project tombstone entry before compacting it; the registry holds at most 4096
|
||||
entries and 1 MiB and refuses further compaction at capacity.
|
||||
|
||||
The store requires transactional revision compare-and-set and a synchronous fresh
|
||||
ownership guard. `opencode/missions/native-mission-document-storage.ts` acquires
|
||||
the private Database in the current RPC graph and performs revision/byte-checked
|
||||
publication inside an immediate transaction. One-time product creation and first
|
||||
Play use this adapter through `mission-document-one-time.ts`; their immutable
|
||||
creation receipt reserves the coordinator identity before native creation, and the
|
||||
first Play retains its original lifecycle request/message and admission count.
|
||||
Panel reads project bounded document metadata through the existing map RPCs.
|
||||
|
||||
`documentCapabilities` qualifies new unified document creation. Journal-only
|
||||
one-time maps keep their existing execution controls, declarations, continuations
|
||||
and final reports through the journal path, with their stored representation
|
||||
preserved. Daily entries remain visible with the controls of their daily contract.
|
||||
The authenticated daily controls documented above use `NativeMissionRecurrenceStore`.
|
||||
|
||||
The storage-only `documentActivation` RPC retains at most two exact configuration
|
||||
targets, a namespace authority digest, original request/revision, attempted barriers
|
||||
and acknowledged result digests. `missionScheduleActivationJournal` in the server
|
||||
routes adapts those phases to the settings executor. Ready follows that executor's
|
||||
positive effective read and keeps the Mission paused. Immediate Play leaves this
|
||||
field null. Keep credentials and raw configuration out of the document.
|
||||
|
||||
Scheduled admission, source baselines/changes, native adoption/cleanup, complete
|
||||
fan-in coverage, terminal-turn correlation and family quiescence belong to their
|
||||
own qualified adapters. Keep unknown data intact and qualification fixtures isolated.
|
||||
|
||||
- Missions autonomy runs in the OpenCode service/plugin with both CodeNomad UI and intermediary backend closed. Never substitute a persistent CodeNomad backend or a window timer. Unified documents use the persisted native calendars and restart recovery above. Daily documents use one process-local native Job per running daily schedule, sleeping until min(nextDueAt, now + 1h), then rereading clock/document and admitting only the latest due civil day newer than lastDaily. A native session-execution terminal event (Bus `listen`, 3 s debounce) also wakes that Job so archives follow family quiescence promptly; the event is a wake hint, never settlement evidence. Pause/Stop cancel the daily Job; desired running without it is Interrupted until explicit Resume. Resume observes native continuation under the original identities. Every remaining claim and missing terminal correlation keeps pending uncertain; a correlated idle and full family/background-delivery quiescence are required for archive. Never infer interruption from claim age or inactivity, and never resend an admitted original prompt.
|
||||
- Core: `missions/recurrence-{contract,clock,store,runner}.ts` (native side `opencode/missions/native-recurrence-*.ts`); omitting a profile variant is valid. Every rule in the sections above binds.
|
||||
- Root and schedule admission read the family identity with `readFamilyAuthorityIdentity` (`workspaces/family-authority-claim.ts`) only for a directory WorkspaceManager already authorized: registered linked worktrees share their Git common directory's identity, and a Git-free conversation has none, so it can never infer a family.
|
||||
|
|
|
|||
|
|
@ -1,11 +1,51 @@
|
|||
# Recurring Missions — native qualification fixture
|
||||
# Daily Mission documents — native qualification fixture
|
||||
|
||||
`scripts/test-recurring-simple-native.mjs` qualifies the acceptance journeys of
|
||||
`scripts/test-recurring-simple-native.mjs` qualifies the daily-document acceptance journeys of
|
||||
[`MISSIONS_RECURRING.md`](MISSIONS_RECURRING.md) against a real
|
||||
isolated OpenCode service and the shipped Missions bundle. That document defines
|
||||
the journeys; this one maps them to fixture letters and describes the fixture.
|
||||
|
||||
## Run
|
||||
## Unified native schedules and supervision
|
||||
|
||||
Unified documents use the separate protocols in
|
||||
[schedule transport](MISSIONS_NATIVE_SCHEDULE_TRANSPORT.md#native-document-composition-and-packaging), with
|
||||
their [consumed native contracts](MISSIONS_NATIVE_CONTRACTS.md#schedule-integration-qualification).
|
||||
Run only against owned isolated runtimes and fresh scratch directories:
|
||||
|
||||
```sh
|
||||
node scripts/test-native-schedule-foundation.mjs <absolute-cli> <fresh-scratch>
|
||||
node --import tsx scripts/test-native-mission-schedule-product.mjs <absolute-cli> <fresh-scratch> --parent-ready --kinds=at,in
|
||||
node scripts/test-missions-terminal-native.mjs <absolute-isolated-cli>
|
||||
```
|
||||
|
||||
The product fixture's `--parent-ready` requires the coordinator's explicit final-ready
|
||||
handoff. Its `--kinds=every --natural-every` exercises the real minimum 15-minute
|
||||
interval. Foundation cold-missing runtime/provider modes verify automatic no-client
|
||||
Location wake and original calendar identity after restart; controlled-maintenance
|
||||
checks automatic local compaction, retained phase/end and zero provider HTTP calls.
|
||||
Product checks cover authenticated paused creation, activation/control receipts,
|
||||
backend closure and managed-service restart. Terminal checks require original-input
|
||||
correlation and whole-family/background-delivery quiescence before settlement.
|
||||
|
||||
OS qualification commands and host boundaries live in
|
||||
[runtime setup](OPENCODE_RUNTIME_SETUP.md#service-supervision-primitives-and-native-adapters):
|
||||
`node --import tsx scripts/test-service-supervision-windows-native.mjs <absolute-cli>`,
|
||||
`node --import tsx scripts/test-service-supervision-wsl-native.mjs <output-directory>`,
|
||||
and the explicitly bundled Linux/macOS native scripts and phases described there.
|
||||
Check foreground hosting, health/ownership, actual exit before relaunch, OFF survival
|
||||
and adoption, login/reboot boundaries, and safe owned-linger restoration separately.
|
||||
Desktop packaging and host-login gates require their own evidence.
|
||||
|
||||
UI qualification uses the real Solid/HTTP fixtures under
|
||||
`packages/ui/tests/browser/` with `npm run test:browser --workspace @codenomad/ui`:
|
||||
verify Create/Edit H9 locks, explicit activation, uncertain original receipts,
|
||||
pagination, diagnostics, carrier links and supervision status across locales.
|
||||
Read-only native capture replay uses
|
||||
`npm run qualify:browser:native-mission-replay --workspace @codenomad/ui` with the
|
||||
captured run's `NATIVE_MISSION_CAPTURE` and its own `NATIVE_MISSION_OUTPUT`.
|
||||
Keep observed results and unexecuted platform gates in delivery evidence.
|
||||
|
||||
## Daily fixture run
|
||||
|
||||
```powershell
|
||||
npm run build:missions --workspace packages/server
|
||||
|
|
@ -24,7 +64,7 @@ code 1 on any failure.
|
|||
| Letter | Acceptance journey | Asserts |
|
||||
| --- | --- | --- |
|
||||
| A | 1, daily useful work | With backend, bridge and presence closed, the due passage runs read → shell → `mission_inspect` → final `mission_report`; completed archive, one start, schedule still running with nextDueAt = due + 24 h |
|
||||
| B | 2, restart with pending | The owned service is killed mid-turn and relaunched: Interrupted (`service-restart`) with the same pending passage; Resume archives `ended-without-report`/`interrupted` with the original single start and no further provider turn |
|
||||
| B | 2, restart with pending | The owned standalone service is killed mid-turn and relaunched: Interrupted (`service-restart`) with the same pending passage; Resume observes the old claim as uncertain, without archive, cursor advance or a second original start |
|
||||
| C | 3, Run now | Two Run now passages on one schedule, each completed with one start and its own session; the evidence records the response to an exact duplicate of the first request |
|
||||
| G | 3, Run now | Run now on a paused schedule completes through the settlement-only observer; the schedule stays paused with nextDueAt null |
|
||||
| D | 4, Pause/Stop | Pause before due leaves no root session through due + 30 s; Stop is terminal and a later Resume is refused |
|
||||
|
|
@ -88,5 +128,9 @@ Paths below are under `packages/server/src/` unless stated.
|
|||
(`MISSIONS.md` §Validation).
|
||||
- Open: the managed service's restart, including its boot-time sweep of orphaned
|
||||
claims (B restarts an owned `serve` process with a hard kill).
|
||||
- `scripts/test-missions-terminal-native.mjs` qualifies the production terminal
|
||||
reader on an isolated runtime: old inactive claim through repeated observations,
|
||||
native resume under the original input, seq-correlated idle, unrelated old
|
||||
failures and an outstanding native background-child delivery.
|
||||
- Open: a native admission whose `recordAdmission` write is lost reads as never
|
||||
admitted once its start message is pruned.
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ view; release notes are at [opencode.ai/changelog](https://opencode.ai/changelog
|
|||
per-version sections here.
|
||||
|
||||
Minimum runtime **2.2.3**, recommended **2.2.5** (`runtime-support.ts`), with
|
||||
**2.1.0** client/plugin pins. The minimum carries the image tool contract of
|
||||
**2.2.3** client/schema/plugin pins. The minimum carries the image tool contract of
|
||||
upstream [#60258](https://github.com/anomalyco/opencode/pull/60258), consumed by
|
||||
the Images gallery, the Design agent and skills and the Images settings: one
|
||||
name, `experimental_image_generate`, for the tool, its permission action, its job
|
||||
|
|
@ -21,7 +21,7 @@ type and its completion notices' `metadata.source` (`IMAGE_TOOL` in
|
|||
model is available unless `experimental.image_generate` is `false`
|
||||
(`image-settings.ts` reads it that way). Notices wrap their text in
|
||||
`<image id="…" state="…">`; the gallery reads the `Saved image: <path> (` line.
|
||||
The minimum also provides the replay contract (below) consumed by the pinned 2.1
|
||||
The minimum also provides the replay contract (below) consumed by the pinned
|
||||
reducer and Code Mode JavaScript run-to-completion, whose regression
|
||||
`scripts/test-codemode-jobs-native.mjs` runs in CI against minimum and latest.
|
||||
Daemons below the minimum are refused with `native_contract`. Plugins share the
|
||||
|
|
@ -30,6 +30,13 @@ undergo authenticated contract recognition; missing canonical APIs, session
|
|||
environment or the replay shape give a concrete reason. OpenCode reads older
|
||||
stored data.
|
||||
|
||||
Missions schedules qualify the consumed Schedule API, effective
|
||||
`experimental.schedules` flag and retained plugin/provider bundle independently
|
||||
of general runtime admission: [native contracts](MISSIONS_NATIVE_CONTRACTS.md#schedule-integration-qualification)
|
||||
and [transport](MISSIONS_NATIVE_SCHEDULE_TRANSPORT.md). Recognition uses the
|
||||
consumed capabilities, including on future versions. Schedule delivery qualification
|
||||
uses the recommended 2.2.5 runtime; keep results in PR/CI evidence.
|
||||
|
||||
### Native Codex subscription usage
|
||||
|
||||
Beyond the usage rules in [PROVIDER_ACCOUNTS.md](PROVIDER_ACCOUNTS.md):
|
||||
|
|
@ -76,6 +83,22 @@ reply the goal reviewer rejects, pausing the goal) runs in the minimum/latest CI
|
|||
|
||||
- Generated client URLs keep their `baseUrl` prefix and pass through unchanged;
|
||||
scheduling classifies API paths relative to the proxy prefix.
|
||||
- The generated `experimental.session` methods and their `session` aliases use
|
||||
identical experimental wire routes for instructions, wait, log, import/export
|
||||
and stats. Stateless `experimental.generate.text` uses
|
||||
`/api/experimental/generate/text`; side questions use the separate session
|
||||
generation API. New client domains retain the proxy's explicit allowlist.
|
||||
- The Solid reducer derives projected message IDs with
|
||||
`Session.Message.ID.fromEvent`: event fixtures must use native `evt_` IDs,
|
||||
producing `msg_` rows. Catalogue invalidations refresh only previously read
|
||||
resources; an active-session snapshot also fetches missing running-session
|
||||
metadata. Tool-file omission is opt-in; transcript and pruning reads preserve
|
||||
their full native projection.
|
||||
- Service discovery/header construction, RPC envelopes, `SessionLegacy`, Form
|
||||
and Permission read codecs keep their consumed contracts. The private loaded
|
||||
Location map canonicalizes path flavor and trailing separators; pending
|
||||
recovery retains its existing-only leases and identity checks. Plugin setup
|
||||
remains activation-scoped; native services are acquired in the request graph.
|
||||
- The proxy allowlist stays closed to pairing (`/api/pair`, `/auth/connect/*`),
|
||||
credential export/creation (`/api/credential`) and experimental voice
|
||||
(`/api/experimental/voice/session`); a runtime update never widens it or
|
||||
|
|
@ -89,7 +112,9 @@ reply the goal reviewer rejects, pausing the goal) runs in the minimum/latest CI
|
|||
`session.environment` applies independently.
|
||||
- Plugin bundles externalize Effect and the plugin SDK because two Effect
|
||||
runtimes cannot share one native fiber and host Schema APIs differ across
|
||||
releases. Test hosts that spread the Promise plugin context must declare the
|
||||
releases. Local plugin directories with a package name and `exports` resolve
|
||||
their entrypoints through that manifest (`session-pruning/package.test.ts`).
|
||||
Test hosts that spread the Promise plugin context must declare the
|
||||
`credential` domain. `%ProgramData%\opencode` is discovered after the global
|
||||
plugin root that `config.get` consumers take first; native fixtures redirect
|
||||
`ProgramData`. Package-install fixtures pass `withProductRuntime`'s `registry`
|
||||
|
|
@ -164,9 +189,16 @@ mutation fences and settlement tombstones stay essential.
|
|||
|
||||
## Agent conventions
|
||||
|
||||
All managed bundle payloads (automation, session-pruning, Missions runtime and
|
||||
Missions carrier provider) share `opencode/desktop-plugin-artifact.ts` for digest
|
||||
verification, atomic repair and bounded lock retries. Provisioning verifies both
|
||||
Missions payloads before publishing its shim or lease. Failures flow through the
|
||||
shared provisioning error deduplication and pending-request recovery notice.
|
||||
|
||||
- Desktop automation and pruning provisioning follows the authenticated OpenCode connection and its `config.get` global discovery directory, including reconnects. Replacing a connection aborts its in-flight provisioning: `opencode/desktop-plugin-provisioning.ts` logs that at debug as superseded (the new connection provisions again) and keeps errors for failures on the current connection, logging each distinct failure once with its consequence and repeats at debug until a provisioning succeeds. Bundle payloads are content-addressed (`<sha256>.mjs`) and shared by every backend of the namespace: `opencode/desktop-plugin-installation.ts` publishes one only by renaming a complete temporary copy, and replaces a name that holds other bytes or is not one regular file (an earlier write cut short, a link) the same way before the entry may point at it, so concurrent installers never read a partial payload and a damaged one never blocks later connections. Never derive a running daemon's roots from backend/startup environment or CLI `debug paths`. OpenCode watches the entire discovery root recursively: keep changing presence leases outside it, in the sibling `.codenomad/<root-hash>/` namespace. Retain existing outside-root storage; migrate inside-root storage while reading older backends' leases without writing there. WSL translates the daemon-reported paths through the selected distro only for filesystem access. Verify heartbeat stability with the isolated native automation fixture.
|
||||
- Plugins register only at their next presence check, so a backend's first plugin RPC could arrive before registration and be refused. Followers acknowledge a lease with a `<lease>.ready` marker (current namespace only) once every follower of that directory in the native process has observed it while registered; routes calling bundled-plugin RPCs await `workspaceManager.desktopPluginsReady()` before reading or authorizing the session, so ownership checks and deletion-fence entry stay adjacent (bounded to 5 s, never rejects). The bound is paid in full only when no marker can arrive: no Location loaded yet, or pre-marker plugin copies still loaded after an upgrade. Do not replace this ordering with retries of refused reads.
|
||||
- The Design agent and skills follow backend leases, never a command or RPC. A backend requests them while an installed, enabled panel extension declares `opencode.designAgent` (`opencode/design/demand.ts`, refreshed on every extension-store change). `DesktopPluginLifecycle.setLeaseContent` writes the request (`{"version":1,"design":true}`, or an empty lease) into every session-pruning lease the backend retains, one per configuration namespace, and each new installation starts from it. Writes happen only when the request changes, through a temporary name outside the lease pattern and a rename, serialized with the heartbeat so a removed lease is never resurrected; a failed write retries at the next heartbeat. The shared presence observation (`desktop-plugin-presence.ts`, versioned process key) reads at most 256 bytes of each live lease: empty, unreadable, oversized or unknown-version leases request nothing, and Design is wanted while any live lease of the plugin's presence directories requests it, so one backend's expiry withdraws only its own request. The session-pruning plugin registers its agent and skill transforms once in its `effect`, before `followPresence` (`opencode/design/register.ts`), with content conditioned on a Location-local flag; reconciliation at the presence interval only flips the flag and calls `agent.reload()` and `skill.reload()` on an actual change, and the content loads on the first request. Never register these transforms from presence, an RPC or a command: a late registration bypasses the user's permission rules and overrides the user's own `design` agent or `design-*` skills, which always win. A runtime without agent or skill transforms gets no Design agent. `opencode/design/status.ts` classifies a Location's `GET /api/agent`: `present` when the prompt starts with the content mark, `user` otherwise, or `absent`. Validate with `opencode/design/*.test.ts` and `scripts/test-design-activation-native.mjs` (build the pruning bundle first); that fixture enables project discovery, so outside CI it refuses to run under the user's home unless `CODENOMAD_NATIVE_FIXTURE_PARENT` names a parent outside it.
|
||||
- The shared native event relay consumes upstream events before slow routing I/O. Preserve per-session/PTY/Shell and per-recipient FIFO, validated full-location ownership, invalidation/connection/workspace fences and bounded-backlog reconnect recovery. When an ownership lookup still fails after its retry, the relay evicts that cache entry and pulses that recipient's status alone (`error` with `event routing failed`, then `connected`), so its clients run their reconnect reconciliation (one pass per instance, with later requests coalesced into one trailing pass, `ui/src/lib/trailing-resync.ts`). Pulses are leading and trailing, at most every 5 s, doubling to 60 s while failures persist. Never fail the shared subscription for one recipient's lookup: that resyncs every instance. A slow recipient must not block another or the shared SDK subscriber. Validate with the relay regressions and isolated native location fixture.
|
||||
- Native OpenCode plugins share `packages/server/scripts/native-plugin-build.mjs`: never embed Effect or the public plugin SDK into daemon-loaded bundles (version boundaries: Qualification policy above). Keep backend-side OpenTunnel isolation separate, and run the bundle regression plus isolated plugin-runtime/pruning/automation/Missions fixtures when changing this boundary.
|
||||
- The Design agent and skills exist once, as native OpenCode files in `packages/server/src/opencode/design/opencode/` (`agents/design.md`, `skills/design-*/SKILL.md`, `NOTICE.md`, `LICENSE-APACHE-2.0.txt`), which also work unchanged when copied into a project's `.opencode/` or the global OpenCode configuration folder; the public help links this folder for that use (`docs/help/<locale>/extensions.md#design-without-codenomad`), so moving it means updating those links. Each file keeps its licence notice in frontmatter comments and its references inside its own body: never add sibling files, CLI or form protocols, URLs or brand names (`design/content.test.ts` checks them, with the attribution of each adapted file). `design/content.ts` embeds the files as text through the build's `.md` loader and parses them strictly: bundles call `loadDesignContent()`; `tsx` cannot import `.md`, so checkout code and tests read the files and call `parseDesignContent()`. The agent prompt starts with `codenomad-design-content: <DESIGN_CONTENT_VERSION>`; raise the version whenever the skill, direction or recipe identifiers change. Validate content changes with that test, the self-contained bundle test in `native-plugin-build.test.mjs` and `scripts/test-design-content-native.mjs`. That native test enables project discovery, which walks every ancestor directory: outside CI it refuses to run under the user's home unless `CODENOMAD_NATIVE_FIXTURE_PARENT` names a parent outside it.
|
||||
- Missions provisioning installs `provider.mjs` as a separate content-addressed local provider beside its content-addressed runtime. The managed shim supplies native absolute `{provider:{file,sha256},runtime:{file,sha256}}` paths as the factory's second argument; WSL translation applies only to backend filesystem I/O. Lease release and upgrades retain both payloads for native carrier recovery. Registration paths are acquired during arming in the current native RPC graph by `missions/native-schedule-installation.ts`, from the own-process record under native Global state and current ServerInfo URLs, then authenticated with exact-file public Service discovery.
|
||||
|
|
|
|||
|
|
@ -20,6 +20,14 @@ never upgrade or restart a user's service.
|
|||
separately from an untested version. Optional configuration reload follows
|
||||
its own route presence. Never probe support with a write.
|
||||
|
||||
Scheduled Missions additionally require the consumed Schedule contract and enabled
|
||||
`experimental.schedules` in the discovered global configuration and effective whole
|
||||
Location block. Targeted, confirmed activation and exact read-back are described in
|
||||
[Missions](MISSIONS.md) and [native contracts](MISSIONS_NATIVE_CONTRACTS.md).
|
||||
Missing APIs, explicit OFF, a masking block, failed activation or missing exact
|
||||
calendar registration keep the schedule paused or in an explained error state.
|
||||
Immediate unconditional Play follows its own admission path.
|
||||
|
||||
## Setup and recovery
|
||||
|
||||
Startup recovery and Preferences share `OpenCodeSetupPanel` and its store, but
|
||||
|
|
@ -185,6 +193,308 @@ terminals/background commands closed for all clients) and never presents it as a
|
|||
process restart or memory reclamation. Conflicting service actions are refused
|
||||
while an explicit action runs.
|
||||
|
||||
## Service supervision primitives and native adapters
|
||||
|
||||
`host-lifetime/service-supervision-contract.ts` defines the private service-wide
|
||||
preference and status projection. The key uses the execution host, native UID/SID,
|
||||
canonical configuration/state roots and exact registration channel. Binary selection,
|
||||
CodeNomad profiles and changing daemon incarnations remain separate from that key.
|
||||
The acquired Settings execution host supplies the canonical namespace and validates
|
||||
the absolute executable selection before using these internal APIs.
|
||||
|
||||
`ServiceSupervisionStore` stores a bounded 64 KiB manifest below a caller-provisioned
|
||||
private service-wide root. Revision-checked controls persist intent before OS effects;
|
||||
fresh adapter read-back acknowledges its revision separately. An exact retained
|
||||
request receipt reconciles a lost reply. An absent receipt stays unknown. First Play
|
||||
sets an unset choice to ON; explicit OFF persists across subsequent Plays. Reads and
|
||||
binary selection preserve the preference. OFF can fence an unfinished ON; stale
|
||||
acknowledgements and stale ON requests conflict.
|
||||
|
||||
Windows mutations and Start admission share an OS `FileShare.None` handle on
|
||||
`admission.lock`. The transaction helper owns both that handle and the atomic
|
||||
manifest replacement. Caller/helper exit releases the handle; a late caller callback
|
||||
cannot write outside exclusion. Linux mutations and Start share a native `flock`
|
||||
descriptor, released by the OS when its holder exits; a short pipe-owned helper
|
||||
holds it across the asynchronous operation. macOS uses a bundled native helper's
|
||||
advisory descriptor lock, also released on caller death. Its `proc_pidinfo` lookup
|
||||
records the microsecond native start identity for health and guard ownership.
|
||||
|
||||
`readSupervisionHealth` reuses the bounded public registration projection and
|
||||
official exact-file read-only discovery. PID start identity and incarnation are
|
||||
rechecked after authenticated discovery. A live unresponsive process remains
|
||||
unhealthy/unknown; a later healthy read preserves its identity. Status reaches
|
||||
Running only with current-revision installed resources, an active guard and healthy
|
||||
service. An absent guard with a responding service exposes a supervision failure.
|
||||
|
||||
`ServiceSupervisionSettings` sequences private intent, adapter attempts and exact
|
||||
receipt reads. The acquired execution-host scope supplies its store/adapter and
|
||||
current-context fence; controls additionally revalidate the live human at the
|
||||
serialized preference write. Requests pin the displayed service key and revision.
|
||||
Adapter failures persist a fixed `adapter-failed` cause and attempt timestamp with
|
||||
ON still requested. Explicit Retry reconciles that same revision and preserves its
|
||||
original receipt. A later OFF fences an older attempt's diagnostics. Passive reads
|
||||
and acknowledged request replays observe supervision resources; Linux also completes
|
||||
an already requested, deferred linger rollback when its safety checks allow it.
|
||||
The internal first-Play
|
||||
entry sets an unset preference to ON and preserves every existing explicit choice.
|
||||
|
||||
The Settings route registrar in `server/routes/service-supervision.ts` defines
|
||||
`GET|POST /api/opencode/supervision`, using the existing live human-session check.
|
||||
Responses expose the key, preference, revision, protection state, fixed cause,
|
||||
responding-service flag, known attempt/backoff and optional exact receipt status.
|
||||
Its POST input permits ON, OFF and Retry; executable paths, arbitrary commands and
|
||||
internal first-Play/binary-selection controls stay behind the execution-host seam.
|
||||
The production HTTP server registers these routes under its authentication hook.
|
||||
The shared response/control types are exported by `api-types.ts`.
|
||||
|
||||
`service-supervision-resolver.ts` resolves the selected Settings executable without
|
||||
project or session discovery. Windows native `debug paths` and the selected CLI's
|
||||
credential identify exactly one service channel; canonical native roots, current SID
|
||||
and host determine the service key. Native Linux uses its canonical roots and UID
|
||||
through the POSIX resolver in `service-supervision-linux-resolver.ts`, shared with
|
||||
native macOS. Windows UNC selections resolve the registered WSL distro spelling,
|
||||
native account/UID and canonical Linux roots through the selected distro.
|
||||
Missing/ambiguous namespaces, unqualified platforms and unsupported command wrappers return a fixed
|
||||
unavailable response.
|
||||
Mission Play additionally pins its already-authorized service connection, registration
|
||||
PID/URL and credential. The immediate, daily and unified Play paths default only an
|
||||
unset service preference to ON; paused creation, passive reads and other controls
|
||||
preserve it. Human/connection/physical-root checks surround the operation. Signed
|
||||
schedule proofs are issued after supervision preparation.
|
||||
|
||||
Focused checks: `node --import tsx --test
|
||||
packages/server/src/host-lifetime/service-supervision.test.ts`. Native exact-file
|
||||
health qualification: `node --import tsx
|
||||
scripts/test-service-supervision-health-native.mjs ABSOLUTE_CLI`; its isolated roots
|
||||
and service are removed after actual process exit.
|
||||
|
||||
`WindowsServiceSupervision` is the internal execution-host adapter. Its caller
|
||||
supplies the acquired service namespace, private store, durable owned runtime and
|
||||
native path-only environment. Its immutable private `windows.json` descriptor
|
||||
contains the PowerShell/Node/probe paths and minimal host roots. The build target
|
||||
`npm run build:supervision --workspace @neuralnomads/codenomad` bundles the read-only
|
||||
probe and copies the PowerShell guard into `dist/host-lifetime/`. On acquisition,
|
||||
the resolver provisions `%LOCALAPPDATA%/CodeNomadServiceSupervision` with a private
|
||||
current-SID/SYSTEM DACL, preserving existing ACLs. Its service directory holds
|
||||
immutable content-addressed copies of the bundled Node, probe and guard; the private
|
||||
descriptor pins their paths across package replacement. Artifact size admission
|
||||
uses a 256 MiB bound with the same complete ancestor/owner/DACL checks as metadata.
|
||||
Unsafe storage leaves supervision unavailable. Explicit executable selection and
|
||||
verified CodeNomad installation publish the validated target under the service-wide
|
||||
revision/admission lock. Before an explicit service restart, publication completes
|
||||
before the CLI stop/start operation. Passive reads preserve the selected target;
|
||||
a different service channel resolves its own independent manifest and preference.
|
||||
|
||||
`service-supervision-target.ts` binds the absolute native executable to complete
|
||||
SHA256 bytes and, for the exact published npm layout, the package manifest and
|
||||
prefix installation lock. Missing/partial images, an active installation lock and
|
||||
changed package provenance wait with the guard's capped backoff. Every spawn reads
|
||||
the current manifest and verifies its target, with no command/PATH fallback.
|
||||
A complete external npm upgrade at the same authorized package/path is admitted
|
||||
after bounded CLI root/credential-channel checks and stable image/package rereads;
|
||||
custom-image replacements require explicit selection publication. A healthy
|
||||
incumbent survives installation and target publication unchanged. The shared server
|
||||
selection/install/restart seams supply the same contract to Electron and Tauri.
|
||||
|
||||
Main uses PowerShell and native `CreateProcessW` with independent NUL stdio and a
|
||||
foreground child handle. Main has IgnoreNew and unlimited execution time; Recovery
|
||||
has Queue, event/logon/60-second triggers and a ten-second OS deadline. Ownership
|
||||
read-back checks names, marker, action, principal, settings and trigger policy.
|
||||
Recovery makes one bounded Start attempt under the shared admission handle; the
|
||||
independent OS trigger keeps the retry obligation after an unavailable action.
|
||||
Owned guard invocations use PowerShell's process-scoped `-ExecutionPolicy Bypass`,
|
||||
matching native executable-wrapper launches; persisted execution-policy settings
|
||||
remain untouched and enforced Group Policy still applies.
|
||||
Main adopts an incumbent, retains live unhealthy children, backs off after actual
|
||||
exit and rereads the manifest before each spawn. The native service `disabled`
|
||||
configuration also gates spawn. Fixed error codes and non-secret status are bounded;
|
||||
OpenCode owns its native logs.
|
||||
The guard records its actual last spawn attempt and next backoff time; the adapter
|
||||
exposes these only with a matching revision and live guard-start identity. A native
|
||||
disabled/unknown service configuration appears as a supervision failure even while
|
||||
an incumbent responds.
|
||||
|
||||
OFF persists before trigger removal, waits for Main to release handles and exit,
|
||||
then unregisters the owned tasks. An incomplete release stays pending for retry.
|
||||
The next ON adopts the surviving incarnation. Native qualification is
|
||||
`node --import tsx scripts/test-service-supervision-windows-native.mjs ABSOLUTE_CLI`:
|
||||
uniquely named tasks and isolated services are removed after confirmed exit. Its
|
||||
private-leaf DACL policy isolates OS mechanics; production storage admission also
|
||||
requires the full ancestor policy.
|
||||
|
||||
Platform packaging qualification remains separate
|
||||
from these Windows checks. Windows native tests exercise the authenticated route
|
||||
with real login sessions and the production resolver in an isolated namespace.
|
||||
Their explicit private-leaf test policy qualifies OS mechanics; production retains
|
||||
the complete ancestor/DACL policy. Settings-controller and HTTP-admission checks run with
|
||||
`node --import tsx --test packages/server/src/host-lifetime/service-supervision-service.test.ts`.
|
||||
|
||||
Binary/package checks run with `node --import tsx --test
|
||||
packages/server/src/host-lifetime/service-supervision-target.test.ts` and updater
|
||||
ordering with `packages/server/src/opencode-update/service.test.ts`.
|
||||
`node --import tsx scripts/test-service-supervision-binary-native.mjs OUTPUT_DIRECTORY`
|
||||
uses real Windows tasks and an isolated npm prefix/home/service with paths containing
|
||||
spaces. It exercises actual native upgrade, healthy-incumbent preservation, automatic
|
||||
same-package replacement, explicit restart publication, installation-lock/missing/
|
||||
partial/provenance backoff and concurrent OFF/selection revisions. The run removes
|
||||
its OS resources and waits for its services to exit, retaining its bounded evidence.
|
||||
|
||||
### Native Linux systemd user supervision
|
||||
|
||||
`LinuxServiceSupervision` installs one exact, privately owned unit under the account's
|
||||
`.config/systemd/user`, enabled for `default.target`. It runs the content-addressed
|
||||
bundled Node/guard in the foreground, with `Restart=always`, two-second restart delay,
|
||||
and five guard starts per sixty seconds. Retry resets a reached systemd StartLimit.
|
||||
The guard's genuine child-exit retries have a separate 2–30 second capped delay.
|
||||
Live but unreachable children and incumbents remain untouched; authenticated health
|
||||
recovery preserves their identity. The native `disabled` flag gates every spawn.
|
||||
Minimal HOME/XDG path roots live in the private descriptor. The unit carries only
|
||||
fixed executable paths and the non-secret namespace key.
|
||||
|
||||
OFF writes its preference under the shared admission lock, stops the foreground
|
||||
guard **while `KillMode=process` remains loaded**, checks it inactive, then disables,
|
||||
unlinks and reloads the unit. Never use `disable --now` or unlink before stop. The
|
||||
released OpenCode process keeps its independent stdio and its populated accounting
|
||||
cgroup until natural exit; the next ON adopts it without a second daemon.
|
||||
|
||||
Linux ON requests `loginctl --no-ask-password enable-linger` for the acquired UID.
|
||||
Linger starts **all enabled user services at boot and keeps them after logout**.
|
||||
A private account-home registry, shared across service namespaces and independent
|
||||
of profiles/XDG roots, records requests, enable intent/acknowledgement and the owned
|
||||
root linger-marker fingerprint. Preexisting linger remains external. Lost enable
|
||||
acknowledgement, altered markers and corrupt provenance preserve linger as uncertain.
|
||||
Polkit refusal preserves ON and exposes `linux-linger-permission-required`; explicit
|
||||
Retry repeats the same intent without sudo, passwords or an embedded auth agent.
|
||||
|
||||
The last OFF restores owned linger only with an active/online logind session or
|
||||
proven exit of every released service and absence of a replacement at its registration.
|
||||
Before guard removal, the account-wide registry retains each released service's exact
|
||||
registration, PID, incarnation and native boot/start identity. Deferred reads compare
|
||||
that identity directly with the OS; a missing registration without retained process
|
||||
proof keeps rollback deferred. Otherwise supervision is removed while
|
||||
the process and linger survive, with `linux-linger-deferred` visible on the OFF
|
||||
status. Startup acquisition, status reads and ON/OFF reconciliation automatically
|
||||
recheck the deferred rollback's requests, ownership and safety; explicit Retry is
|
||||
also available. A new ON cancels deferred rollback. Unknown service/session evidence preserves linger.
|
||||
The parent user manager's stop policy applies to all its remaining children.
|
||||
|
||||
Focused Linux logic checks run on every host with fake logind/health responses:
|
||||
`node --import tsx --test packages/server/src/host-lifetime/service-supervision-linux.test.ts`.
|
||||
The disposable Linux VM check is `scripts/test-service-supervision-linux-native.ts`;
|
||||
bundle it and the production guard with esbuild, then run its explicit phases against
|
||||
an isolated CLI/XDG sandbox. It covers real refusal, foreground hosting, OFF/adoption,
|
||||
health loss/recovery, service and guard crashes, StartLimit, reboot boot/login timing,
|
||||
and cleanup. Keep the personal installation and initial linger state intact.
|
||||
`service-supervision-windows-recovery.ts` reads the exact owned Recovery task and
|
||||
its native `NextRunTime` independently of the guard script. A missing guard/action
|
||||
keeps ON, shows failure and exposes `recoveryNextTickAt` when its OS obligation is
|
||||
verified. The guard's own backoff remains `retryAt`; stale revisions and OFF clear
|
||||
the Recovery date. Missing action artifacts use the fixed `artifact-missing` cause.
|
||||
|
||||
### Native macOS LaunchAgent supervision
|
||||
|
||||
`MacosServiceSupervision` owns one exact plist per service namespace under the real
|
||||
account's `Library/LaunchAgents`. It uses `gui/<uid>`, `RunAtLoad`, `KeepAlive`, a
|
||||
five-second `ThrottleInterval` and `AbandonProcessGroup=true`. The adapter verifies
|
||||
both private plist bytes and the loaded LaunchAgent's complete argv, path, type,
|
||||
throttle and restart/release flags before mutation. Missing graphical domains and
|
||||
incomplete ownership remain explicit failures with the requested preference intact.
|
||||
The account's graphical login supplies the automatic startup boundary.
|
||||
|
||||
The shared POSIX foreground guard (`service-supervision-linux-guard.ts`) starts the
|
||||
managed service with independent null stdio and the descriptor's minimal HOME/XDG
|
||||
paths. Live unresponsive children and incumbents are retained; authenticated health
|
||||
probes back off from one to sixteen seconds. Only confirmed process absence permits
|
||||
another start, under the same native descriptor lock as ON/OFF. The native service
|
||||
`disabled` setting gates each spawn. Fixed status records bind guard PID/start time
|
||||
and preference revision; a loaded plist alone provides the Installed state.
|
||||
|
||||
OFF persists before `bootout`, waits for confirmed guard exit and removes the exact
|
||||
owned plist. `AbandonProcessGroup` preserves the hosted service during normal removal
|
||||
and guard crashes. ON adopts its surviving incarnation before considering a spawn.
|
||||
OpenCode retains its own registration, authentication and logs.
|
||||
|
||||
`build:supervision` compiles a universal x64/ARM64 `service-supervision-macos-native.c`
|
||||
helper with the macOS build toolchain and the desktop's 13.5 deployment target.
|
||||
The packaged helper provides only `proc_pidinfo` identity and a pipe-owned
|
||||
`flock`; the runtime provisions content-addressed private copies of it, Node and the
|
||||
guard. The user installation consumes these built artifacts. Run focused logic
|
||||
checks with `node --import tsx --test
|
||||
packages/server/src/host-lifetime/service-supervision-macos.test.ts`.
|
||||
`scripts/test-service-supervision-macos-native.ts`, bundled beside the production
|
||||
guard and native helper, checks isolated foreground hosting, OFF/adoption, native
|
||||
lock release, artifact recovery, service/guard crashes, CLI stop/restart and live
|
||||
unreachable child/incumbent recovery. It requires an open graphical session and a
|
||||
separate CLI/XDG sandbox. Real logout/login/reboot and desktop packaging remain
|
||||
separate qualification gates; keep personal applications and services untouched.
|
||||
|
||||
### Windows-to-WSL supervision
|
||||
|
||||
`service-supervision-wsl-resolver.ts` acquires the selected UNC executable without
|
||||
project discovery. It pins the registered distro name, Linux account and numeric
|
||||
UID, canonical native HOME/XDG roots and exact credential-selected channel. The
|
||||
service key uses Linux-native roots and UID plus the Windows host/SID, distinguishing
|
||||
the per-Windows-user distro registry. The private descriptor and task principal also
|
||||
pin that Windows owner. `wsl.exe --user` receives the acquired
|
||||
account name, and each observation revalidates its numeric UID and physical roots.
|
||||
|
||||
The adapter reuses `WindowsServiceSupervision`, including Main/Recovery ownership,
|
||||
the shared admission handle, IgnoreNew/Queue, the independent sixty-second backstop
|
||||
and Recovery's ten-second deadline. Main keeps a native foreground handle to
|
||||
`wsl.exe --distribution … --user … --exec /bin/sh …`. The fixed Linux shell bridge
|
||||
waits for the managed `serve --service` child with independent null stdio and
|
||||
inherited ignored SIGHUP. It observes the atomically published private preference
|
||||
through its acquired mounted manifest path solely to release at OFF; Start admission
|
||||
uses the Windows handle plus a native Linux `flock` shared with preference writes.
|
||||
The final Linux ON/revision check and child fork hold that lock; the child closes
|
||||
its inherited lock descriptor. This fences a cold WSL activation that reaches Linux
|
||||
after OFF or a later ON. A pipe-owned Linux helper keeps preference writes excluded
|
||||
through atomic publication of the mounted manifest. The Linux descriptor holder
|
||||
performs the bounded read, temporary-file flush and rename itself; Windows sends
|
||||
only the proposed bytes through its pipe. Holder exit closes publication authority,
|
||||
including for a callback still awaiting validation. Every WSL manifest mutation,
|
||||
including diagnostics and acknowledgements, uses this publisher and requires the
|
||||
acquired distro to be available.
|
||||
The bridge stays resident throughout ON and releases
|
||||
the child without a signal at OFF, allowing its Windows client to exit before task
|
||||
removal. A subsequent ON observes the same surviving service before considering
|
||||
another spawn.
|
||||
|
||||
WSL OFF acknowledgement compares the exact registration PID/id/URL/version and
|
||||
Linux boot/start identity before and after owned task removal, independently of
|
||||
HTTP health. A live HTTP 500 or slow service survives removal with the same
|
||||
incarnation. A lost, replaced or unreadable identity for an observed live service
|
||||
keeps acknowledgement pending for the original OFF and explicit Retry;
|
||||
installed-resource and namespace checks remain required.
|
||||
|
||||
Health uses exact native registration paths mapped through the selected distro,
|
||||
Linux `/proc` boot/start identities and official read-only authenticated discovery
|
||||
from Windows. This also verifies localhost forwarding. A live Linux PID with failed
|
||||
forwarding or unknown health stays preserved; Windows process APIs only inspect
|
||||
the Windows guard. Read-only native CLI status distinguishes an internally healthy
|
||||
service with failed Windows discovery as `wsl-forwarding-unavailable`; ambiguous
|
||||
native health remains unknown. The descriptor contains minimal paths rather than profile
|
||||
environment or credentials. WSL follows the Windows login boundary; its distro
|
||||
startup/idle/sleep policies apply independently. Linger stays under the distro's
|
||||
own policy.
|
||||
|
||||
Focused checks: `node --import tsx --test
|
||||
packages/server/src/host-lifetime/service-supervision-wsl.test.ts`.
|
||||
`node --import tsx scripts/test-service-supervision-wsl-native.mjs OUTPUT_DIRECTORY`
|
||||
uses an isolated downloaded Linux CLI and Ubuntu roots, private run-owned Windows
|
||||
artifacts and unique tasks. It covers managed foreground
|
||||
hosting, real health loss/recovery, guard/action loss and OS backstop, OFF survival
|
||||
without Windows clients, adoption, crash/stop/restart and native disabled behavior.
|
||||
It removes its tasks/service/Linux roots, preserves distribution lifetime under the
|
||||
user's control and compares the shared Windows service registration fingerprint.
|
||||
Stopped-distro startup requires a separately prepared, verified disposable distro.
|
||||
Use an expendable Ubuntu distro
|
||||
with the fixture's explicit account; real login/reboot and desktop packaging remain
|
||||
host-release gates. The forwarding diagnostic check injects a failed Windows
|
||||
discovery against real healthy Linux status. Physical forwarding disablement, an
|
||||
occupied Windows port, running-distro shutdown and the Recovery kill/timeout/Start
|
||||
refusal combinations have separate OS qualification gates.
|
||||
|
||||
## Stored-history migration
|
||||
|
||||
`scripts/test-opencode-history-migration.mjs OLD_CLI TARGET_CLI` seeds synthetic
|
||||
|
|
|
|||
|
|
@ -172,9 +172,10 @@ spec validation, package identity and removal edit).
|
|||
WSL performs bounded inspect/read/create/chmod/sync/rename operations inside the
|
||||
selected distro, retaining native modes such as `0600`. Shell helpers prefer portable fallbacks (`realpath`,
|
||||
`sync`, `sha256sum`/`shasum`); Ubuntu remains the tested distro for minimal busybox
|
||||
images. A toggle costs four unpooled `wsl.exe` cold starts (`inspectMany`, `load`,
|
||||
`prepare`, `commit`; cleanup only after a failed commit): roughly 2–8 s of
|
||||
filesystem time before daemon RPCs on slow hosts.
|
||||
images. A toggle uses five unpooled `wsl.exe` starts (`inspectMany`, `load`,
|
||||
`prepare`, `commit`, `dispose`); batch edits add explicit source/nonce verification
|
||||
while both physical locks remain held. Filesystem latency remains visible before
|
||||
daemon RPCs on slow hosts.
|
||||
|
||||
Never add a server display cache: every read and mutation reissues `config.get` +
|
||||
`plugin.list` (plus one `location.get` for session-scoped locations) and
|
||||
|
|
@ -206,6 +207,29 @@ blocked write fails closed. The route never accepts a caller-provided config pat
|
|||
and refuses foreign locations, malformed runtime inventory, or IDs absent from the
|
||||
authorized runtime/configured inventory.
|
||||
|
||||
Native settings share `readConfigDocuments` / `editConfigDocument`; the editor
|
||||
receives the current acquired configuration entries and native target position.
|
||||
Missions settings request strict document acquisition, rejecting JSON and JSONC
|
||||
coexistence in the same layer; plugin source edits retain the selected target
|
||||
and propagate their deadline signal through acquisition and commit. Missions'
|
||||
`native-experimental-settings.ts` edits only schedules/depth, retaining explicit
|
||||
companion values. Depth writes preserve inherited schedule activation when the
|
||||
local block omits its flag. Schedule activation materializes inherited depth
|
||||
only when creating a whole-block override. Adding schedules to an existing block
|
||||
keeps its effective depth choice, including the native default selected by an
|
||||
absent depth key. Existing file-byte expectations and the
|
||||
physical replacement lock fence concurrent writers.
|
||||
|
||||
`editConfigDocuments` prepares and verifies each required target before its first
|
||||
rename. The Missions activation journal records the plan before effects, an
|
||||
attempt before each rename, and each exact acknowledgement. A read-only target
|
||||
or conflicting source refuses preparation; a partial global/project write keeps
|
||||
its durable receipts and preserves the successful write. An uncertain attempted
|
||||
write is reconciled only by its exact result bytes, preserving a later explicit
|
||||
OFF. `native-schedule-settings-activation.ts` acknowledges readiness only after
|
||||
the native global and effective Location blocks both enable schedules, within
|
||||
ten seconds, under the acquired connection and host/distro authority.
|
||||
|
||||
OpenCode watches these configuration roots. Never restart the daemon or report a
|
||||
successful file write as an active plugin: the mutation response publishes the
|
||||
durable configured state, and `config.updated` and `plugin.updated` trigger
|
||||
|
|
|
|||
|
|
@ -9,6 +9,18 @@ server awaits `session.environment({ sessionID, variables })` before forwarding.
|
|||
|
||||
- Both writes use the connection acquired after ownership and worktree-fence
|
||||
admission; a stale connection or disconnected request prevents forwarding.
|
||||
- Ordinary session mutations first use `server/mission-schedule-input-guard.ts`
|
||||
to point-read `codenomad.missions.scheduleCarrierRead` at the session's admitted
|
||||
Location. Durable current/retiring carrier membership and reserved carrier
|
||||
metadata block ordinary sends, compaction, forks, imports and session edits.
|
||||
The `ses_cn_schedule_` session-ID namespace is reserved for owned technical
|
||||
controls, including when mutable native markers have been removed.
|
||||
Imports validate metadata throughout nested sources; supplied create/import
|
||||
IDs retain their existing native Location. The proxy revalidates that Location
|
||||
after the read, before environment or other native writes. A structured
|
||||
`rpc.method_not_found` preserves journal-only plugin compatibility; unknown
|
||||
ownership stays blocked. Family moves, worktree evacuation and temporary discard
|
||||
check their resolved session targets before their first destructive effect.
|
||||
- `WorkspaceManager.getSessionEnvironment` reads current settings for each send.
|
||||
- Bundled Missions also applies this snapshot before its assignment prompts and
|
||||
report synthetics, through the authenticated desktop bridge's narrow mission
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ fence. It pins `codenomad.session-pruning`, the method and runtime location; the
|
|||
instance proxy blocks arbitrary RPC. Server authentication middleware protects
|
||||
these routes.
|
||||
|
||||
Ordinary single-message and batch pruning apply the shared Mission schedule input
|
||||
guard before native content writes. Technical carrier sessions stay read-only
|
||||
through these brokers; their preview and history reads remain available.
|
||||
|
||||
After commit the caller re-reads native history, never a synthesized remainder.
|
||||
`rpc.codenomad.session-pruning.pruned` contains session/message IDs and the content
|
||||
revision. The UI invalidates both message-load authority and its native SDK transcript,
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ Settings rows and actions, the native Preferences window and its project context
|
|||
|
||||
## Agent conventions
|
||||
|
||||
- Shared service supervision uses the authenticated `GET|POST /api/opencode/supervision` routes and `ServiceSupervisionSettings`, independently of project selection. Its resolver consumes the exact selected native or Windows-to-WSL runtime namespace; unavailable or ambiguous hosts fail closed. Controls pin service key/revision and revalidate a live human; reads are observation-only. The service-wide manifest and durable guard artifacts live outside CodeNomad profiles. `recoveryNextTickAt` is the independently verified OS Recovery date, while `retryAt` is a live guard's backoff. Frontend presentation consumes the shared API types and localized fixed cause codes. Runtime, storage and platform gates are in [OPENCODE_RUNTIME_SETUP.md](OPENCODE_RUNTIME_SETUP.md#service-supervision-primitives-and-native-adapters).
|
||||
|
||||
- Settings → OpenCode's `ServiceSupervisionCard` presents the shared preference with the lone-setting switch, protection state, fixed localized cause, responding-service distinction and known attempt/retry/recovery dates. Status, errors and actionable recovery information stay visible. The shared focusable **i** tip beside the label, available on hover, keyboard focus and touch, explains the service-wide scope, default-ON behavior, OFF leaving the service running, Linux lingering's effect on every enabled user service, login scope and OS sleep limits. `useServiceSupervision` reads on section demand, visibility restoration, reconnect and explicit refresh, coalesces trailing reads and fences selection/unmount responses. Passive failures retain the last successful display and disable writes until a successful read. In-flight and uncertain controls survive card remounts in per-window memory, keyed by service and exact request; status reads reconcile receipts and only an explicit original-action retry can resend. A known revision conflict triggers a read and review message. Deferred linger rollback remains visible with explicit Retry. Browser regression: `tests/browser/service-supervision.test.ts`; all ten locale cause/placeholder contracts: `src/lib/i18n/service-supervision.test.ts`.
|
||||
|
||||
- Settings → Providers separates Models, Web search and Images into three shared `.settings-disclosure` groups (no icons): Models starts open, Web search and Images folded, and a folded group mounts nothing, so its connection metadata, configuration contract and image-model catalog are read only once opened. Models and Web search each start with a provider selector, Connect action and configured-provider list; Web search then shows its global default and project override. Images (`opencode/image-settings.ts`, `settings/image-settings-card.tsx`, `styles/components/image-settings.css`) edits the Global document's `experimental.image_generate` and `experimental.media_blob`. OpenCode reads `image_generate` per field from the highest-priority document that sets it (unset means on, with the automatic model; only `false` turns the image tool off), but `media_blob` (like `subagent_depth`) from the whole `experimental` object of the highest-priority document that has one, so writing a project block would hide inherited experimental settings. The document that decides each value is read from the native configuration entries and reported under its control when it is not the Global one. When the selected Location's own project document (the project write target) decides `media_blob` through its existing `experimental` object, the snapshot names it under `mediaStorage.project` instead, the box shows the effective value and saving also writes `media_blob` there; the edit is refused if that object has disappeared, because creating one would hide inherited settings. The Missions depth editor's Inherit removes a project object left holding only that `media_blob` when it equals the Global one (`dev-docs/MISSIONS.md`); Off is written as `false`, because removing the key would expose a lower-priority document that enables it. When the fresh Global write target has no `experimental` object while a lower-priority document has one, creating it would hide that whole object (subagent depth, todos) from every Location without its own, even when the selected Location overrides it, so the group is read-only and the backend refuses the write; never copy the inherited values into the Global document instead. Each control appears only when the connected daemon's `Config.InfoEncoded` declares its key, because the native `experimental` object rejects unknown keys; never write either key without that capability. Image models come from native `GET /api/model?types=image` for the selected Location. Reuse provider rows, selectors, inputs and action styles; do not add an API-key disclosure or a separate card design. Search-only integrations stay out of the Models group. Web search configuration lives entirely here. Read connection metadata while this Settings surface is visible, never credential secret values; keep API-key entry explicit and clear drafts on context changes. Use the authorized Global/Project document operations shared with plugin controls and `opencode/native-setting-document.ts` for targeted JSONC edits. Preserve unrelated fields, native discovery roots, WSL atomic writes and conflict/connection/deletion fences. Group/search styling lives in `styles/components/websearch-settings.css`; credentials use native integration methods and remain global.
|
||||
- Stats preferences explicitly aggregate the entire connected OpenCode service via `opencode/service-usage.ts`. Reserve the Usage label for the provider subscription panel. Native project IDs span independent clones, so never label native `session.stats` as physical-folder/Git-family scoped. Keep connection/workspace fences, `tools: "none"` and periods bounded to 366 days; never load transcripts for statistics or imply billing/quota precision. Last snapshots and period choices restore through backend-owned `state.ui.serviceUsage` (eight each/1 MiB, validated service/channel/folder identity), never a Preferences writer to main-window client state or a workspace-UUID cache. Native aggregation must not await optional discovery/cache reads; cached responses cannot replace fresh data. Backend authorized admission order, not completion order or client time, chooses the period across windows; optional publication is bounded and shutdown-fenced. Keep last-known-owner totals explicitly stale through disconnect/errors and revoke them on verified owner/discovery changes. Read while mounted and document-visible; hide cancels queued/trailing demand, activation revalidates once and terminal hints use the shared activity cooldown. Never treat document visibility as native minimize/occlusion detection or equate blur with hidden. See `dev-docs/CACHE_REFRESH_CONVENTIONS.md` for the remaining native scope. Styles live in `styles/components/usage-dashboard.css`.
|
||||
- Settings rows keep only a title unless a caption is essential; secondary explanations go behind `components/settings/settings-info-tip.tsx` (a focusable "i" with a hover/focus tooltip that a tap toggles on touch screens, a span so it never becomes a row label's control or toggles it), styled in `styles/components/settings-info-tip.css`. Behavior settings opt in with `infoKey` instead of `subtitleKey`. Folded settings groups use the one `.settings-disclosure` (`styles/components/settings-disclosure.css`; a summary may hold an inline heading that keeps the summary's look): Missions gathers every agent/model choice under "Agents and models" with the current request in its summary, and Advanced ends with the environment-variable editor folded under a single title.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode liefert den Motor. CodeNomad liefert das Cockpit. Es verbindet sich mit
|
|||
|
||||
## Lange Arbeit an Missionen übergeben {#missions}
|
||||
|
||||
Beschreiben Sie die Arbeit einmal. Der Koordinator der Mission plant sie als Aufgabenbaum, delegiert begrenzte Aufgaben an native OpenCode-Unterhaltungen und meldet Ergebnisse mit ihren Belegen. Führen Sie eine Mission einmal oder täglich aus; eine Mission zu erstellen startet keine Arbeit, bevor Sie sich dafür entscheiden. Playbooks decken Fehlerbehebung, Produktauslieferung und den Vergleich zweier Modelle mit einem blinden Gutachter ab. Siehe [Missionen](missions.md).
|
||||
Beschreiben Sie die Arbeit einmal. Der Koordinator der Mission plant sie als Aufgabenbaum, delegiert begrenzte Aufgaben an native OpenCode-Unterhaltungen und meldet Ergebnisse mit ihren Belegen. Führen Sie Arbeit einmalig, zu gewählten Zeiten oder Intervallen oder nach einer bestätigten Projektänderung aus; Erstellen speichert eine pausierte Mission für Ihren ausdrücklichen Start. Playbooks decken Fehlerbehebung, Produktauslieferung und den Vergleich zweier Modelle mit einem blinden Gutachter ab. Siehe [Missionen](missions.md).
|
||||
|
||||
## Vom Smartphone aus weiterarbeiten {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ Das Arbeitsbereichs-Stammverzeichnis begrenzt, welche Ordner CodeNomad öffnet;
|
|||
| Über Checkouts und Unterhaltungen hinweg arbeiten | [Projekte](projects.md), [Unterhaltungen](conversations.md), [Dateien](files.md) |
|
||||
| Verlauf finden oder sicher bereinigen | [Verlauf](history.md) |
|
||||
| Native Fragen und Berechtigungsanfragen beantworten | [Berechtigungen](permissions.md) |
|
||||
| Begrenzte oder tägliche Arbeit koordinieren | [Missionen](missions.md) |
|
||||
| Begrenzte oder geplante Arbeit koordinieren | [Missionen](missions.md) |
|
||||
| Ausführung und Integrationen konfigurieren | [OpenCode](opencode.md), [Anbieter](providers.md), [Integrationen](integrations.md) |
|
||||
| Weiteres Gerät verbinden | [Fernsteuerung](remote-control.md), [Sicherheit](security.md), [Mobil](mobile.md) |
|
||||
| Arbeitsbereich anpassen | [Einstellungen](settings.md), [Tastenkürzel](shortcuts.md), [Sprache](voice.md), [Erweiterungen](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ Missionen verbinden einen dauerhaften Projektplan mit nativen OpenCode-Unterhalt
|
|||
|
||||
1. Öffnen Sie **Missionen** im rechten Panel und wählen Sie **Mission erstellen**. Das Formular ersetzt die Missionsliste und die Aufgaben der ausgewählten Mission, bis Sie erstellen oder abbrechen.
|
||||
2. Füllen Sie **Was soll die Mission tun?** aus und prüfen Sie **Titel**, der aus dem ersten Satz vorbelegt wird (bis 60 Zeichen).
|
||||
3. Wählen Sie unter **Wann** entweder **Einmal** oder **Täglich um**. Prüfen Sie für tägliche Arbeit die angezeigte Zeitzone; **Ändern** erlaubt eine IANA-Zone.
|
||||
3. Lassen Sie **Zeitplanung** für **Einmal, sofort** und **Ohne Bedingung** eingeklappt oder klappen Sie sie auf, um Häufigkeit, Bedingung und optionale Grenzen zu wählen. Prüfen Sie **Gespeicherte Zeitzone (IANA)** und die Vorschau, bevor Sie geplante Arbeit erstellen.
|
||||
4. Klappen Sie **Optionen** für **Playbook**, **Notizen**, **Agenten und Modelle**, **Aufgabensitzungen**, die **Verschachtelungstiefe der Untersitzungen** des Projekts, gespeicherte Aufträge und optional verfolgte Unterhaltungen auf. Mit **Zwei Modelle vergleichen** erscheinen die Auswahlfelder **Modell A** und **Modell B** im Hauptformular.
|
||||
5. Wählen Sie **Erstellen**. Wählen Sie den neuen Eintrag und verwenden Sie bei Bereitschaft die separate ▶-Aktion: **Mission starten** für einmalige Arbeit oder **Zeitplan {id} starten** für einen täglichen Zeitplan.
|
||||
5. Wählen Sie **Erstellen**. Wählen Sie den pausierten Eintrag, prüfen Sie ihn und verwenden Sie bei Bereitschaft ▶ **Starten**. Sofortige, bedingungslose Arbeit startet eine Mission; eine verzögerte, wiederkehrende oder bedingte Regel aktiviert die native Zeitplanung.
|
||||
|
||||
**Erstellen startet niemals Arbeit.** Eine einmalige Mission ist vorbereitet; ein täglicher Zeitplan pausiert. Weder Erstellen noch das Speichern eines Auftrags aktiviert einen Timer. Die Erstellung gilt für das ausgewählte Projekt/die Location und friert angeforderte Profile sowie die Aufgabensitzungsrichtlinie ein. Validierung steht neben dem betroffenen Feld; eine Verweigerung mit dem Hinweis, dass nichts gesendet wurde, lässt den Entwurf zur Korrektur verfügbar. Der Koordinator einer einmaligen Mission kann auch eine separate Mission für Sie vorbereiten, etwa eine Folgemission: Sie erscheint in der Liste als **Vorbereitet** und startet wie jede Mission nur über ihre ▶-Aktion.
|
||||
**Erstellen speichert eine pausierte Mission.** Erst ▶ **Starten** aktiviert Arbeit und native Kalender; Erstellen oder Speichern eines Auftrags erhält die Einstellung zur Überwachung des gemeinsamen Dienstes. Die Erstellung gehört zum ausgewählten Projekt/zur ausgewählten Location und erfasst die angeforderten Profile und die Aufgabensitzungsrichtlinie. Validierung steht neben dem betroffenen Feld; eine Ablehnung mit dem Hinweis, dass nichts gesendet wurde, erhält den Entwurf zur Korrektur. Ein einmaliger Koordinator kann auch eine separate Folgemission vorbereiten: Sie erscheint als **Vorbereitet** und startet über ihre eigene ▶-Aktion.
|
||||
|
||||
Gespeicherte einmalige Missionen behalten ihre Ausführungssteuerung, ihren Aufgabenbaum, ihre Unterhaltungen und ihre Abschlussberichte. Bestehende tägliche Zeitpläne bleiben mit ihren täglichen Steuerelementen und [täglichen Zeitregeln](#existing-daily-schedules) aufgelistet. Wählen Sie den Eintrag und verwenden Sie die für dieses Objekt verfügbaren Steuerelemente.
|
||||
|
||||
### Playbooks {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@ Ergebnisse gehen auf demselben Weg zurück. Ein nativer Subagent gibt sein Ergeb
|
|||
|
||||
Eine spätere Aufgabe baut auf zwei Wegen auf früherer Arbeit auf. Abhängigkeiten ordnen die Arbeit: Der Koordinator startet eine Aufgabe, sobald die Aufgaben, von denen sie abhängt, erledigt sind, und gibt die relevanten Ergebnisse in den Brief oder neben die Zuweisung; unabhängige Sitzungen können diese Berichte auch in der Karte lesen. Um die eigene Unterhaltung einer erledigten Aufgabe fortzusetzen, erklärt der Koordinator eine Folgeaufgabe, die sie wiederverwendet: Die Folgeaufgabe setzt dort mit dem aktuellen Kontext dieser Unterhaltung fort, den OpenCode möglicherweise [komprimiert](history.md#compact-session) hat, während frühere Einzelheiten für die Suche im gespeicherten Verlauf bleiben. Das Playbook Fehlerbehebung gibt so beide Prüfungen an den Implementierer zurück. Mit **Unabhängige Sitzungen** kann der Koordinator eine Aufgabe auch einer bestehenden Hauptunterhaltung zuweisen, die ihren Kontext behält, einschließlich früherer, nicht zugehöriger Arbeit. Andernfalls beginnt eine Aufgabe in einer neuen Unterhaltung mit ihrer Zuweisung, und der Verlauf einer anderen Aufgabe erreicht sie über den Koordinator oder eine Suche. Bei **Zwei Modelle vergleichen** lassen Zuweisung und Kontext des Gutachters Ziel und Notizen weg; sein Brief enthält die anonymisierten Ergebnisse.
|
||||
|
||||
In einem täglichen Zeitplan beginnt jeder Lauf mit eigenem Koordinator und eigener Karte. Seine erste Nachricht enthält die Anweisungen des Zeitplans und den neuen Benutzer- und Assistententext seiner [verfolgten Unterhaltungen](#daily-schedules) seit dem letzten abgeschlossenen Lauf; Ergebnisse früherer Läufe bleiben im Verlauf des Zeitplans. Innerhalb eines Laufs gestartete Unterhaltungen können die Karte dieses Laufs lesen. Auch eine neue einmalige Mission beginnt mit einer neuen Karte: Um Ergebnisse weiterzugeben, schreiben Sie sie in ihr Ziel oder ihre **Notizen**, oder lassen Sie den Koordinator eine Folgemission vorbereiten, deren Ziel und Notizen er schreibt.
|
||||
Jeder geplante Durchlauf beginnt mit eigenem Koordinator und eigener Missionskarte, anhand seiner gespeicherten Anweisungen und festgehaltenen Eingaben. Verfolgte Unterhaltungen liefern begrenzten neuen Benutzer- und Assistententext; ihre Verbrauchszeiger werden nach einem erfolgreichen Durchlauf weitergesetzt. Frühere Durchläufe bleiben im Verlauf des Zeitplans. Innerhalb eines Durchlaufs gestartete Unterhaltungen können dessen Karte lesen. Eine neue einmalige Mission hat eine separate Karte: Schreiben Sie weiterzugebende Ergebnisse in ihr Ziel oder ihre **Notizen** oder bitten Sie den Koordinator, eine Folgemission mit diesem Kontext vorzubereiten.
|
||||
|
||||
### Frühere Unterhaltungen durchsuchen {#context-search}
|
||||
|
||||
Solange CodeNomad läuft, hat jede Agentenunterhaltung im Projekt, auch Missionskoordinatoren und Aufgaben, das Sitzungssuchwerkzeug von CodeNomad (`codenomad.session_search`, genutzt über den Code Mode von OpenCode). Es durchsucht die gespeicherten Unterhaltungen des Projektordners der Unterhaltung mit derselben Rangfolge wie die [Suche](history.md#full-history-search) **Gesamter Arbeitsbereich** und liefert kurze Auszüge mit Titel und Kennung jeder Unterhaltung sowie einen Weg, eine ganze Unterhaltung zu lesen. Es gelten die nativen Werkzeugberechtigungen. Ein Agent sucht, wenn er sich dafür entscheidet oder wenn Ziel, Notizen oder Ihre Nachricht ihn darum bitten, etwa um herauszufinden, was eine frühere Mission oder Aufgabe ergeben hat. Ein täglicher Lauf, der bei geschlossenem CodeNomad ausgeführt wird, arbeitet ohne dieses Werkzeug.
|
||||
Solange CodeNomad läuft, hat jede Agentenunterhaltung im Projekt, auch Missionskoordinatoren und Aufgaben, das Sitzungssuchwerkzeug von CodeNomad (`codenomad.session_search`, genutzt über den Code Mode von OpenCode). Es durchsucht die gespeicherten Unterhaltungen des Projektordners der Unterhaltung mit derselben Rangfolge wie die [Suche](history.md#full-history-search) **Gesamter Arbeitsbereich** und liefert kurze Auszüge mit Titel und Kennung jeder Unterhaltung sowie einen Weg, eine ganze Unterhaltung zu lesen. Es gelten die nativen Werkzeugberechtigungen. Ein Agent sucht, wenn er sich dafür entscheidet oder wenn Ziel, Notizen oder Ihre Nachricht ihn darum bitten, etwa um herauszufinden, was eine frühere Mission oder Aufgabe ergeben hat. Ein geplanter Durchlauf, der bei geschlossenem CodeNomad ausgeführt wird, arbeitet ohne dieses Werkzeug.
|
||||
|
||||
## Profile, Delegation und Tiefe {#profiles}
|
||||
|
||||
Das Speichern einer Tiefe erhält aktivierte native Zeitpläne in der Projektkonfiguration. Ein ausdrückliches `schedules:false` im Projekt bleibt deaktiviert; das Entfernen der Tiefe erhält die Zeitplaneinstellung. Gleichzeitige JSON- und JSONC-Dateien müssen vor dem Speichern nativer Einstellungen bereinigt werden.
|
||||
|
||||
1. Verwenden Sie das Missionszahnrad oder **Einstellungen → Missionen**, um **Vorgaben für neue Missionen** zu bearbeiten.
|
||||
2. Wählen Sie unter **Anwenden auf** entweder **Alle Missionen** oder ein Playbook und legen Sie dann **Aufgabensitzungen** sowie unter **Agenten und Modelle** eine Zeile pro Rolle fest (Koordinator, dann die Rollen des Playbooks). Jede Auswahl wird sofort gespeichert; es gibt keine Speichern-Schaltfläche.
|
||||
3. In einem Playbook-Bereich zeigen die Zeilen die globalen Werte mit der Kennzeichnung **Global**. Das Ändern eines Felds passt diese Rolle ausgehend von ihnen an; ihr Zurücksetzen-Symbol verwendet wieder die globalen Werte. **Zurücksetzen** (nach Bestätigung) löscht nur die Agenten und Modelle des angezeigten Bereichs.
|
||||
|
|
@ -118,7 +122,7 @@ Solange CodeNomad läuft, hat jede Agentenunterhaltung im Projekt, auch Missions
|
|||
| Einstellung | Geltungsbereich und Wirkung |
|
||||
| --- | --- |
|
||||
| Vorgaben für neue Missionen | Konfigurationsweite Vorgaben nur für **neue** Missionen; eine Playbook-Ausnahme übersteuert den allgemeinen Standard. |
|
||||
| Agent / Modell / Denken | Bei Erstellung erfasste Koordinator-/Rollenauswahl; ein beschäftigter Akteur wird für spätere Arbeit nicht stillschweigend umgestellt. Tägliche Läufe verwenden ihre eingefrorenen angeforderten Profile erneut. |
|
||||
| Agent / Modell / Denken | Bei Erstellung erfasste Koordinator-/Rollenauswahl; ein beschäftigter Akteur wird für spätere Arbeit nicht stillschweigend umgestellt. Geplante Durchläufe verwenden ihre gespeicherten angeforderten Profile; Änderungen gelten für künftige Durchläufe. |
|
||||
| Native Subagenten | Aufgabensitzungen verwenden native subagent/all-Agenten unter dem Koordinator. |
|
||||
| Unabhängige Sitzungen | Erklärte Aufgabensitzungen verwenden primary/all-Agenten und unabhängige Unterhaltungen; dies deaktiviert keine internen nativen Helfer. |
|
||||
| Verschachtelungstiefe der Untersitzungen | Projektkonfiguration für **alle** dortigen OpenCode-Unterhaltungen, nicht nur Missionen. Leer bedeutet Vererbung: **Erben** entfernt die Projektüberschreibung und einen dadurch leer gewordenen `experimental`-Block oder einen, der nur noch den globalen Wert von **Bilder nur einmal speichern** enthält. OpenCode liest den `experimental`-Block des Projekts als Ganzes, sodass dort verbleibende andere experimentelle Einstellungen die globale Tiefe weiterhin verdecken. OpenCodes eingebauter **General**-Unteragent kann standardmäßig keine Unteragenten starten, daher verschachteln sich seine Unterhaltungen nie: Das Speichern einer beliebigen Projekttiefe fügt derselben Datei außerdem eine Regel hinzu, die Unteragenten erlaubt, bis zu dieser Tiefe eigene Unteragenten zu starten, und Vorrang vor Einschränkungen von General aus anderen Konfigurationsdateien hat (bei einer Tiefe von 1 verhindert OpenCodes Tiefenbegrenzung die Verschachtelung weiterhin); **Erben** entfernt sie. Unter **Effektive OpenCode-Tiefe** zeigt der Editor an, ob sich General-Unteragenten verschachteln können. Definiert das Projekt General im älteren Format mit einem einzelnen `permission`-Wert oder mit bereits gesetztem `task` und `subagent`, wird nur die Tiefe gespeichert und der Editor weist darauf hin: Erlauben Sie Unteragenten für General selbst in dieser Datei. Speichern lädt OpenCode nicht neu und ändert keine laufenden Unterhaltungen. |
|
||||
|
|
@ -137,9 +141,51 @@ Eine abgelehnte oder unbestätigte Auswahl wird nie erneut gesendet: Die gespeic
|
|||
|
||||
Aufträge speichern Anweisungen und angeforderte Auswahl, keine Aufgabenpläne, Ergebnisse, Unterhaltungen, Zeitplanuhren oder Ausführungsbefugnis. Laden füllt einen normalen Erstellungsentwurf; es startet oder plant nichts. Entfernen eines Auftrags behält bestehende Missionen/Unterhaltungen. **Einstellungen → Missionen → Gespeicherte Aufträge** verwaltet die Bibliothek; wird die Bibliothek unlesbar, entfernt **Gespeicherte Bibliothek zurücksetzen** (nach Bestätigung) gespeicherte Aufträge, keine Ausführungen. Zeitpläne selbst haben keine Aktion zum Speichern als Auftrag.
|
||||
|
||||
## Tägliche Zeit, Nachholen und verfolgte Unterhaltungen {#daily-schedules}
|
||||
## Zeitplanung, Nachholen und verfolgte Unterhaltungen {#daily-schedules}
|
||||
|
||||
Tägliche Zeitpläne verwenden die gespeicherte IANA-Zeitzone und Ortszeit, kein verstrichenes 24-Stunden-Intervall. Zeiten werden in der Zeitplanzone angezeigt und bei Abweichung von der Betrachterzone gekennzeichnet. Jeder Lauf erhält eigenen Koordinator und isolierte Missionskarte; der ausgewählte Zeitplan zeigt aktuellen Aufgabenbaum und Datums-/Ergebnisverlauf. Wählen Sie einen vergangenen Lauf zur zentralen Anzeige. Der Verlauf ist auf 30 Ergebnisse begrenzt.
|
||||
### Native Zeitplanung {#native-scheduling}
|
||||
|
||||
Die **i**-Symbole neben den Beschriftungen erklären die Auswahl: per Mauszeiger, Tastaturfokus oder Antippen.
|
||||
|
||||
Klappen Sie **Zeitplanung** beim Erstellen oder Bearbeiten auf. **Häufigkeit** bietet:
|
||||
|
||||
| Auswahl | Zeitpunkt |
|
||||
| --- | --- |
|
||||
| Einmal, sofort | Ein Start über ▶ **Starten**, mit einer optionalen Bedingung, die wie unten beschrieben geprüft wird. |
|
||||
| Einmal, an einem Datum | Ein gespeichertes Datum mit Uhrzeit in der IANA-Zone; ein beim Start bereits vergangenes Datum wird abgelehnt. |
|
||||
| Einmal, nach einer Wartezeit | Eine positive ganzzahlige Dauer in Minuten oder Stunden, beginnend beim Start. |
|
||||
| In einem Intervall | Mindestens 15 Minuten; die erste Aktivierung erfolgt ein Intervall nach dem Start. |
|
||||
| Jeden Tag | Eine Ortszeit in der gespeicherten Zone. |
|
||||
| Ausgewählte Wochentage | Eine Uhrzeit an den markierten Wochentagen. |
|
||||
| Mehrmals täglich | Bis zu acht unabhängige Uhrzeiten, etwa 09:15 und 17:45; doppelte Uhrzeiten teilen sich einen Durchlauf. |
|
||||
| Tag jedes Monats | Tag 1–31 zur gewählten Uhrzeit; Monate ohne diesen Tag werden übersprungen. |
|
||||
| Erweitert (cron) | Ein nativer Cron-Ausdruck, höchstens einmal pro Minute. |
|
||||
|
||||
**Nächste Termine (bis zu fünf)** zeigt eine Vorschau mit gespeicherter Zone und angenommenem Startzeitpunkt. Mit einer Bedingung zeigt **Nächste Prüfungen (bis zu fünf)** die Prüfungen; ein Durchlauf hängt von einer bestätigten Änderung ab. Ein unverändertes gespeichertes Intervall behält seine native Phase. Eine pausierte oder unbekannte Phase wird als nicht verfügbar angezeigt, bis ihr nativer Zustand gelesen werden kann. Wartezeiten und Intervalle nutzen verstrichene Zeit; Ortszeitkalender folgen der gespeicherten Zone und den Sommerzeitregeln von OpenCode. Die angezeigten Daten sind Prognosen, abhängig von Dienst- und Hostverfügbarkeit.
|
||||
|
||||
Nach einem Ausfall liefert die native Zeitplanung höchstens einen nachgeholten Durchlauf, bevor sie zu künftigen Terminen zurückkehrt. Sie nutzt den gespeicherten nativen Fälligkeitstermin, der vor später verpassten Terminen liegen kann. Mehrere tägliche Uhrzeiten teilen sich dieses Nachholen und denselben Überschneidungsschutz. Ein ungeklärter Durchlauf wird unter seinen ursprünglichen Unterhaltungs- und Nachrichtenidentitäten abgeglichen, bevor ein weiterer starten kann. Das Fortsetzen einer ausdrücklich pausierten Wiederholung nutzt ihren nächsten zukünftigen Termin; ein pausierter einmaliger Start kann einmal verspätet eintreffen.
|
||||
|
||||
### Bedingungen und Quellen {#scheduling-conditions}
|
||||
|
||||
Wählen Sie eine optionale **Bedingung** und ihre **Quelle in diesem Projekt**: **Ein anderer Zeitplan ist erfolgreich**, **Neue Commits auf einem lokalen Branch**, **Eine einmalige Mission wird erfolgreich abgeschlossen**, **Neue Nachricht in einer verfolgten Unterhaltung** oder **Änderung expliziter Dateien**. Für eine Bedingung allein behalten Sie **Einmal, sofort**: Native Prüfungen erfolgen alle 15 Minuten, bis ihr einziger Durchlauf startet. Mit einem Kalender wird die Bedingung zu dessen Terminen geprüft. Wiederkehrende Bedingungen können nach neuen passenden Änderungen erneut starten.
|
||||
|
||||
Der erste Start legt die Ausgangsreferenz fest. Fortsetzen erhält sie, einschließlich nach einer Pause beobachteter Änderungen. Erfolgsquellen erfordern ein bestätigtes erfolgreiches Ergebnis und eine abschließend geklärte native Familie. Branchprüfungen nutzen lokale Commits und deren Abstammung ohne Abruf vom entfernten Repository. Unterhaltungsprüfungen warten auf vollständige, stabile neue Nachrichten und schließen technische Aktivierungen und die eigenen Unterhaltungen des Durchlaufs aus. Dateien verwenden bis zu 128 explizite projektbezogene relative Pfade, einen pro Zeile; rekursive Muster und verknüpfte Pfade werden abgelehnt. Eine fehlende, verschobene, mehrdeutige oder zu große Quelle blockiert die Zulassung mit einer Erklärung. Änderungen zwischen Prüfungen benötigen eine dauerhafte Spur in der Quelle, um erkannt zu werden.
|
||||
|
||||
Zusammen beobachtete Änderungen werden gebündelt. Ein Durchlauf hält die beobachteten Quellenbelege fest; ein erfolgreicher Abschluss verbraucht diesen Schnappschuss und lässt spätere Änderungen für eine künftige Prüfung übrig. Ein fehlgeschlagener Durchlauf behält seinen Quellenprüfpunkt und erfordert einen Eingriff. **Verfolgte Gespräche (optional)** unter **Optionen** liefert Kontext unabhängig von der Wahl einer Bedingung für Unterhaltungsnachrichten.
|
||||
|
||||
### Enden und Zählungen {#scheduling-limits}
|
||||
|
||||
Lassen Sie Grenzen für eine fortlaufende Wiederholung leer. **Letzter erlaubter Termin** und **Letzter Termin nach dieser Anzahl Stunden seit dem ersten Start (optional)** begrenzen zulässige Termine; die früheste Grenze gilt. Die Dauer beginnt beim ersten Start und läuft während einer Pause weiter. Ein vor dem Ende gespeicherter Termin kann nach einem langen Ausfall verspätet eintreffen. Ein bereits zugelassener Durchlauf bleibt bis zu seinem Ergebnis beobachtbar.
|
||||
|
||||
**Maximale gestartete Durchläufe** ist eine gemeinsame Gesamtzahl für alle Uhrzeiten und **Jetzt ausführen**. Ein Durchlauf zählt einmal bei Annahme der ersten Eingabe seines Koordinators, auch wenn er später fehlschlägt. Prüfungen, unveränderte Bedingungen und abgelehnte Aktivierungen verbrauchen keinen Platz; eine unbestätigte Zulassung hält ihren Platz bis zum Abgleich. Einmalige Regeln erlauben höchstens einen Durchlauf. Verbrauchte Plätze bleiben bei Änderungen und Neustarts erhalten; unveränderte Endgrenzen behalten ihren Ursprung beim ersten Start und ihr absolutes Terminende. Eine Änderung des Maximums ändert die Gesamtzahl über die gesamte Laufzeit; eine Gesamtzahl kleiner oder gleich der bereits verbrauchten Anzahl beendet den Zeitplan. Ein Häufigkeitswechsel kann beim Speichern eine neue Intervallphase beginnen, wie der Editor erklärt.
|
||||
|
||||
Der ausgewählte native Zeitplan zeigt **Gewünschter Zustand**, **Nativer Kalender**, **Nächste Aktivierung** oder **Nächste Prüfung**, **Prüfungen** mit **Erfasst seit…**, **Gestartete Durchläufe**, **Erfolgreiche Durchläufe**, sein Ende und **Frühere Läufe**. Lesen Sie einen früheren Durchlauf über **Unterhaltung öffnen**. **Zeitplan beendet** beschreibt das Ende des Zeitplans; prüfen Sie das Ergebnis des Durchlaufs separat. Ergebnisse behalten die neuesten 30 Einträge; Gesamtzählungen überdauern dieses Fenster. **Weitere Missions laden** lädt die nächste Seite gespeicherter Objekte.
|
||||
|
||||
Die Liste zeigt Bedingung und Ende nur, wenn festgelegt, und **Prüfungen** nur mit einer Bedingung.
|
||||
|
||||
### Bestehende tägliche Zeitpläne {#existing-daily-schedules}
|
||||
|
||||
Bestehende tägliche Zeitpläne verwenden die gespeicherte IANA-Zone und Ortszeit. Jeder Lauf hat eigenen Koordinator und eigene Missionskarte; wählen Sie seinen aktuellen Aufgabenbaum oder ein früheres Datum/Ergebnis zur zentralen Anzeige. Ihr aufbewahrter Verlauf enthält 30 Ergebnisse. Für diese Einträge gelten die folgenden täglichen Regeln:
|
||||
|
||||
| Situation | Planungsverhalten |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@ Tägliche Zeitpläne verwenden die gespeicherte IANA-Zeitzone und Ortszeit, kein
|
|||
| Uhr wird zurückgestellt | Macht einen bereits verarbeiteten Tag nicht erneut fällig. |
|
||||
| Vorhandener Lauf noch offen | Überschneidet ihn nicht mit einem neuen Tageslauf. |
|
||||
|
||||
Auch die Erstellungszeit begrenzt zulässige tägliche Arbeit; der Start eines neuen Zeitplans erlaubt keine Verarbeitung von Daten vor seiner Existenz. **Jetzt ausführen** startet, soweit angeboten, einen separaten manuellen Lauf; es ersetzt weder die Tagesuhr noch umgeht es einen offenen Lauf. Eine angezeigte nächste Zeit ist eine Projektion, keine Garantie minutengenauer Ausführung.
|
||||
Auch die Erstellungszeit begrenzt zulässige tägliche Arbeit. **Jetzt ausführen** startet, wenn bei einem bestehenden täglichen Eintrag angeboten, einen separaten manuellen Lauf unter dem Schutz für offene Läufe und erhält dessen Tagesuhr. Eine angezeigte nächste Zeit ist eine Prognose, abhängig von Dienst- und Hostverfügbarkeit.
|
||||
|
||||
**Verfolgte Gespräche (optional)** ergänzt begrenzten Kontext aus ausgewählten Unterhaltungen. Nur ein abgeschlossener Lauf bewegt deren verarbeitete Nachrichtenzeiger vor; fehlgeschlagene/unterbrochene Läufe verbrauchen diesen Kontext nicht stillschweigend. Überschreiten Anweisungen plus Kontext die Kapazität, kürzen Sie Anweisungen oder entfernen Sie verfolgte Unterhaltungen, statt unbegrenzt gelesenen Verlauf anzunehmen.
|
||||
|
||||
## Lebensdauer und Dienstneustarts {#lifetime}
|
||||
|
||||
Das **i** neben **Technische Details** erklärt technische Aktivität und Kosten. Unter **Einstellungen → OpenCode** bleiben Überwachungsstatus und Fehler sichtbar; allgemeine Erklärungen stehen im **i**.
|
||||
|
||||
Klappen Sie bei einem nativen Zeitplan **Technische Details** auf, um seinen **Hauptträger** und **Abgleichträger**, deren native Kalender und eine etwaige Meldung **Bereinigung wartet auf Abgleich** zu sehen. Diese technischen Unterhaltungen bieten keine Aktion zur Nachrichteneingabe und erscheinen nicht in den üblichen Unterhaltungs- und Quellenauswahlen. Technische Aktivierungen, Titel und Kompaktierung nutzen das lokale Modell ohne HTTP-Anfragen an Anbieter; native Ereignisse und Statistiken können sie dennoch enthalten, während tatsächliche Durchläufe ihre üblichen Modellkosten behalten. Kompaktierung begrenzt den verarbeiteten Kontext bei gleichbleibender Identität der Unterhaltung und des Kalenders. Gespeicherte native Nachrichten und Ereignisse können bis zur ausdrücklichen Bereinigung eines beendeten Zeitplans anwachsen; die 30 aufbewahrten Mission-Ergebnisse sind davon getrennt. Die Tiefeneinstellung und **Zeitplanung** zeigen den Pfad unter **OpenCode-Konfigurationsdatei:**, wenn er verfügbar ist; die Tiefeneinstellung kennzeichnet das **Schreibziel des Projekts** separat.
|
||||
|
||||
| Komponente | Bedeutung des Schließens |
|
||||
| --- | --- |
|
||||
| CodeNomad-Projekt-Tab | Schließt das Projekt in CodeNomad; native Sitzungen laufen in OpenCode weiter. Öffnen Sie das Projekt erneut, um sie wiederzufinden. |
|
||||
| Ganzes Fenster, App beenden oder Renderer neu laden | Bewahrt Projektverbindungen für die Wiederherstellung; stoppt weder native Arbeit noch den OpenCode-Dienst. |
|
||||
| CodeNomad-Backend | Stoppt OpenCode nicht. Eine laufende einmalige Mission läuft weiter: Ihr Koordinator, ihre Aufgaben als native Subagenten und die Missionswerkzeuge laufen in OpenCode. Das Zuweisen von Aufgaben an unabhängige Sitzungen und das Zustellen ihrer Berichte an den Koordinator, vom Koordinator vorbereitete Missionen, das Stoppen der Unterhaltungen einer zurückgezogenen Aufgabe, Wiederherstellung und die Missionszentrale warten auf das Backend, ebenso das Fortsetzen einer einmaligen Mission nach einem OpenCode-Dienstneustart. |
|
||||
| Laufender täglicher Zeitplan | Uhr und Durchlauf laufen im OpenCode-Plugin/-Dienst, auch bei geschlossener CodeNomad-Oberfläche und Backend. Ausführungshost und OpenCode-Dienst müssen weiterlaufen. |
|
||||
| OpenCode-Dienstneustart | Verliert prozesslokale Zeitplanjobs. Ein auf laufend gesetzter Zeitplan wird **Unterbrochen** bis zu einem ausdrücklichen **Fortsetzen**; erneutes Öffnen von CodeNomad aktiviert ihn nicht wieder. |
|
||||
| Aktivierter nativer Zeitplan | Dauerhaft gespeicherte OpenCode-Kalender aktivieren ihr Projekt und führen Durchläufe bei geschlossener CodeNomad-Oberfläche und geschlossenem Backend aus, solange Ausführungshost und Dienst verfügbar sind. |
|
||||
| OpenCode-Dienstneustart — gesunder nativer Zeitplan | Ein gesunder Zeitplan mit gewünschtem Zustand „Aktiv“ setzt seinen Kalender automatisch fort. Offene Arbeit wird vor einer neuen Zulassung unter ihren ursprünglichen Identitäten abgeglichen. |
|
||||
| Bestehender täglicher Zeitplan | Seine Plugin-Uhr läuft bei geschlossenem CodeNomad, solange der Dienst läuft. Ein Dienstneustart lässt ihn **Unterbrochen**; ausdrückliches **Fortsetzen** gleicht seinen offenen Lauf ab und aktiviert seine Tagesuhr erneut. |
|
||||
|
||||
Prüfen Sie nach einem Neustart den offenen Lauf und wählen Sie bei Bereitschaft **Fortsetzen**. Dies gleicht den vorhandenen Lauf ab, ohne die Koordinatoreingabe doppelt zu senden; es garantiert keine Fortsetzung eines unterbrochenen nativen Turns. Ein durch Neustart abgeschnittener Turn kann ohne Bericht enden, mit **Durch Neustart unterbrochen**. Verwenden Sie Neustarts nicht zum Wiederholen von Werkzeugnebenwirkungen.
|
||||
Bei nativen Zeitplänen behalten ausdrückliches Pausieren, Stoppen, fehlgeschlagene Arbeit und ungewisse Ergebnisse ihre Zulassungssperren nach einem Neustart. Prüfen Sie den aufbewahrten Durchlauf und die ursprüngliche Anfrage; verwenden Sie gegebenenfalls **Durchlauf prüfen** oder **Gespeicherte Anfrage aktualisieren**, dann **Fortsetzen** nur, wenn der Zustand es erlaubt. Eine native Fortsetzung wird unter ihrer bestehenden Identität beobachtet. Ein belegter Fehlschlag oder ein Ende ohne Bericht erfordert menschliche Wiederherstellung; unvollständige Belege halten den Durchlauf ungewiss. Verwenden Sie einen Neustart niemals zum Wiederholen bereits ausgelöster Werkzeugeffekte.
|
||||
|
||||
[OpenCode gestartet halten](opencode.md#service-supervision) überwacht den gemeinsamen Dienst auf seinem Ausführungshost separat. Der erste Missionsstart aktiviert dies, sofern Sie nicht Aus gespeichert haben; das Erstellen einer pausierten Mission erhält diese Auswahl. Beim Ausschalten läuft OpenCode ohne automatischen Neustart weiter. Windows, macOS und WSL richten sich nach der Benutzeranmeldung; natives Linux nutzt Linger für Benutzerdienste, abhängig von Berechtigungen. Schlaf, Ruhezustand, die Lebensdauer eines inaktiven WSL und die Steuerung externer Hosts begrenzen die Verfügbarkeit; prüfen Sie den Überwachungszustand unabhängig vom Zeitplan.
|
||||
|
||||
Missionen funktionieren mit den Desktop-Apps und mit einem eigenständigen CodeNomad-[Server](server.md). Zeigt das Panel **Missionszentrale ist nicht verfügbar**, nennt seine Meldung die Ursache:
|
||||
|
||||
|
|
@ -184,7 +237,9 @@ Missionen funktionieren mit den Desktop-Apps und mit einem eigenständigen CodeN
|
|||
|
||||
Einmalige Pause lässt laufende Arbeit zu Ende gehen: Der Koordinator und seine Sub-Agenten schließen ihren aktuellen Schritt ab, beginnen nichts Neues und halten an; das Panel zeigt währenddessen **Wird pausiert: Laufende Schritte werden abgeschlossen.** **Jetzt unterbrechen** hält sie stattdessen sofort an: Es unterbricht die native Missionsfamilie und parkt die Koordination. Stop unterbricht ebenfalls, storniert wartende Familieneingaben/-zustellungen und ist endgültig. Unterbrechungen sind begrenzt: **Einige Sub-Agenten laufen möglicherweise noch.** bedeutet, dass eine unvollständig ist. Prüfen Sie das erhaltene Ergebnis und verwenden Sie dessen ausdrücklichen Wiederholungsversuch, statt den Abbruch aller Shell-/Werkzeugauswirkungen anzunehmen. Fortsetzen informiert den Koordinator darüber, was pausiert oder unterbrochen wurde; er entscheidet über die Fortsetzung der Arbeit.
|
||||
|
||||
Tägliche Pause/Stop beenden die Planung und fordern die Unterbrechung registrierter Wurzelakteure einschließlich Koordinator und unabhängiger Spezialisten an, **beweisen aber nicht, dass diese Unterhaltungen oder ihre Nachfahren gestoppt wurden**. Prüfen Sie die nativen Familien. Stop ist endgültig: Ein gestoppter Zeitplan kann weder gestartet noch fortgesetzt noch sofort ausgeführt werden. **Durchlauf prüfen** gleicht einen offenen Lauf ab, ohne erneut zu senden. Vorübergehende geplante Prüffehler zeigen den letzten Fehler und werden automatisch wiederholt; eine verlorene Benutzersteuerungsantwort verursacht kein automatisches erneutes Senden der Steuerung.
|
||||
Bei nativen Zeitplänen schließt **Pausieren** künftige Zulassungen und fordert die Unterbrechung eines zugelassenen Durchlaufs an; **Fortsetzen** gleicht bestehende Arbeit vor der erneuten Aktivierung ab. **Stoppen…** ist endgültig. **Jetzt ausführen** nutzt denselben Schutz für offene Durchläufe und dasselbe Startkontingent und erhält dabei den Kalender. **Durchlauf prüfen** gleicht den ursprünglichen Durchlauf ab. Behalten Sie eine unbestätigte Aktion und verwenden Sie **Gespeicherte Anfrage aktualisieren**; passives Aktualisieren und Wiederverbinden erhalten diese Anfrage. Bereits ausgelöste Werkzeugeffekte können nach einer Unterbrechung weiterlaufen.
|
||||
|
||||
Bestehende tägliche Einträge behalten ihre täglichen Steuerelemente Pausieren/Stoppen/Fortsetzen/Jetzt ausführen/Durchlauf prüfen. Pausieren/Stoppen beenden ihre Uhr und fordern die Unterbrechung registrierter Wurzeln einschließlich unabhängiger Spezialisten an. Prüfen Sie deren native Familien und etwaige teilweise Unterbrechungsergebnisse. Vorübergehende tägliche Prüffehler werden mit ihrer angezeigten Warnung wiederholt; eine verlorene Antwort auf eine Benutzersteuerung bleibt an ihre ursprüngliche Anfrage gebunden. Ein gestoppter Eintrag bleibt endgültig gestoppt.
|
||||
|
||||
Bei einem einmaligen Koordinator, der ohne Bericht inaktiv ist, kann die primäre ▶-Aktion ihn prüfen und wiederherstellen; Aufgaben bieten gegebenenfalls gezielte Wiederherstellung fehlender Berichte. Wiederherstellung prüft zuerst native Aktivität, Nachfahren und Wartezustände; unbekannte Aktivität verweigert die Annahme. Sie wiederholt keine Zuweisungen und ersetzt keinen Plan. Öffnen Sie Unterhaltungen, um ausstehende Fragen, Berechtigungen, Shells oder Hintergrundarbeit zu klären.
|
||||
|
||||
|
|
@ -192,12 +247,28 @@ Ist **Erstellen** unbestätigt, behält das Fenster die übermittelte Anfrage un
|
|||
|
||||
## Unterhaltungen bearbeiten, löschen und bereinigen {#cleanup}
|
||||
|
||||
Verwenden Sie **Ausgewählte Mission bearbeiten** für eine bearbeitbare einmalige Mission; schließen/öffnen Sie bei Konflikten den Editor, um die aktuelle Revision zu laden, statt sie zu überschreiben. Ändern sich Ziel oder Notizen, während das Formular noch lädt, meldet es, dass sich die Mission seit dem Öffnen geändert hat, statt sie einzutragen; wählen Sie **Erneut öffnen**, um die aktuelle Fassung zu bearbeiten. Tägliche Zeitpläne bieten keine Bearbeiten-/Löschen-Steuerung.
|
||||
Ändern sich Ziel oder Notizen, während das Formular noch lädt, meldet es, dass sich die Mission seit dem Öffnen geändert hat, statt sie einzutragen; wählen Sie **Erneut öffnen**, um die aktuelle Fassung zu bearbeiten.
|
||||
|
||||
Zwischen Durchläufen kann ein pausierter oder laufender nativer Zeitplan Titel, Anweisungen, Notizen, angeforderte Profile, Aufgabensitzungsmodus, Quellen, Kalender und Grenzen bearbeiten, sofern kein Durchlauf und keine ungeklärte Steuerung das Objekt sperrt. Ein pausierter Zeitplan bleibt pausiert; ein aktivierter Zeitplan gleicht seinen Kalender vor dem Fortfahren ab. Speichern erhält frühere Ergebnisse, verbrauchte Quellen, Startanzahl und den Ursprung beim ersten Start; unveränderte Endgrenzen behalten ihr absolutes Terminende.
|
||||
|
||||
Während eines zugelassenen Durchlaufs folgen nur Anweisungen und Notizen dem Bearbeitungsvertrag aktiver Texte: Änderungen werden im Verlauf und für künftige Durchläufe gespeichert. Bestehende Aufgaben, Aufträge, Profile und Ausführungsidentitäten behalten ihre zugelassenen Werte. Strukturfelder bleiben bis zum Abschluss des Durchlaufs gesperrt, auch nach dem Pausieren. Ein Playbook bleibt gesperrt, sobald eine Aufgabe erstellt wurde, auch wenn diese abgeschlossen oder zurückgezogen ist. Unbestätigte Zulassung/Steuerung oder unvollständige Integritätsbelege halten strukturelle Bearbeitung schreibgeschützt. Erstellen und Bearbeiten prüfen denselben Kalender; die Vorschau berücksichtigt den geänderten letzten erlaubten Termin auch bei der gespeicherten nächsten, eventuell verspäteten Aktivierung.
|
||||
|
||||
Die Branchquelle enthält auch lokale Branches ohne Worktree und liest ausschließlich lokale Referenzen. Die Auswahl unterstützt bis zu 200 lokale Branches; bei größeren Beständen wird die Quelle als nicht verfügbar angezeigt. **Starten** und **Fortsetzen** fragen bei verfügbarer Aktivierung nach Zustimmung mit den genauen Konfigurationspfaden. **Jetzt ausführen** bleibt mit Erklärung deaktiviert, solange die native Zeitplanung inaktiv ist; aktivieren Sie `experimental.schedules` in der OpenCode-Konfiguration und aktualisieren Sie anschließend. Bei verlorener Aktivierungsantwort bleibt die ursprüngliche Anfrage zur Belegprüfung erhalten.
|
||||
|
||||
Vor dem ersten Play können Sie zum sofortigen, bedingungslosen Start zurückkehren und die verfolgten Unterhaltungen unter **Optionen** behalten. Play liest und fixiert deren vollständiges, stabiles Nachrichtenpräfix (höchstens 32 Nachrichten je Quelle, innerhalb von 16.384 Zeichen für die Starteingabe). Fehlende, verschobene oder zu große Quellen blockieren den Start mit einer Erklärung. Ein erneuter Versuch behält den fixierten Kontext und die ursprüngliche Startidentität.
|
||||
|
||||
Bestätigt das Lesen des Belegs eine teilweise Vorbereitung, erscheint **Erneut versuchen**. Der Klick prüft denselben Beleg erneut und setzt ausschließlich die ursprünglich gespeicherte Änderung fort; die Mission bleibt pausiert.
|
||||
|
||||
Vor dem ersten Start einer neu vorbereiteten Mission können Sie Titel, Anweisungen, Notizen, Playbook, gewünschte Agenten/Modelle/Denkeinstellungen, Aufgabensitzungsmodus und **Zeitplanung** bearbeiten. Sie können zwischen sofortigem, verzögertem, wiederkehrendem und bedingtem Start wechseln und dabei dieselbe Mission behalten. Speichern hält sie pausiert und bereitet einen neuen, leeren Koordinator vor; die ursprüngliche Unterhaltung bleibt erhalten. Erst Play startet Arbeit oder aktiviert einen Zeitplan. Nach dem Start gelten die Bearbeitungsregeln der aktiven Mission für Anweisungen und Notizen. Bei unbestätigtem Speichern liest **Gespeicherte Anfrage aktualisieren** das ursprüngliche Ergebnis; behalten Sie diese Anfrage, während eine teilweise Vorbereitung Start, Textänderungen und Löschen blockiert.
|
||||
|
||||
Verwenden Sie **Ausgewählte Mission bearbeiten** für eine bearbeitbare einmalige Mission oder einen nativen Zeitplan. Beachten Sie die Erklärungen neben gesperrten Feldern. Wählen Sie bei einem Revisionskonflikt **Aktuelle Konfiguration neu laden** und öffnen Sie anschließend **Vorheriger Entwurf zum Vergleich**, um den erhaltenen Entwurf vor dem erneuten Speichern zu prüfen. Bestehende tägliche Zeitpläne behalten die Steuerelemente ihrer eigenen Symbolleiste.
|
||||
|
||||
Bei einem nativen Zeitplan deaktiviert **Ausgewählte Mission löschen…** künftige Durchläufe vor dem Entfernen des Eintrags. Unterhaltungen und Ergebnisse bleiben erhalten; auf Abgleich wartende Bereinigung bleibt unter **Technische Details** sichtbar. Behalten Sie eine unbestätigte Löschung und aktualisieren Sie ihre ursprüngliche Anfrage.
|
||||
|
||||
1. Wählen Sie für eine nicht mehr benötigte einmalige Mission **Ausgewählte Mission löschen…** und prüfen Sie die Bestätigung.
|
||||
2. Wählen Sie optional vor dem Bestätigen **Auch die für diese Mission erstellten Fachunterhaltungen löschen**.
|
||||
3. Prüfen Sie **Gespräche bereinigen** auf entfernt/behalten/offen-Zahlen. Wiederholen Sie gegebenenfalls nur die verbleibende erfasste Bereinigung.
|
||||
|
||||
Eine Mission zu löschen ist nicht dasselbe wie sie zu stoppen; verwenden Sie zuerst Stop und prüfen Sie verbleibende Arbeit. Koordinatoren und wiederverwendete Unterhaltungen bleiben erhalten. Auch geteilte, verschobene oder nicht nachweislich zugehörige Spezialisten-Unterhaltungen sowie solche mit Untersitzungen können erhalten bleiben. Teilbereinigung wird nicht zurückgerollt und nicht stillschweigend im Hintergrund abgeschlossen. Die Übersicht behält die 20 zuletzt abgeschlossenen Bereinigungen. Gelöschte Missionen geben ihren Speicher im Projekt frei: Ältere gelöschte Missionen werden automatisch entfernt.
|
||||
Eine Mission zu löschen ist nicht dasselbe wie sie zu stoppen; verwenden Sie zuerst Stop und prüfen Sie verbleibende Arbeit. Koordinatoren und wiederverwendete Unterhaltungen bleiben erhalten. Auch geteilte, verschobene oder nicht nachweislich zugehörige Spezialisten-Unterhaltungen sowie solche mit Untersitzungen können erhalten bleiben. Teilbereinigung wird nicht zurückgerollt und nicht stillschweigend im Hintergrund abgeschlossen. Die Missionskarte kann verschwinden, während die Bestätigung der Löschung noch aussteht, auch wenn die Gesprächsbereinigung 0 ausstehende Ziele anzeigt. Verwenden Sie weiterhin die ursprüngliche Aktion **Erneut versuchen**, bis die Löschung bestätigt ist; dabei bleiben derselbe Löschauftrag und dieselben Optionen erhalten. Die Übersicht behält die 20 zuletzt abgeschlossenen Bereinigungen. Gelöschte Missionen geben ihren Speicher im Projekt frei: Ältere gelöschte Missionen werden automatisch entfernt.
|
||||
|
||||
Siehe [Berechtigungen](permissions.md), [Unterhaltungen](conversations.md) und [OpenCode-Einrichtung](opencode.md) für native Arbeit und Dienststeuerung.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Installierte Programmdatei und laufender Dienst können unterschiedliche Version
|
|||
| Empfohlen | OpenCode **2.2.5** | Qualifiziertes Ziel, getrennt vom technischen Minimum. |
|
||||
| Ungeprüft | Neuere oder angepasste, nicht vollständig validierte Version | Nicht allein wegen fehlender Tests abgelehnt; geprüft werden die tatsächlichen API-Verträge des verbundenen Dienstes. |
|
||||
|
||||
Auch die erforderlichen Verträge für Sitzungen, Berechtigungen, Forms, Posteingang und Sitzungsumgebung müssen verfügbar sein. Eine Programmversion allein beweist nicht deren Unterstützung durch den verbundenen Dienst. Meldet die Einrichtung einen nicht unterstützten laufenden Dienst, wählen/installieren Sie eine unterstützte CLI und führen Sie bei Bereitschaft den expliziten Neustart des gemeinsamen Dienstes aus.
|
||||
Der verbundene Dienst muss die erforderlichen Verträge für Sitzungen, Berechtigungen, Forms, Posteingang und Sitzungsumgebung bereitstellen. Native Missionszeitplanung prüft auch die Zeitplan-APIs, das Plugin-/Anbieterpaket und die wirksame Einstellung `experimental.schedules`. Die Verfügbarkeit richtet sich nach diesen genutzten Fähigkeiten, auch bei kompatiblen künftigen Versionen. Meldet die Einrichtung einen nicht unterstützten laufenden Dienst, wählen/installieren Sie eine unterstützte CLI und führen Sie bei Bereitschaft den expliziten Neustart des gemeinsamen Dienstes aus.
|
||||
|
||||
## Host, WSL und gemeinsamer Dienst {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ Unter Windows läuft eine ausgewählte WSL-Programmdatei in der gewählten Distr
|
|||
|
||||
OpenCode verwaltet seine globale Dienstregistrierung, Sitzungen, Nachrichten und Datenbank. CodeNomad erstellt keine private OpenCode-Datenbank pro Fenster oder Projekt. Das Schließen eines Projekt-Tabs schließt das Projekt in CodeNomad; Sitzungen laufen in OpenCode weiter und erscheinen wieder, wenn Sie das Projekt erneut öffnen. CodeNomad entlädt niemals native Locations. Das Schließen eines ganzen Fensters, Beenden oder Neuladen bewahrt Projektverbindungen für die Wiederherstellung; weder diese Aktionen noch das Herunterfahren des Backends beenden den gemeinsamen Daemon. Siehe [Projekte](projects.md).
|
||||
|
||||
## OpenCode gestartet halten {#service-supervision}
|
||||
|
||||
Vor dem ersten Mission-Start speichert **Beim ersten Mission-Start ausgeschaltet lassen** ein ausdrückliches Aus, ohne zuvor die Überwachung zu installieren.
|
||||
|
||||
Unter **Einstellungen → OpenCode** installiert **OpenCode gestartet halten** eine Systemüberwachung auf dem Ausführungshost, gemeinsam für alle CodeNomad-Profile dieses Dienstes. Sie arbeitet unabhängig von Missions, auch bei geschlossenem CodeNomad. Der erste Mission-Start aktiviert sie, sofern Sie sie nicht vorher ausdrücklich ausschalten; das Erstellen einer pausierten Mission behält Ihre Auswahl bei. Pause, Stopp und Löschen einer Mission lassen diese Einstellung bestehen.
|
||||
|
||||
Beim Ausschalten wird die Überwachung entfernt; OpenCode läuft weiter und wird nicht mehr neu gestartet. Für einen dauerhaften Stopp zuerst ausschalten und dann OpenCode ausdrücklich stoppen. Bei aktiver Überwachung folgt auf `opencode service stop` ein Neustart.
|
||||
|
||||
**Läuft · Überwachung aktiv** erfordert einen aktiven Systemwächter und einen gesunden OpenCode-Dienst. **Installiert · Dienst startet**, **Überwachung wird installiert oder entfernt…** und **Überwachung fehlgeschlagen** unterscheiden die Zustände. Ein antwortender Dienst allein bestätigt keine aktive Überwachung. Details zeigen Ursache, letzten Versuch, nächsten Wächterversuch und System-Sicherheitsprüfung, soweit bekannt. **Überwachungsstatus prüfen** liest nur; **Überwachung erneut versuchen** repariert ausdrücklich die angeforderte Einrichtung. Eine unbestätigte Aktion behält ihre ursprüngliche Anfrage; **Ursprüngliche Aktion erneut versuchen** sendet genau diese erneut. Eine geänderte Revision wird zur Prüfung aktualisiert statt überschrieben.
|
||||
|
||||
Windows, macOS und WSL starten nach der Anmeldung. WSL benötigt Localhost-Weiterleitung für die gewählte Distribution. Unter nativem Linux startet Linger **alle aktivierten Benutzerdienste** vor der Anmeldung und hält sie nach der Abmeldung aktiv. Beim Ausschalten wird Linger nur zurückgesetzt, wenn CodeNomad es aktiviert hat und keine andere Überwachung es benötigt; persönliche Einstellungen bleiben erhalten. Könnte das Zurücksetzen die freigegebenen OpenCode-Prozesse beenden, während keine Benutzersitzung geöffnet ist, wird es aufgeschoben und bleibt mit **Überwachung erneut versuchen** sichtbar. Versuchen Sie es erneut, sobald eine Benutzersitzung geöffnet ist oder die freigegebenen OpenCode-Prozesse beendet sind und ein sicheres Zurücksetzen möglich ist. Benötigt Linux eine Berechtigung, diese auf dem Ausführungshost erteilen und erneut versuchen; die angeforderte Einstellung bleibt gespeichert. Schlaf und Ruhezustand steuert das Betriebssystem. Nicht verfügbare oder nicht unterstützte Hosts zeigen eine deaktivierte Steuerung mit Erklärung.
|
||||
|
||||
## Aktualisieren, neu starten oder neu laden {#maintenance}
|
||||
|
||||
1. Wählen Sie unter **Einstellungen → OpenCode** die Aktion **Status und Updates prüfen**.
|
||||
|
|
@ -41,7 +53,7 @@ OpenCode verwaltet seine globale Dienstregistrierung, Sitzungen, Nachrichten und
|
|||
| Aktion | Umfang und Wirkung | Sichere Wiederherstellung |
|
||||
| --- | --- | --- |
|
||||
| CLI aktualisieren | Ändert die unterstützte installierte Programmdatei; startet den laufenden Daemon nicht implizit neu. | Prüfen Sie nach Fehlern installierte und laufende Version; wiederholen Sie erst nach Fehlerbehebung ausdrücklich. |
|
||||
| Dienst neu starten | Ersetzt den gemeinsamen Prozess und unterbricht aktive Arbeit in allen verbundenen OpenCode-Clients; Sitzungsverlauf bleibt erhalten. | Verbinden Sie neu und prüfen Sie Unterhaltungen und geplante Missionen; Neustart bedeutet keine automatische Fortsetzung. |
|
||||
| Dienst neu starten | Ersetzt den gemeinsamen Prozess und unterbricht aktive Arbeit in allen verbundenen OpenCode-Clients; Sitzungsverlauf bleibt erhalten. | Gesunde laufende native Missionszeitpläne werden automatisch wiederhergestellt; prüfen Sie offene Durchläufe und jede Pause/jeden Fehler/jede Ungewissheit. Bestehende tägliche Einträge erfordern **Fortsetzen**. Prüfen Sie einmalige Unterhaltungen und deren Wiederherstellungssteuerung separat; siehe [Missionen](missions.md#lifetime). |
|
||||
| OpenCode-Konfiguration neu laden | Behält den Prozess, erstellt aber **jede geladene Location** für alle Clients neu. Bricht offene Berechtigungen/Forms ab und schließt Terminals/Hintergrundbefehle; aktive Arbeit kann unterbrochen werden. | Prüfen Sie Ressourcen und offene Arbeit erneut. Wiederholen Sie keinen Prompt und keine Änderung nur wegen einer verlorenen Antwort. |
|
||||
|
||||
Verwenden Sie **OpenCode-Konfiguration neu laden** (unter **Fehlerbehebung**, wenn der laufende Dienst es unterstützt) ausdrücklich, wenn gespeicherte Konfiguration oder installierte Plugins weiter fehlen und die Unterbrechung akzeptabel ist. Dies ist keine harmlose Anzeigeaktualisierung. CodeNomad verwendet keine ältere ausgewählte CLI, um einen neueren laufenden Daemon herunterzustufen.
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Das sind Anweisungen. Was ein Agent tatsächlich tun kann, bestimmen die OpenCod
|
|||
|
||||
### Fragen an Sie {#questions}
|
||||
|
||||
Nur der Koordinator stellt Ihnen Fragen. Aufgaben und ihre Helfer geben ihre an die Unterhaltung zurück, die sie gestartet hat, mit Optionen, Belegen, Folgen, Umkehrbarkeit und einer Empfehlung, und arbeiten an dem weiter, was nicht von der Antwort abhängt. Der Koordinator klärt zuerst, was er kann, und bündelt die übrigen Fragen in einem Formular unter **Braucht Sie**; [Fragen und Entscheidungen](missions.md#questions) erklärt, was er fragt und wie Entscheidungen bezeichnet werden. Jeder Koordinator, einmalig oder täglich, und jede Aufgabenzuweisung erhalten diese Regel als Anweisung; die Erkundung fügt die Entscheidungsprüfungen [weiter unten](#wayfinder-checks) hinzu.
|
||||
Nur der Koordinator stellt Ihnen Fragen. Aufgaben und ihre Helfer geben ihre an die Unterhaltung zurück, die sie gestartet hat, mit Optionen, Belegen, Folgen, Umkehrbarkeit und einer Empfehlung, und arbeiten an dem weiter, was nicht von der Antwort abhängt. Der Koordinator klärt zuerst, was er kann, und bündelt die übrigen Fragen in einem Formular unter **Braucht Sie**; [Fragen und Entscheidungen](missions.md#questions) erklärt, was er fragt und wie Entscheidungen bezeichnet werden. Jeder Koordinator, einmalig oder geplant, und jede Aufgabenzuweisung erhalten diese Regel als Anweisung; die Erkundung fügt die Entscheidungsprüfungen [weiter unten](#wayfinder-checks) hinzu.
|
||||
|
||||
Antworten Sie im Unterbrechungsdock (**Antworten** unter **Braucht Sie**): Nur das zählt als Ihre Antwort. Eine im Chat getippte Antwort ist eine gewöhnliche Nachricht.
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ Antworten Sie im Unterbrechungsdock (**Antworten** unter **Braucht Sie**): Nur d
|
|||
|
||||
Der Koordinator ist angewiesen, einen Lagebericht nach seinem ersten Plan, nach jeder abgeschlossenen Aufgabe, immer wenn er auf Sie zu warten beginnt, und vor seinem Abschlussbericht zu veröffentlichen; siehe [Lageberichte und Aktualisierungsanfragen](missions.md#briefings). Lageberichte, Aufgabenzusammenfassungen und der Abschlussbericht sind für Sie geschrieben: in Ihrer Sprache, in wenigen kurzen Sätzen, das Ergebnis zuerst, Kennungen und Befehle bleiben in den Belegen.
|
||||
|
||||
Der Abschlussbericht nennt, was geliefert wurde, was bleibt und was Ihre Entscheidung braucht. Eine einmalige Mission endet nur als abgeschlossen: Wenn ohne Sie nichts weitergeht, fragt der Koordinator Sie und wartet, statt zu scheitern, und nur Sie stoppen eine Mission. Ein täglicher Lauf kann auch als fehlgeschlagen enden.
|
||||
Der Abschlussbericht nennt, was geliefert wurde, was bleibt und was Ihre Entscheidung braucht. Eine einmalige Mission endet nur als abgeschlossen: Wenn ohne Sie nichts weitergeht, fragt der Koordinator Sie und wartet, statt zu scheitern, und nur Sie stoppen eine Mission. Ein geplanter Durchlauf kann auch als fehlgeschlagen enden.
|
||||
|
||||
### Was CodeNomad in jeder Mission prüft {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ Er schreibt Ihre Antwort nie selbst und gibt seine eigene Entscheidung nie als I
|
|||
- Nur die fünf Rollen der Erkundung werden akzeptiert.
|
||||
- Eine Entscheidungsaufgabe wird nur mit der eigenen Entscheidung des Koordinators oder mit Ihrer Antwort abgeschlossen. Eine Entscheidung des Koordinators nennt die Frage, die Entscheidung und ihre Begründung und wird nie als Ihre Antwort erfasst.
|
||||
- Ihre Antwort zählt nur, wenn Sie sie in der CodeNomad-Oberfläche gegeben haben. CodeNomad ordnet sie über die Markierung der Frage einer Aufgabe zu: Die Frage wurde aus der Unterhaltung des Koordinators gestellt, ihr Text endet mit der Markierung dieser Aufgabe und nennt keine andere Entscheidung, sie wurde nach der Erklärung der Aufgabe beantwortet und hat keine andere Entscheidung geklärt. Jede Frage eines gebündelten Formulars wird für sich zugeordnet; eine Frage, die zwei Entscheidungen nennt, klärt keine davon. Eine Antwort auf eine Frage aus der eigenen Unterhaltung der Entscheidungsaufgabe wird ebenfalls akzeptiert.
|
||||
- Der eigene Bericht einer Aufgabe kann nicht angeben, wer entschieden hat. Bevor eine Mission oder ein täglicher Lauf erfolgreich endet, prüft CodeNomad jede abgeschlossene Entscheidung erneut, Markierungen eingeschlossen.
|
||||
- Der eigene Bericht einer Aufgabe kann nicht angeben, wer entschieden hat. Bevor eine Mission oder ein geplanter Durchlauf erfolgreich endet, prüft CodeNomad jede abgeschlossene Entscheidung erneut, Markierungen eingeschlossen.
|
||||
- Diese Prüfungen gelten mit **Native Unteragenten**. Mit **Unabhängige Sitzungen** melden Entscheidungsbearbeiter ihre Papiere selbst, und der Koordinator hält jedes Ergebnis in den Notizen fest und nennt, wer entschieden hat; CodeNomad prüft das nicht.
|
||||
|
||||
Nur Anweisungen: wann eine Wahl als leicht umkehrbar gilt, die Qualität von Papieren und Recherche und das Beschränken auf Planung.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode gives you the engine. CodeNomad gives you the cockpit. It connects to O
|
|||
|
||||
## Hand long work to Missions {#missions}
|
||||
|
||||
Describe the work once. The mission's coordinator plans it as a task tree, delegates bounded tasks to native OpenCode conversations and reports results with their evidence. Run a mission once or every day; creating one never starts work until you choose to. Playbooks cover debugging, product delivery and comparing two models with a blind judge. See [Missions](missions.md).
|
||||
Describe the work once. The mission's coordinator plans it as a task tree, delegates bounded tasks to native OpenCode conversations and reports results with their evidence. Run once, at chosen times or intervals, or after a verified project change; creation saves a paused Mission for your explicit start. Playbooks cover debugging, product delivery and comparing two models with a blind judge. See [Missions](missions.md).
|
||||
|
||||
## Keep working from your phone {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ The workspace root limits which folders CodeNomad opens; it is not an agent sand
|
|||
| Work across checkouts and conversations | [Projects](projects.md), [Conversations](conversations.md), [Files](files.md) |
|
||||
| Find history or safely clean it up | [History](history.md) |
|
||||
| Answer native questions and permissions | [Permissions](permissions.md) |
|
||||
| Coordinate bounded or daily work | [Missions](missions.md) |
|
||||
| Coordinate bounded or scheduled work | [Missions](missions.md) |
|
||||
| Configure execution and integrations | [OpenCode](opencode.md), [Providers](providers.md), [Integrations](integrations.md) |
|
||||
| Connect another device | [Remote Control](remote-control.md), [Security](security.md), [Mobile](mobile.md) |
|
||||
| Adapt the workspace | [Settings](settings.md), [Shortcuts](shortcuts.md), [Voice](voice.md), [Extensions](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ Missions pair a durable project plan with native OpenCode conversations. The coo
|
|||
|
||||
1. Open **Missions** in the right panel and choose **Create mission**. The form replaces the mission list and the selected mission's tasks until you create or cancel.
|
||||
2. Fill **What should the mission do?** and check **Title**, prefilled from the first sentence (up to 60 characters).
|
||||
3. Under **When**, select **Once** or **Every day at**. For daily work, verify the displayed time zone; **Change** allows an IANA zone.
|
||||
3. Leave **Scheduling** folded for **Once, immediately** and **Without a condition**, or expand it to choose a frequency, a condition and optional limits. Verify **Saved time zone (IANA)** and the preview before creating scheduled work.
|
||||
4. Expand **Options** for **Playbook**, **Notes**, **Agents and models**, **Task sessions**, the project's **Subsession nesting depth**, saved briefs and optional followed conversations. With **Compare two models**, the **Model A** and **Model B** selectors appear in the main form.
|
||||
5. Choose **Create**. Select the new entry, then use its separate ▶ action when ready: **Start mission** for one-time work, or **Play schedule {id}** for a daily schedule.
|
||||
5. Choose **Create**. Select the paused entry, review it, then use ▶ **Start** when ready. Immediate, unconditional work starts one mission; a delayed, recurring or conditional policy arms native scheduling.
|
||||
|
||||
**Create never starts work.** A one-time mission is prepared; a daily schedule is paused. Neither creation nor saving a brief arms a timer. Creation is scoped to the selected project/Location and freezes the requested profiles and task-session policy. Validation is beside the affected field; a refusal that says nothing was sent leaves the draft available for correction. The coordinator of a one-time mission can also prepare a separate mission for you, such as a follow-up: it appears in the list as **Prepared** and, like any mission, starts only from its ▶ action.
|
||||
**Create saves a paused Mission.** Work and native calendars are armed only by Play; creating or saving a brief preserves the shared service-supervision preference. Creation belongs to the selected project/Location and records the requested profiles and task-session policy. Validation appears beside the affected field; a refusal that says nothing was sent keeps the draft for correction. A one-time coordinator can also prepare a separate follow-up: it appears as **Prepared** and starts from its own ▶ action.
|
||||
|
||||
Saved one-time missions retain their execution controls, task tree, conversations and final reports. Existing daily schedules remain listed with their daily controls and [daily timing rules](#existing-daily-schedules). Select the entry and use the controls available for that object.
|
||||
|
||||
### Playbooks {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@ Results travel back the same way. A native subagent returns its result to the co
|
|||
|
||||
A later task builds on earlier work in two ways. Dependencies order the work: the coordinator starts a task once the tasks it depends on are complete, then puts the relevant results in the brief or alongside the assignment; independent sessions can also read those reports in the map. To continue a finished task's own conversation, the coordinator declares a follow-up task that reuses it: the follow-up continues there with that conversation's current context, which OpenCode may have [compacted](history.md#compact-session), while earlier details stay in its stored history for search. The Debugging playbook returns both reviews to the implementer this way. With **Independent sessions**, the coordinator can also assign a task to an existing main conversation, which keeps that conversation's context, including earlier unrelated work. Otherwise a task starts in a new conversation from its assignment, and another task's transcript reaches it through the coordinator or a search. For **Compare two models**, the judge's assignment and context leave out the objective and notes; its brief carries the anonymized outputs.
|
||||
|
||||
In a daily schedule, each run starts with its own coordinator and map. Its first message holds the schedule's instructions plus the new user and assistant text of its [followed conversations](#daily-schedules) since the last completed run; earlier runs' results stay in the schedule history. Conversations launched within a run can read that run's map. A new one-time mission also starts with a new map: to carry results forward, write them into its objective or **Notes**, or let the coordinator prepare a follow-up mission, whose objective and notes it writes.
|
||||
Each scheduled passage starts with its own coordinator and mission map, using its saved instructions and frozen inputs. Followed conversations contribute bounded new user and assistant text; their consumed cursors advance after a successful passage. Previous passages remain in the schedule history. Conversations launched within a passage can read its map. A new one-time mission has a separate map: put results to carry forward in its objective or **Notes**, or ask the coordinator to prepare a follow-up with that context.
|
||||
|
||||
### Search earlier conversations {#context-search}
|
||||
|
||||
While CodeNomad is running, every agent conversation in the project, mission coordinators and tasks included, has CodeNomad's session search tool (`codenomad.session_search`, used through OpenCode's Code Mode). It searches the stored conversations of the conversation's project folder with the same ranking as **Entire workspace** [search](history.md#full-history-search), and returns short excerpts with each conversation's title and identifier, plus a way to read a whole conversation. Native tool permissions apply. An agent searches when it decides to or when the objective, notes or your message ask it to, for example to find what an earlier mission or task concluded. A daily run that executes while CodeNomad is closed works without this tool.
|
||||
While CodeNomad is running, every agent conversation in the project, mission coordinators and tasks included, has CodeNomad's session search tool (`codenomad.session_search`, used through OpenCode's Code Mode). It searches the stored conversations of the conversation's project folder with the same ranking as **Entire workspace** [search](history.md#full-history-search), and returns short excerpts with each conversation's title and identifier, plus a way to read a whole conversation. Native tool permissions apply. An agent searches when it decides to or when the objective, notes or your message ask it to, for example to find what an earlier mission or task concluded. A scheduled passage that executes while CodeNomad is closed works without this tool.
|
||||
|
||||
## Profiles, delegation and depth {#profiles}
|
||||
|
||||
Saving a depth preserves enabled native schedules in the project configuration. An explicit project `schedules:false` stays disabled; removing the depth keeps the schedules setting. Conflicting JSON and JSONC files must be resolved before saving native settings.
|
||||
|
||||
1. Use the Missions gear or **Settings → Missions** to edit **New mission defaults**.
|
||||
2. Under **Apply to**, choose **All missions** or one playbook, then set **Task sessions** and, under **Agents and models**, one row per role (coordinator, then the playbook's roles). Each choice is saved immediately; there is no Save button.
|
||||
3. In a playbook scope, rows show the global values marked **Global**. Changing a field customizes that role starting from them; its revert icon uses the global values again. **Reset** (after confirmation) clears only the agents and models of the shown scope.
|
||||
|
|
@ -118,7 +122,7 @@ While CodeNomad is running, every agent conversation in the project, mission coo
|
|||
| Setting | Scope and effect |
|
||||
| --- | --- |
|
||||
| New mission defaults | Configuration-wide defaults for **new** missions only; a playbook exception overrides the general default. |
|
||||
| Agent / Model / Thinking | Requested coordinator/role selections recorded with creation; a busy actor is not silently switched to satisfy later work. Daily runs reuse their frozen requested profiles. |
|
||||
| Agent / Model / Thinking | Requested coordinator/role selections recorded with creation; a busy actor is not silently switched to satisfy later work. Scheduled passages use their saved requested profiles; edits apply to future passages. |
|
||||
| Native subagents | Task sessions use native subagent/all agents under the coordinator. |
|
||||
| Independent sessions | Declared task sessions use primary/all agents and independent conversations; this does not disable internal native helpers. |
|
||||
| Subsession nesting depth | Project configuration for **all** OpenCode conversations there, not just Missions. Empty inherits: **Inherit** removes the project override and an `experimental` block it leaves empty or holding only the Global **Store images once** value. OpenCode reads the project's `experimental` block as a whole, so other experimental settings kept there still hide the global depth. OpenCode's built-in **General** subagent cannot launch subagents by default, so its conversations never nest: saving any project depth also adds a rule to the same file that lets subagents launch their own subagents up to that depth and takes precedence over General's restrictions from other config files (at a depth of 1, OpenCode's depth limit still stops nesting), and **Inherit** removes it. Under **OpenCode effective depth**, the editor says whether General subagents can nest. If the project defines General in the legacy format with a single `permission` value, or with both `task` and `subagent` already set, only the depth is saved and the editor says so: allow subagents for General in that file yourself. Saving does not reload OpenCode or change running conversations. |
|
||||
|
|
@ -137,9 +141,51 @@ A refused or unconfirmed choice is never resent: the stored values are shown aga
|
|||
|
||||
Briefs store instructions and requested choices, not task plans, results, conversations, schedule clocks or execution authority. Loading one fills an ordinary creation draft; it does not start or schedule anything. Removing a brief keeps existing missions/conversations. **Settings → Missions → Saved briefs** manages the library; if the library becomes unreadable, **Reset the saved library** (after confirmation) removes saved briefs, not executions. Schedules themselves have no save-as-brief action.
|
||||
|
||||
## Daily time, catch-up and followed conversations {#daily-schedules}
|
||||
## Scheduling, catch-up and followed conversations {#daily-schedules}
|
||||
|
||||
Daily schedules use the saved IANA time zone and civil time, not an elapsed 24-hour interval. Times are shown in the schedule's zone, labelled when different from the viewer's. Each run gets its own coordinator and isolated mission map; the selected schedule shows the current task tree and date/outcome history. Select a past run to read it centrally. History is bounded to 30 results.
|
||||
### Native scheduling {#native-scheduling}
|
||||
|
||||
The **i** icons beside scheduling labels explain the choices: hover, focus with the keyboard or tap.
|
||||
|
||||
Expand **Scheduling** in Create or Edit. **Frequency** offers:
|
||||
|
||||
| Choice | Timing |
|
||||
| --- | --- |
|
||||
| Once, immediately | One launch at Play, with an optional condition checked as described below. |
|
||||
| Once, at a date | One saved date and time in the IANA zone; a date already past at Play is refused. |
|
||||
| Once, after a delay | A positive whole duration in minutes or hours, beginning at Play. |
|
||||
| At an interval | At least 15 minutes; the first wake is one interval after Play. |
|
||||
| Every day | One civil time in the saved zone. |
|
||||
| Selected weekdays | One time on the checked weekdays. |
|
||||
| Several times a day | Up to eight independent times, such as 09:15 and 17:45; duplicate times share a passage. |
|
||||
| Day of each month | Day 1–31 at the chosen time; months without that day are skipped. |
|
||||
| Advanced (cron) | A native cron expression, at most once per minute. |
|
||||
|
||||
**Next slots (up to five)** previews the saved zone and assumed Play time. With a condition, **Next checks (up to five)** previews checks; a passage depends on a verified change. An unchanged saved interval keeps its native phase. A paused or unknown phase is shown as unavailable until its native state can be read. Delays and intervals use elapsed time; civil calendars follow the saved zone and OpenCode's daylight-saving rules. The displayed dates are projections subject to service and host availability.
|
||||
|
||||
After an outage, native scheduling delivers at most one catch-up passage before returning to future slots. It uses the recorded native due slot, which may precede later missed slots. Multiple daily times share this catch-up and the same non-overlap guard. An unresolved passage is reconciled under its original conversation and message identities before another passage can start. Resuming an explicitly paused recurrence uses its next future slot; a paused one-shot can arrive once, late.
|
||||
|
||||
### Conditions and sources {#scheduling-conditions}
|
||||
|
||||
Choose one optional **Condition** and its **Source in this project**: **Another schedule succeeds**, **New commits on a local branch**, **A one-time Mission completes successfully**, **New message in a followed conversation**, or **Explicit files change**. For a condition alone, keep **Once, immediately**: native checks run every 15 minutes until its one passage launches. With a calendar, the condition is checked at that calendar's slots. Recurring conditions can launch again after new qualifying changes.
|
||||
|
||||
First Play establishes the starting reference. Resume preserves it, including changes observed after a pause. Success sources require a verified successful result and a settled native family. Branch checks use local commits and ancestry without fetching. Conversation checks wait for complete, stable new messages and exclude technical wakes and the passage's own conversations. Files use up to 128 explicit project-relative paths, one per line; recursive patterns and linked paths are refused. A missing, moved, ambiguous or over-capacity source blocks admission with an explanation. Changes between checks require a durable trace in the source to be detected.
|
||||
|
||||
Changes observed together are grouped. A passage freezes the observed source evidence; successful settlement consumes that snapshot, leaving later changes for a future check. A failed passage keeps its source checkpoint and requires intervention. **Followed conversations (optional)** under **Options** supplies context separately from choosing a conversation-message condition.
|
||||
|
||||
### Ends and counts {#scheduling-limits}
|
||||
|
||||
Leave limits blank for ongoing recurrence. **Last allowed slot** and **Last allowed slot after this many hours from first Play (optional)** bound eligible slots; the earliest bound applies. The duration starts at first Play and keeps running through Pause. A slot recorded before the end can arrive late after a long outage. A passage already admitted remains observable through its result.
|
||||
|
||||
**Maximum launched passages** is a shared total across all times and **Run now**. A passage counts once when its coordinator's first input is admitted, including a passage that later fails. Checks, unchanged conditions and refused wakes consume no place; uncertain admission holds its place pending reconciliation. One-time policies allow at most one passage. Consumed places survive edits and restarts; unchanged end limits retain their first-Play origin and absolute slot end. Changing the maximum changes the lifetime total; a total at or below the consumed count finishes the schedule. A frequency change can start a new interval phase at Save, as the editor explains.
|
||||
|
||||
The selected native schedule shows **Desired state**, **Native calendar**, **Next wake** or **Next check**, **Checks** with **Tracked since**, **Launched passages**, **Successful passages**, its end and **Past runs**. Read a past passage through **Open conversation**. **Schedule finished** describes the schedule's end; inspect the passage outcome separately. Results retain the latest 30 entries; lifetime counts survive that window. **Load more Missions** loads the next saved-object page.
|
||||
|
||||
The list shows condition and end details only when configured, and **Checks** only with a condition.
|
||||
|
||||
### Existing daily schedules {#existing-daily-schedules}
|
||||
|
||||
Existing daily schedules use the saved IANA zone and civil time. Each run has its own coordinator and mission map; select its current task tree or a past date/outcome to read the result centrally. Their retained history holds 30 results. The following daily rules apply to these entries:
|
||||
|
||||
| Situation | Scheduling behavior |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@ Daily schedules use the saved IANA time zone and civil time, not an elapsed 24-h
|
|||
| Clock moves backward | Does not make an already processed day due again. |
|
||||
| Existing run still pending | Does not overlap it with a new daily run. |
|
||||
|
||||
Creation time also bounds eligible daily work; starting a new schedule is not permission to process dates before it existed. **Run now** starts a separate manual run when offered; it does not replace the daily clock or bypass a pending run. A displayed next time is a projection, not a guarantee of exact-time execution.
|
||||
Creation time also bounds eligible daily work. **Run now**, when offered on an existing daily entry, starts a separate manual run under the pending-run guard and preserves its daily clock. A displayed next time is a projection subject to service and host availability.
|
||||
|
||||
**Followed conversations (optional)** adds bounded context from chosen conversations. Only a completed run advances their processed-message cursors; a failed/interrupted run does not silently consume that context. If instructions plus context exceed capacity, shorten instructions or remove followed conversations rather than assuming unlimited history is read.
|
||||
|
||||
## Lifetime and service restarts {#lifetime}
|
||||
|
||||
The **i** beside **Technical details** explains technical activity and costs. In **Settings → OpenCode**, supervision keeps status and errors visible; its **i** holds routine explanations.
|
||||
|
||||
For a native schedule, expand **Technical details** to identify its **Principal carrier** and **Reconciliation carrier**, their native calendars and any **Cleanup awaiting reconciliation**. These technical conversations have no composer action and stay out of ordinary conversation/source pickers. Technical wakes, titles and compaction use the local model without provider HTTP requests; native events/statistics can still include them, while actual passages keep their ordinary model costs. Compaction bounds the processed context with the same conversation/calendar identity. Stored native messages and events can grow until explicit cleanup of a finished schedule; the 30 retained Mission results are separate. The depth control and **Scheduling** show the path under **OpenCode configuration file:** when available; the depth control labels its **Project write target** separately.
|
||||
|
||||
| Component | What closing it means |
|
||||
| --- | --- |
|
||||
| CodeNomad project tab | Closes the project in CodeNomad; native sessions continue in OpenCode. Reopen the project to find them again. |
|
||||
| Whole window, app quit or renderer reload | Preserves project connections for restoration; does not stop native work or the OpenCode service. |
|
||||
| CodeNomad backend | Does not stop OpenCode. A running one-time mission continues: its coordinator, native subagent tasks and mission tools run in OpenCode. Assigning tasks to independent sessions and delivering their reports to the coordinator, missions the coordinator prepares, stopping a retired task's conversations, recovery and Mission Control wait for the backend, as does continuing a one-time mission after an OpenCode service restart. |
|
||||
| Running daily schedule | Its clock and passage execution run in the OpenCode plugin/service, including with CodeNomad UI and backend closed. The execution host and OpenCode service must stay running. |
|
||||
| OpenCode service restart | Loses process-local schedule jobs. A desired-running schedule becomes **Interrupted** until an explicit **Resume**; reopening CodeNomad does not rearm it. |
|
||||
| Armed native schedule | Persisted OpenCode calendars wake their project and run passages with CodeNomad UI and backend closed, while the execution host and service are available. |
|
||||
| OpenCode service restart — healthy native schedule | A healthy schedule whose desired state is running resumes its calendar automatically. Pending work is reconciled under its original identities before new admission. |
|
||||
| Existing daily schedule | Its plugin clock runs with CodeNomad closed while the service lives. A service restart leaves it **Interrupted**; explicit **Resume** reconciles its pending run and rearms its daily clock. |
|
||||
|
||||
After a restart, inspect the pending run and choose **Resume** when ready. Resume reconciles the existing run without sending its coordinator input twice; it does not promise continuation of an interrupted native turn. A turn cut off by restart can settle as ended without a report, with **Interrupted by a restart**. Do not use restarting as a way to retry tool side effects.
|
||||
For native schedules, explicit Pause, Stop, failed work and uncertain outcomes keep their admission barriers after restart. Inspect the retained passage and original request; use **Check passage** or **Refresh saved request** where offered, then **Resume** only when the state permits it. A native continuation is observed under its existing identity. Proven failure or an end without a report requires human recovery; incomplete proof keeps the passage uncertain. Never use a restart to retry engaged tool effects.
|
||||
|
||||
[Keep OpenCode running](opencode.md#service-supervision) separately supervises the shared service on its execution host. First Mission Play enables it unless you saved OFF; creating a paused Mission preserves that choice. Turning it off leaves OpenCode running without automatic relaunch. Windows, macOS and WSL use the login boundary; native Linux uses user lingering subject to permissions. Sleep, hibernation, WSL idle lifetime and external-host control limit availability; inspect the supervision status independently of the schedule.
|
||||
|
||||
Missions work with the desktop apps and with a standalone CodeNomad [server](server.md). When the panel shows **Mission Control is unavailable**, its message names the cause:
|
||||
|
||||
|
|
@ -184,7 +237,9 @@ Missions work with the desktop apps and with a standalone CodeNomad [server](ser
|
|||
|
||||
One-time Pause lets the work in progress finish: the coordinator and its sub-agents complete the step they are on, start nothing new and stop, while the panel shows **Pausing: the steps in progress are finishing.** **Interrupt now** stops them at once instead: it interrupts the mission's native family and parks coordination. Stop also interrupts, cancels queued family inputs/deliveries and is terminal. Interruptions are bounded operations: **Some sub-agents may still be running.** means one is incomplete. Inspect the retained result and use its explicit retry rather than assuming all shell/tool effects were cancelled. Resume tells the coordinator what was paused or interrupted; it decides how to continue the work.
|
||||
|
||||
Daily Pause/Stop cancel scheduling and request interruption of registered root actors, including the coordinator and independent specialists, but **do not prove that those conversations or their descendants have stopped**. Inspect the native families. Stop is terminal: a stopped schedule cannot Play, Resume or Run now. **Check passage** reconciles a pending run without sending it again. Transient scheduled-check failures display their last failure and retry automatically; a lost user-control reply does not cause automatic control resending.
|
||||
Native schedule **Pause** closes future admission and requests interruption of an admitted passage; **Resume** reconciles existing work before rearming. **Stop…** is terminal. **Run now** uses the same pending guard and launched-passage quota while preserving the calendar. **Check passage** reconciles the original passage. Keep an unconfirmed action and use **Refresh saved request**; passive refresh and reconnect preserve that request. Engaged tool effects may continue after interruption.
|
||||
|
||||
Existing daily entries keep their daily Pause/Stop/Resume/Run now/Check passage controls. Pause/Stop cancel their clock and request interruption of registered roots, including independent specialists. Inspect their native families and any partial interruption result. Transient daily-check failures retry with their displayed warning; a lost user-control reply stays attached to its original request. A stopped entry remains terminal.
|
||||
|
||||
For a one-time coordinator that is idle without its report, the primary ▶ action can verify and recover it; tasks have targeted missing-report recovery when eligible. Recovery checks native activity, descendants and waits first; unknown activity refuses admission. It does not replay assignments or replace the plan. Open conversations to resolve outstanding questions, permissions, shells or background work.
|
||||
|
||||
|
|
@ -192,12 +247,28 @@ If **Create** is unconfirmed, the window retains the submitted request and disab
|
|||
|
||||
## Edit, delete and clean up conversations {#cleanup}
|
||||
|
||||
Use **Edit selected mission** on an editable one-time mission; on a conflict, close/reopen the editor to load the current revision rather than overwrite it. If the objective or notes change while the form is still loading, it says the mission changed since you opened it instead of filling them; choose **Reopen** to edit the current version. Daily schedules do not offer edit/delete controls.
|
||||
If the objective or notes change while the form is still loading, it says the mission changed since you opened it instead of filling them; choose **Reopen** to edit the current version.
|
||||
|
||||
Between passages, a native schedule that is paused or running can edit its title, instructions, notes, requested profiles, task-session mode, sources, calendar and limits when no passage or unresolved control holds the object. A paused schedule stays paused; an armed schedule reconciles its calendar before continuing. Saving preserves past results, consumed sources, launched count and first-Play origin; unchanged end limits retain their absolute slot end.
|
||||
|
||||
During an admitted passage, only instructions and notes follow the active text-edit contract: changes are recorded in its history and saved for future passages. Existing tasks, briefs, profiles and execution identities keep their admitted values. Structural fields stay locked until the passage settles, including after Pause. A playbook stays locked once any task has been created, including completed or retired tasks. Uncertain admission/control or incomplete integrity evidence keeps structural editing read-only. Create and Edit share calendar validation; the preview applies the edited last allowed slot to the saved next wake, including a late wake.
|
||||
|
||||
The local-branch source includes branches without a worktree, using local refs only. The picker supports up to 200 local branches; a larger inventory shows the source as unavailable. **Start** and **Resume** ask for consent with the exact configuration paths when activation is available. **Run now** stays disabled with an explanation while native scheduling is inactive; enable `experimental.schedules` in OpenCode configuration, then refresh. A lost activation reply keeps the original request for receipt refresh.
|
||||
|
||||
Before first Play, you can return to an immediate, unconditional launch while keeping followed conversations under **Options**. Play reads and freezes their complete, stable message prefix (up to 32 messages per source, within the 16,384-character launch envelope). Missing, moved or over-capacity sources block the launch with an explanation. A retry keeps the original frozen context and launch identity.
|
||||
|
||||
When a receipt read confirms partial preparation, **Try again** appears. Clicking it checks that same receipt again and resumes only the original saved change, keeping the mission paused.
|
||||
|
||||
Before the first launch of a newly prepared mission, you can edit its title, instructions, notes, playbook, requested agents/models/thinking, task-session mode and **Scheduling**. You can switch between immediate, delayed, recurring and conditional launches, keeping the same Mission. Saving keeps it paused and prepares a fresh, empty coordinator; the initial conversation is kept. Only Play starts work or arms a schedule. After launch, instructions and notes follow the active mission's editing rules. If saving is unconfirmed, **Refresh saved request** reads the original outcome; keep that request while a partial preparation blocks Play, text edits and deletion.
|
||||
|
||||
Use **Edit selected mission** on an editable one-time mission or native schedule. Read the explanations beside locked fields. On a revision conflict, choose **Reload current configuration**, then open **Previous draft for comparison** to review the retained draft before saving again. Existing daily schedules keep their own toolbar controls.
|
||||
|
||||
For a native schedule, **Delete selected mission…** disables future passages before removing the entry. Conversations and results are kept; cleanup awaiting reconciliation remains visible in **Technical details**. Preserve an unconfirmed deletion and refresh its original request.
|
||||
|
||||
1. For a one-time mission you no longer need, choose **Delete selected mission…** and review the confirmation.
|
||||
2. Optionally select **Also delete specialist conversations created for this mission** before confirming.
|
||||
3. Check **Conversation cleanup** for removed/kept/pending counts. Retry only the remaining recorded cleanup if offered.
|
||||
|
||||
Deleting a mission is not the same as stopping it; use Stop and inspect remaining work first. Coordinators and reused conversations are kept. Shared, moved, child-bearing or unverifiably owned specialists may also be retained. Partial cleanup is not rolled back and is not silently completed in the background. The overview reader keeps the 20 most recent completed cleanups. Deleting missions frees their project storage: older deleted missions are cleared automatically.
|
||||
Deleting a mission is not the same as stopping it; use Stop and inspect remaining work first. Coordinators and reused conversations are kept. Shared, moved, child-bearing or unverifiably owned specialists may also be retained. Partial cleanup is not rolled back and is not silently completed in the background. The mission card can disappear while deletion confirmation is still pending, even when conversation cleanup shows 0 pending. Keep using the original **Try again** action until deletion is confirmed; it retains the same deletion request and options. The overview reader keeps the 20 most recent completed cleanups. Deleting missions frees their project storage: older deleted missions are cleared automatically.
|
||||
|
||||
See [Permissions](permissions.md), [Conversations](conversations.md) and [OpenCode setup](opencode.md) for native work and service controls.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ The installed executable and the running service can have different versions. Co
|
|||
| Recommended | OpenCode **2.2.5** | Qualified target, distinct from the technical minimum. |
|
||||
| Unverified | A newer or custom version not fully validated | Not rejected merely for being untested; the connected service's actual API contracts are checked. |
|
||||
|
||||
The required session, permission, Form, inbox and session-environment contracts must also be available. An executable version alone does not prove that the connected service supports them. If setup reports an unsupported running service, select/install a supported CLI and use the explicit shared-service restart when ready.
|
||||
The connected service must expose the required session, permission, Form, inbox and session-environment contracts. Native Mission scheduling also checks the schedule APIs, plugin/provider bundle and effective `experimental.schedules` setting. Availability follows those consumed capabilities, including on compatible future versions. If setup reports an unsupported running service, select/install a supported CLI and use the explicit shared-service restart when ready.
|
||||
|
||||
## Host, WSL and shared-service scope {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ On Windows, a selected WSL executable runs inside its chosen distribution. Insta
|
|||
|
||||
OpenCode owns its global service registration, sessions, messages and database. CodeNomad does not create a private OpenCode database per window or project. Closing a project tab closes that project in CodeNomad; sessions continue in OpenCode and reappear when you reopen the project. CodeNomad never unloads native Locations. Closing a whole window, quitting or reloading preserves project connections for restoration; neither those actions nor backend shutdown stops the shared daemon. See [Projects](projects.md).
|
||||
|
||||
## Keep OpenCode running {#service-supervision}
|
||||
|
||||
Before any Mission has played, **Keep off at first Mission Play** saves an explicit OFF without first installing supervision.
|
||||
|
||||
In **Settings → OpenCode**, **Keep OpenCode running** installs OS supervision on the execution host, shared by every CodeNomad profile using that service. It works independently of Missions, including with CodeNomad closed. The first Mission Play enables it unless you explicitly turn it off beforehand; creating a paused Mission preserves your choice. Pausing, stopping or deleting a Mission keeps this preference unchanged.
|
||||
|
||||
Turning it off removes supervision while OpenCode continues running and will no longer be restarted. For a durable service stop, turn it off first, then stop OpenCode explicitly. With supervision on, `opencode service stop` is followed by a restart.
|
||||
|
||||
**Running · supervision active** requires an active OS supervisor and a healthy OpenCode service. **Installed · service starting**, **Installing or removing supervision…** and **Supervision failed** describe different states. A responding OpenCode service alone leaves supervision unconfirmed. Failure details show the cause, last attempt, supervisor retry and OS recovery safety check when known. **Check supervision status** only reads; **Retry supervision** explicitly repairs the requested setup. An unconfirmed action retains its original request; **Retry original action** resends only that exact action. A changed revision is refreshed for review rather than overwritten.
|
||||
|
||||
Windows, macOS and WSL start after login. WSL requires localhost forwarding for the selected distribution. On native Linux, enabling lingering starts **all enabled user services** before login and keeps them after logout. Turning supervision off restores lingering only when CodeNomad enabled it and no other supervision needs it; personal lingering settings are preserved. If restoring lingering could stop the released OpenCode processes while no user session is open, restoration is deferred and remains visible with **Retry supervision**. Retry once a user session is open or the released OpenCode processes have exited, allowing safe restoration. If Linux requires permission, authorize on the execution host and retry; the requested preference remains saved. Sleep and hibernation remain OS-controlled. Unavailable or unsupported execution hosts show a disabled control with an explanation.
|
||||
|
||||
## Update, restart or reload {#maintenance}
|
||||
|
||||
1. In **Settings → OpenCode**, choose **Check status and updates**.
|
||||
|
|
@ -41,7 +53,7 @@ OpenCode owns its global service registration, sessions, messages and database.
|
|||
| Action | Scope and effect | Safe recovery |
|
||||
| --- | --- | --- |
|
||||
| Update CLI | Changes the supported installed executable; does not implicitly restart the running daemon. | Check both installed and running versions after a failure; retry explicitly only after resolving the error. |
|
||||
| Restart service | Replaces the shared process and interrupts active work in all connected OpenCode clients; session history remains. | Reconnect and inspect conversations and scheduled Missions; a restart is not automatic continuation. |
|
||||
| Restart service | Replaces the shared process and interrupts active work in all connected OpenCode clients; session history remains. | Healthy running native Mission schedules recover automatically; inspect pending passages and any pause/error/uncertainty. Existing daily entries require Resume. Inspect one-time conversations and their recovery controls separately; see [Missions](missions.md#lifetime). |
|
||||
| Reload OpenCode configuration | Keeps the process but rebuilds **every loaded Location** for all clients. Cancels pending permissions/Forms and closes terminals/background commands; active work may be interrupted. | Recheck resources and pending work after reload. Do not repeat a prompt or mutation merely because its reply was lost. |
|
||||
|
||||
Use **Reload OpenCode configuration**, under **Troubleshooting** when the running service supports it, explicitly when a saved configuration or installed plugin remains unavailable and the disruption is acceptable. It is not a harmless display refresh. CodeNomad will not use an older selected CLI to downgrade a newer running daemon.
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ These are instructions. What an agent can actually do is decided by OpenCode's p
|
|||
|
||||
### Questions to you {#questions}
|
||||
|
||||
Only the coordinator asks you questions. Tasks and their helpers return theirs to the conversation that launched them, with the options, evidence, consequences, reversibility and a recommendation, and keep working on what does not depend on the answer. The coordinator first settles what it can and groups the remaining questions into one form under **Needs you**; [Questions and decisions](missions.md#questions) explains what it asks and how decisions are labelled. Every coordinator, one-time or daily, and every task assignment carries this rule as an instruction; Exploration adds the decision checks [below](#wayfinder-checks).
|
||||
Only the coordinator asks you questions. Tasks and their helpers return theirs to the conversation that launched them, with the options, evidence, consequences, reversibility and a recommendation, and keep working on what does not depend on the answer. The coordinator first settles what it can and groups the remaining questions into one form under **Needs you**; [Questions and decisions](missions.md#questions) explains what it asks and how decisions are labelled. Every coordinator, one-time or scheduled, and every task assignment carries this rule as an instruction; Exploration adds the decision checks [below](#wayfinder-checks).
|
||||
|
||||
Answer in the interruption dock (**Answer** under **Needs you**): only that counts as your answer. A reply typed in the chat is an ordinary message.
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ Answer in the interruption dock (**Answer** under **Needs you**): only that coun
|
|||
|
||||
The coordinator is told to publish a briefing after its initial plan, after each task it settles, whenever it starts waiting for you and before its final report; see [Briefings and update requests](missions.md#briefings). Briefings, task summaries and the final report are written for you: in your language, a few short sentences, outcome first, with identifiers and commands kept in the evidence.
|
||||
|
||||
The final report states what was delivered, what remains and what needs your decision. A one-time mission finishes only as completed: when nothing can proceed without you, the coordinator asks you and waits instead of failing, and only you stop a mission. A daily run can also finish as failed.
|
||||
The final report states what was delivered, what remains and what needs your decision. A one-time mission finishes only as completed: when nothing can proceed without you, the coordinator asks you and waits instead of failing, and only you stop a mission. A scheduled passage can also finish as failed.
|
||||
|
||||
### What CodeNomad checks in every mission {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ It never writes your answer itself or presents its own decision as yours. Where
|
|||
- Only the five Exploration roles are accepted.
|
||||
- A decision task completes only with the coordinator's own decision or your answer. A coordinator decision states the question, the decision and its rationale, and is never recorded as your answer.
|
||||
- Your answer counts only when you gave it in CodeNomad's interface. CodeNomad binds it to a task by the question's tag: the question was asked from the coordinator's conversation, its text ends with that task's tag and names no other decision, it was answered after the task was declared, and it has not settled another decision. Each question of a batched form binds on its own; a question naming two decisions settles neither. An answer to a question asked from the decision task's own conversation is also accepted.
|
||||
- A task's own report cannot claim who decided. Before a mission or daily run finishes as completed, CodeNomad checks every completed decision again, tags included.
|
||||
- A task's own report cannot claim who decided. Before a mission or scheduled passage finishes as completed, CodeNomad checks every completed decision again, tags included.
|
||||
- These checks apply with **Native subagents**. With **Independent sessions**, decision workers report their briefs themselves and the coordinator records each outcome in the notes, stating who decided; CodeNomad does not verify it.
|
||||
|
||||
Instructions only: when a choice counts as cheap to reverse, the quality of briefs and research, and keeping to planning.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode te da el motor. CodeNomad te da la cabina de mando. Se conecta al servi
|
|||
|
||||
## Delega el trabajo largo a las Misiones {#missions}
|
||||
|
||||
Describe el trabajo una sola vez. El coordinador de la misión lo planifica como un árbol de tareas, delega tareas acotadas a conversaciones nativas de OpenCode e informa de los resultados con sus pruebas. Ejecuta una misión una vez o cada día; crear una nunca inicia el trabajo hasta que tú lo decidas. Los guiones cubren la depuración, la entrega de producto y la comparación de dos modelos con un juez a ciegas. Consulta [Misiones](missions.md).
|
||||
Describe el trabajo una sola vez. El coordinador de la misión lo planifica como un árbol de tareas, delega tareas acotadas a conversaciones nativas de OpenCode e informa de los resultados con sus pruebas. Ejecuta una vez, en horarios o intervalos elegidos, o tras un cambio verificado del proyecto; crear guarda una Misión en pausa para que la inicies explícitamente. Los guiones cubren la depuración, la entrega de producto y la comparación de dos modelos con un juez a ciegas. Consulta [Misiones](missions.md).
|
||||
|
||||
## Sigue trabajando desde tu teléfono {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ La raíz del espacio de trabajo limita qué carpetas abre CodeNomad; no es un en
|
|||
| Trabajar con varias copias de trabajo y conversaciones | [Proyectos](projects.md), [Conversaciones](conversations.md), [Archivos](files.md) |
|
||||
| Buscar en el historial o limpiarlo de forma segura | [Historial](history.md) |
|
||||
| Responder preguntas y permisos nativos | [Permisos](permissions.md) |
|
||||
| Coordinar trabajo acotado o diario | [Misiones](missions.md) |
|
||||
| Coordinar trabajo acotado o programado | [Misiones](missions.md) |
|
||||
| Configurar la ejecución y las integraciones | [OpenCode](opencode.md), [Proveedores](providers.md), [Integraciones](integrations.md) |
|
||||
| Conectar otro dispositivo | [Control remoto](remote-control.md), [Seguridad](security.md), [Móvil](mobile.md) |
|
||||
| Adaptar el espacio de trabajo | [Configuración](settings.md), [Atajos](shortcuts.md), [Voz](voice.md), [Extensiones](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ Las Misiones combinan un plan de proyecto duradero con conversaciones nativas de
|
|||
|
||||
1. Abre **Misiones** en el panel derecho y elige **Crear misión**. El formulario sustituye la lista de misiones y las tareas de la misión seleccionada hasta que crees o canceles.
|
||||
2. Completa **¿Qué debe hacer la misión?** y comprueba el **Título**, rellenado a partir de la primera frase (hasta 60 caracteres).
|
||||
3. En **Cuándo**, selecciona **Una vez** o **Cada día a las**. Para trabajo diario, verifica la zona horaria mostrada; **Cambiar** permite indicar una zona IANA.
|
||||
3. Deja **Programación** plegada para **Una vez, inmediatamente** y **Sin condición**, o despliégala para elegir una frecuencia, una condición y límites opcionales. Verifica **Zona horaria guardada (IANA)** y la vista previa antes de crear trabajo programado.
|
||||
4. Expande **Opciones** para acceder a **Guion**, **Notas**, **Agentes y modelos**, **Sesiones de tareas**, la **Profundidad de anidamiento de subsesiones** del proyecto, descripciones guardadas y conversaciones seguidas opcionales. Con **Comparar dos modelos**, los selectores **Modelo A** y **Modelo B** aparecen en el formulario principal.
|
||||
5. Elige **Crear**. Selecciona la entrada nueva y usa su acción ▶ independiente cuando esté lista: **Iniciar misión** para trabajo puntual o **Iniciar programación {id}** para una programación diaria.
|
||||
5. Elige **Crear**. Selecciona la entrada en pausa, revísala y usa ▶ **Iniciar** cuando estés listo. El trabajo inmediato sin condición inicia una misión; una política diferida, recurrente o condicional activa la programación nativa.
|
||||
|
||||
**Crear nunca inicia el trabajo.** Una misión puntual queda preparada; una programación diaria queda pausada. Ni la creación ni guardar una descripción activan un temporizador. La creación tiene el ámbito del proyecto/Location seleccionado y fija los perfiles solicitados y la política de sesiones de tareas. La validación aparece junto al campo afectado; un rechazo que indique que no se envió nada deja el borrador disponible para corregirlo. El coordinador de una misión puntual también puede preparar una misión independiente para ti, como un seguimiento: aparece en la lista como **Preparada** y, como cualquier misión, solo se inicia desde su acción ▶.
|
||||
**Crear guarda una Misión en pausa.** Solo ▶ **Iniciar** activa el trabajo y los calendarios nativos; crear o guardar una descripción conserva la preferencia de supervisión del servicio compartido. La creación pertenece al proyecto/Location seleccionado y registra los perfiles solicitados y la política de sesiones de tareas. La validación aparece junto al campo afectado; un rechazo que indique que no se envió nada conserva el borrador para corregirlo. Un coordinador puntual también puede preparar un seguimiento independiente: aparece como **Preparada** y se inicia desde su propia acción ▶.
|
||||
|
||||
Las misiones puntuales guardadas conservan sus controles de ejecución, árbol de tareas, conversaciones e informes finales. Las programaciones diarias existentes siguen en la lista con sus controles diarios y sus [reglas horarias diarias](#existing-daily-schedules). Selecciona la entrada y usa los controles disponibles para ese objeto.
|
||||
|
||||
### Guiones {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@ Los resultados vuelven por el mismo camino. Un subagente nativo devuelve su resu
|
|||
|
||||
Una tarea posterior se apoya en el trabajo anterior de dos formas. Las dependencias ordenan el trabajo: el coordinador inicia una tarea cuando las tareas de las que depende están completadas y coloca los resultados pertinentes en el encargo o junto a la asignación; las sesiones independientes también pueden leer esos informes en el mapa. Para continuar la propia conversación de una tarea terminada, el coordinador declara una tarea de seguimiento que la reutiliza: el seguimiento continúa allí con el contexto actual de esa conversación, que OpenCode puede haber [compactado](history.md#compact-session), mientras los detalles anteriores siguen en su historial guardado para la búsqueda. El guion Depuración devuelve así ambas revisiones al implementador. Con **Sesiones independientes**, el coordinador también puede asignar una tarea a una conversación principal existente, que conserva su propio contexto, incluido trabajo anterior no relacionado. En los demás casos, una tarea empieza en una conversación nueva a partir de su asignación, y la transcripción de otra tarea le llega a través del coordinador o de una búsqueda. En **Comparar dos modelos**, la asignación y el contexto del juez omiten el objetivo y las notas; su encargo contiene los resultados anonimizados.
|
||||
|
||||
En una programación diaria, cada ejecución empieza con su propio coordinador y su propio mapa. Su primer mensaje contiene las instrucciones de la programación y el texto nuevo de usuario y asistente de sus [conversaciones seguidas](#daily-schedules) desde la última ejecución completada; los resultados de ejecuciones anteriores quedan en el historial de la programación. Las conversaciones lanzadas dentro de una ejecución pueden leer el mapa de esa ejecución. Una misión puntual nueva también empieza con un mapa nuevo: para trasladar resultados, escríbelos en su objetivo o en sus **Notas**, o deja que el coordinador prepare una misión de seguimiento, cuyo objetivo y notas redacta él.
|
||||
Cada pasada programada empieza con su propio coordinador y mapa de misión, usando sus instrucciones guardadas y entradas fijadas. Las conversaciones seguidas aportan una cantidad acotada de texto nuevo de usuario y asistente; sus cursores de consumo avanzan tras una pasada exitosa. Las pasadas anteriores permanecen en el historial de la programación. Las conversaciones lanzadas dentro de una pasada pueden leer su mapa. Una misión puntual nueva tiene un mapa independiente: pon los resultados que quieras trasladar en su objetivo o sus **Notas**, o pide al coordinador que prepare un seguimiento con ese contexto.
|
||||
|
||||
### Buscar en conversaciones anteriores {#context-search}
|
||||
|
||||
Mientras CodeNomad está en ejecución, cada conversación de agente del proyecto, incluidos los coordinadores y las tareas de misiones, dispone de la herramienta de búsqueda de sesiones de CodeNomad (`codenomad.session_search`, usada mediante el Code Mode de OpenCode). Busca en las conversaciones guardadas de la carpeta del proyecto de la conversación con la misma clasificación que la [búsqueda](history.md#full-history-search) **Todo el espacio de trabajo**, y devuelve fragmentos breves con el título y el identificador de cada conversación, además de una forma de leer una conversación completa. Se aplican los permisos nativos de las herramientas. Un agente busca cuando lo decide o cuando el objetivo, las notas o tu mensaje se lo piden, por ejemplo para averiguar qué concluyó una misión o tarea anterior. Una ejecución diaria que se ejecuta con CodeNomad cerrado trabaja sin esta herramienta.
|
||||
Mientras CodeNomad está en ejecución, cada conversación de agente del proyecto, incluidos los coordinadores y las tareas de misiones, dispone de la herramienta de búsqueda de sesiones de CodeNomad (`codenomad.session_search`, usada mediante el Code Mode de OpenCode). Busca en las conversaciones guardadas de la carpeta del proyecto de la conversación con la misma clasificación que la [búsqueda](history.md#full-history-search) **Todo el espacio de trabajo**, y devuelve fragmentos breves con el título y el identificador de cada conversación, además de una forma de leer una conversación completa. Se aplican los permisos nativos de las herramientas. Un agente busca cuando lo decide o cuando el objetivo, las notas o tu mensaje se lo piden, por ejemplo para averiguar qué concluyó una misión o tarea anterior. Una pasada programada que se ejecuta con CodeNomad cerrado trabaja sin esta herramienta.
|
||||
|
||||
## Perfiles, delegación y profundidad {#profiles}
|
||||
|
||||
Guardar una profundidad conserva las programaciones nativas activadas en la configuración del proyecto. Un `schedules:false` explícito del proyecto sigue desactivado; eliminar la profundidad conserva el ajuste de programación. Los archivos JSON y JSONC en conflicto deben resolverse antes de guardar los ajustes nativos.
|
||||
|
||||
1. Usa el engranaje de Misiones o **Configuración → Misiones** para editar **Valores para nuevas misiones**.
|
||||
2. En **Aplicar a**, elige **Todas las misiones** o un guion; después establece **Sesiones de tareas** y, en **Agentes y modelos**, una fila por rol (el coordinador y luego los roles del guion). Cada elección se guarda de inmediato; no hay botón Guardar.
|
||||
3. En el ámbito de un guion, las filas muestran los valores globales marcados como **Global**. Cambiar un campo personaliza ese rol a partir de ellos; su icono de revertir vuelve a usar los valores globales. **Restablecer** (tras confirmar) borra solo los agentes y modelos del ámbito mostrado.
|
||||
|
|
@ -118,7 +122,7 @@ Mientras CodeNomad está en ejecución, cada conversación de agente del proyect
|
|||
| Ajuste | Ámbito y efecto |
|
||||
| --- | --- |
|
||||
| Valores para nuevas misiones | Valores de toda la configuración solo para misiones **nuevas**; una excepción de guion sustituye el valor general. |
|
||||
| Agente / Modelo / Razonamiento | Selecciones solicitadas para coordinador/roles registradas al crear; un actor ocupado no se cambia silenciosamente para atender trabajo posterior. Las ejecuciones diarias reutilizan los perfiles solicitados fijados. |
|
||||
| Agente / Modelo / Razonamiento | Selecciones solicitadas para coordinador/roles registradas al crear; un actor ocupado no se cambia silenciosamente para atender trabajo posterior. Las pasadas programadas usan sus perfiles solicitados guardados; los cambios se aplican a futuras pasadas. |
|
||||
| Subagentes nativos | Las sesiones de tareas usan agentes nativos subagent/all bajo el coordinador. |
|
||||
| Sesiones independientes | Las sesiones de tareas declaradas usan agentes primary/all y conversaciones independientes; esto no desactiva los ayudantes nativos internos. |
|
||||
| Profundidad de anidamiento de subsesiones | Configuración del proyecto para **todas** las conversaciones de OpenCode de allí, no solo Misiones. Vacío hereda: **Heredar** elimina la anulación del proyecto y el bloque `experimental` que deje vacío o solo con el valor Global de **Guardar las imágenes una sola vez**. OpenCode lee el bloque `experimental` del proyecto como un todo, así que otros ajustes experimentales que se conserven allí siguen ocultando la profundidad global. Por defecto, el subagente integrado **General** de OpenCode no puede lanzar subagentes, así que sus conversaciones nunca se anidan: guardar cualquier profundidad de proyecto también añade al mismo archivo una regla que permite a los subagentes lanzar sus propios subagentes hasta esa profundidad y prevalece sobre las restricciones de General de otros archivos de configuración (con una profundidad de 1, el límite de profundidad de OpenCode sigue impidiendo el anidamiento), y **Heredar** la quita. Bajo **Profundidad efectiva de OpenCode**, el editor indica si los subagentes General pueden anidarse. Si el proyecto define General en el formato heredado con un único valor `permission`, o con `task` y `subagent` ya definidos, solo se guarda la profundidad y el editor lo indica: permite tú mismo los subagentes para General en ese archivo. Guardar no recarga OpenCode ni cambia las conversaciones en ejecución. |
|
||||
|
|
@ -137,9 +141,51 @@ Una elección rechazada o sin confirmar nunca se reenvía: se vuelven a mostrar
|
|||
|
||||
Las descripciones guardan instrucciones y elecciones solicitadas, no planes de tareas, resultados, conversaciones, relojes de programación ni autoridad de ejecución. Cargar una rellena un borrador de creación normal; no inicia ni programa nada. Eliminar una descripción conserva las misiones/conversaciones existentes. **Configuración → Misiones → Descripciones guardadas** gestiona la biblioteca; si la biblioteca deja de poder leerse, **Restablecer la biblioteca guardada** (tras confirmar) elimina las descripciones guardadas, no las ejecuciones. Las programaciones no tienen acción para guardarlas como descripción.
|
||||
|
||||
## Hora diaria, recuperación de días y conversaciones seguidas {#daily-schedules}
|
||||
## Programación, recuperación y conversaciones seguidas {#daily-schedules}
|
||||
|
||||
Las programaciones diarias usan la zona horaria IANA y la hora civil guardadas, no un intervalo transcurrido de 24 horas. Las horas se muestran en la zona de la programación, etiquetada cuando difiere de la del lector. Cada ejecución obtiene su propio coordinador y mapa de misión aislado; la programación seleccionada muestra el árbol de tareas actual y el historial de fecha/resultado. Selecciona una ejecución pasada para leerla en el centro. El historial se limita a 30 resultados.
|
||||
### Programación nativa {#native-scheduling}
|
||||
|
||||
Los iconos **i** junto a las etiquetas explican las opciones: pasa el puntero, enfócalos con el teclado o tócalos.
|
||||
|
||||
Despliega **Programación** al crear o editar. **Frecuencia** ofrece:
|
||||
|
||||
| Opción | Momento |
|
||||
| --- | --- |
|
||||
| Una vez, inmediatamente | Un inicio mediante ▶ **Iniciar**, con una condición opcional que se comprueba como se describe abajo. |
|
||||
| Una vez, en una fecha | Una fecha y hora guardadas en la zona IANA; se rechaza una fecha ya pasada al iniciar. |
|
||||
| Una vez, tras un plazo | Una duración entera positiva en minutos u horas, a partir del inicio. |
|
||||
| A intervalos | Al menos 15 minutos; la primera activación llega un intervalo después del inicio. |
|
||||
| Todos los días | Una hora civil en la zona guardada. |
|
||||
| Días de la semana elegidos | Una hora en los días marcados. |
|
||||
| Varias veces al día | Hasta ocho horarios independientes, como 09:15 y 17:45; los horarios duplicados comparten una pasada. |
|
||||
| Un día de cada mes | Día 1–31 a la hora elegida; se omiten los meses sin ese día. |
|
||||
| Avanzado (cron) | Una expresión cron nativa, como máximo una vez por minuto. |
|
||||
|
||||
**Próximos horarios (hasta cinco)** muestra la zona guardada y la hora de inicio supuesta. Con una condición, **Próximas comprobaciones (hasta cinco)** muestra las comprobaciones; una pasada depende de un cambio verificado. Un intervalo guardado sin cambios conserva su fase nativa. Una fase pausada o desconocida se muestra como no disponible hasta que se pueda leer su estado nativo. Los plazos e intervalos usan tiempo transcurrido; los calendarios civiles siguen la zona guardada y las reglas de cambio de hora de OpenCode. Las fechas mostradas son proyecciones sujetas a la disponibilidad del servicio y del equipo de ejecución.
|
||||
|
||||
Tras una interrupción, la programación nativa entrega como máximo una pasada de recuperación antes de volver a horarios futuros. Usa el horario nativo vencido registrado, que puede preceder a otros horarios omitidos posteriores. Los horarios diarios múltiples comparten esta recuperación y la misma protección contra solapamientos. Una pasada sin resolver se reconcilia con sus identidades originales de conversación y mensaje antes de que otra pueda iniciarse. Reanudar una recurrencia pausada explícitamente usa su próximo horario futuro; un inicio puntual pausado puede llegar una vez, con retraso.
|
||||
|
||||
### Condiciones y fuentes {#scheduling-conditions}
|
||||
|
||||
Elige una **Condición** opcional y su **Fuente en este proyecto**: **Otra programación tiene éxito**, **Nuevos commits en una rama local**, **Una Misión única termina con éxito**, **Nuevo mensaje en una conversación seguida** o **Cambios en archivos explícitos**. Para una condición sola, conserva **Una vez, inmediatamente**: las comprobaciones nativas se ejecutan cada 15 minutos hasta iniciar su única pasada. Con un calendario, la condición se comprueba en sus horarios. Las condiciones recurrentes pueden volver a iniciar pasadas tras nuevos cambios que cumplan los requisitos.
|
||||
|
||||
El primer inicio establece la referencia inicial. Reanudar la conserva, incluidos los cambios observados tras una pausa. Las fuentes de éxito exigen un resultado exitoso verificado y una familia nativa resuelta. Las comprobaciones de ramas usan commits locales y su ascendencia sin obtener datos remotos. Las comprobaciones de conversaciones esperan mensajes nuevos completos y estables, y excluyen las activaciones técnicas y las propias conversaciones de la pasada. Los archivos usan hasta 128 rutas explícitas relativas al proyecto, una por línea; se rechazan los patrones recursivos y las rutas enlazadas. Una fuente ausente, movida, ambigua o que supere la capacidad bloquea la admisión con una explicación. Los cambios entre comprobaciones necesitan dejar una huella duradera en la fuente para detectarse.
|
||||
|
||||
Los cambios observados juntos se agrupan. Una pasada fija las evidencias observadas de las fuentes; la resolución exitosa consume esa instantánea y deja los cambios posteriores para una comprobación futura. Una pasada fallida conserva su punto de control de la fuente y exige intervención. **Conversaciones seguidas (opcional)** en **Opciones** aporta contexto independientemente de elegir una condición de mensaje de conversación.
|
||||
|
||||
### Finales y recuentos {#scheduling-limits}
|
||||
|
||||
Deja los límites vacíos para una recurrencia continua. **Último horario permitido** y **Último horario tras estas horas desde el primer inicio (opcional)** acotan los horarios admisibles; se aplica el límite más temprano. La duración empieza en el primer inicio y sigue transcurriendo durante la pausa. Un horario registrado antes del final puede llegar tarde tras una interrupción larga. Una pasada ya admitida sigue siendo observable hasta su resultado.
|
||||
|
||||
**Número máximo de ejecuciones iniciadas** es un total compartido entre todos los horarios y **Ejecutar ahora**. Una pasada cuenta una vez al admitirse la primera entrada de su coordinador, aunque falle después. Las comprobaciones, las condiciones sin cambios y las activaciones rechazadas no consumen plazas; una admisión incierta retiene su plaza hasta reconciliarse. Las políticas puntuales permiten como máximo una pasada. Las plazas consumidas se conservan tras editar o reiniciar; los límites de finalización sin cambios mantienen su origen en el primer inicio y el final absoluto de los horarios. Cambiar el máximo modifica el total de toda la duración; un total inferior o igual al número ya consumido termina la programación. Un cambio de frecuencia puede iniciar otra fase de intervalo al guardar, como explica el editor.
|
||||
|
||||
La programación nativa seleccionada muestra **Estado deseado**, **Calendario nativo**, **Próxima activación** o **Próxima comprobación**, **Comprobaciones** con **Seguimiento desde…**, **Pasadas iniciadas**, **Pasadas exitosas**, su final y **Ejecuciones anteriores**. Lee una pasada anterior mediante **Abrir conversación**. **Programación terminada** describe el final de la programación; inspecciona por separado el resultado de la pasada. Se conservan los últimos 30 resultados; los recuentos acumulados persisten más allá de esa ventana. **Cargar más Missions** carga la siguiente página de objetos guardados.
|
||||
|
||||
La lista muestra la condición y el final solo si están definidos, y **Comprobaciones** solo con una condición.
|
||||
|
||||
### Programaciones diarias existentes {#existing-daily-schedules}
|
||||
|
||||
Las programaciones diarias existentes usan la zona IANA y la hora civil guardadas. Cada ejecución tiene su propio coordinador y mapa de misión; selecciona su árbol de tareas actual o una fecha/resultado anterior para leerlo en el centro. Su historial conservado contiene 30 resultados. Las siguientes reglas diarias se aplican a estas entradas:
|
||||
|
||||
| Situación | Comportamiento de la programación |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@ Las programaciones diarias usan la zona horaria IANA y la hora civil guardadas,
|
|||
| El reloj retrocede | No hace que un día ya procesado vuelva a vencer. |
|
||||
| Una ejecución existente sigue pendiente | No la solapa con una ejecución diaria nueva. |
|
||||
|
||||
La fecha de creación también limita el trabajo diario elegible; iniciar una programación nueva no autoriza a procesar fechas anteriores a su existencia. **Ejecutar ahora** inicia una ejecución manual independiente cuando se ofrece; no reemplaza el reloj diario ni elude una ejecución pendiente. La próxima hora mostrada es una proyección, no una garantía de ejecución exacta.
|
||||
La fecha de creación también acota el trabajo diario admisible. **Ejecutar ahora**, cuando se ofrece en una entrada diaria existente, inicia una ejecución manual independiente bajo la protección de ejecuciones pendientes y conserva su reloj diario. La próxima hora mostrada es una proyección sujeta a la disponibilidad del servicio y del equipo de ejecución.
|
||||
|
||||
**Conversaciones seguidas (opcional)** añade contexto acotado de conversaciones elegidas. Solo una ejecución completada avanza sus cursores de mensajes procesados; una ejecución fallida/interrumpida no consume silenciosamente ese contexto. Si las instrucciones más el contexto superan la capacidad, acorta las instrucciones o retira conversaciones seguidas en lugar de suponer que se lee un historial ilimitado.
|
||||
|
||||
## Duración y reinicios del servicio {#lifetime}
|
||||
|
||||
El **i** junto a **Detalles técnicos** explica la actividad técnica y los costes. En **Configuración → OpenCode**, la supervisión mantiene visibles el estado y los errores; su **i** contiene las explicaciones generales.
|
||||
|
||||
En una programación nativa, despliega **Detalles técnicos** para identificar su **Portador principal** y su **Portador de reconciliación**, sus calendarios nativos y cualquier **Limpieza pendiente de reconciliación**. Estas conversaciones técnicas no ofrecen una acción de composición y quedan fuera de los selectores habituales de conversaciones y fuentes. Las activaciones técnicas, los títulos y la compactación usan el modelo local sin solicitudes HTTP a los proveedores; los eventos y estadísticas nativos aún pueden incluirlos, mientras que las pasadas reales mantienen sus costes de modelo habituales. La compactación limita el contexto procesado y conserva la identidad de la conversación y del calendario. Los mensajes y eventos nativos almacenados pueden acumularse hasta la limpieza explícita de una programación terminada; los 30 resultados de Mission conservados son independientes. El control de profundidad y **Programación** muestran la ruta bajo **Archivo de configuración de OpenCode:** cuando está disponible; el control de profundidad indica por separado su **Destino de escritura del proyecto**.
|
||||
|
||||
| Componente | Qué significa cerrarlo |
|
||||
| --- | --- |
|
||||
| Pestaña de proyecto de CodeNomad | Cierra el proyecto en CodeNomad; las sesiones nativas siguen en OpenCode. Vuelve a abrir el proyecto para encontrarlas. |
|
||||
| Ventana entera, salir de la aplicación o recargar el renderer | Conserva las conexiones de proyecto para restaurarlas; no detiene el trabajo nativo ni el servicio OpenCode. |
|
||||
| Backend de CodeNomad | No detiene OpenCode. Una misión puntual en curso continúa: su coordinador, sus tareas como subagentes nativos y las herramientas de misión se ejecutan en OpenCode. Asignar tareas a sesiones independientes y entregar sus informes al coordinador, las misiones que prepara el coordinador, detener las conversaciones de una tarea retirada, la recuperación y el Control de misión esperan al backend, igual que continuar una misión puntual tras un reinicio del servicio OpenCode. |
|
||||
| Programación diaria en ejecución | Su reloj y la ejecución de sus pasadas se ejecutan en el plugin/servicio OpenCode, incluso con la interfaz y el backend de CodeNomad cerrados. El equipo de ejecución y el servicio OpenCode deben seguir funcionando. |
|
||||
| Reinicio del servicio OpenCode | Pierde los trabajos de programación locales del proceso. Una programación que debía ejecutarse pasa a **Interrumpido** hasta una **Reanudar** explícita; reabrir CodeNomad no la reactiva. |
|
||||
| Programación nativa activada | Los calendarios persistentes de OpenCode activan su proyecto y ejecutan pasadas con la interfaz y el backend de CodeNomad cerrados, mientras el equipo de ejecución y el servicio estén disponibles. |
|
||||
| Reinicio del servicio OpenCode — programación nativa saludable | Una programación saludable cuyo estado deseado es en marcha reanuda su calendario automáticamente. El trabajo pendiente se reconcilia con sus identidades originales antes de nuevas admisiones. |
|
||||
| Programación diaria existente | Su reloj del plugin funciona con CodeNomad cerrado mientras el servicio siga activo. Un reinicio del servicio la deja **Interrumpido**; **Reanudar** explícitamente reconcilia su ejecución pendiente y reactiva su reloj diario. |
|
||||
|
||||
Tras un reinicio, inspecciona la ejecución pendiente y elige **Reanudar** cuando estés listo. Reanudar reconcilia la ejecución existente sin enviar dos veces la entrada de su coordinador; no promete continuar un turno nativo interrumpido. Un turno cortado por un reinicio puede resolverse como terminado sin informe, con **Interrumpida por un reinicio**. No uses un reinicio para reintentar efectos secundarios de herramientas.
|
||||
En las programaciones nativas, pausar o detener explícitamente, el trabajo fallido y los resultados inciertos conservan sus barreras de admisión tras reiniciar. Inspecciona la pasada conservada y la solicitud original; usa **Comprobar pasada** o **Actualizar la solicitud guardada** cuando se ofrezcan, y después **Reanudar** solo cuando el estado lo permita. Una continuación nativa se observa con su identidad existente. Un fallo demostrado o un final sin informe requiere recuperación humana; las pruebas incompletas mantienen la pasada incierta. Nunca uses un reinicio para repetir efectos de herramientas ya iniciados.
|
||||
|
||||
[Mantener OpenCode iniciado](opencode.md#service-supervision) supervisa por separado el servicio compartido en su equipo de ejecución. El primer inicio de una Misión lo activa salvo que hayas guardado su desactivación; crear una Misión en pausa conserva esa elección. Desactivarlo deja OpenCode funcionando sin reinicio automático. Windows, macOS y WSL dependen del inicio de sesión; Linux nativo mantiene los servicios de usuario tras cerrar sesión, sujeto a permisos. La suspensión, la hibernación, la duración de WSL inactivo y el control de un equipo externo limitan la disponibilidad; inspecciona el estado de supervisión independientemente de la programación.
|
||||
|
||||
Las Misiones funcionan con las aplicaciones de escritorio y con un [servidor](server.md) CodeNomad independiente. Cuando el panel muestra **Control de misión no está disponible**, su mensaje indica la causa:
|
||||
|
||||
|
|
@ -184,7 +237,9 @@ Las Misiones funcionan con las aplicaciones de escritorio y con un [servidor](se
|
|||
|
||||
Pausar una misión puntual deja terminar el trabajo en curso: el coordinador y sus subagentes completan el paso en el que están, no inician nada nuevo y se detienen, mientras el panel muestra **Pausando: los pasos en curso están terminando.** **Interrumpir ahora** los detiene en cambio de inmediato: interrumpe la familia nativa de la misión y suspende la coordinación. Detener también interrumpe, cancela las entradas/entregas familiares en cola y es terminal. Las interrupciones son acotadas: **Es posible que algunos subagentes sigan en ejecución.** significa que una está incompleta. Inspecciona el resultado conservado y usa su reintento explícito en lugar de suponer que se cancelaron todos los efectos de shells/herramientas. Reanudar informa al coordinador de lo que se pausó o interrumpió; este decide cómo continuar el trabajo.
|
||||
|
||||
Pausar/Detener una programación diaria cancela la programación y solicita interrumpir los actores raíz registrados, incluidos el coordinador y los especialistas independientes, pero **no demuestra que esas conversaciones ni sus descendientes se hayan detenido**. Inspecciona las familias nativas. Detener es terminal: una programación detenida no puede iniciarse, reanudarse ni ejecutarse ahora. **Comprobar pasada** reconcilia una ejecución pendiente sin volver a enviarla. Los fallos transitorios de comprobación programada muestran su último error y se reintentan automáticamente; perder la respuesta a un control del usuario no causa su reenvío automático.
|
||||
En una programación nativa, **Pausar** cierra futuras admisiones y solicita interrumpir una pasada admitida; **Reanudar** reconcilia el trabajo existente antes de reactivarla. **Detener…** es terminal. **Ejecutar ahora** usa la misma protección de pasadas pendientes y la misma cuota de pasadas iniciadas, y conserva el calendario. **Comprobar pasada** reconcilia la pasada original. Conserva una acción sin confirmar y usa **Actualizar la solicitud guardada**; la actualización pasiva y la reconexión conservan esa solicitud. Los efectos de herramientas iniciados pueden continuar tras la interrupción.
|
||||
|
||||
Las entradas diarias existentes mantienen sus controles diarios Pausar/Detener/Reanudar/Ejecutar ahora/Comprobar pasada. Pausar/Detener cancelan su reloj y solicitan interrumpir las raíces registradas, incluidos los especialistas independientes. Inspecciona sus familias nativas y cualquier resultado de interrupción parcial. Los fallos transitorios de comprobación diaria se reintentan con su advertencia visible; una respuesta perdida a un control del usuario sigue vinculada a su solicitud original. Una entrada detenida permanece en estado terminal.
|
||||
|
||||
Para un coordinador puntual inactivo sin informe, la acción principal ▶ puede verificarlo y recuperarlo; las tareas tienen recuperación específica de informes ausentes cuando cumplen las condiciones. La recuperación comprueba primero actividad nativa, descendientes y esperas; una actividad desconocida rechaza la admisión. No repite asignaciones ni reemplaza el plan. Abre las conversaciones para resolver preguntas, permisos, shells o trabajo en segundo plano pendientes.
|
||||
|
||||
|
|
@ -192,12 +247,28 @@ Si **Crear** queda sin confirmar, la ventana conserva la solicitud enviada y des
|
|||
|
||||
## Editar, eliminar y limpiar conversaciones {#cleanup}
|
||||
|
||||
Usa **Editar la misión seleccionada** en una misión puntual editable; ante un conflicto, cierra/reabre el editor para cargar la revisión actual en lugar de sobrescribirla. Si el objetivo o las notas cambian mientras el formulario aún carga, indica que la misión cambió desde que la abriste en lugar de rellenarlos; elige **Volver a abrir** para editar la versión actual. Las programaciones diarias no ofrecen controles de edición/eliminación.
|
||||
Si el objetivo o las notas cambian mientras el formulario aún carga, indica que la misión cambió desde que la abriste en lugar de rellenarlos; elige **Volver a abrir** para editar la versión actual.
|
||||
|
||||
Entre pasadas, una programación nativa pausada o en marcha permite editar título, instrucciones, notas, perfiles solicitados, modo de sesiones de tareas, fuentes, calendario y límites cuando ninguna pasada ni control sin resolver retiene el objeto. Una programación pausada permanece pausada; una activada reconcilia su calendario antes de continuar. Guardar conserva resultados anteriores, fuentes consumidas, recuento de inicios y el origen en el primer inicio; los límites de finalización sin cambios mantienen el final absoluto de sus horarios.
|
||||
|
||||
Durante una pasada admitida, solo las instrucciones y notas siguen el contrato de edición de textos activos: los cambios se registran en su historial y se guardan para futuras pasadas. Las tareas, encargos, perfiles e identidades de ejecución existentes conservan sus valores admitidos. Los campos estructurales permanecen bloqueados hasta resolver la pasada, incluso tras pausar. Un guion queda bloqueado en cuanto se haya creado alguna tarea, incluidas las completadas o retiradas. Una admisión/control incierto o evidencias de integridad incompletas mantienen la edición estructural en solo lectura. Crear y Editar comparten la validación del calendario; la vista previa aplica el último horario permitido modificado a la próxima activación guardada, incluso si llega tarde.
|
||||
|
||||
La fuente de ramas locales incluye ramas sin worktree y lee solo referencias locales. El selector admite hasta 200 ramas locales; un inventario mayor muestra la fuente como no disponible. **Iniciar** y **Reanudar** solicitan consentimiento con las rutas exactas cuando la activación está disponible. **Ejecutar ahora** permanece desactivado con una explicación mientras la programación nativa esté inactiva; activa `experimental.schedules` en la configuración de OpenCode y actualiza. Si se pierde la respuesta de activación, se conserva la solicitud original para consultar su recibo.
|
||||
|
||||
Antes del primer Play puede volver al inicio inmediato sin condición conservando las conversaciones seguidas en **Opciones**. Play lee y fija su prefijo de mensajes completos y estables (hasta 32 mensajes por fuente, dentro de una entrada de 16 384 caracteres). Una fuente ausente, movida o demasiado grande bloquea el inicio con una explicación. Un reintento conserva el contexto fijado y la identidad del inicio original.
|
||||
|
||||
Cuando la lectura del recibo confirma una preparación parcial, aparece **Intentar de nuevo**. El clic vuelve a comprobar ese mismo recibo y reanuda únicamente el cambio guardado original, manteniendo la misión en pausa.
|
||||
|
||||
Antes del primer inicio de una nueva misión preparada, puede editar título, instrucciones, notas, playbook, agentes/modelos/razonamiento solicitados, modo de sesiones de tareas y **Programación**. Puede cambiar entre inicios inmediatos, diferidos, recurrentes y condicionales conservando la misma Misión. Guardar la mantiene en pausa y prepara un coordinador nuevo y vacío; se conserva la conversación inicial. Solo Play inicia el trabajo o activa una programación. Tras el inicio, instrucciones y notas siguen las reglas de edición de la misión activa. Si guardar queda sin confirmar, **Actualizar la solicitud guardada** lee el resultado original; conserve esa solicitud mientras una preparación parcial bloquea el inicio, la edición de textos y la eliminación.
|
||||
|
||||
Usa **Editar la misión seleccionada** en una misión puntual o programación nativa editable. Lee las explicaciones junto a los campos bloqueados. Ante un conflicto de revisión, elige **Recargar la configuración actual** y abre **Borrador anterior para comparar** para revisar el borrador conservado antes de volver a guardar. Las programaciones diarias existentes mantienen los controles de su propia barra de herramientas.
|
||||
|
||||
En una programación nativa, **Eliminar la misión seleccionada…** desactiva futuras pasadas antes de retirar la entrada. Se conservan las conversaciones y resultados; la limpieza pendiente de reconciliación sigue visible en **Detalles técnicos**. Conserva una eliminación sin confirmar y actualiza su solicitud original.
|
||||
|
||||
1. Para una misión puntual que ya no necesites, elige **Eliminar la misión seleccionada…** y revisa la confirmación.
|
||||
2. Opcionalmente, selecciona **Eliminar también las conversaciones de especialistas creadas para esta misión** antes de confirmar.
|
||||
3. Comprueba **Limpieza de conversaciones** para ver los recuentos de eliminadas/conservadas/pendientes. Reintenta solo la limpieza restante registrada si se ofrece.
|
||||
|
||||
Eliminar una misión no equivale a detenerla; usa Detener e inspecciona primero el trabajo restante. Se conservan los coordinadores y las conversaciones reutilizadas. También pueden conservarse especialistas compartidos, movidos, con hijos o cuya propiedad no se pueda verificar. La limpieza parcial no se revierte ni se completa silenciosamente en segundo plano. El lector general conserva las 20 limpiezas completadas más recientes. Eliminar misiones libera su almacenamiento en el proyecto: las misiones eliminadas más antiguas se borran automáticamente.
|
||||
Eliminar una misión no equivale a detenerla; usa Detener e inspecciona primero el trabajo restante. Se conservan los coordinadores y las conversaciones reutilizadas. También pueden conservarse especialistas compartidos, movidos, con hijos o cuya propiedad no se pueda verificar. La limpieza parcial no se revierte ni se completa silenciosamente en segundo plano. La tarjeta de la misión puede desaparecer mientras la confirmación de la eliminación sigue pendiente, incluso cuando la limpieza de conversaciones muestra 0 pendientes. Sigue usando la acción original **Intentar de nuevo** hasta que se confirme la eliminación; conserva la misma solicitud de eliminación y las mismas opciones. El lector general conserva las 20 limpiezas completadas más recientes. Eliminar misiones libera su almacenamiento en el proyecto: las misiones eliminadas más antiguas se borran automáticamente.
|
||||
|
||||
Consulta [Permisos](permissions.md), [Conversaciones](conversations.md) y [configuración de OpenCode](opencode.md) para el trabajo nativo y los controles del servicio.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ El ejecutable instalado y el servicio en ejecución pueden tener versiones disti
|
|||
| Recomendada | OpenCode **2.2.5** | Objetivo verificado, distinto del mínimo técnico. |
|
||||
| No verificada | Una versión más nueva o personalizada no validada por completo | No se rechaza simplemente por no estar probada; se comprueban los contratos API reales del servicio conectado. |
|
||||
|
||||
También deben estar disponibles los contratos requeridos de sesiones, permisos, formularios, bandeja de entrada y entorno de sesión. La versión de un ejecutable no demuestra por sí sola que el servicio conectado los admita. Si la configuración inicial informa de un servicio en ejecución no compatible, selecciona/instala una CLI compatible y usa el reinicio explícito del servicio compartido cuando estés listo.
|
||||
El servicio conectado debe exponer los contratos requeridos de sesiones, permisos, formularios, bandeja de entrada y entorno de sesión. La programación nativa de Misiones también comprueba las API de programación, el paquete de plugin/proveedor y el ajuste efectivo `experimental.schedules`. La disponibilidad depende de esas capacidades utilizadas, también en versiones futuras compatibles. Si la configuración inicial informa de un servicio en ejecución no compatible, selecciona/instala una CLI compatible y usa el reinicio explícito del servicio compartido cuando estés listo.
|
||||
|
||||
## Equipo, WSL y ámbito del servicio compartido {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ En Windows, un ejecutable WSL seleccionado se ejecuta dentro de su distribución
|
|||
|
||||
OpenCode es propietario de su registro global de servicio, sesiones, mensajes y base de datos. CodeNomad no crea una base de datos privada de OpenCode por ventana o proyecto. Cerrar una pestaña de proyecto cierra ese proyecto en CodeNomad; las sesiones siguen ejecutándose en OpenCode y vuelven a aparecer al reabrir el proyecto. CodeNomad nunca descarga las Locations nativas. Cerrar una ventana entera, salir o recargar conserva las conexiones de proyecto para restaurarlas; ni esas acciones ni el apagado del backend detienen el daemon compartido. Consulta [Proyectos](projects.md).
|
||||
|
||||
## Mantener OpenCode iniciado {#service-supervision}
|
||||
|
||||
Antes del primer inicio de Mission, **Mantener desactivado al iniciar la primera Mission** guarda un OFF explícito sin instalar antes la supervisión.
|
||||
|
||||
En **Ajustes → OpenCode**, **Mantener OpenCode iniciado** instala supervisión del sistema en el host de ejecución, compartida por todos los perfiles CodeNomad que usan el servicio. Funciona independientemente de Missions, incluso con CodeNomad cerrado. El primer inicio de Mission la activa, salvo desactivación explícita previa; crear una Mission pausada conserva la elección. Pausar, detener o eliminar una Mission conserva esta preferencia.
|
||||
|
||||
Desactivar retira la supervisión mientras OpenCode sigue funcionando y ya no se reiniciará. Para detenerlo de forma duradera, desactiva primero y luego detén OpenCode explícitamente. Con supervisión activa, `opencode service stop` provoca un reinicio.
|
||||
|
||||
**En marcha · supervisión activa** exige un supervisor activo y un servicio OpenCode saludable. **Instalado · servicio iniciándose**, **Instalando o retirando la supervisión…** y **Fallo de supervisión** distinguen los estados. Un servicio que responde por sí solo deja la supervisión sin confirmar. Los detalles muestran la causa, último intento, reintento del supervisor y comprobación de recuperación del sistema cuando se conocen. **Comprobar supervisión** solo lee; **Reintentar supervisión** repara explícitamente la instalación solicitada. Una acción no confirmada conserva la solicitud original; **Reintentar acción original** reenvía solo esa acción exacta. Una revisión cambiada se actualiza para revisión en lugar de sobrescribirse.
|
||||
|
||||
Windows, macOS y WSL arrancan tras iniciar sesión. WSL necesita reenvío localhost para la distribución elegida. En Linux nativo, linger inicia **todos los servicios de usuario habilitados** antes de iniciar sesión y los mantiene tras cerrarla. Al desactivar, se restaura linger solo si CodeNomad lo activó y ninguna otra supervisión lo necesita; se conservan los ajustes personales. Si restaurar linger pudiera detener los procesos OpenCode liberados mientras no hay ninguna sesión de usuario abierta, la restauración se aplaza y sigue visible con **Reintentar supervisión**. Reintenta cuando haya una sesión de usuario abierta o hayan terminado los procesos OpenCode liberados, permitiendo una restauración segura. Si Linux exige permiso, autoriza en el host de ejecución y reintenta; la preferencia solicitada permanece guardada. El sistema controla suspensión e hibernación. Un host indisponible o no compatible muestra un control desactivado con explicación.
|
||||
|
||||
## Actualizar, reiniciar o recargar {#maintenance}
|
||||
|
||||
1. En **Configuración → OpenCode**, elige **Comprobar estado y actualizaciones**.
|
||||
|
|
@ -41,7 +53,7 @@ OpenCode es propietario de su registro global de servicio, sesiones, mensajes y
|
|||
| Acción | Ámbito y efecto | Recuperación segura |
|
||||
| --- | --- | --- |
|
||||
| Actualizar CLI | Cambia el ejecutable instalado compatible; no reinicia implícitamente el daemon en ejecución. | Comprueba las versiones instalada y en ejecución tras un fallo; reintenta explícitamente solo después de resolver el error. |
|
||||
| Reiniciar servicio | Reemplaza el proceso compartido e interrumpe el trabajo activo de todos los clientes OpenCode conectados; el historial de sesiones permanece. | Reconecta e inspecciona conversaciones y Misiones programadas; un reinicio no implica continuación automática. |
|
||||
| Reiniciar servicio | Reemplaza el proceso compartido e interrumpe el trabajo activo de todos los clientes OpenCode conectados; el historial de sesiones permanece. | Las programaciones nativas de Misiones saludables y en marcha se recuperan automáticamente; inspecciona las pasadas pendientes y cualquier pausa/error/incertidumbre. Las entradas diarias existentes requieren **Reanudar**. Inspecciona por separado las conversaciones puntuales y sus controles de recuperación; consulta [Misiones](missions.md#lifetime). |
|
||||
| Recargar la configuración de OpenCode | Mantiene el proceso, pero reconstruye **todas las Locations cargadas** para todos los clientes. Cancela permisos/formularios pendientes y cierra terminales/comandos en segundo plano; puede interrumpirse el trabajo activo. | Vuelve a comprobar recursos y trabajo pendiente tras recargar. No repitas un mensaje o modificación solo porque se perdió su respuesta. |
|
||||
|
||||
Usa **Recargar la configuración de OpenCode**, en **Solución de problemas** cuando el servicio en ejecución lo admite, explícitamente cuando una configuración guardada o un plugin instalado siga sin estar disponible y la interrupción sea aceptable. No es una actualización visual inofensiva. CodeNomad no usará una CLI seleccionada más antigua para rebajar un daemon en ejecución más nuevo.
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Son instrucciones. Lo que un agente puede hacer realmente lo deciden los permiso
|
|||
|
||||
### Preguntas que se te hacen {#questions}
|
||||
|
||||
Solo el coordinador te hace preguntas. Las tareas y sus auxiliares devuelven las suyas a la conversación que las lanzó, con las opciones, las evidencias, las consecuencias, la reversibilidad y una recomendación, y siguen con el trabajo que no depende de la respuesta. El coordinador resuelve primero lo que puede y agrupa las preguntas restantes en un solo formulario en **Te necesita**; [Preguntas y decisiones](missions.md#questions) explica qué pregunta y cómo se etiquetan las decisiones. Cada coordinador, único o diario, y cada asignación de tarea reciben esta regla como instrucción; Exploración añade las comprobaciones de decisiones [más abajo](#wayfinder-checks).
|
||||
Solo el coordinador te hace preguntas. Las tareas y sus auxiliares devuelven las suyas a la conversación que las lanzó, con las opciones, las evidencias, las consecuencias, la reversibilidad y una recomendación, y siguen con el trabajo que no depende de la respuesta. El coordinador resuelve primero lo que puede y agrupa las preguntas restantes en un solo formulario en **Te necesita**; [Preguntas y decisiones](missions.md#questions) explica qué pregunta y cómo se etiquetan las decisiones. Cada coordinador, puntual o programado, y cada asignación de tarea reciben esta regla como instrucción; Exploración añade las comprobaciones de decisiones [más abajo](#wayfinder-checks).
|
||||
|
||||
Responde en el panel de interrupciones (**Responder** en **Te necesita**): solo eso cuenta como tu respuesta. Una respuesta escrita en el chat es un mensaje normal.
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ Responde en el panel de interrupciones (**Responder** en **Te necesita**): solo
|
|||
|
||||
El coordinador tiene instrucciones de publicar un informe de situación tras su plan inicial, tras cada tarea que cierra, cada vez que empieza a esperarte y antes de su informe final; consulta [Informes de situación y solicitudes de actualización](missions.md#briefings). Los informes de situación, los resúmenes de tareas y el informe final están escritos para ti: en tu idioma, en pocas frases cortas, primero el resultado, con identificadores y comandos en las evidencias.
|
||||
|
||||
El informe final indica lo entregado, lo que queda y lo que requiere tu decisión. Una misión única solo termina como completada: cuando nada puede avanzar sin ti, el coordinador te pregunta y espera en lugar de fallar, y solo tú detienes una misión. Una ejecución diaria también puede terminar como fallida.
|
||||
El informe final indica lo entregado, lo que queda y lo que requiere tu decisión. Una misión única solo termina como completada: cuando nada puede avanzar sin ti, el coordinador te pregunta y espera en lugar de fallar, y solo tú detienes una misión. Una pasada programada también puede terminar como fallida.
|
||||
|
||||
### Lo que CodeNomad comprueba en cada misión {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ Nunca escribe tu respuesta por sí mismo ni presenta su propia decisión como tu
|
|||
- Solo se aceptan los cinco roles de Exploración.
|
||||
- Una tarea de decisión solo se completa con la decisión propia del coordinador o con tu respuesta. Una decisión del coordinador indica la pregunta, la decisión y su justificación, y nunca se registra como tu respuesta.
|
||||
- Tu respuesta solo cuenta si la diste en la interfaz de CodeNomad. CodeNomad la vincula a una tarea por la etiqueta de la pregunta: la pregunta se hizo desde la conversación del coordinador, su texto termina con la etiqueta de esa tarea y no nombra otra decisión, se respondió después de declararse la tarea y no ha resuelto otra decisión. Cada pregunta de un formulario agrupado se vincula por separado; una pregunta que nombra dos decisiones no resuelve ninguna. También se acepta una respuesta a una pregunta hecha desde la propia conversación de la tarea de decisión.
|
||||
- El informe propio de una tarea no puede afirmar quién decidió. Antes de que una misión o una ejecución diaria termine con éxito, CodeNomad vuelve a comprobar cada decisión completada, etiquetas incluidas.
|
||||
- El informe propio de una tarea no puede afirmar quién decidió. Antes de que una misión o una pasada programada termine con éxito, CodeNomad vuelve a comprobar cada decisión completada, etiquetas incluidas.
|
||||
- Estas comprobaciones se aplican con **Subagentes nativos**. Con **Sesiones independientes**, los responsables de decisión envían sus informes ellos mismos y el coordinador registra cada resultado en las notas indicando quién decidió; CodeNomad no lo verifica.
|
||||
|
||||
Solo instrucciones: cuándo una elección se considera fácil de revertir, la calidad de los informes y la investigación, y limitarse a planificar.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode vous donne le moteur. CodeNomad vous donne le cockpit. Il se connecte a
|
|||
|
||||
## Confier le travail de longue haleine aux Missions {#missions}
|
||||
|
||||
Décrivez le travail une seule fois. Le coordinateur de la mission le planifie sous forme d’arbre de tâches, délègue des tâches délimitées à des conversations natives OpenCode et rend compte des résultats avec leurs preuves. Exécutez une mission une fois ou chaque jour ; en créer une ne démarre jamais le travail tant que vous ne l’avez pas décidé. Des scénarios couvrent le débogage, la livraison de produit et la comparaison de deux modèles avec un juge à l’aveugle. Voir [Missions](missions.md).
|
||||
Décrivez le travail une seule fois. Le coordinateur de la mission le planifie sous forme d’arbre de tâches, délègue des tâches délimitées à des conversations natives OpenCode et rend compte des résultats avec leurs preuves. Exécutez une fois, aux horaires ou intervalles choisis, ou après un changement vérifié du projet ; la création enregistre une Mission en pause que vous démarrez explicitement. Des scénarios couvrent le débogage, la livraison de produit et la comparaison de deux modèles avec un juge à l’aveugle. Voir [Missions](missions.md).
|
||||
|
||||
## Continuer à travailler depuis votre téléphone {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ La racine de l’espace de travail limite les dossiers que CodeNomad ouvre ; ce
|
|||
| Travailler avec plusieurs copies de travail et conversations | [Projets](projects.md), [Conversations](conversations.md), [Fichiers](files.md) |
|
||||
| Rechercher dans l’historique ou le nettoyer sans risque | [Historique](history.md) |
|
||||
| Répondre aux questions et autorisations natives | [Autorisations](permissions.md) |
|
||||
| Coordonner un travail délimité ou quotidien | [Missions](missions.md) |
|
||||
| Coordonner un travail délimité ou programmé | [Missions](missions.md) |
|
||||
| Configurer l’exécution et les intégrations | [OpenCode](opencode.md), [Fournisseurs](providers.md), [Intégrations](integrations.md) |
|
||||
| Connecter un autre appareil | [Contrôle à distance](remote-control.md), [Sécurité](security.md), [Mobile](mobile.md) |
|
||||
| Adapter l’espace de travail | [Paramètres](settings.md), [Raccourcis](shortcuts.md), [Voix](voice.md), [Extensions](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ Les Missions associent un plan de projet durable aux conversations natives OpenC
|
|||
|
||||
1. Ouvrez **Missions** dans le panneau droit et choisissez **Créer une mission**. Le formulaire remplace la liste des missions et les tâches de la mission sélectionnée jusqu’à ce que vous créiez ou annuliez.
|
||||
2. Remplissez **Que doit faire la mission ?** et vérifiez **Titre**, prérempli depuis la première phrase (jusqu’à 60 caractères).
|
||||
3. Sous **Quand**, sélectionnez **Une fois** ou **Chaque jour à**. Pour un travail quotidien, vérifiez le fuseau affiché ; **Modifier** permet un fuseau IANA.
|
||||
3. Laissez **Programmation** repliée pour **Une fois, immédiatement** et **Sans condition**, ou développez-la pour choisir une fréquence, une condition et des limites facultatives. Vérifiez **Fuseau enregistré (IANA)** et l’aperçu avant de créer un travail programmé.
|
||||
4. Développez **Options** pour **Scénario**, **Notes**, **Agents et modèles**, **Sessions des tâches**, la **Profondeur d’imbrication des sous-sessions** du projet, les consignes enregistrées et les conversations suivies facultatives. Avec **Comparer deux modèles**, les sélecteurs **Modèle A** et **Modèle B** apparaissent dans le formulaire principal.
|
||||
5. Choisissez **Créer**. Sélectionnez la nouvelle entrée, puis utilisez son action ▶ distincte lorsque vous êtes prêt : **Démarrer la mission** pour un travail ponctuel, ou **Démarrer le programme {id}** pour un programme quotidien.
|
||||
5. Choisissez **Créer**. Sélectionnez l’entrée en pause, examinez-la, puis utilisez ▶ **Démarrer** lorsque vous êtes prêt. Un travail immédiat sans condition lance une mission ; une politique différée, récurrente ou conditionnelle arme la programmation native.
|
||||
|
||||
**Créer ne démarre jamais le travail.** Une mission ponctuelle est préparée ; un programme quotidien est en pause. Ni la création ni l’enregistrement d’une consigne n’arment un minuteur. La création est limitée au projet/à la Location sélectionné et fige les profils demandés ainsi que la politique des sessions de tâches. La validation se trouve près du champ concerné ; un refus indiquant que rien n’a été envoyé laisse le brouillon disponible pour correction. Le coordinateur d’une mission ponctuelle peut aussi préparer pour vous une mission distincte, par exemple une suite : elle apparaît dans la liste comme **Préparée** et, comme toute mission, ne démarre que par son action ▶.
|
||||
**Créer enregistre une Mission en pause.** Seule l’action ▶ **Démarrer** arme le travail et les calendriers natifs ; créer ou enregistrer une consigne conserve la préférence de supervision du service partagé. La création appartient au projet/à la Location sélectionné et enregistre les profils demandés et la politique des sessions de tâches. La validation apparaît près du champ concerné ; un refus indiquant que rien n’a été envoyé conserve le brouillon pour correction. Un coordinateur ponctuel peut aussi préparer une mission de suite distincte : elle apparaît comme **Préparée** et démarre depuis sa propre action ▶.
|
||||
|
||||
Les missions ponctuelles enregistrées conservent leurs commandes d’exécution, leur arbre de tâches, leurs conversations et leurs rapports finaux. Les programmations quotidiennes existantes restent dans la liste avec leurs commandes quotidiennes et leurs [règles horaires quotidiennes](#existing-daily-schedules). Sélectionnez l’entrée et utilisez les commandes disponibles pour cet objet.
|
||||
|
||||
### Scénarios {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@ Les résultats remontent par le même chemin. Un sous-agent natif renvoie son r
|
|||
|
||||
Une tâche ultérieure s’appuie sur le travail antérieur de deux façons. Les dépendances ordonnent le travail : le coordinateur démarre une tâche une fois terminées les tâches dont elle dépend, puis place les résultats utiles dans la consigne ou à côté de l’affectation ; les sessions indépendantes peuvent aussi lire ces rapports dans le plan. Pour poursuivre la conversation même d’une tâche terminée, le coordinateur déclare une tâche de suite qui la réutilise : la suite y continue avec le contexte actuel de cette conversation, qu’OpenCode a pu [compacter](history.md#compact-session), tandis que les détails antérieurs restent dans son historique enregistré pour la recherche. Le scénario Débogage renvoie ainsi les deux revues à l’implémenteur. Avec **Sessions indépendantes**, le coordinateur peut aussi affecter une tâche à une conversation principale existante, qui conserve son propre contexte, y compris un travail antérieur sans rapport. Sinon, une tâche démarre dans une nouvelle conversation à partir de son affectation, et la transcription d’une autre tâche lui parvient par le coordinateur ou par une recherche. Pour **Comparer deux modèles**, l’affectation et le contexte du juge omettent l’objectif et les notes ; sa consigne contient les résultats anonymisés.
|
||||
|
||||
Dans un programme quotidien, chaque exécution démarre avec son propre coordinateur et son propre plan. Son premier message contient les instructions du programme ainsi que le nouveau texte utilisateur et assistant de ses [conversations suivies](#daily-schedules) depuis la dernière exécution terminée ; les résultats des exécutions précédentes restent dans l’historique du programme. Les conversations lancées au cours d’une exécution peuvent lire le plan de cette exécution. Une nouvelle mission ponctuelle démarre aussi avec un nouveau plan : pour reporter des résultats, écrivez-les dans son objectif ou ses **Notes**, ou laissez le coordinateur préparer une mission de suite, dont il rédige l’objectif et les notes.
|
||||
Chaque passage programmé démarre avec son propre coordinateur et son propre plan de mission, à partir des instructions enregistrées et des entrées figées. Les conversations suivies apportent une quantité bornée de nouveau texte utilisateur et assistant ; leurs curseurs de consommation avancent après un passage réussi. Les passages précédents restent dans l’historique de la programmation. Les conversations lancées pendant un passage peuvent lire son plan. Une nouvelle mission ponctuelle possède un plan distinct : placez les résultats à transmettre dans son objectif ou ses **Notes**, ou demandez au coordinateur de préparer une suite avec ce contexte.
|
||||
|
||||
### Rechercher dans les conversations antérieures {#context-search}
|
||||
|
||||
Tant que CodeNomad fonctionne, chaque conversation d’agent du projet, coordinateurs de mission et tâches compris, dispose de l’outil de recherche de sessions de CodeNomad (`codenomad.session_search`, utilisé via le Code Mode d’OpenCode). Il recherche dans les conversations enregistrées du dossier de projet de la conversation avec le même classement que la [recherche](history.md#full-history-search) **Workspace entier**, et renvoie de courts extraits avec le titre et l’identifiant de chaque conversation, ainsi qu’un moyen de lire une conversation entière. Les autorisations natives des outils s’appliquent. Un agent cherche lorsqu’il le décide ou lorsque l’objectif, les notes ou votre message le lui demandent, par exemple pour retrouver ce qu’a conclu une mission ou une tâche antérieure. Une exécution quotidienne qui tourne pendant que CodeNomad est fermé travaille sans cet outil.
|
||||
Tant que CodeNomad fonctionne, chaque conversation d’agent du projet, coordinateurs de mission et tâches compris, dispose de l’outil de recherche de sessions de CodeNomad (`codenomad.session_search`, utilisé via le Code Mode d’OpenCode). Il recherche dans les conversations enregistrées du dossier de projet de la conversation avec le même classement que la [recherche](history.md#full-history-search) **Workspace entier**, et renvoie de courts extraits avec le titre et l’identifiant de chaque conversation, ainsi qu’un moyen de lire une conversation entière. Les autorisations natives des outils s’appliquent. Un agent cherche lorsqu’il le décide ou lorsque l’objectif, les notes ou votre message le lui demandent, par exemple pour retrouver ce qu’a conclu une mission ou une tâche antérieure. Un passage programmé exécuté pendant que CodeNomad est fermé travaille sans cet outil.
|
||||
|
||||
## Profils, délégation et profondeur {#profiles}
|
||||
|
||||
Enregistrer une profondeur préserve les programmations natives activées dans la configuration du projet. Un `schedules:false` explicite du projet reste désactivé ; retirer la profondeur conserve le réglage de programmation. Les fichiers JSON et JSONC concurrents doivent être départagés avant d’enregistrer les réglages natifs.
|
||||
|
||||
1. Utilisez l’engrenage des Missions ou **Paramètres → Missions** pour modifier **Défauts des nouvelles missions**.
|
||||
2. Sous **Appliquer à**, choisissez **Toutes les missions** ou un scénario, puis réglez **Sessions des tâches** et, sous **Agents et modèles**, une ligne par rôle (coordinateur, puis les rôles du scénario). Chaque choix est enregistré immédiatement ; il n’y a pas de bouton Enregistrer.
|
||||
3. Dans la portée d’un scénario, les lignes affichent les valeurs globales marquées **Global**. Modifier un champ personnalise ce rôle à partir de ces valeurs ; son icône de retour réutilise les valeurs globales. **Réinitialiser** (après confirmation) efface seulement les agents et modèles de la portée affichée.
|
||||
|
|
@ -118,7 +122,7 @@ Tant que CodeNomad fonctionne, chaque conversation d’agent du projet, coordina
|
|||
| Réglage | Portée et effet |
|
||||
| --- | --- |
|
||||
| Défauts des nouvelles missions | Valeurs par défaut de la configuration pour les **nouvelles** missions seulement ; une exception de scénario remplace la valeur générale. |
|
||||
| Agent / Modèle / Réflexion | Sélections demandées du coordinateur/des rôles enregistrées à la création ; un acteur occupé ne change pas silencieusement pour un travail ultérieur. Les exécutions quotidiennes réutilisent leurs profils demandés figés. |
|
||||
| Agent / Modèle / Réflexion | Sélections demandées du coordinateur/des rôles enregistrées à la création ; un acteur occupé ne change pas silencieusement pour un travail ultérieur. Les passages programmés utilisent leurs profils demandés enregistrés ; les modifications s’appliquent aux futurs passages. |
|
||||
| Sous-agents natifs | Les sessions de tâches utilisent les agents natifs subagent/all sous le coordinateur. |
|
||||
| Sessions indépendantes | Les sessions de tâches déclarées utilisent les agents primary/all et des conversations indépendantes ; cela ne désactive pas les assistants natifs internes. |
|
||||
| Profondeur d’imbrication des sous-sessions | Configuration de projet pour **toutes** les conversations OpenCode qui s’y trouvent, pas seulement les Missions. Vide hérite : **Hériter** supprime le remplacement du projet ainsi qu’un bloc `experimental` qu’il laisse vide ou qui ne contient plus que la valeur globale de **Stocker les images une seule fois**. OpenCode lit le bloc `experimental` du projet comme un tout ; les autres réglages expérimentaux qui y restent masquent donc toujours la profondeur globale. Par défaut, le sous-agent intégré **General** d’OpenCode ne peut pas lancer de sous-agents, donc ses conversations ne s’imbriquent jamais : enregistrer une profondeur de projet, quelle qu’elle soit, ajoute aussi au même fichier une règle qui permet aux sous-agents de lancer leurs propres sous-agents jusqu’à cette profondeur et qui prime sur les restrictions de General venant d’autres fichiers de configuration (à une profondeur de 1, la limite de profondeur d’OpenCode empêche toujours l’imbrication), et **Hériter** la retire. Sous **Profondeur OpenCode effective**, l’éditeur indique si les sous-agents General peuvent s’imbriquer. Si le projet définit General dans l’ancien format avec une valeur `permission` unique, ou avec `task` et `subagent` déjà définis, seule la profondeur est enregistrée et l’éditeur le signale : autorisez vous-même les sous-agents pour General dans ce fichier. L’enregistrement ne recharge pas OpenCode et ne change pas les conversations actives. |
|
||||
|
|
@ -137,9 +141,51 @@ Un choix refusé ou non confirmé n’est jamais renvoyé : les valeurs enregist
|
|||
|
||||
Les consignes stockent les instructions et choix demandés, pas les plans de tâches, résultats, conversations, horaires de programme ou autorité d’exécution. En charger une remplit un brouillon de création ordinaire ; cela ne démarre ni ne planifie rien. Retirer une consigne conserve les missions/conversations existantes. **Paramètres → Missions → Consignes enregistrées** gère la bibliothèque ; si celle-ci devient illisible, **Réinitialiser la bibliothèque enregistrée** (après confirmation) retire les consignes enregistrées, pas les exécutions. Les programmes eux-mêmes n’ont pas d’action d’enregistrement comme consigne.
|
||||
|
||||
## Heure quotidienne, rattrapage et conversations suivies {#daily-schedules}
|
||||
## Programmation, rattrapage et conversations suivies {#daily-schedules}
|
||||
|
||||
Les programmes quotidiens utilisent le fuseau IANA et l’heure civile enregistrés, pas un intervalle écoulé de 24 heures. Les heures sont affichées dans le fuseau du programme, indiqué s’il diffère de celui du lecteur. Chaque exécution reçoit son coordinateur et son plan de mission isolé ; le programme sélectionné affiche l’arbre actuel et l’historique date/résultat. Sélectionnez une ancienne exécution pour la lire au centre. L’historique est limité à 30 résultats.
|
||||
### Programmation native {#native-scheduling}
|
||||
|
||||
Les icônes **i** près des libellés expliquent les choix : survolez-les, placez-y le focus au clavier ou touchez-les.
|
||||
|
||||
Développez **Programmation** dans le formulaire de création ou de modification. **Fréquence** propose :
|
||||
|
||||
| Choix | Déclenchement |
|
||||
| --- | --- |
|
||||
| Une fois, immédiatement | Un lancement par ▶ **Démarrer**, avec une condition facultative vérifiée comme décrit ci-dessous. |
|
||||
| Une fois, à une date | Une date et une heure enregistrées dans le fuseau IANA ; une date déjà passée au démarrage est refusée. |
|
||||
| Une fois, dans un délai | Une durée entière positive en minutes ou en heures, à compter du démarrage. |
|
||||
| À intervalles réguliers | Au moins 15 minutes ; le premier réveil survient un intervalle après le démarrage. |
|
||||
| Tous les jours | Une heure civile dans le fuseau enregistré. |
|
||||
| Certains jours de la semaine | Une heure les jours cochés. |
|
||||
| Plusieurs fois par jour | Jusqu’à huit horaires indépendants, comme 09:15 et 17:45 ; les doublons partagent un passage. |
|
||||
| Un jour de chaque mois | Le jour 1–31 à l’heure choisie ; les mois dépourvus de ce jour sont sautés. |
|
||||
| Avancé (cron) | Une expression cron native, au plus une fois par minute. |
|
||||
|
||||
**Prochains créneaux (cinq au plus)** prévisualise le fuseau enregistré et l’heure de démarrage supposée. Avec une condition, **Prochaines vérifications (cinq au plus)** prévisualise les vérifications ; un passage dépend d’un changement vérifié. Un intervalle enregistré inchangé conserve sa phase native. Une phase en pause ou inconnue est affichée comme indisponible jusqu’à ce que son état natif puisse être lu. Délais et intervalles utilisent le temps écoulé ; les calendriers civils suivent le fuseau enregistré et les règles de changement d’heure d’OpenCode. Les dates affichées sont des projections soumises à la disponibilité du service et de l’hôte.
|
||||
|
||||
Après une panne, la programmation native livre au plus un passage de rattrapage avant de revenir aux créneaux futurs. Elle utilise le créneau natif dû enregistré, qui peut précéder d’autres créneaux manqués. Les horaires quotidiens multiples partagent ce rattrapage et la même protection contre le chevauchement. Un passage non résolu est réconcilié avec ses identités de conversation et de message d’origine avant qu’un autre puisse démarrer. Reprendre une récurrence explicitement mise en pause utilise son prochain créneau futur ; un lancement ponctuel en pause peut être livré une fois, en retard.
|
||||
|
||||
### Conditions et sources {#scheduling-conditions}
|
||||
|
||||
Choisissez une **Condition** facultative et sa **Source dans ce projet** : **Réussite d’une autre programmation**, **Nouveaux commits sur une branche locale**, **Réussite d’une Mission unique**, **Nouveau message dans une conversation suivie** ou **Modification de fichiers explicites**. Pour une condition seule, gardez **Une fois, immédiatement** : les vérifications natives ont lieu toutes les 15 minutes jusqu’au lancement de son unique passage. Avec un calendrier, la condition est vérifiée à ses créneaux. Les conditions récurrentes peuvent relancer un passage après de nouveaux changements admissibles.
|
||||
|
||||
Le premier démarrage établit la référence initiale. La reprise la conserve, y compris les changements observés après une pause. Les sources de réussite exigent un résultat réussi vérifié et une famille native dont l’exécution est réglée. Les vérifications de branche utilisent les commits locaux et leur ascendance sans récupération distante. Celles de conversations attendent des messages nouveaux complets et stables et excluent les réveils techniques et les conversations du passage lui-même. Les fichiers utilisent jusqu’à 128 chemins explicites relatifs au projet, un par ligne ; les motifs récursifs et les chemins liés sont refusés. Une source absente, déplacée, ambiguë ou dépassant la capacité bloque l’admission avec une explication. Pour être détectés, les changements entre vérifications doivent laisser une trace durable dans la source.
|
||||
|
||||
Les changements observés ensemble sont regroupés. Un passage fige les preuves observées dans la source ; un règlement réussi consomme cet instantané, laissant les changements ultérieurs pour une prochaine vérification. Un passage échoué conserve son point de contrôle de source et exige une intervention. **Conversations suivies (facultatif)** sous **Options** fournit du contexte indépendamment du choix d’une condition de message de conversation.
|
||||
|
||||
### Fins et décomptes {#scheduling-limits}
|
||||
|
||||
Laissez les limites vides pour une récurrence continue. **Dernier créneau autorisé** et **Dernier créneau après ce nombre d’heures depuis le premier Play (facultatif)** bornent les créneaux admissibles ; la première borne l’emporte. La durée commence au premier démarrage et continue pendant la pause. Un créneau enregistré avant la fin peut arriver tardivement après une longue panne. Un passage déjà admis reste observable jusqu’à son résultat.
|
||||
|
||||
**Nombre maximal de passages lancés** est un total partagé entre tous les horaires et **Lancer maintenant**. Un passage compte une fois dès l’admission de la première entrée de son coordinateur, même s’il échoue ensuite. Les vérifications, les conditions inchangées et les réveils refusés ne consomment aucune place ; une admission incertaine retient sa place jusqu’à réconciliation. Les politiques ponctuelles permettent au plus un passage. Les places consommées sont conservées après modification ou redémarrage ; les limites de fin inchangées conservent leur origine au premier démarrage et la fin absolue des créneaux. Modifier le maximum change le total sur toute la durée de vie ; un total inférieur ou égal au nombre déjà consommé termine la programmation. Changer la fréquence peut créer une nouvelle phase d’intervalle à l’enregistrement, comme l’explique l’éditeur.
|
||||
|
||||
La programmation native sélectionnée affiche **État voulu**, **Calendrier natif**, **Prochain réveil** ou **Prochaine vérification**, **Vérifications** avec **Suivi depuis le…**, **Passages lancés**, **Passages réussis**, sa fin et **Exécutions passées**. Lisez un ancien passage via **Ouvrir la conversation**. **Programmation terminée** décrit la fin de la programmation ; inspectez séparément le résultat du passage. Les résultats conservent les 30 dernières entrées ; les totaux cumulés persistent au-delà de cette fenêtre. **Charger d’autres Missions** charge la page suivante d’objets enregistrés.
|
||||
|
||||
La liste affiche la condition et la fin si elles sont définies, et **Vérifications** seulement avec une condition.
|
||||
|
||||
### Programmations quotidiennes existantes {#existing-daily-schedules}
|
||||
|
||||
Les programmations quotidiennes existantes utilisent le fuseau IANA et l’heure civile enregistrés. Chaque exécution possède son propre coordinateur et son propre plan de mission ; sélectionnez son arbre actuel ou une date/un résultat passé pour le lire au centre. Leur historique conservé contient 30 résultats. Les règles quotidiennes suivantes s’appliquent à ces entrées :
|
||||
|
||||
| Situation | Comportement de planification |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@ Les programmes quotidiens utilisent le fuseau IANA et l’heure civile enregistr
|
|||
| Horloge reculée | Ne rend pas de nouveau dû un jour déjà traité. |
|
||||
| Exécution existante encore en attente | Ne la fait pas chevaucher une nouvelle exécution quotidienne. |
|
||||
|
||||
La date de création limite aussi les jours admissibles ; démarrer un nouveau programme n’autorise pas à traiter des dates antérieures à son existence. **Lancer maintenant** démarre une exécution manuelle séparée si proposé ; cela ne remplace pas l’horaire quotidien et ne contourne pas une exécution en attente. La prochaine heure affichée est une projection, pas une garantie d’exécution à l’heure exacte.
|
||||
La date de création borne aussi le travail quotidien admissible. **Lancer maintenant**, lorsque proposé pour une entrée quotidienne existante, lance une exécution manuelle distincte sous la protection contre les exécutions en attente et conserve son horloge quotidienne. L’heure suivante affichée est une projection soumise à la disponibilité du service et de l’hôte.
|
||||
|
||||
**Conversations suivies (facultatif)** ajoute un contexte borné des conversations choisies. Seule une exécution terminée avance leurs curseurs de messages traités ; une exécution échouée/interrompue ne consomme pas silencieusement ce contexte. Si les instructions et le contexte dépassent la capacité, raccourcissez les instructions ou retirez des conversations suivies au lieu de supposer qu’un historique illimité est lu.
|
||||
|
||||
## Durée de vie et redémarrages du service {#lifetime}
|
||||
|
||||
Le **i** près de **Détails techniques** explique l’activité technique et les coûts. Dans **Paramètres → OpenCode**, la supervision garde l’état et les erreurs visibles ; son **i** contient les explications courantes.
|
||||
|
||||
Pour une programmation native, développez **Détails techniques** pour identifier son **Porteur principal** et son **Porteur de réconciliation**, leurs calendriers natifs et tout **Nettoyage en attente de réconciliation**. Ces conversations techniques n'ont pas d'action de composition et restent absentes des sélecteurs ordinaires de conversations et de sources. Les réveils techniques, les titres et la compaction utilisent le modèle local sans requêtes HTTP vers les fournisseurs ; les événements et statistiques natifs peuvent quand même les inclure, tandis que les passages effectifs conservent leurs coûts de modèle habituels. La compaction borne le contexte traité en conservant l'identité de la conversation et du calendrier. Les messages et événements natifs stockés peuvent s'accumuler jusqu'au nettoyage explicite d'une programmation terminée ; les 30 résultats de Mission conservés sont distincts. Le réglage de profondeur et **Programmation** affichent le chemin sous **Fichier de configuration OpenCode :** lorsqu’il est disponible ; le réglage de profondeur indique séparément sa **Cible d’écriture du projet**.
|
||||
|
||||
| Composant | Ce que signifie sa fermeture |
|
||||
| --- | --- |
|
||||
| Onglet de projet CodeNomad | Ferme le projet dans CodeNomad ; les sessions natives continuent dans OpenCode. Rouvrez le projet pour les retrouver. |
|
||||
| Fenêtre entière, fermeture de l’application ou rechargement du moteur de rendu | Conserve les connexions de projet pour la restauration ; n’arrête pas le travail natif ni le service OpenCode. |
|
||||
| Backend CodeNomad | N’arrête pas OpenCode. Une mission ponctuelle en cours continue : son coordinateur, ses tâches en sous-agents natifs et les outils de mission fonctionnent dans OpenCode. L’affectation de tâches à des sessions indépendantes et la remise de leurs rapports au coordinateur, les missions que prépare le coordinateur, l’arrêt des conversations d’une tâche retirée, la récupération et le centre de mission attendent le backend, tout comme la poursuite d’une mission ponctuelle après un redémarrage du service OpenCode. |
|
||||
| Programme quotidien actif | Son horloge et l’exécution des passages fonctionnent dans le plugin/service OpenCode, y compris avec l’interface et le backend CodeNomad fermés. L’hôte d’exécution et le service OpenCode doivent rester actifs. |
|
||||
| Redémarrage du service OpenCode | Perd les tâches de planification propres au processus. Un programme censé être actif devient **Interrompue** jusqu’à un **Reprendre** explicite ; rouvrir CodeNomad ne le réarme pas. |
|
||||
| Programmation native armée | Les calendriers persistants d’OpenCode réveillent leur projet et exécutent les passages avec l’interface et le backend CodeNomad fermés, tant que l’hôte d’exécution et le service sont disponibles. |
|
||||
| Redémarrage du service OpenCode — programmation native saine | Une programmation saine dont l’état voulu est en marche reprend automatiquement son calendrier. Le travail en attente est réconcilié avec ses identités d’origine avant toute nouvelle admission. |
|
||||
| Programmation quotidienne existante | Son horloge de plugin fonctionne avec CodeNomad fermé tant que le service reste actif. Un redémarrage du service la laisse **Interrompue** ; **Reprendre** explicitement réconcilie l’exécution en attente et réarme son horloge quotidienne. |
|
||||
|
||||
Après un redémarrage, inspectez l’exécution en attente et choisissez **Reprendre** lorsque vous êtes prêt. La reprise réconcilie l’exécution existante sans envoyer deux fois l’entrée au coordinateur ; elle ne promet pas de poursuivre un tour natif interrompu. Un tour coupé par un redémarrage peut se régler comme terminé sans rapport, avec **Interrompue par un redémarrage**. N’utilisez pas le redémarrage pour réessayer les effets d’outils.
|
||||
Pour les programmations natives, une pause ou un arrêt explicite, un travail échoué et un résultat incertain conservent leurs barrières d’admission après redémarrage. Inspectez le passage conservé et la requête d’origine ; utilisez **Vérifier le passage** ou **Actualiser la requête enregistrée** lorsque proposé, puis **Reprendre** seulement quand l’état le permet. Une continuation native est observée avec son identité existante. Un échec prouvé ou une fin sans rapport exige une récupération humaine ; des preuves incomplètes gardent le passage incertain. N’utilisez jamais un redémarrage pour réessayer des effets d’outils engagés.
|
||||
|
||||
[Garder OpenCode démarré](opencode.md#service-supervision) supervise séparément le service partagé sur son hôte d’exécution. Le premier démarrage d’une Mission l’active, sauf si vous avez enregistré sa désactivation ; créer une Mission en pause conserve ce choix. Désactiver laisse OpenCode fonctionner sans relance automatique. Windows, macOS et WSL dépendent de la connexion utilisateur ; Linux natif utilise le maintien des services utilisateur après déconnexion, sous réserve d’autorisations. Veille, hibernation, durée de vie de WSL inactif et contrôle d’un hôte externe limitent la disponibilité ; inspectez l’état de supervision indépendamment de la programmation.
|
||||
|
||||
Les Missions fonctionnent avec les applications de bureau et avec un [serveur](server.md) CodeNomad autonome. Lorsque le panneau affiche **Le centre de mission est indisponible**, son message en nomme la cause :
|
||||
|
||||
|
|
@ -184,7 +237,9 @@ Les Missions fonctionnent avec les applications de bureau et avec un [serveur](s
|
|||
|
||||
La pause ponctuelle laisse finir le travail en cours : le coordinateur et ses sous-agents terminent l'étape en cours, ne lancent rien de nouveau puis s'arrêtent, pendant que le panneau affiche **Mise en pause : les étapes en cours se terminent.** **Interrompre maintenant** les arrête au contraire tout de suite : cela interrompt la famille native de la mission et suspend la coordination. L'arrêt interrompt aussi, annule les entrées/livraisons familiales en file et est terminal. Ces interruptions sont bornées : **Certains sous-agents sont peut-être encore actifs.** signifie que l'une d'elles est incomplète. Inspectez le résultat conservé et utilisez son nouvel essai explicite au lieu de supposer que tous les effets shell/outils ont été annulés. La reprise informe le coordinateur de ce qui a été mis en pause ou interrompu ; il décide comment poursuivre.
|
||||
|
||||
La pause/l’arrêt quotidien annule la planification et demande l’interruption des acteurs racines enregistrés, dont le coordinateur et les spécialistes indépendants, mais **ne prouve pas que ces conversations ou leurs descendants sont arrêtés**. Inspectez les familles natives. L’arrêt est terminal : un programme arrêté ne peut pas démarrer, reprendre ou lancer maintenant. **Vérifier le passage** réconcilie une exécution en attente sans la renvoyer. Les échecs transitoires de vérification planifiée affichent leur dernier échec et sont réessayés automatiquement ; une réponse perdue à une commande utilisateur ne provoque pas son renvoi automatique.
|
||||
Pour une programmation native, **Pause** ferme les futures admissions et demande l’interruption d’un passage admis ; **Reprendre** réconcilie le travail existant avant de réarmer. **Arrêter…** est terminal. **Lancer maintenant** utilise la même protection contre les passages en attente et le même quota de passages lancés tout en conservant le calendrier. **Vérifier le passage** réconcilie le passage d’origine. Conservez une action non confirmée et utilisez **Actualiser la requête enregistrée** ; l’actualisation passive et la reconnexion conservent cette requête. Les effets d’outils engagés peuvent continuer après interruption.
|
||||
|
||||
Les entrées quotidiennes existantes conservent leurs commandes quotidiennes Pause/Arrêter/Reprendre/Lancer maintenant/Vérifier le passage. Pause/Arrêter annulent leur horloge et demandent l’interruption des racines enregistrées, dont les spécialistes indépendants. Inspectez leurs familles natives et tout résultat d’interruption partielle. Les échecs transitoires de vérification quotidienne sont réessayés avec leur avertissement affiché ; une réponse perdue à une commande utilisateur reste rattachée à sa requête d’origine. Une entrée arrêtée reste dans un état terminal.
|
||||
|
||||
Pour un coordinateur ponctuel inactif sans rapport, l’action principale ▶ peut le vérifier et le récupérer ; les tâches disposent d’une récupération ciblée de rapport manquant si admissible. La récupération vérifie d’abord l’activité native, les descendants et les attentes ; une activité inconnue refuse l’admission. Elle ne rejoue pas les affectations et ne remplace pas le plan. Ouvrez les conversations pour résoudre les questions, autorisations, shells ou travaux en arrière-plan restants.
|
||||
|
||||
|
|
@ -192,12 +247,28 @@ Si **Créer** n’est pas confirmé, la fenêtre conserve la demande soumise et
|
|||
|
||||
## Modifier, supprimer et nettoyer les conversations {#cleanup}
|
||||
|
||||
Utilisez **Modifier la mission sélectionnée** sur une mission ponctuelle modifiable ; en cas de conflit, fermez/rouvrez l’éditeur pour charger la révision actuelle plutôt que l'écraser. Si l'objectif ou les notes changent pendant le chargement du formulaire, il indique que la mission a changé depuis son ouverture au lieu de les remplir ; choisissez **Rouvrir** pour modifier la version actuelle. Les programmes quotidiens ne proposent pas de modification/suppression.
|
||||
Si l'objectif ou les notes changent pendant le chargement du formulaire, il indique que la mission a changé depuis son ouverture au lieu de les remplir ; choisissez **Rouvrir** pour modifier la version actuelle.
|
||||
|
||||
Entre les passages, une programmation native en pause ou en marche permet de modifier titre, instructions, notes, profils demandés, mode des sessions de tâches, sources, calendrier et limites quand aucun passage ni aucune commande non résolue ne retient l’objet. Une programmation en pause reste en pause ; une programmation armée réconcilie son calendrier avant de continuer. Enregistrer conserve les résultats passés, les sources consommées, le nombre de lancements et l’origine au premier démarrage ; les limites de fin inchangées conservent la fin absolue de leurs créneaux.
|
||||
|
||||
Pendant un passage admis, seules les instructions et les notes suivent le contrat d’édition des textes actifs : les modifications sont inscrites dans son historique et enregistrées pour les futurs passages. Les tâches, consignes, profils et identités d’exécution existants conservent leurs valeurs admises. Les champs structurels restent verrouillés jusqu’au règlement du passage, y compris après une pause. Un scénario reste verrouillé dès qu’une tâche a été créée, même terminée ou retirée. Une admission/commande incertaine ou des preuves d’intégrité incomplètes maintiennent l’édition structurelle en lecture seule. Créer et Modifier partagent la validation du calendrier ; l’aperçu applique le dernier créneau autorisé modifié au prochain réveil enregistré, même tardif.
|
||||
|
||||
La source de branche locale comprend les branches sans worktree et lit uniquement les références locales. Le sélecteur accepte jusqu’à 200 branches locales ; un inventaire plus grand affiche la source comme indisponible. **Démarrer** et **Reprendre** demandent votre accord avec les chemins exacts quand l’activation est disponible. **Lancer maintenant** reste désactivé avec une explication tant que la programmation native est inactive ; activez `experimental.schedules` dans la configuration OpenCode, puis actualisez. Une réponse d’activation perdue conserve la requête d’origine pour actualiser son reçu.
|
||||
|
||||
Avant le premier Play, vous pouvez revenir à un lancement immédiat sans condition en gardant les conversations suivies dans **Options**. Play lit et fige leur préfixe de messages complets et stables (32 messages par source au plus, dans une enveloppe de lancement de 16 384 caractères). Une source absente, déplacée ou dépassant la capacité bloque le lancement avec une explication. Une reprise conserve le contexte figé et l’identité du lancement d’origine.
|
||||
|
||||
Lorsqu’une lecture du reçu confirme une préparation partielle, **Réessayer** apparaît. Ce clic vérifie de nouveau le même reçu et reprend uniquement la modification enregistrée d’origine, en gardant la mission en pause.
|
||||
|
||||
Avant le premier lancement d’une nouvelle mission préparée, vous pouvez modifier son titre, ses instructions, ses notes, son playbook, ses agents/modèles/réflexion demandés, le mode des sessions de tâches et sa **Programmation**. Vous pouvez passer d’un lancement immédiat à un lancement différé, récurrent ou conditionnel, et revenir en arrière, en conservant la même Mission. Enregistrer la garde en pause et prépare un nouveau coordinateur vide ; la conversation initiale est conservée. Seul Play démarre le travail ou arme une programmation. Après lancement, les instructions et notes suivent les règles d’édition de la mission active. Si l’enregistrement reste incertain, **Actualiser la requête enregistrée** lit son résultat d’origine ; conservez cette requête tant qu’une préparation partielle bloque Play, l’édition des textes et la suppression.
|
||||
|
||||
Utilisez **Modifier la mission sélectionnée** sur une mission ponctuelle ou une programmation native modifiable. Lisez les explications près des champs verrouillés. En cas de conflit de révision, choisissez **Recharger la configuration actuelle**, puis ouvrez **Ancien brouillon pour comparaison** pour examiner le brouillon conservé avant d’enregistrer à nouveau. Les programmations quotidiennes existantes conservent les commandes de leur propre barre d’outils.
|
||||
|
||||
Pour une programmation native, **Supprimer la mission sélectionnée…** désactive les futurs passages avant de retirer l’entrée. Conversations et résultats sont conservés ; le nettoyage en attente de réconciliation reste visible dans **Détails techniques**. Conservez une suppression non confirmée et actualisez sa requête d’origine.
|
||||
|
||||
1. Pour une mission ponctuelle devenue inutile, choisissez **Supprimer la mission sélectionnée…** et examinez la confirmation.
|
||||
2. Sélectionnez éventuellement **Supprimer aussi les conversations spécialistes créées pour cette mission** avant de confirmer.
|
||||
3. Vérifiez dans **Nettoyage des conversations** les nombres supprimés/conservés/en attente. Réessayez seulement le nettoyage restant enregistré si proposé.
|
||||
|
||||
Supprimer une mission n’est pas l’arrêter ; utilisez d’abord l’arrêt et inspectez le travail restant. Les coordinateurs et conversations réutilisées sont conservés. Les spécialistes partagés, déplacés, ayant des enfants ou dont la propriété n’est pas vérifiable peuvent aussi être conservés. Un nettoyage partiel n’est ni annulé ni terminé silencieusement en arrière-plan. Le lecteur d’ensemble conserve les 20 nettoyages terminés les plus récents. Supprimer des missions libère leur stockage dans le projet : les missions supprimées plus anciennes sont effacées automatiquement.
|
||||
Supprimer une mission n’est pas l’arrêter ; utilisez d’abord l’arrêt et inspectez le travail restant. Les coordinateurs et conversations réutilisées sont conservés. Les spécialistes partagés, déplacés, ayant des enfants ou dont la propriété n’est pas vérifiable peuvent aussi être conservés. Un nettoyage partiel n’est ni annulé ni terminé silencieusement en arrière-plan. La carte de la mission peut disparaître alors que la confirmation de la suppression reste en attente, même lorsque le nettoyage des conversations affiche 0 en attente. Continuez à utiliser l’action **Réessayer** d’origine jusqu’à confirmation de la suppression ; elle conserve la même demande de suppression et les mêmes options. Le lecteur d’ensemble conserve les 20 nettoyages terminés les plus récents. Supprimer des missions libère leur stockage dans le projet : les missions supprimées plus anciennes sont effacées automatiquement.
|
||||
|
||||
Voir [Autorisations](permissions.md), [Conversations](conversations.md) et [Configuration d’OpenCode](opencode.md) pour le travail natif et les commandes du service.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ L’exécutable installé et le service actif peuvent avoir des versions différ
|
|||
| Recommandée | OpenCode **2.2.5** | Cible qualifiée, distincte du minimum technique. |
|
||||
| Non vérifiée | Version plus récente ou personnalisée non entièrement validée | Non rejetée simplement parce que non testée ; les contrats API réels du service connecté sont vérifiés. |
|
||||
|
||||
Les contrats requis de session, permission, Form, boîte de réception et environnement de session doivent aussi être disponibles. Une version d’exécutable seule ne prouve pas que le service connecté les prend en charge. Si la configuration signale un service actif non pris en charge, sélectionnez/installez une CLI prise en charge et utilisez le redémarrage explicite du service partagé lorsque vous êtes prêt.
|
||||
Le service connecté doit exposer les contrats requis de session, permission, Form, boîte de réception et environnement de session. La programmation native des Missions vérifie aussi les API de programmation, l’ensemble plugin/fournisseur et le réglage effectif `experimental.schedules`. La disponibilité suit les capacités consommées, y compris sur les versions futures compatibles. Si la configuration signale un service actif non pris en charge, sélectionnez/installez une CLI prise en charge et utilisez le redémarrage explicite du service partagé lorsque vous êtes prêt.
|
||||
|
||||
## Hôte, WSL et portée du service partagé {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ Sous Windows, un exécutable WSL sélectionné s’exécute dans la distribution
|
|||
|
||||
OpenCode possède l’enregistrement global de son service, ses sessions, messages et base de données. CodeNomad ne crée pas une base OpenCode privée par fenêtre ou projet. Fermer un onglet de projet ferme ce projet dans CodeNomad ; les sessions continuent dans OpenCode et réapparaissent quand vous rouvrez le projet. CodeNomad ne décharge jamais les Locations natives. Fermer une fenêtre entière, quitter ou recharger conserve les connexions de projet pour la restauration ; ni ces actions ni l’arrêt du backend n’arrêtent le démon partagé. Voir [Projets](projects.md).
|
||||
|
||||
## Garder OpenCode démarré {#service-supervision}
|
||||
|
||||
Avant tout lancement de Mission, **Rester désactivé au premier Play** enregistre un OFF explicite sans installer d'abord la supervision.
|
||||
|
||||
Dans **Paramètres → OpenCode**, **Garder OpenCode démarré** installe une supervision système sur l'hôte d'exécution, partagée par tous les profils CodeNomad utilisant ce service. Elle fonctionne indépendamment des Missions, même CodeNomad fermé. Le premier Play d'une Mission l'active, sauf désactivation explicite préalable ; créer une Mission en pause conserve ce choix. Pause, Arrêter ou Supprimer une Mission conserve cette préférence.
|
||||
|
||||
La désactivation retire la supervision tandis qu'OpenCode continue de fonctionner et ne sera plus relancé. Pour un arrêt durable, désactivez d'abord cette préférence, puis arrêtez explicitement OpenCode. Avec la supervision active, `opencode service stop` est suivi d'un redémarrage.
|
||||
|
||||
**En marche · supervision active** exige un superviseur système actif et un service OpenCode sain. **Installé · service en démarrage**, **Installation ou retrait de la supervision…** et **Supervision en échec** désignent des états distincts. Un service qui répond laisse à lui seul la supervision non confirmée. Le détail affiche la cause, le dernier essai, le nouvel essai du superviseur et la vérification système de secours lorsqu'ils sont connus. **Vérifier la supervision** lit seulement ; **Réessayer la supervision** répare explicitement l'installation demandée. Une action non confirmée garde sa requête d'origine ; **Réessayer l'action d'origine** renvoie uniquement cette action exacte. Une révision modifiée est actualisée pour examen plutôt qu'écrasée.
|
||||
|
||||
Windows, macOS et WSL démarrent après connexion. WSL exige la redirection localhost de la distribution choisie. Sous Linux natif, linger démarre **tous les services utilisateur activés** avant connexion et les maintient après déconnexion. Désactiver restaure linger seulement si CodeNomad l'a activé et qu'aucune autre supervision n'en dépend ; le réglage personnel préexistant est conservé. Si restaurer linger risque d'arrêter les processus OpenCode libérés alors qu'aucune session utilisateur n'est ouverte, la restauration est différée et reste visible avec **Réessayer la supervision**. Réessayez lorsqu'une session utilisateur est ouverte ou que les processus OpenCode libérés sont terminés, permettant une restauration sûre. Si Linux exige une autorisation, accordez-la sur l'hôte d'exécution puis réessayez ; la préférence demandée reste enregistrée. Veille et hibernation restent gérées par le système. Un hôte indisponible ou non pris en charge affiche une commande grisée avec explication.
|
||||
|
||||
## Mettre à jour, redémarrer ou recharger {#maintenance}
|
||||
|
||||
1. Dans **Paramètres → OpenCode**, choisissez **Vérifier l’état et les mises à jour**.
|
||||
|
|
@ -41,7 +53,7 @@ OpenCode possède l’enregistrement global de son service, ses sessions, messag
|
|||
| Action | Portée et effet | Récupération sûre |
|
||||
| --- | --- | --- |
|
||||
| Mettre à jour la CLI | Change l’exécutable installé pris en charge ; ne redémarre pas implicitement le démon actif. | Vérifiez les versions installée et active après un échec ; réessayez explicitement après résolution de l’erreur seulement. |
|
||||
| Redémarrer le service | Remplace le processus partagé et interrompt le travail actif de tous les clients OpenCode connectés ; l’historique reste. | Reconnectez-vous et inspectez conversations et Missions planifiées ; un redémarrage n’est pas une continuation automatique. |
|
||||
| Redémarrer le service | Remplace le processus partagé et interrompt le travail actif de tous les clients OpenCode connectés ; l’historique reste. | Les programmations natives de Missions saines et en marche récupèrent automatiquement ; inspectez les passages en attente et toute pause/erreur/incertitude. Les entrées quotidiennes existantes exigent **Reprendre**. Inspectez séparément les conversations ponctuelles et leurs commandes de récupération ; voir [Missions](missions.md#lifetime). |
|
||||
| Recharger la configuration OpenCode | Conserve le processus mais reconstruit **chaque Location chargée** pour tous les clients. Annule les permissions/Forms en attente et ferme terminaux/commandes en arrière-plan ; le travail actif peut être interrompu. | Revérifiez ressources et travaux en attente après rechargement. Ne répétez pas un prompt ou une modification simplement parce que sa réponse a été perdue. |
|
||||
|
||||
Utilisez explicitement **Recharger la configuration OpenCode**, sous **Dépannage** lorsque le service actif le prend en charge, lorsqu’une configuration enregistrée ou un plugin installé reste indisponible et que l’interruption est acceptable. Ce n’est pas une actualisation d’affichage anodine. CodeNomad n’utilisera pas une CLI sélectionnée plus ancienne pour rétrograder un démon actif plus récent.
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Ce sont des instructions. Ce qu’un agent peut réellement faire dépend des au
|
|||
|
||||
### Questions qui vous sont posées {#questions}
|
||||
|
||||
Seul le coordinateur vous pose des questions. Les tâches et leurs aides renvoient les leurs à la conversation qui les a lancées, avec les options, les éléments à l’appui, les conséquences, la réversibilité et une recommandation, et poursuivent le travail qui ne dépend pas de la réponse. Le coordinateur tranche d’abord ce qu’il peut et regroupe les questions restantes dans un seul formulaire sous **Votre réponse est attendue** ; [Questions et décisions](missions.md#questions) explique ce qu’il demande et comment les décisions sont libellées. Chaque coordinateur, ponctuel ou quotidien, et chaque affectation de tâche reçoivent cette règle comme instruction ; Exploration ajoute les vérifications de décision [ci-dessous](#wayfinder-checks).
|
||||
Seul le coordinateur vous pose des questions. Les tâches et leurs aides renvoient les leurs à la conversation qui les a lancées, avec les options, les éléments à l’appui, les conséquences, la réversibilité et une recommandation, et poursuivent le travail qui ne dépend pas de la réponse. Le coordinateur tranche d’abord ce qu’il peut et regroupe les questions restantes dans un seul formulaire sous **Votre réponse est attendue** ; [Questions et décisions](missions.md#questions) explique ce qu’il demande et comment les décisions sont libellées. Chaque coordinateur, ponctuel ou programmé, et chaque affectation de tâche reçoivent cette règle comme instruction ; Exploration ajoute les vérifications de décision [ci-dessous](#wayfinder-checks).
|
||||
|
||||
Répondez dans le dock d’interruption (**Répondre** sous **Votre réponse est attendue**) : seule cette réponse compte comme la vôtre. Une réponse tapée dans le chat est un message ordinaire.
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ Répondez dans le dock d’interruption (**Répondre** sous **Votre réponse est
|
|||
|
||||
Le coordinateur a pour instruction de publier un point de situation après son plan initial, après chaque tâche qu’il clôt, chaque fois qu’il commence à vous attendre et avant son rapport final ; voir [Points de situation et demandes de mise à jour](missions.md#briefings). Les points de situation, les résumés de tâches et le rapport final sont rédigés pour vous : dans votre langue, en quelques phrases courtes, le résultat d’abord, les identifiants et commandes restant dans les preuves.
|
||||
|
||||
Le rapport final indique ce qui a été livré, ce qui reste à faire et ce qui demande votre décision. Une mission ponctuelle se termine uniquement comme réussie : quand plus rien ne peut avancer sans vous, le coordinateur vous pose la question et attend au lieu d’échouer, et vous seul arrêtez une mission. Une exécution quotidienne peut aussi se terminer en échec.
|
||||
Le rapport final indique ce qui a été livré, ce qui reste à faire et ce qui demande votre décision. Une mission ponctuelle se termine uniquement comme réussie : quand plus rien ne peut avancer sans vous, le coordinateur vous pose la question et attend au lieu d’échouer, et vous seul arrêtez une mission. Un passage programmé peut aussi se terminer en échec.
|
||||
|
||||
### Ce que CodeNomad vérifie dans chaque mission {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ Il n’écrit jamais votre réponse lui-même et ne présente jamais sa propre d
|
|||
- Seuls les cinq rôles d’Exploration sont acceptés.
|
||||
- Une tâche de décision ne se termine qu’avec la décision propre du coordinateur ou avec votre réponse. Une décision du coordinateur énonce la question, la décision et sa justification, et n’est jamais enregistrée comme votre réponse.
|
||||
- Votre réponse ne compte que si vous l’avez donnée dans l’interface de CodeNomad. CodeNomad la rattache à une tâche par l’étiquette de la question : la question a été posée depuis la conversation du coordinateur, son texte se termine par l’étiquette de cette tâche et ne nomme aucune autre décision, la réponse est venue après la déclaration de la tâche et n’a pas tranché une autre décision. Chaque question d’un formulaire groupé est rattachée séparément ; une question qui nomme deux décisions n’en tranche aucune. Une réponse à une question posée depuis la propre conversation de la tâche de décision est aussi acceptée.
|
||||
- Le rapport propre d’une tâche ne peut pas indiquer qui a décidé. Avant qu’une mission ou une exécution quotidienne se termine avec succès, CodeNomad revérifie chaque décision terminée, étiquettes comprises.
|
||||
- Le rapport propre d’une tâche ne peut pas indiquer qui a décidé. Avant qu’une mission ou un passage programmé se termine avec succès, CodeNomad revérifie chaque décision terminée, étiquettes comprises.
|
||||
- Ces vérifications s’appliquent avec **Sous-agents natifs**. Avec **Sessions indépendantes**, les responsables de décision rapportent eux-mêmes leurs notes et le coordinateur consigne chaque issue dans les notes en précisant qui a décidé ; CodeNomad ne le vérifie pas.
|
||||
|
||||
Simples instructions : quand un choix est considéré comme peu coûteux à annuler, la qualité des notes et des recherches, et le fait de s’en tenir à la planification.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode מספק את המנוע. CodeNomad מספק את תא הטייס. הו
|
|||
|
||||
## העבירו עבודה ארוכה למשימות {#missions}
|
||||
|
||||
תארו את העבודה פעם אחת. המתאם של המשימה מתכנן אותה כעץ מטלות, מאציל מטלות תחומות לשיחות מקוריות של OpenCode ומדווח על התוצאות עם הראיות שלהן. הריצו משימה פעם אחת או כל יום; יצירת משימה לעולם אינה מתחילה עבודה עד שתבחרו בכך. מדריכי העבודה מכסים ניפוי תקלות, אספקת מוצר והשוואת שני מודלים עם שופט עיוור. ראו [משימות](missions.md).
|
||||
תארו את העבודה פעם אחת. המתאם של המשימה מתכנן אותה כעץ מטלות, מאציל מטלות תחומות לשיחות מקוריות של OpenCode ומדווח על התוצאות עם הראיות שלהן. הריצו פעם אחת, בשעות או במרווחים שנבחרו, או לאחר שינוי מאומת בפרויקט; היצירה שומרת משימה מושהית להפעלה מפורשת שלכם. מדריכי העבודה מכסים ניפוי תקלות, אספקת מוצר והשוואת שני מודלים עם שופט עיוור. ראו [משימות](missions.md).
|
||||
|
||||
## המשיכו לעבוד מהטלפון {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ npx @neuralnomads/codenomad --generate-token --launch
|
|||
| עבודה בעותקי עבודה ובשיחות | [פרויקטים](projects.md), [שיחות](conversations.md), [קבצים](files.md) |
|
||||
| איתור היסטוריה או ניקוי בטוח שלה | [היסטוריה](history.md) |
|
||||
| מענה לשאלות ולאישורים מקוריים | [אישורים](permissions.md) |
|
||||
| תיאום עבודה תחומה או יומית | [משימות](missions.md) |
|
||||
| תיאום עבודה תחומה או מתוזמנת | [משימות](missions.md) |
|
||||
| הגדרת הרצה ושילובים | [OpenCode](opencode.md), [ספקים](providers.md), [שילובים](integrations.md) |
|
||||
| חיבור מכשיר נוסף | [שליטה מרחוק](remote-control.md), [אבטחה](security.md), [נייד](mobile.md) |
|
||||
| התאמת סביבת העבודה | [הגדרות](settings.md), [קיצורים](shortcuts.md), [קול](voice.md), [הרחבות](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
1. פתחו **משימות** בלוח הימני ובחרו **יצירת משימה**. הטופס מחליף את רשימת המשימות ואת המטלות של המשימה שנבחרה עד שתיצרו או תבטלו.
|
||||
2. מלאו **מה המשימה צריכה לעשות?** ובדקו **כותרת**, הממולאת מראש מהמשפט הראשון (עד 60 תווים).
|
||||
3. תחת **מתי**, בחרו **פעם אחת** או **כל יום בשעה**. לעבודה יומית, אמתו את אזור הזמן המוצג; **שינוי** מאפשר אזור IANA.
|
||||
3. השאירו את **תזמון** מקופל עבור **פעם אחת, מיד** ו־**ללא תנאי**, או הרחיבו אותו לבחירת תדירות, תנאי ומגבלות לפי הצורך. בדקו את **אזור זמן שמור (IANA)** ואת התצוגה המקדימה לפני יצירת עבודה מתוזמנת.
|
||||
4. הרחיבו **אפשרויות** עבור **מדריך**, **הערות**, **סוכנים ומודלים**, **שיחות משימות**, **עומק הקינון של תתי־סשנים** של הפרויקט, תקצירים שמורים ושיחות במעקב לפי בחירה. עם **השוואת שני מודלים**, הבוררים **מודל A** ו־**מודל B** מופיעים בטופס הראשי.
|
||||
5. בחרו **יצירה**. בחרו את הרשומה החדשה, ואז השתמשו בפעולת ▶ הנפרדת שלה כשתהיו מוכנים: **הפעלת משימה** לעבודה חד־פעמית, או **הפעלת לוח הזמנים {id}** ללוח זמנים יומי.
|
||||
5. בחרו **יצירה**. בחרו את הרשומה המושהית, בדקו אותה ואז השתמשו ב־▶ **התחלה** כשתהיו מוכנים. עבודה מיידית ללא תנאי מתחילה משימה אחת; מדיניות דחויה, חוזרת או מותנית מפעילה תזמון מקורי.
|
||||
|
||||
**יצירה לעולם אינה מתחילה עבודה.** משימה חד־פעמית מוכנה; לוח זמנים יומי מושהה. לא יצירה ולא שמירת תקציר מפעילות שעון עצר. היצירה תחומה לפרויקט/ל־Location שנבחרו ומקפיאה את הפרופילים המבוקשים ואת מדיניות סשני המטלות. האימות מופיע לצד השדה המושפע; סירוב שאומר שלא נשלח דבר משאיר את הטיוטה זמינה לתיקון. המתאם של משימה חד־פעמית יכול גם להכין עבורכם משימה נפרדת, כגון משימת המשך: היא מופיעה ברשימה כ־**מוכנה** וכמו כל משימה, מתחילה רק מפעולת ▶ שלה.
|
||||
**יצירה שומרת משימה מושהית.** רק פעולת ההפעלה מפעילה עבודה ולוחות זמנים מקוריים; יצירה או שמירת תקציר משמרות את העדפת ההשגחה על השירות המשותף. היצירה שייכת לפרויקט/ל־Location שנבחרו ומתעדת את הפרופילים המבוקשים ואת מדיניות שיחות המטלות. האימות מופיע לצד השדה המתאים; סירוב שמציין שלא נשלח דבר שומר את הטיוטה לתיקון. מתאם חד־פעמי יכול גם להכין משימת המשך נפרדת: היא מופיעה כ־**מוכנה** ומתחילה מפעולת ▶ שלה.
|
||||
|
||||
משימות חד־פעמיות שמורות שומרות על בקרות ההפעלה, עץ המשימות, השיחות והדוחות הסופיים שלהן. לוחות זמנים יומיים קיימים נשארים ברשימה עם הבקרות היומיות ועם [כללי התזמון היומי](#existing-daily-schedules). בחרו את הרשומה והשתמשו בבקרות הזמינות לאותו אובייקט.
|
||||
|
||||
### מדריכי עבודה {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@
|
|||
|
||||
מטלה מאוחרת יותר נשענת על עבודה קודמת בשתי דרכים. התלויות קובעות את סדר העבודה: המתאם מתחיל מטלה כשהמטלות שהיא תלויה בהן הושלמו, ומכניס את התוצאות הרלוונטיות לתיאור או לצד ההקצאה; שיחות עצמאיות יכולות גם לקרוא את הדוחות האלה במפה. כדי להמשיך את השיחה עצמה של מטלה שהסתיימה, המתאם מצהיר על מטלת המשך שעושה בה שימוש חוזר: מטלת ההמשך ממשיכה שם עם ההקשר הנוכחי של אותה שיחה, ש־OpenCode אולי [סיכם](history.md#compact-session), ואילו פרטים מוקדמים יותר נשארים בהיסטוריה השמורה שלה לחיפוש. כך מדריך ניפוי התקלות מחזיר את שתי הסקירות למממש. עם **שיחות עצמאיות**, המתאם יכול גם להקצות מטלה לשיחה ראשית קיימת, ששומרת על ההקשר שלה, כולל עבודה קודמת שאינה קשורה. אחרת, מטלה מתחילה בשיחה חדשה מההקצאה שלה, והתמליל של מטלה אחרת מגיע אליה דרך המתאם או דרך חיפוש. ב־**השוואת שני מודלים**, ההקצאה וההקשר של השופט משמיטים את המטרה ואת ההערות; התיאור שלו מכיל את התוצרים האנונימיים.
|
||||
|
||||
בלוח זמנים יומי, כל הרצה מתחילה עם מתאם ומפה משלה. ההודעה הראשונה שלה מכילה את הוראות לוח הזמנים ואת הטקסט החדש של המשתמש ושל הסוכן מתוך [השיחות במעקב](#daily-schedules) שלה מאז ההרצה האחרונה שהושלמה; תוצאות של הרצות קודמות נשארות בהיסטוריית לוח הזמנים. שיחות שהופעלו בתוך הרצה יכולות לקרוא את המפה של אותה הרצה. גם משימה חד־פעמית חדשה מתחילה עם מפה חדשה: כדי להעביר תוצאות הלאה, כתבו אותן במטרה שלה או ב־**הערות**, או תנו למתאם להכין משימת המשך, שאת המטרה וההערות שלה הוא כותב.
|
||||
כל הרצה מתוזמנת מתחילה עם מתאם ומפת משימה משלה, באמצעות ההוראות השמורות והקלטים המוקפאים שלה. שיחות במעקב מספקות טקסט חדש ומוגבל של המשתמש והעוזר; סמני הצריכה שלהן מתקדמים לאחר הרצה מוצלחת. הרצות קודמות נשארות בהיסטוריית התזמון. שיחות שהופעלו בתוך הרצה יכולות לקרוא את המפה שלה. למשימה חד־פעמית חדשה יש מפה נפרדת: שימו תוצאות שברצונכם להעביר הלאה במטרה שלה או ב־**הערות**, או בקשו מהמתאם להכין משימת המשך עם ההקשר הזה.
|
||||
|
||||
### חיפוש בשיחות קודמות {#context-search}
|
||||
|
||||
כל עוד CodeNomad פועל, לכל שיחת סוכן בפרויקט, כולל מתאמי משימות ומטלות, יש את כלי חיפוש הסשנים של CodeNomad (`codenomad.session_search`, שמשמש דרך Code Mode של OpenCode). הוא מחפש בשיחות השמורות של תיקיית הפרויקט של השיחה באותו דירוג כמו [החיפוש](history.md#full-history-search) ב־**כל סביבת העבודה**, ומחזיר קטעים קצרים עם הכותרת והמזהה של כל שיחה, וגם דרך לקרוא שיחה שלמה. חלות הרשאות הכלים המקוריות. סוכן מחפש כשהוא מחליט לעשות זאת או כשהמטרה, ההערות או ההודעה שלכם מבקשות זאת, למשל כדי לברר למה הגיעה משימה או מטלה קודמת. הרצה יומית שמתבצעת כש־CodeNomad סגור עובדת בלי הכלי הזה.
|
||||
כל עוד CodeNomad פועל, לכל שיחת סוכן בפרויקט, כולל מתאמי משימות ומטלות, יש את כלי חיפוש הסשנים של CodeNomad (`codenomad.session_search`, שמשמש דרך Code Mode של OpenCode). הוא מחפש בשיחות השמורות של תיקיית הפרויקט של השיחה באותו דירוג כמו [החיפוש](history.md#full-history-search) ב־**כל סביבת העבודה**, ומחזיר קטעים קצרים עם הכותרת והמזהה של כל שיחה, וגם דרך לקרוא שיחה שלמה. חלות הרשאות הכלים המקוריות. סוכן מחפש כשהוא מחליט לעשות זאת או כשהמטרה, ההערות או ההודעה שלכם מבקשות זאת, למשל כדי לברר למה הגיעה משימה או מטלה קודמת. הרצה מתוזמנת שמתבצעת כש־CodeNomad סגור עובדת בלי הכלי הזה.
|
||||
|
||||
## פרופילים, האצלה ועומק {#profiles}
|
||||
|
||||
שמירת עומק משמרת תזמונים מקוריים פעילים בתצורת הפרויקט. הגדרת `schedules:false` מפורשת בפרויקט נשארת כבויה; הסרת העומק משמרת את הגדרת התזמון. יש לפתור התנגשות בין קובצי JSON ו־JSONC לפני שמירת הגדרות מקוריות.
|
||||
|
||||
1. השתמשו בגלגל השיניים של משימות או ב־**הגדרות → משימות** לעריכת **ברירות מחדל למשימות חדשות**.
|
||||
2. תחת **החלה על**, בחרו **כל המשימות** או מדריך אחד, ואז הגדירו את **שיחות משימות**, ותחת **סוכנים ומודלים** שורה אחת לכל תפקיד (המתאם, ואז תפקידי המדריך). כל בחירה נשמרת מיד; אין כפתור שמירה.
|
||||
3. בהיקף של מדריך, השורות מציגות את הערכים הגלובליים המסומנים **גלובלי**. שינוי שדה מתאים אישית את אותו תפקיד החל מהם; סמל השחזור שלו חוזר להשתמש בערכים הגלובליים. **איפוס** (לאחר אישור) מנקה רק את הסוכנים והמודלים של ההיקף המוצג.
|
||||
|
|
@ -118,7 +122,7 @@
|
|||
| הגדרה | היקף והשפעה |
|
||||
| --- | --- |
|
||||
| ברירות מחדל למשימות חדשות | ברירות מחדל לכל התצורה למשימות **חדשות** בלבד; חריג מדריך גובר על ברירת המחדל הכללית. |
|
||||
| סוכן / מודל / חשיבה | בחירות מתאם/תפקיד מבוקשות המתועדות ביצירה; גורם עסוק אינו מוחלף בשקט כדי להתאים לעבודה מאוחרת יותר. הרצות יומיות משתמשות שוב בפרופילים המבוקשים הקפואים שלהן. |
|
||||
| סוכן / מודל / חשיבה | בחירות מתאם/תפקיד מבוקשות המתועדות ביצירה; גורם עסוק אינו מוחלף בשקט כדי להתאים לעבודה מאוחרת יותר. הרצות מתוזמנות משתמשות בפרופילים המבוקשים השמורים שלהן; עריכות חלות על הרצות עתידיות. |
|
||||
| תת־סוכנים מקוריים | סשני מטלות משתמשים בסוכני subagent/all מקוריים תחת המתאם. |
|
||||
| סשנים עצמאיים | סשני מטלות מוצהרים משתמשים בסוכני primary/all ובשיחות עצמאיות; הדבר אינו משבית עוזרים מקוריים פנימיים. |
|
||||
| עומק הקינון של תתי־סשנים | תצורת פרויקט ל־**כל** שיחות OpenCode שם, לא רק למשימות. ריק יורש: **ירושה** מסירה את העקיפה של הפרויקט ואת בלוק `experimental` שהיא משאירה ריק או רק עם הערך הגלובלי של **שמירת תמונות פעם אחת**. OpenCode קורא את בלוק `experimental` של הפרויקט כמכלול, ולכן הגדרות ניסיוניות אחרות שנשארות בו עדיין מסתירות את העומק הגלובלי. כברירת מחדל, תת-הסוכן המובנה **General** של OpenCode אינו יכול להפעיל תתי-סוכנים, ולכן השיחות שלו אף פעם אינן מקוננות: שמירת עומק פרויקט כלשהו מוסיפה לאותו קובץ גם כלל שמאפשר לתתי-סוכנים להפעיל תתי-סוכנים משלהם עד לעומק הזה וגובר על ההגבלות של General מקובצי הגדרות אחרים (בעומק 1, מגבלת העומק של OpenCode עדיין מונעת קינון), ו**ירושה** מסירה אותו. מתחת ל**עומק OpenCode אפקטיבי**, העורך מציין אם תתי-הסוכנים General יכולים לקנן. אם הפרויקט מגדיר את General בתבנית הישנה עם ערך `permission` יחיד, או עם `task` ו-`subagent` שכבר מוגדרים, נשמר רק העומק והעורך מציין זאת: אפשרו בעצמכם תתי-סוכנים עבור General בקובץ זה. שמירה אינה טוענת מחדש את OpenCode או משנה שיחות פועלות. |
|
||||
|
|
@ -137,9 +141,51 @@
|
|||
|
||||
תקצירים שומרים הוראות ובחירות מבוקשות, לא תוכניות מטלות, תוצאות, שיחות, שעוני לוח זמנים או סמכות הרצה. טעינת תקציר ממלאת טיוטת יצירה רגילה; היא אינה מתחילה או מתזמנת דבר. הסרת תקציר משאירה משימות/שיחות קיימות. **הגדרות → משימות → תיאורים שמורים** מנהל את הספרייה; אם הספרייה הופכת לבלתי קריאה, **איפוס הספרייה השמורה** (לאחר אישור) מסיר תקצירים שמורים, לא הרצות. ללוחות הזמנים עצמם אין פעולת שמירה כתקציר.
|
||||
|
||||
## שעה יומית, השלמת פערים ושיחות במעקב {#daily-schedules}
|
||||
## תזמון, השלמת פערים ושיחות במעקב {#daily-schedules}
|
||||
|
||||
לוחות זמנים יומיים משתמשים באזור הזמן IANA ובשעה האזרחית השמורים, לא במרווח חולף של 24 שעות. השעות מוצגות באזור של לוח הזמנים, עם תווית כשהוא שונה מזה של הצופה. כל הרצה מקבלת מתאם ומפת משימה מבודדת משלה; לוח הזמנים הנבחר מציג את עץ המטלות הנוכחי והיסטוריית תאריכים/תוצאות. בחרו הרצה קודמת לקריאתה במרכז. ההיסטוריה מוגבלת ל־30 תוצאות.
|
||||
### תזמון מקורי {#native-scheduling}
|
||||
|
||||
סמלי **i** ליד התוויות מסבירים את האפשרויות: רחפו מעליהם, העבירו אליהם מיקוד במקלדת או הקישו עליהם במסך מגע.
|
||||
|
||||
הרחיבו **תזמון** ביצירה או בעריכה. **תדירות** מציעה:
|
||||
|
||||
| בחירה | מועד |
|
||||
| --- | --- |
|
||||
| פעם אחת, מיד | הפעלה אחת בעת ההפעלה, עם תנאי לפי בחירה שנבדק כמתואר להלן. |
|
||||
| פעם אחת, בתאריך מסוים | תאריך ושעה שמורים באזור IANA; תאריך שכבר חלף בעת ההפעלה נדחה. |
|
||||
| פעם אחת, לאחר השהיה | משך שלם וחיובי בדקות או בשעות, שמתחיל בהפעלה. |
|
||||
| במרווח קבוע | לפחות 15 דקות; ההתעוררות הראשונה היא מרווח אחד לאחר ההפעלה. |
|
||||
| מדי יום | שעה אזרחית אחת באזור השמור. |
|
||||
| בימי שבוע נבחרים | שעה אחת בימי השבוע המסומנים. |
|
||||
| כמה פעמים ביום | עד שמונה שעות עצמאיות, כגון 09:15 ו־17:45; שעות כפולות חולקות הרצה אחת. |
|
||||
| ביום מסוים בכל חודש | יום 1–31 בשעה שנבחרה; חודשים ללא אותו יום מדולגים. |
|
||||
| מתקדם (cron) | ביטוי cron מקורי, לכל היותר פעם בדקה. |
|
||||
|
||||
**המועדים הבאים (עד חמישה)** מציגים תצוגה מקדימה לפי האזור השמור ושעת ההפעלה המשוערת. עם תנאי, **הבדיקות הבאות (עד חמש)** מציגות בדיקות; הרצה תלויה בשינוי מאומת. מרווח שמור שלא השתנה שומר על המופע המקורי שלו. מופע מושהה או לא ידוע מוצג כלא זמין עד שניתן לקרוא את מצבו המקורי. השהיות ומרווחים משתמשים בזמן שחלף; לוחות שנה אזרחיים פועלים לפי האזור השמור וכללי שעון הקיץ של OpenCode. התאריכים המוצגים הם תחזיות הכפופות לזמינות השירות והמארח.
|
||||
|
||||
לאחר השבתה, התזמון המקורי מבצע לכל היותר הרצת השלמה אחת לפני חזרה למועדים עתידיים. הוא משתמש במועד המקורי המתועד שהגיע זמנו, שעשוי להיות מוקדם ממועדים מאוחרים יותר שהוחמצו. כמה שעות יומיות חולקות את השלמת הפערים ואת אותה הגנה מחפיפה. הרצה שלא הוסדרה מתואמת לפי זהויות השיחה וההודעה המקוריות שלה לפני שהרצה נוספת יכולה להתחיל. חידוש חזרה שהושהתה במפורש משתמש במועד העתידי הבא שלה; הרצה חד־פעמית מושהית יכולה להגיע פעם אחת, באיחור.
|
||||
|
||||
### תנאים ומקורות {#scheduling-conditions}
|
||||
|
||||
בחרו **תנאי** אחד לפי הצורך ואת ה־**מקור בפרויקט זה** שלו: **הצלחה של תזמון אחר**, **קומיטים חדשים בענף מקומי**, **משימה חד־פעמית מסתיימת בהצלחה**, **הודעה חדשה בשיחה במעקב** או **שינוי בקבצים מפורשים**. לתנאי לבדו, השאירו **פעם אחת, מיד**: בדיקות מקוריות מתבצעות כל 15 דקות עד שההרצה היחידה מתחילה. עם לוח זמנים, התנאי נבדק במועדיו. תנאים חוזרים יכולים להפעיל שוב לאחר שינויים חדשים שעומדים בדרישות.
|
||||
|
||||
ההפעלה הראשונה קובעת את נקודת הייחוס ההתחלתית. המשך משמר אותה, כולל שינויים שנצפו לאחר השהיה. מקורות הצלחה דורשים תוצאה מוצלחת מאומתת ומשפחה מקורית שהוסדרה. בדיקות ענפים משתמשות בקומיטים מקומיים ובקשרי האבות שלהם ללא משיכה מרחוק. בדיקות שיחה ממתינות להודעות חדשות שלמות ויציבות ומחריגות התעוררויות טכניות ואת השיחות של ההרצה עצמה. קבצים משתמשים בעד 128 נתיבים מפורשים יחסיים לפרויקט, אחד בכל שורה; תבניות רקורסיביות ונתיבים מקושרים נדחים. מקור חסר, מועבר, עמום או חורג מהקיבולת חוסם קבלה להרצה עם הסבר. כדי לזהות שינויים בין בדיקות, נדרשת עקבה מתמשכת במקור.
|
||||
|
||||
שינויים שנצפו יחד מקובצים. הרצה מקפיאה את ראיות המקור שנצפו; סיום מוצלח צורך את תמונת המצב הזו ומשאיר שינויים מאוחרים יותר לבדיקה עתידית. הרצה שנכשלה שומרת את נקודת הביקורת של המקור שלה ודורשת התערבות. **שיחות במעקב (לא חובה)** תחת **אפשרויות** מספקות הקשר בנפרד מבחירת תנאי של הודעה בשיחה.
|
||||
|
||||
### סיום וספירות {#scheduling-limits}
|
||||
|
||||
השאירו מגבלות ריקות לחזרה מתמשכת. **המועד האחרון המותר** ו־**המועד האחרון לאחר מספר שעות זה מההפעלה הראשונה (רשות)** מגבילים מועדים זכאים; הגבול המוקדם קובע. המשך מתחיל בהפעלה הראשונה וממשיך להימדד בזמן השהיה. מועד שתועד לפני הסיום עשוי להגיע באיחור לאחר השבתה ארוכה. הרצה שכבר התקבלה נשארת זמינה למעקב עד לתוצאתה.
|
||||
|
||||
**מספר מרבי של ריצות שהתחילו** הוא סך משותף לכל השעות ול־**הרצה עכשיו**. הרצה נספרת פעם אחת כשהקלט הראשון של המתאם שלה מתקבל, גם אם היא נכשלת בהמשך. בדיקות, תנאים שלא השתנו והתעוררויות שנדחו אינם צורכים מקום; קבלה לא ודאית מחזיקה את מקומה עד להתאמה. מדיניות חד־פעמית מאפשרת לכל היותר הרצה אחת. מקומות שנצרכו נשמרים בעריכות ובהפעלות מחדש; מגבלות סיום שלא השתנו שומרות על נקודת המוצא בהפעלה הראשונה ועל מועד הסיום המוחלט. שינוי המספר המרבי משנה את הסך לכל משך החיים; סך השווה למספר שנצרך או נמוך ממנו מסיים את התזמון. שינוי תדירות עשוי להתחיל מופע מרווח חדש בשמירה, כפי שהעורך מסביר.
|
||||
|
||||
התזמון המקורי הנבחר מציג **מצב רצוי**, **לוח זמנים מקורי**, **ההתעוררות הבאה** או **הבדיקה הבאה**, **בדיקות** עם **במעקב מאז**, **הרצות שהופעלו**, **הרצות שהצליחו**, את סיומו ואת **הרצות קודמות**. קראו הרצה קודמת דרך **פתיחת השיחה**. **התזמון הסתיים** מתאר את סיום התזמון; בדקו בנפרד את תוצאת ההרצה. התוצאות שומרות את 30 הרשומות האחרונות; הספירות הכוללות נשמרות מעבר לחלון הזה. **טעינת משימות נוספות** טוענת את העמוד הבא של האובייקטים השמורים.
|
||||
|
||||
הרשימה מציגה תנאי וסיום רק כשהוגדרו, ו־**בדיקות** רק כשיש תנאי.
|
||||
|
||||
### לוחות זמנים יומיים קיימים {#existing-daily-schedules}
|
||||
|
||||
לוחות זמנים יומיים קיימים משתמשים באזור IANA ובשעה האזרחית השמורים. לכל הרצה מתאם ומפת משימה משלה; בחרו את עץ המטלות הנוכחי שלה או תאריך/תוצאה קודמים כדי לקרוא את התוצאה במרכז. ההיסטוריה השמורה שלהם מכילה 30 תוצאות. כללי היומיום הבאים חלים על רשומות אלה:
|
||||
|
||||
| מצב | התנהגות התזמון |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@
|
|||
| השעון זז לאחור | אינו הופך יום שכבר עובד ליום שהגיע מועדו שוב. |
|
||||
| הרצה קיימת עדיין ממתינה | אינו חופף אותה עם הרצה יומית חדשה. |
|
||||
|
||||
גם זמן היצירה מגביל עבודה יומית זכאית; הפעלת לוח זמנים חדש אינה היתר לעבד תאריכים מלפני שהיה קיים. **הרצה עכשיו** מתחיל הרצה ידנית נפרדת כשמוצע; אינו מחליף את השעון היומי או עוקף הרצה ממתינה. השעה הבאה המוצגת היא תחזית, לא הבטחה להרצה בשעה מדויקת.
|
||||
גם זמן היצירה מגביל עבודה יומית זכאית. **הרצה עכשיו**, כשהיא מוצעת ברשומה יומית קיימת, מתחילה הרצה ידנית נפרדת בכפוף להגנה מפני הרצה ממתינה ומשמרת את השעון היומי שלה. השעה הבאה המוצגת היא תחזית הכפופה לזמינות השירות והמארח.
|
||||
|
||||
**שיחות במעקב (לא חובה)** מוסיף הקשר מוגבל משיחות שנבחרו. רק הרצה שהושלמה מקדמת את סמני ההודעות שעובדו שלהן; הרצה שנכשלה/נקטעה אינה צורכת בשקט את ההקשר הזה. אם ההוראות וההקשר יחד חורגים מהקיבולת, קצרו הוראות או הסירו שיחות במעקב במקום להניח שנקראת היסטוריה ללא גבול.
|
||||
|
||||
## משך חיים והפעלות שירות מחדש {#lifetime}
|
||||
|
||||
ה־**i** ליד **פרטים טכניים** מסביר פעילות טכנית ועלויות. תחת **הגדרות → OpenCode**, מצב ההשגחה והשגיאות נשארים גלויים; ההסברים הכלליים נמצאים ב־**i**.
|
||||
|
||||
בתזמון מקורי, הרחיבו את **פרטים טכניים** כדי לזהות את ה־**נשא ראשי** וה־**נשא התאמה** שלו, את לוחות הזמנים המקוריים שלהם וכל מצב **ניקוי ממתין להתאמה**. שיחות טכניות אלה אינן מציעות פעולת כתיבת הודעה ואינן מופיעות בבוררי השיחות והמקורות הרגילים. התעוררויות טכניות, כותרות ודחיסת הקשר משתמשות במודל המקומי ללא בקשות HTTP לספקים; אירועים וסטטיסטיקות מקוריים עדיין עשויים לכלול אותם, בעוד שההרצות בפועל שומרות על עלויות המודל הרגילות שלהן. דחיסת ההקשר מגבילה את ההקשר המעובד תוך שמירה על זהות השיחה ולוח הזמנים. הודעות ואירועים מקוריים מאוחסנים עשויים להצטבר עד לניקוי מפורש של תזמון שהסתיים; 30 תוצאות המשימה הנשמרות הן נפרדות. בקרת העומק ו־**תזמון** מציגות את הנתיב תחת **קובץ התצורה של OpenCode:** כשהוא זמין; בקרת העומק מסמנת בנפרד את **יעד הכתיבה של הפרויקט**.
|
||||
|
||||
| רכיב | משמעות סגירתו |
|
||||
| --- | --- |
|
||||
| לשונית פרויקט CodeNomad | סוגרת את הפרויקט ב־CodeNomad; סשנים מקוריים ממשיכים ב־OpenCode. פתחו את הפרויקט מחדש כדי למצוא אותם שוב. |
|
||||
| חלון שלם, יציאה מהיישום או טעינה מחדש של תהליך התצוגה | משמרים את חיבורי הפרויקטים לשחזור; אינם עוצרים עבודה מקורית או את שירות OpenCode. |
|
||||
| שרת הקצה של CodeNomad | אינו עוצר את OpenCode. משימה חד־פעמית פעילה ממשיכה: המתאם שלה, המטלות שלה כתת־סוכנים מקוריים וכלי המשימה פועלים ב־OpenCode. הקצאת מטלות לסשנים עצמאיים ומסירת הדוחות שלהם למתאם, משימות שהמתאם מכין, עצירת השיחות של מטלה שהוצאה מהתוכנית, התאוששות ובקרת המשימה ממתינות לשרת הקצה, וכך גם המשך של משימה חד־פעמית אחרי הפעלה מחדש של שירות OpenCode. |
|
||||
| לוח זמנים יומי פועל | השעון והרצת המחזור שלו פועלים בתוסף/בשירות OpenCode, לרבות כשהממשק ושרת הקצה של CodeNomad סגורים. מארח ההרצה ושירות OpenCode חייבים להמשיך לפעול. |
|
||||
| הפעלה מחדש של שירות OpenCode | מאבדת עבודות תזמון מקומיות לתהליך. לוח זמנים שאמור לפעול הופך ל־**נקטע** עד **המשך** מפורש; פתיחת CodeNomad מחדש אינה מפעילה אותו מחדש. |
|
||||
| תזמון מקורי מופעל | לוחות זמנים מתמשכים של OpenCode מעירים את הפרויקט ומבצעים הרצות כשהממשק ושרת הקצה של CodeNomad סגורים, כל עוד מארח ההרצה והשירות זמינים. |
|
||||
| הפעלה מחדש של שירות OpenCode — תזמון מקורי תקין | תזמון תקין שמצבו הרצוי הוא ״פועל״ מחדש את לוח הזמנים שלו אוטומטית. עבודה ממתינה מתואמת לפי זהויותיה המקוריות לפני קבלה חדשה. |
|
||||
| לוח זמנים יומי קיים | שעון התוסף שלו פועל כש־CodeNomad סגור כל עוד השירות חי. הפעלה מחדש של השירות משאירה אותו במצב **נקטע**; **המשך** מפורש מתאם את ההרצה הממתינה שלו ומפעיל שוב את שעונו היומי. |
|
||||
|
||||
לאחר הפעלה מחדש, בדקו את ההרצה הממתינה ובחרו **המשך** כשתהיו מוכנים. המשך מיישב את ההרצה הקיימת בלי לשלוח את קלט המתאם שלה פעמיים; הוא אינו מבטיח המשך של תור מקורי שנקטע. תור שנחתך בהפעלה מחדש יכול להסתיים ללא דוח, עם **הופסקה בגלל הפעלה מחדש**. אל תשתמשו בהפעלה מחדש כדרך לנסות שוב השפעות צד של כלים.
|
||||
בתזמונים מקוריים, השהיה ועצירה מפורשות, עבודה שנכשלה ותוצאות לא ודאיות משמרות את מחסומי הקבלה שלהן לאחר הפעלה מחדש. בדקו את ההרצה שנשמרה ואת הבקשה המקורית; השתמשו ב־**בדיקת ההרצה** או ב־**רענון הבקשה השמורה** כשמוצעות, ואז ב־**המשך** רק כשהמצב מאפשר זאת. המשך מקורי נצפה תחת זהותו הקיימת. כישלון מוכח או סיום ללא דוח דורשים התאוששות אנושית; ראיות לא שלמות משאירות את ההרצה לא ודאית. לעולם אל תשתמשו בהפעלה מחדש כדי לנסות שוב השפעות כלים שכבר החלו.
|
||||
|
||||
[השארת OpenCode פועל](opencode.md#service-supervision) משגיחה בנפרד על השירות המשותף במארח ההרצה שלו. ההפעלה הראשונה של משימה מפעילה אותה אלא אם שמרתם כיבוי; יצירת משימה מושהית משמרת את הבחירה הזו. כיבויה משאיר את OpenCode פועל ללא הפעלה מחדש אוטומטית. Windows, macOS ו־WSL משתמשים בגבול הכניסה; Linux מקורי משתמש בהשארת שירותי המשתמש פועלים, בכפוף להרשאות. שינה, תרדמה, משך החיים של WSL במצב סרק ושליטה במארח חיצוני מגבילים זמינות; בדקו את מצב ההשגחה בנפרד מהתזמון.
|
||||
|
||||
משימות פועלות עם יישומי שולחן העבודה ועם [שרת](server.md) CodeNomad עצמאי. כשהלוח מציג **בקרת המשימה אינה זמינה**, ההודעה שלו מציינת את הסיבה:
|
||||
|
||||
|
|
@ -184,7 +237,9 @@
|
|||
|
||||
השהיה חד־פעמית מאפשרת לעבודה הנוכחית להסתיים: המתאם ותתי-הסוכנים שלו משלימים את השלב שבו הם נמצאים, אינם מתחילים דבר חדש ועוצרים, בזמן שהלוח מציג **בהשהיה: השלבים הפעילים מסתיימים.** **הפסקה עכשיו** עוצר אותם מיד במקום זאת: הוא קוטע את המשפחה המקורית של המשימה ומשהה תיאום. עצירה גם קוטעת, מבטלת קלטים/מסירות בתור של המשפחה והיא סופית. הקטיעות מוגבלות: **ייתכן שחלק מתתי-הסוכנים עדיין פועלים.** אומר שאחת מהן אינה שלמה. בדקו את התוצאה שנשמרה והשתמשו בניסיון החוזר המפורש שלה במקום להניח שכל השפעות המעטפת/הכלים בוטלו. חידוש מודיע למתאם מה הושהה או נקטע; הוא מחליט כיצד להמשיך את העבודה.
|
||||
|
||||
השהיה/עצירה יומיות מבטלות תזמון ומבקשות קטיעה של גורמי שורש רשומים, לרבות המתאם והמומחים העצמאיים, אך **אינן מוכיחות שאותן שיחות או צאצאיהן נעצרו**. בדקו את המשפחות המקוריות. עצירה היא סופית: לוח זמנים שנעצר אינו יכול לפעול, להתחדש או לרוץ עכשיו. **בדיקת ההרצה** מיישב הרצה ממתינה בלי לשלוח אותה שוב. כשלי בדיקה מתוזמנת זמניים מציגים את הכשל האחרון שלהם ומנסים שוב אוטומטית; אובדן תשובה לבקרת משתמש אינו גורם לשליחה אוטומטית מחדש של הבקרה.
|
||||
**השהיה** בתזמון מקורי סוגרת קבלה עתידית ומבקשת קטיעה של הרצה שהתקבלה; **המשך** מתאם עבודה קיימת לפני הפעלה מחדש של התזמון. **עצירה…** היא סופית. **הרצה עכשיו** משתמשת באותה הגנה מפני הרצה ממתינה ובאותה מכסת הרצות שהופעלו, תוך שמירת לוח הזמנים. **בדיקת ההרצה** מתאמת את ההרצה המקורית. שמרו פעולה שלא אושרה והשתמשו ב־**רענון הבקשה השמורה**; רענון פסיבי וחיבור מחדש משמרים אותה. השפעות כלים שכבר החלו עשויות להימשך לאחר קטיעה.
|
||||
|
||||
רשומות יומיות קיימות שומרות את בקרותיהן היומיות: השהיה, עצירה, המשך, הרצה עכשיו ובדיקת ההרצה. השהיה/עצירה מבטלות את שעונן ומבקשות קטיעה של שורשים רשומים, כולל מומחים עצמאיים. בדקו את משפחותיהם המקוריות וכל תוצאת קטיעה חלקית. כשלי בדיקה יומית זמניים נבדקים שוב עם האזהרה המוצגת; תשובת בקרת משתמש שאבדה נשארת צמודה לבקשתה המקורית. רשומה שנעצרה נשארת סופית.
|
||||
|
||||
למתאם חד־פעמי הנמצא בסרק ללא הדוח שלו, פעולת ▶ הראשית יכולה לאמת ולהתאושש; למטלות יש התאוששות ממוקדת לדוח חסר כשזכאיות. ההתאוששות בודקת תחילה פעילות מקורית, צאצאים והמתנות; פעילות לא ידועה מסרבת לקבלה להרצה. היא אינה שולחת מחדש הקצאות או מחליפה את התוכנית. פתחו שיחות כדי לפתור שאלות, אישורים, מעטפות או עבודת רקע שנותרו.
|
||||
|
||||
|
|
@ -192,12 +247,28 @@
|
|||
|
||||
## עריכה, מחיקה וניקוי שיחות {#cleanup}
|
||||
|
||||
השתמשו ב־**עריכת המשימה שנבחרה** במשימה חד־פעמית הניתנת לעריכה; בהתנגשות, סגרו/פתחו מחדש את העורך לטעינת המהדורה הנוכחית במקום לדרוס אותה. אם המטרה או ההערות משתנות בזמן שהטופס עדיין נטען, הוא מציין שהמשימה השתנתה מאז שפתחתם אותה במקום למלא אותן; בחרו **פתיחה מחדש** כדי לערוך את הגרסה הנוכחית. לוחות זמנים יומיים אינם מציעים פקדי עריכה/מחיקה.
|
||||
אם המטרה או ההערות משתנות בזמן שהטופס עדיין נטען, הוא מציין שהמשימה השתנתה מאז שפתחתם אותה במקום למלא אותן; בחרו **פתיחה מחדש** כדי לערוך את הגרסה הנוכחית.
|
||||
|
||||
בין הרצות, אפשר לערוך בתזמון מקורי מושהה או פועל את הכותרת, ההוראות, ההערות, הפרופילים המבוקשים, מצב שיחות המטלות, המקורות, לוח הזמנים והמגבלות, כשאין הרצה או בקרה לא מוסדרת שמחזיקות את האובייקט. תזמון מושהה נשאר מושהה; תזמון מופעל מתאם את לוח הזמנים שלו לפני ההמשך. שמירה משמרת תוצאות קודמות, מקורות שנצרכו, מספר הרצות שהופעלו ואת נקודת המוצא בהפעלה הראשונה; מגבלות סיום שלא השתנו שומרות על מועד הסיום המוחלט שלהן.
|
||||
|
||||
במהלך הרצה שהתקבלה, רק הוראות והערות כפופות לחוזה עריכת הטקסט הפעיל: שינויים נרשמים בהיסטוריה שלה ונשמרים להרצות עתידיות. מטלות, תקצירים, פרופילים וזהויות ביצוע קיימים שומרים את הערכים שהתקבלו להפעלה. שדות מבניים נשארים נעולים עד להסדרת ההרצה, גם לאחר השהיה. מדריך נשאר נעול מרגע שנוצרה מטלה כלשהי, כולל מטלות שהושלמו או הוסרו. קבלה/בקרה לא ודאיות או ראיות לא שלמות לתקינות הנתונים משאירות עריכה מבנית לקריאה בלבד. יצירה ועריכה משתמשות באותו אימות לוח זמנים; התצוגה המקדימה מחילה את המועד האחרון המותר שנערך על ההתעוררות הבאה השמורה, גם כשהיא מאוחרת.
|
||||
|
||||
מקור הענפים המקומיים כולל ענפים ללא worktree וקורא רק הפניות מקומיות. הבורר תומך בעד 200 ענפים מקומיים; במלאי גדול יותר המקור מוצג כלא זמין. **התחלה** ו**המשך** מבקשים הסכמה עם נתיבי התצורה המדויקים כשההפעלה זמינה. **הרצה עכשיו** נשארת מושבתת עם הסבר כשהתזמון המקורי כבוי; יש להפעיל `experimental.schedules` בתצורת OpenCode ואז לרענן. תשובת הפעלה שאבדה שומרת על הבקשה המקורית לבדיקת האישור שלה.
|
||||
|
||||
לפני Play הראשון אפשר לחזור להפעלה מיידית ללא תנאי ולשמור את השיחות שבמעקב ב**אפשרויות**. Play קורא ומקפיא את רצף ההודעות השלם והיציב שלהן (עד 32 הודעות לכל מקור, במסגרת קלט הפעלה של 16,384 תווים). מקור חסר, שהועבר או שחורג מהקיבולת חוסם את ההפעלה עם הסבר. ניסיון חוזר שומר את ההקשר המוקפא ואת זהות ההפעלה המקורית.
|
||||
|
||||
כאשר קריאת האישור מאשרת הכנה חלקית, מופיע **ניסיון נוסף**. הלחיצה בודקת שוב את אותו אישור וממשיכה רק את השינוי המקורי שנשמר, כשהמשימה נשארת מושהית.
|
||||
|
||||
לפני ההפעלה הראשונה של משימה חדשה שהוכנה, אפשר לערוך כותרת, הוראות, הערות, ספר מהלכים, סוכנים/מודלים/חשיבה מבוקשים, מצב שיחות המשימות ו**תזמון**. אפשר לעבור בין הפעלה מיידית, דחויה, חוזרת ומותנית תוך שמירה על אותה משימה. שמירה משאירה אותה מושהית ומכינה מתאם חדש וריק; השיחה המקורית נשמרת. רק Play מתחיל עבודה או מפעיל תזמון. לאחר ההפעלה, ההוראות וההערות כפופות לכללי העריכה של המשימה הפעילה. אם השמירה לא אושרה, **רענון הבקשה השמורה** קורא את התוצאה המקורית; שמרו על אותה בקשה כל עוד הכנה חלקית חוסמת הפעלה, עריכת טקסט ומחיקה.
|
||||
|
||||
השתמשו ב־**עריכת המשימה שנבחרה** במשימה חד־פעמית או בתזמון מקורי הניתנים לעריכה. קראו את ההסברים לצד השדות הנעולים. בהתנגשות גרסאות, בחרו **טעינת ההגדרות הנוכחיות מחדש**, ואז פתחו **טיוטה קודמת להשוואה** כדי לבדוק את הטיוטה שנשמרה לפני שמירה נוספת. לוחות זמנים יומיים קיימים שומרים את הבקרות שבסרגל הכלים שלהם.
|
||||
|
||||
בתזמון מקורי, **מחיקת המשימה שנבחרה…** משביתה הרצות עתידיות לפני הסרת הרשומה. השיחות והתוצאות נשמרות; ניקוי שממתין להתאמה נשאר גלוי ב־**פרטים טכניים**. שמרו מחיקה שלא אושרה ורעננו את בקשתה המקורית.
|
||||
|
||||
1. למשימה חד־פעמית שכבר אינה נחוצה, בחרו **מחיקת המשימה שנבחרה…** ובדקו את האישור.
|
||||
2. אפשר לסמן **למחוק גם שיחות מומחים שנוצרו עבור משימה זו** לפני האישור.
|
||||
3. בדקו **ניקוי שיחות** למספרים שהוסרו/נשמרו/ממתינים. נסו שוב רק את הניקוי הרשום שנותר, אם מוצע.
|
||||
|
||||
מחיקת משימה אינה זהה לעצירתה; השתמשו בעצירה ובדקו עבודה שנותרה תחילה. מתאמים ושיחות בשימוש חוזר נשמרים. גם מומחים משותפים, מועברים, בעלי ילדים או שלא ניתן לאמת בעלות עליהם עשויים להישמר. ניקוי חלקי אינו מוחזר לאחור ואינו מושלם בשקט ברקע. קורא הסקירה שומר את 20 הניקויים האחרונים שהושלמו. מחיקת משימות מפנה את האחסון שלהן בפרויקט: משימות מחוקות ישנות יותר מנוקות אוטומטית.
|
||||
מחיקת משימה אינה זהה לעצירתה; השתמשו בעצירה ובדקו עבודה שנותרה תחילה. מתאמים ושיחות בשימוש חוזר נשמרים. גם מומחים משותפים, מועברים, בעלי ילדים או שלא ניתן לאמת בעלות עליהם עשויים להישמר. ניקוי חלקי אינו מוחזר לאחור ואינו מושלם בשקט ברקע. כרטיס המשימה עשוי להיעלם בזמן שאימות המחיקה עדיין ממתין, גם כשניקוי השיחות מציג 0 בהמתנה. המשיכו להשתמש בפעולת **ניסיון נוסף** המקורית עד שהמחיקה תאומת; היא שומרת על אותה בקשת מחיקה ועל אותן אפשרויות. קורא הסקירה שומר את 20 הניקויים האחרונים שהושלמו. מחיקת משימות מפנה את האחסון שלהן בפרויקט: משימות מחוקות ישנות יותר מנוקות אוטומטית.
|
||||
|
||||
ראו [אישורים](permissions.md), [שיחות](conversations.md) ו־[הגדרת OpenCode](opencode.md) לעבודה מקורית ולפקדי שירות.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ CodeNomad מתחבר לשירות OpenCode משותף במארח ההרצה. ב
|
|||
| מומלצת | OpenCode **2.2.5** | יעד שנבדק, בנפרד מהמינימום הטכני. |
|
||||
| לא מאומתת | גרסה חדשה או מותאמת שלא אומתה במלואה | אינה נדחית רק מפני שלא נבדקה; חוזי ה־API בפועל של השירות המחובר נבדקים. |
|
||||
|
||||
גם החוזים הנדרשים לסשנים, אישורים, טפסים, תיבת קלט וסביבת סשן חייבים להיות זמינים. גרסת קובץ הרצה לבדה אינה מוכיחה שהשירות המחובר תומך בהם. אם ההגדרה מדווחת על שירות פועל לא נתמך, בחרו/התקינו CLI נתמך והשתמשו בהפעלה מחדש מפורשת של השירות המשותף כשתהיו מוכנים.
|
||||
השירות המחובר חייב לחשוף את החוזים הנדרשים לסשנים, אישורים, טפסים, תיבת קלט וסביבת סשן. תזמון משימות מקורי בודק גם את ממשקי ה־API של התזמון, חבילת התוסף/הספק ואת הגדרת `experimental.schedules` שבתוקף. הזמינות נקבעת לפי היכולות הנצרכות האלה, גם בגרסאות עתידיות תואמות. אם ההגדרה מדווחת על שירות פועל לא נתמך, בחרו/התקינו CLI נתמך והשתמשו בהפעלה מחדש מפורשת של השירות המשותף כשתהיו מוכנים.
|
||||
|
||||
## מארח, WSL והיקף השירות המשותף {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ CodeNomad מתחבר לשירות OpenCode משותף במארח ההרצה. ב
|
|||
|
||||
OpenCode הוא הבעלים של רישום השירות הגלובלי, הסשנים, ההודעות ומסד הנתונים שלו. CodeNomad אינו יוצר מסד נתונים פרטי של OpenCode לכל חלון או פרויקט. סגירת לשונית פרויקט סוגרת את הפרויקט ב־CodeNomad; הסשנים ממשיכים ב־OpenCode ומופיעים שוב כשפותחים את הפרויקט מחדש. CodeNomad לעולם אינו פורק Locations מקוריים. סגירת חלון שלם, יציאה או טעינה מחדש משמרות את חיבורי הפרויקטים לשחזור; לא פעולות אלה ולא כיבוי שרת הקצה עוצרים את הדמון המשותף. ראו [פרויקטים](projects.md).
|
||||
|
||||
## השארת OpenCode פועל {#service-supervision}
|
||||
|
||||
לפני ההפעלה הראשונה של Mission, **להשאיר כבוי בהפעלת Mission הראשונה** שומר כיבוי מפורש בלי להתקין תחילה את ההשגחה.
|
||||
|
||||
ב־**הגדרות → OpenCode**, האפשרות **השארת OpenCode פועל** מתקינה השגחה של מערכת ההפעלה במארח ההרצה, המשותפת לכל פרופילי CodeNomad המשתמשים בשירות. היא פועלת בנפרד מ־Missions, גם כש־CodeNomad סגור. ההפעלה הראשונה של Mission מפעילה אותה אלא אם כיבית מראש במפורש; יצירת Mission מושהה שומרת על הבחירה. השהיה, עצירה או מחיקה של Mission משאירות העדפה זו ללא שינוי.
|
||||
|
||||
כיבוי מסיר את ההשגחה בזמן ש־OpenCode ממשיך לפעול ולא יופעל מחדש. לעצירה מתמשכת, כבה תחילה ואז עצור במפורש את OpenCode. כשההשגחה פעילה, אחרי `opencode service stop` מתבצעת הפעלה מחדש.
|
||||
|
||||
**פועל · השגחה פעילה** דורש משגיח מערכת פעיל ושירות OpenCode תקין. **מותקן · השירות מתחיל**, **התקנה או הסרה של ההשגחה…** ו־**ההשגחה נכשלה** מבדילים בין המצבים. שירות שמגיב לבדו משאיר את ההשגחה ללא אישור. הפרטים מציגים את הסיבה, הניסיון האחרון, הניסיון הבא של המשגיח ובדיקת ההתאוששות של המערכת כשאלה ידועים. **בדיקת מצב ההשגחה** קוראת בלבד; **ניסיון חוזר להשגחה** מתקן במפורש את ההתקנה המבוקשת. פעולה שלא אושרה שומרת על הבקשה המקורית; **ניסיון חוזר לפעולה המקורית** שולח שוב רק אותה פעולה מדויקת. גרסת הגדרות שהשתנתה מתרעננת לבדיקה במקום להידרס.
|
||||
|
||||
Windows, macOS ו־WSL מתחילים לאחר הכניסה. WSL דורש העברת localhost להפצה שנבחרה. ב־Linux מקורי, linger מפעיל **את כל שירותי המשתמש המאופשרים** לפני הכניסה ומשאיר אותם לאחר היציאה. בכיבוי, linger משוחזר רק אם CodeNomad הפעיל אותו ואין השגחה אחרת שזקוקה לו; הגדרות אישיות נשמרות. אם שחזור linger עלול לעצור את תהליכי OpenCode ששוחררו כשאין הפעלת משתמש פתוחה, השחזור נדחה ונשאר גלוי עם **ניסיון חוזר להשגחה**. נסה שוב כשנפתחת הפעלת משתמש או לאחר שתהליכי OpenCode ששוחררו הסתיימו, כך שניתן לשחזר בבטחה. אם Linux דורש הרשאה, אשר במארח ההרצה ונסה שוב; ההעדפה המבוקשת נשמרת. שינה ותרדמה נשלטות על ידי מערכת ההפעלה. מארחים שאינם זמינים או אינם נתמכים מציגים פקד מושבת עם הסבר.
|
||||
|
||||
## עדכון, הפעלה מחדש או טעינה מחדש {#maintenance}
|
||||
|
||||
1. ב־**הגדרות → OpenCode**, בחרו **בדיקת מצב ועדכונים**.
|
||||
|
|
@ -41,7 +53,7 @@ OpenCode הוא הבעלים של רישום השירות הגלובלי, הסש
|
|||
| פעולה | היקף והשפעה | התאוששות בטוחה |
|
||||
| --- | --- | --- |
|
||||
| עדכון CLI | משנה את קובץ ההרצה המותקן הנתמך; אינו מפעיל מחדש את הדמון הפועל במשתמע. | בדקו גם גרסה מותקנת וגם פועלת לאחר כשל; נסו שוב במפורש רק לאחר פתרון השגיאה. |
|
||||
| הפעלה מחדש של השירות | מחליפה את התהליך המשותף וקוטעת עבודה פעילה בכל לקוחות OpenCode המחוברים; היסטוריית הסשנים נשארת. | התחברו מחדש ובדקו שיחות ומשימות מתוזמנות; הפעלה מחדש אינה המשך אוטומטי. |
|
||||
| הפעלה מחדש של השירות | מחליפה את התהליך המשותף וקוטעת עבודה פעילה בכל לקוחות OpenCode המחוברים; היסטוריית הסשנים נשארת. | תזמוני משימות מקוריים תקינים ופועלים מתאוששים אוטומטית; בדקו הרצות ממתינות וכל השהיה/שגיאה/אי־ודאות. רשומות יומיות קיימות דורשות המשך. בדקו בנפרד שיחות חד־פעמיות ואת בקרות ההתאוששות שלהן; ראו [משימות](missions.md#lifetime). |
|
||||
| טעינה מחדש של תצורת OpenCode | משמרת את התהליך אך בונה מחדש **כל Location טעון** לכל הלקוחות. מבטלת אישורים/טפסים ממתינים וסוגרת מסופים/פקודות רקע; עבודה פעילה עלולה להיקטע. | בדקו מחדש משאבים ועבודה ממתינה לאחר טעינה מחדש. אל תחזרו על קלט או פעולת שינוי רק מפני שתשובתם אבדה. |
|
||||
|
||||
השתמשו ב־**טעינה מחדש של תצורת OpenCode**, תחת **פתרון בעיות** כשהשירות הפועל תומך בכך, במפורש כשתצורה שמורה או תוסף מותקן נשארים לא זמינים וההפרעה מקובלת. זה אינו רענון תצוגה תמים. CodeNomad לא ישתמש ב־CLI נבחר ישן יותר כדי לשנמך דמון פועל חדש יותר.
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
### שאלות אליכם {#questions}
|
||||
|
||||
רק המתאם שואל אתכם שאלות. מטלות והעוזרים שלהן מחזירים את השאלות שלהם לשיחה שהפעילה אותם, עם האפשרויות, הראיות, ההשלכות, ההפיכות והמלצה, וממשיכים בעבודה שאינה תלויה בתשובה. המתאם מכריע קודם במה שהוא יכול ומאגד את השאלות שנותרו בטופס אחד תחת **נדרשת תגובתך**; [שאלות והחלטות](missions.md#questions) מסביר מה הוא שואל ואיך מסומנות החלטות. כל מתאם, חד־פעמי או יומי, וכל הקצאת מטלה מקבלים את הכלל הזה כהנחיה; מסע חקר מוסיף את בדיקות ההחלטה [בהמשך](#wayfinder-checks).
|
||||
רק המתאם שואל אתכם שאלות. מטלות והעוזרים שלהן מחזירים את השאלות שלהם לשיחה שהפעילה אותם, עם האפשרויות, הראיות, ההשלכות, ההפיכות והמלצה, וממשיכים בעבודה שאינה תלויה בתשובה. המתאם מכריע קודם במה שהוא יכול ומאגד את השאלות שנותרו בטופס אחד תחת **נדרשת תגובתך**; [שאלות והחלטות](missions.md#questions) מסביר מה הוא שואל ואיך מסומנות החלטות. כל מתאם, חד־פעמי או מתוזמן, וכל הקצאת מטלה מקבלים את הכלל הזה כהנחיה; מסע חקר מוסיף את בדיקות ההחלטה [בהמשך](#wayfinder-checks).
|
||||
|
||||
השיבו באזור הבקשות (**מענה** תחת **נדרשת תגובתך**): רק זה נחשב לתשובה שלכם. תשובה שהוקלדה בצ'אט היא הודעה רגילה.
|
||||
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
|
||||
המתאם מונחה לפרסם עדכון מצב אחרי התוכנית הראשונית, אחרי כל מטלה שהוא מסכם, בכל פעם שהוא מתחיל להמתין לכם ולפני הדוח הסופי; ראו [עדכוני מצב ובקשות עדכון](missions.md#briefings). עדכוני מצב, סיכומי מטלות והדוח הסופי נכתבים בשבילכם: בשפה שלכם, בכמה משפטים קצרים, התוצאה קודם, כשהמזהים והפקודות נשארים בראיות.
|
||||
|
||||
הדוח הסופי מציין מה נמסר, מה נותר ומה דורש את החלטתכם. משימה חד־פעמית מסתיימת רק כהושלמה: כשדבר אינו יכול להתקדם בלעדיכם, המתאם שואל אתכם וממתין במקום להיכשל, ורק אתם עוצרים משימה. הרצה יומית יכולה גם להסתיים ככושלת.
|
||||
הדוח הסופי מציין מה נמסר, מה נותר ומה דורש את החלטתכם. משימה חד־פעמית מסתיימת רק כהושלמה: כשדבר אינו יכול להתקדם בלעדיכם, המתאם שואל אתכם וממתין במקום להיכשל, ורק אתם עוצרים משימה. הרצה מתוזמנת יכולה גם להסתיים ככושלת.
|
||||
|
||||
### מה CodeNomad בודק בכל משימה {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
- רק חמשת התפקידים של מסע חקר מתקבלים.
|
||||
- מטלת החלטה מושלמת רק עם החלטה של המתאם עצמו או עם התשובה שלכם. החלטת מתאם מציינת את השאלה, ההחלטה והנימוק, ולעולם אינה נרשמת כתשובה שלכם.
|
||||
- התשובה שלכם נחשבת רק אם נתתם אותה בממשק של CodeNomad. CodeNomad קושר אותה למטלה לפי התג של השאלה: השאלה נשאלה משיחת המתאם, הטקסט שלה מסתיים בתג של המטלה הזו ואינו מציין החלטה אחרת, היא נענתה אחרי שהמטלה הוצהרה, והיא לא הכריעה החלטה אחרת. כל שאלה בטופס מאוגד נקשרת בנפרד; שאלה שמציינת שתי החלטות אינה מכריעה באף אחת. גם תשובה לשאלה שנשאלה מהשיחה של מטלת ההחלטה עצמה מתקבלת.
|
||||
- הדוח של המטלה עצמה אינו יכול לטעון מי החליט. לפני שמשימה או הרצה יומית מסתיימות בהצלחה, CodeNomad בודק שוב כל החלטה שהושלמה, כולל התגים.
|
||||
- הדוח של המטלה עצמה אינו יכול לטעון מי החליט. לפני שמשימה או הרצה מתוזמנת מסתיימות בהצלחה, CodeNomad בודק שוב כל החלטה שהושלמה, כולל התגים.
|
||||
- בדיקות אלה חלות עם **תת־סוכנים מקוריים**. עם **שיחות עצמאיות**, אחראי ההחלטות מדווחים את התדריכים שלהם בעצמם, והמתאם רושם כל תוצאה בהערות ומציין מי החליט; CodeNomad אינו מאמת זאת.
|
||||
|
||||
הנחיות בלבד: מתי בחירה נחשבת זולה לביטול, איכות התדריכים והמחקר, והיצמדות לתכנון.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode はエンジンを提供し、CodeNomad はコックピットを提供
|
|||
|
||||
## 長い作業をミッションに任せる {#missions}
|
||||
|
||||
作業内容は一度説明するだけです。ミッションのコーディネーターがそれをタスクツリーとして計画し、範囲を限定したタスクを OpenCode ネイティブの会話に委任し、結果を根拠とともに報告します。ミッションは 1 回または毎日実行できます。作成しても、開始を選ぶまで作業は始まりません。プレイブックには、デバッグ、プロダクトデリバリー、匿名評価担当による 2 つのモデルの比較があります。[ミッション](missions.md)を参照してください。
|
||||
作業内容は一度説明するだけです。ミッションのコーディネーターがそれをタスクツリーとして計画し、範囲を限定したタスクを OpenCode ネイティブの会話に委任し、結果を根拠とともに報告します。1 回、指定した時刻や間隔、または検証済みのプロジェクトの変更後に実行できます。作成すると、明示的な開始を待つ一時停止中のミッションを保存します。プレイブックには、デバッグ、プロダクトデリバリー、匿名評価担当による 2 つのモデルの比較があります。[ミッション](missions.md)を参照してください。
|
||||
|
||||
## スマートフォンから作業を続ける {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ npx @neuralnomads/codenomad --generate-token --launch
|
|||
| 複数のチェックアウトや会話で作業 | [プロジェクト](projects.md)、[会話](conversations.md)、[ファイル](files.md) |
|
||||
| 履歴の検索や安全な整理 | [履歴](history.md) |
|
||||
| ネイティブの質問や権限リクエストへの回答 | [権限](permissions.md) |
|
||||
| 範囲を限定した作業や日々の作業の調整 | [ミッション](missions.md) |
|
||||
| 範囲を限定した作業やスケジュールした作業の調整 | [ミッション](missions.md) |
|
||||
| 実行環境と連携の設定 | [OpenCode](opencode.md)、[プロバイダー](providers.md)、[連携](integrations.md) |
|
||||
| 別の端末からの接続 | [リモートコントロール](remote-control.md)、[セキュリティ](security.md)、[モバイル](mobile.md) |
|
||||
| ワークスペースのカスタマイズ | [設定](settings.md)、[ショートカット](shortcuts.md)、[音声](voice.md)、[拡張機能](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
1. 右パネルの **ミッション** を開き、**ミッションを作成** を選びます。作成またはキャンセルするまで、フォームがミッション一覧と選択中のミッションのタスクに代わって表示されます。
|
||||
2. **ミッションで何をしますか?** を入力し、最初の文から事前入力される **タイトル**(最大 60 文字)を確認します。
|
||||
3. **実行タイミング** で **1 回** または **毎日** を選びます。毎日の作業では、表示されたタイムゾーンを確認してください。**変更** で IANA ゾーンを指定できます。
|
||||
3. **1回、今すぐ**、**条件なし** の場合は **スケジュール** を折りたたんだままにします。頻度、条件、任意の上限を選ぶには展開します。作業を予約する前に、**保存するタイムゾーン(IANA)** とプレビューを確認してください。
|
||||
4. **オプション** を展開すると、**プレイブック**、**メモ**、**エージェントとモデル**、**タスクセッション**、プロジェクトの **サブセッションの入れ子の深さ**、保存済みの概要、任意の参照する会話を設定できます。**2つのモデルを比較** では、**モデルA** と **モデルB** のセレクターがメインフォームに表示されます。
|
||||
5. **作成** を選びます。新しい項目を選び、準備ができたら別の ▶ 操作を使用します。1 回限りの作業では **ミッションを開始**、毎日のスケジュールでは **スケジュール {id} を開始** です。
|
||||
5. **作成** を選びます。一時停止中の項目を選んで確認し、準備ができたら ▶ **開始** を使用します。条件なしの即時実行は 1 つのミッションを開始し、遅延、繰り返し、条件付きの方針はネイティブスケジュールを有効にします。
|
||||
|
||||
**作成だけでは作業は決して開始しません。** 1 回限りのミッションは準備状態、毎日のスケジュールは一時停止状態になります。作成も概要の保存も、タイマーを作動させません。作成は選択したプロジェクト/Location を範囲とし、要求したプロファイルとタスクセッションポリシーを固定します。検証結果は該当フィールドの横に表示されます。何も送信されなかったとする拒否の場合は、修正用に下書きが残ります。1 回限りのミッションのコーディネーターが、フォローアップなどの別のミッションを準備することもあります。そのミッションは一覧に **準備済み** と表示され、他のミッションと同じく ▶ 操作からのみ開始します。
|
||||
**作成すると一時停止中のミッションが保存されます。** 作業とネイティブカレンダーを有効にするのは再生操作だけです。作成や概要の保存では、共有サービスの監視設定を保持します。作成は選択したプロジェクト/Location に属し、要求されたプロファイルとタスクセッションの方針を記録します。検証結果は該当項目の横に表示され、何も送信されなかったとする拒否では修正用の下書きが残ります。単発のコーディネーターが別のフォローアップを準備することもできます。**準備済み** と表示され、その項目自身の ▶ 操作で開始します。
|
||||
|
||||
保存済みの単発ミッションでは、実行操作、タスクツリー、会話、最終レポートが保持されます。既存の日次スケジュールは、日次の操作と [日次の時刻規則](#existing-daily-schedules) とともに一覧に残ります。項目を選び、その対象で利用できる操作を使用してください。
|
||||
|
||||
### プレイブック {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@
|
|||
|
||||
後のタスクは、2 つの方法で以前の作業を活用します。依存関係は作業の順序を決めます。コーディネーターは依存先のタスクが完了してからタスクを開始し、関係する結果を依頼内容に入れるか割り当てに添えます。独立したセッションは、それらのレポートをマップで読むこともできます。完了したタスクの会話そのものを続けるには、コーディネーターがその会話を再利用するフォローアップタスクを宣言します。フォローアップはその会話の現在のコンテキストで続行します。このコンテキストは OpenCode によって [コンパクト化](history.md#compact-session) されている場合があり、以前の詳細は保存済みの履歴に残って検索で見つけられます。デバッグのプレイブックは、この方法で両方のレビューを実装担当に戻します。**独立したセッション** では、コーディネーターが既存のメインの会話にタスクを割り当てることもできます。その会話は、無関係な以前の作業を含め、自身のコンテキストを保ちます。それ以外の場合、タスクは割り当てをもとに新しい会話で始まり、別のタスクの会話記録はコーディネーター経由か検索で届きます。**2つのモデルを比較** では、評価担当の割り当てとコンテキストから目的とメモが除かれ、依頼内容に匿名化された出力が入ります。
|
||||
|
||||
毎日のスケジュールでは、実行ごとに専用のコーディネーターとマップで始まります。最初のメッセージには、スケジュールの指示に加えて、前回完了した実行以降に [参照する会話](#daily-schedules) に追加されたユーザーとアシスタントのテキストが含まれます。過去の実行の結果はスケジュールの履歴に残ります。実行中に起動された会話は、その実行のマップを読めます。新しい 1 回限りのミッションも新しいマップで始まります。結果を引き継ぐには、その目的や **メモ** に書き込むか、コーディネーターにフォローアップのミッションを準備させます。その目的とメモはコーディネーターが書きます。
|
||||
スケジュールされた各実行は、保存済みの指示と固定された入力を使い、専用のコーディネーターとミッションマップで始まります。参照する会話からは、範囲を限定した新しいユーザーとアシスタントのテキストが加わり、実行の成功後に消費済みカーソルが進みます。過去の実行はスケジュール履歴に残ります。実行内で起動した会話はそのマップを読めます。新しい単発ミッションは別のマップを持ちます。引き継ぐ結果は目的や **メモ** に書くか、そのコンテキストを含むフォローアップをコーディネーターに準備させてください。
|
||||
|
||||
### 以前の会話を検索する {#context-search}
|
||||
|
||||
CodeNomad の実行中は、ミッションのコーディネーターやタスクを含め、プロジェクト内のすべてのエージェントの会話で CodeNomad のセッション検索ツール(`codenomad.session_search`、OpenCode の Code Mode から使用)を利用できます。会話のプロジェクトフォルダーに保存された会話を、**ワークスペース全体** の [検索](history.md#full-history-search) と同じ順位付けで検索し、各会話のタイトルと識別子付きの短い抜粋と、会話全体を読む方法を返します。ネイティブのツール権限が適用されます。エージェントは、自ら判断したとき、または目的、メモ、あなたのメッセージで求められたときに検索します。たとえば、以前のミッションやタスクの結論を探すときです。CodeNomad を閉じている間に動く毎日の実行は、このツールなしで作業します。
|
||||
CodeNomad の実行中は、ミッションのコーディネーターやタスクを含め、プロジェクト内のすべてのエージェントの会話で CodeNomad のセッション検索ツール(`codenomad.session_search`、OpenCode の Code Mode から使用)を利用できます。会話のプロジェクトフォルダーに保存された会話を、**ワークスペース全体** の [検索](history.md#full-history-search) と同じ順位付けで検索し、各会話のタイトルと識別子付きの短い抜粋と、会話全体を読む方法を返します。ネイティブのツール権限が適用されます。エージェントは、自ら判断したとき、または目的、メモ、あなたのメッセージで求められたときに検索します。たとえば、以前のミッションやタスクの結論を探すときです。CodeNomad を閉じている間に動くスケジュール実行は、このツールなしで作業します。
|
||||
|
||||
## プロファイル、委任、深さ {#profiles}
|
||||
|
||||
深さを保存すると、プロジェクト設定で有効なネイティブスケジュールが保持されます。プロジェクトで明示した `schedules:false` は無効のままです。深さを削除してもスケジュール設定は保持されます。ネイティブ設定を保存する前に、JSON と JSONC ファイルの競合を解消してください。
|
||||
|
||||
1. ミッションの歯車、または **設定 → ミッション** から **新規ミッションの既定値** を編集します。
|
||||
2. **適用先** で **すべてのミッション** または 1 つのプレイブックを選び、**タスクセッション** と、**エージェントとモデル** の下で役割ごとに 1 行(コーディネーター、続いてプレイブックの各役割)を設定します。各選択はすぐに保存され、保存ボタンはありません。
|
||||
3. プレイブックの範囲では、各行に **グローバル** の印付きでグローバルの値が表示されます。項目を変更すると、その値をもとにその役割をカスタマイズします。元に戻すアイコンで、再びグローバルの値を使用します。**リセット**(確認後)は、表示中の範囲のエージェントとモデルだけを消去します。
|
||||
|
|
@ -118,7 +122,7 @@ CodeNomad の実行中は、ミッションのコーディネーターやタス
|
|||
| 設定 | 範囲と効果 |
|
||||
| --- | --- |
|
||||
| 新規ミッションの既定値 | **新しい** ミッションのみに適用する、設定全体の既定値。プレイブックの例外は一般の既定値を上書きします。 |
|
||||
| エージェント/モデル/思考 | 作成時に記録する、要求されたコーディネーター/役割の選択。後の作業に合わせて処理中の担当を無断で切り替えません。毎日の実行は固定済みの要求プロファイルを再利用します。 |
|
||||
| エージェント/モデル/思考 | 作成時に記録する、要求されたコーディネーター/役割の選択。後の作業に合わせて処理中の担当を無断で切り替えません。スケジュール実行は、保存済みの要求プロファイルを使用します。編集は今後の実行に適用されます。 |
|
||||
| ネイティブサブエージェント | タスクセッションはコーディネーター配下で、ネイティブの subagent/all エージェントを使用します。 |
|
||||
| 独立したセッション | 宣言したタスクセッションは primary/all エージェントと独立した会話を使用します。内部のネイティブヘルパーは無効にしません。 |
|
||||
| サブセッションの入れ子の深さ | ミッションだけでなく、そこにある **すべての** OpenCode 会話に適用するプロジェクト設定。空欄は継承です。**継承** はプロジェクトの上書きと、それによって空になるか、グローバルの **画像を一度だけ保存** の値だけが残る `experimental` ブロックを削除します。OpenCode はプロジェクトの `experimental` ブロックを全体として読み込むため、そこに他の実験的設定が残っていると、グローバルの深さは引き続き隠されます。OpenCode に組み込まれた **General** サブエージェントは既定ではサブエージェントを起動できないため、その会話は入れ子になりません。プロジェクトの深さを保存すると、値に関係なく、サブエージェントがその深さまで独自のサブエージェントを起動できるルールも同じファイルに追加されます。このルールは他の設定ファイルにある General の制限より優先されます(深さ 1 では、OpenCode の深さの上限により引き続き入れ子になりません)。**継承** でこのルールは削除されます。**OpenCode の有効な深さ** の下に、General サブエージェントが入れ子にできるかどうかが表示されます。プロジェクトが旧形式で General を単一の `permission` 値、またはすでに `task` と `subagent` の両方を設定して定義している場合は、深さだけが保存され、エディターがその旨を表示します。そのファイルで General のサブエージェントをご自身で許可してください。保存しても OpenCode の再読み込みや実行中の会話の変更は行いません。 |
|
||||
|
|
@ -137,9 +141,51 @@ CodeNomad の実行中は、ミッションのコーディネーターやタス
|
|||
|
||||
概要が保存するのは指示と要求した選択であり、タスク計画、結果、会話、スケジュール時刻、実行権限ではありません。読み込むと通常の作成下書きを埋めますが、開始やスケジュールはしません。概要を削除しても既存のミッション/会話は残ります。**設定 → ミッション → 保存済みの概要** でライブラリを管理します。ライブラリを読み取れなくなった場合、**保存済みライブラリをリセット**(確認後)は、実行ではなく保存済みの概要を削除します。スケジュール自体に概要として保存する操作はありません。
|
||||
|
||||
## 毎日の時刻、遅延実行、参照する会話 {#daily-schedules}
|
||||
## スケジュール、遅延実行、参照する会話 {#daily-schedules}
|
||||
|
||||
毎日のスケジュールは、経過時間の 24 時間間隔ではなく、保存した IANA タイムゾーンと現地時刻を使用します。時刻はスケジュールのゾーンで表示し、閲覧者のゾーンと異なる場合は明記します。実行ごとに専用コーディネーターと分離されたミッションマップを作ります。選択したスケジュールには現在のタスクツリーと日付/結果の履歴が表示されます。過去の実行を選ぶと中央で読めます。履歴は 30 結果までです。
|
||||
### ネイティブスケジュール {#native-scheduling}
|
||||
|
||||
ラベル横の **i** で選択肢の説明を確認できます。マウスを重ねる、キーボードでフォーカスする、またはタップしてください。
|
||||
|
||||
作成または編集で **スケジュール** を展開します。**頻度** には次の選択肢があります。
|
||||
|
||||
| 選択肢 | タイミング |
|
||||
| --- | --- |
|
||||
| 1回、今すぐ | 再生時に 1 回起動します。任意の条件は後述の方法で確認します。 |
|
||||
| 1回、日時を指定 | IANA ゾーンで保存した 1 つの日時です。再生時に過去になっている日時は拒否されます。 |
|
||||
| 1回、一定時間後 | 分または時間の正の整数で指定した期間です。再生時に始まります。 |
|
||||
| 一定間隔 | 最短 15 分です。最初の起動通知は再生から 1 間隔後です。 |
|
||||
| 毎日 | 保存したゾーンの現地時刻を 1 つ指定します。 |
|
||||
| 指定した曜日 | チェックした曜日に 1 つの時刻で実行します。 |
|
||||
| 1日に複数回 | 09:15 と 17:45 など、最大 8 つの独立した時刻です。同じ時刻は 1 回の実行にまとめられます。 |
|
||||
| 毎月の指定日 | 1〜31 日の選んだ時刻です。その日がない月はスキップします。 |
|
||||
| 詳細(cron) | ネイティブの cron 式で、最多で 1 分に 1 回です。 |
|
||||
|
||||
**次の予定(最大5件)** は、保存したゾーンと仮定した再生時刻でプレビューします。条件がある場合、**次の確認(最大5件)** は確認の予定を示し、実行には検証済みの変更が必要です。変更していない保存済み間隔はネイティブの位相を保持します。一時停止中または不明の位相は、ネイティブ状態を読めるまで利用不可と表示されます。遅延と間隔は経過時間を使い、現地暦によるカレンダーは保存したゾーンと OpenCode の夏時間規則に従います。表示する日時は、サービスとホストの稼働状況に左右される予測です。
|
||||
|
||||
障害後、ネイティブスケジュールは最大 1 回の遅延実行を行ってから将来の予定に戻ります。記録されたネイティブの予定枠を使うため、後から逃した枠より前の枠になる場合があります。1 日の複数の時刻は、この遅延実行と同じ重複防止を共有します。未解決の実行は、元の会話とメッセージの識別情報で照合されてから次の実行を開始できます。明示的に一時停止した繰り返しを再開すると、次の将来の予定を使います。一時停止した単発の予定は、遅れて 1 回届く場合があります。
|
||||
|
||||
### 条件とソース {#scheduling-conditions}
|
||||
|
||||
任意の **条件** を 1 つと、その **このプロジェクトの対象** を選びます。**別のスケジュールの成功**、**ローカルブランチの新しいコミット**、**単発ミッションの正常完了**、**フォロー中の会話の新しいメッセージ**、**明示したファイルの変更** から選べます。条件だけの場合は **1回、今すぐ** のままにします。1 回の実行が始まるまで、ネイティブの確認を 15 分ごとに行います。カレンダーがある場合は、その予定時刻に条件を確認します。繰り返す条件は、新たに条件を満たす変更があると再び起動できます。
|
||||
|
||||
最初の再生で開始基準を設定します。再開は、一時停止後に観測した変更も含めてその基準を保持します。成功を条件にするソースには、検証済みの成功結果と確定済みのネイティブファミリーが必要です。ブランチの確認は、取得操作をせずにローカルのコミットと祖先関係を使います。会話の確認は、完全で安定した新しいメッセージを待ち、技術用の起動通知と実行自身の会話を除外します。ファイルはプロジェクト相対の明示的なパスを最大 128 件、1 行に 1 件指定します。再帰パターンとリンクされたパスは拒否します。ソースが欠落、移動、曖昧、または容量超過の場合、説明とともに実行受付をブロックします。確認の間に起きた変更を検出するには、ソースに永続的な痕跡が必要です。
|
||||
|
||||
同時に観測した変更はまとめられます。実行は観測したソースの根拠を固定し、成功として確定するとそのスナップショットを消費して、以後の変更を将来の確認に残します。失敗した実行はソースのチェックポイントを保持し、介入が必要になります。**オプション** の **参照する会話(任意)** は、会話メッセージの条件の選択とは別にコンテキストを提供します。
|
||||
|
||||
### 終了と回数 {#scheduling-limits}
|
||||
|
||||
繰り返しを継続するには上限を空欄にします。**最後に許可する予定日時** と **初回再生から最後の予定までの時間数(任意)** は対象となる予定を制限し、早い方の上限が適用されます。期間は最初の再生で始まり、一時停止中も進みます。終了前に記録された予定は、長い障害の後に遅れて届く場合があります。受け付け済みの実行は、結果が出るまで観測できます。
|
||||
|
||||
**開始する実行の最大数** は、すべての時刻と **今すぐ実行** で共有する総数です。コーディネーターの最初の入力を受け付けた時点で 1 回と数え、後に失敗する実行も含みます。確認、変化のない条件、拒否された起動通知は回数を消費しません。受付が不確かな場合は照合まで 1 回分を確保します。単発の方針は最大 1 回の実行を許可します。消費済み回数は編集や再起動後も残ります。変更していない終了上限は、最初の再生を起点とした基準と絶対的な終了予定を保持します。最大数を変更すると累積回数の上限が変わり、消費済み回数以下にするとスケジュールが終了します。頻度を変えると、エディターの説明どおり、保存時に新しい間隔の位相が始まる場合があります。
|
||||
|
||||
選択中のネイティブスケジュールには、**希望状態**、**ネイティブカレンダー**、**次の起動通知** または **次の確認**、**確認回数** と記録の開始日時、**開始した実行**、**成功した実行**、終了条件、**過去の実行** が表示されます。以前の実行は **会話を開く** から読みます。**スケジュール終了** はスケジュールの終了を表します。各実行の結果は別に確認してください。結果は最新 30 件を保持し、累積回数はその範囲を越えて残ります。**Missionをさらに読み込む** は、保存済み対象の次のページを読み込みます。
|
||||
|
||||
一覧では設定済みの条件と終了条件だけを表示し、**確認回数** は条件がある場合だけ表示します。
|
||||
|
||||
### 既存の日次スケジュール {#existing-daily-schedules}
|
||||
|
||||
既存の日次スケジュールは、保存した IANA ゾーンと現地時刻を使います。実行ごとに専用のコーディネーターとミッションマップを持ちます。現在のタスクツリーや過去の日付/結果を選ぶと、中央で結果を読めます。保存する履歴は 30 件です。次の日次規則がこれらの項目に適用されます。
|
||||
|
||||
| 状況 | スケジュールの動作 |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@ CodeNomad の実行中は、ミッションのコーディネーターやタス
|
|||
| 時計が後退した | 処理済みの日を再び実行対象にしません。 |
|
||||
| 既存の実行がまだ保留中 | 新しい日次実行を重複させません。 |
|
||||
|
||||
作成時刻も、対象となる日次作業の範囲を制限します。新しいスケジュールの開始は、存在する前の日付の処理を許可しません。**今すぐ実行** が利用できる場合は、別の手動実行を開始します。日次の時計の置き換えや、保留中の実行の回避はしません。表示される次回時刻は予測であり、正確な時刻での実行保証ではありません。
|
||||
作成時刻も対象となる日次作業を制限します。既存の日次項目で **今すぐ実行** が利用できる場合、保留中の実行を防護する仕組みのもとで別の手動実行を開始し、日次の時計を保持します。表示される次回時刻は、サービスとホストの稼働状況に左右される予測です。
|
||||
|
||||
**参照する会話(任意)** は、選んだ会話から範囲を限定したコンテキストを追加します。処理済みメッセージのカーソルを進めるのは完了した実行だけであり、失敗/中断した実行が無断でコンテキストを消費することはありません。指示とコンテキストが容量を超える場合は、履歴を無制限に読むと思い込まず、指示を短くするか参照する会話を削除します。
|
||||
|
||||
## 寿命とサービスの再起動 {#lifetime}
|
||||
|
||||
**技術詳細** 横の **i** で技術的な活動と費用の説明を確認できます。**設定 → OpenCode** の監視では状態とエラーを表示し、一般的な説明は **i** にまとめています。
|
||||
|
||||
ネイティブスケジュールでは、**技術詳細**を展開すると、**主キャリア**と**整合用キャリア**、それぞれのネイティブカレンダー、および**クリーンアップは整合処理待ち**の状態を確認できます。これらの技術用会話にはメッセージ入力の操作がなく、通常の会話やソースの選択候補にも表示されません。技術用の起動通知、タイトル、コンパクションにはローカルモデルを使用し、プロバイダーへのHTTPリクエストは行いません。ネイティブのイベントや統計にはこれらが含まれる場合がありますが、実際の実行には通常のモデル費用がかかります。コンパクションは、会話とカレンダーの識別情報を維持しながら処理対象のコンテキストを制限します。保存されたネイティブのメッセージとイベントは、終了したスケジュールを明示的にクリーンアップするまで増える場合があります。保持される30件のMission結果とは別です。深さの設定と**スケジュール**には、パスが取得できる場合に**OpenCode 設定ファイル:**として表示され、深さの設定では**プロジェクトの書き込み先**を別に示します。
|
||||
|
||||
| 要素 | 閉じた場合の意味 |
|
||||
| --- | --- |
|
||||
| CodeNomad のプロジェクトタブ | CodeNomad 内でそのプロジェクトを閉じます。ネイティブのセッションは OpenCode で続き、プロジェクトを開き直すと再び確認できます。 |
|
||||
| ウィンドウ全体、アプリの終了、レンダラーの再読み込み | 復元のためにプロジェクト接続を保持します。ネイティブの作業や OpenCode サービスは停止しません。 |
|
||||
| CodeNomad バックエンド | OpenCode は停止しません。実行中の 1 回限りのミッションは続行します。コーディネーター、ネイティブサブエージェントのタスク、ミッションツールは OpenCode 内で動作します。独立したセッションへのタスクの割り当てとそのレポートのコーディネーターへの配信、コーディネーターが準備するミッション、取り下げ済みタスクの会話の停止、復旧、ミッションコントロールはバックエンドを待ちます。OpenCode サービスの再起動後に 1 回限りのミッションを続行する場合も同様です。 |
|
||||
| 稼働中の毎日スケジュール | 時計と各回の実行は、CodeNomad UI とバックエンドを閉じても、OpenCode プラグイン/サービス内で動きます。実行ホストと OpenCode サービスを稼働させておく必要があります。 |
|
||||
| OpenCode サービスの再起動 | プロセス内のスケジュールジョブが失われます。実行を希望する状態のスケジュールは、明示的に **再開** するまで **中断** になります。CodeNomad を開き直しても再作動しません。 |
|
||||
| 有効化したネイティブスケジュール | 永続化された OpenCode カレンダーがプロジェクトを起動し、実行ホストとサービスが利用できる間は CodeNomad UI とバックエンドを閉じても実行します。 |
|
||||
| OpenCode サービスの再起動 — 正常なネイティブスケジュール | 希望状態が稼働中の正常なスケジュールは、カレンダーを自動で再開します。保留中の作業は新たな受付の前に元の識別情報で照合されます。 |
|
||||
| 既存の日次スケジュール | サービスが動作する間、CodeNomad を閉じてもプラグインの時計は動きます。サービスの再起動で **中断** となり、明示的な **再開** で保留中の実行を照合して日次の時計を再び有効にします。 |
|
||||
|
||||
再起動後は保留中の実行を確認し、準備ができたら **再開** を選びます。再開は、コーディネーターへの入力を二重送信せずに既存の実行を照合します。中断したネイティブターンの継続を保証するものではありません。再起動で切れたターンは、**再起動により中断** として、レポートなしで終了する場合があります。ツールの副作用を再試行する手段として、再起動を使わないでください。
|
||||
ネイティブスケジュールでは、明示的な一時停止、停止、失敗した作業、不確かな結果による受付の障壁は再起動後も保持されます。保持された実行と元のリクエストを確認し、利用できる **実行を確認** または **保存済みリクエストを更新** を使用してから、状態が許す場合にだけ **再開** してください。ネイティブの続行は既存の識別情報で観測します。確認された失敗やレポートなしの終了には人による復旧が必要で、根拠が不十分なら実行は不確かなままです。開始済みのツールの影響を再試行するために、再起動を使わないでください。
|
||||
|
||||
[OpenCode の起動を維持](opencode.md#service-supervision) は、実行ホストの共有サービスを別途監視します。明示的な無効化を保存していなければ、ミッションの最初の再生で有効になります。一時停止中のミッションを作成しても、その選択を保持します。無効にすると OpenCode は動作を続けますが、自動再起動は行いません。Windows、macOS、WSL はログインの境界を使い、ネイティブ Linux は権限に応じてユーザーサービスの常駐を使います。スリープ、休止、WSL のアイドル時の寿命、外部ホストの制御により稼働状況は制限されます。監視状態はスケジュールと別に確認してください。
|
||||
|
||||
ミッションは、デスクトップアプリでも、スタンドアロンの CodeNomad [サーバー](server.md)でも動作します。パネルに **ミッションコントロールを利用できません** と表示された場合、そのメッセージに原因が示されます。
|
||||
|
||||
|
|
@ -184,7 +237,9 @@ CodeNomad の実行中は、ミッションのコーディネーターやタス
|
|||
|
||||
1 回限りの一時停止では、進行中の作業を終えてから止まります。コーディネーターとサブエージェントは現在のステップを完了し、新しい作業を始めずに停止します。その間、パネルには **一時停止中: 実行中のステップを完了しています。** と表示されます。**今すぐ中断** を使うと代わりにすぐ止まり、ミッションのネイティブファミリーを中断して調整を保留します。停止も中断を行い、キューにあるファミリーの入力/配信をキャンセルし、終端状態になります。中断には上限があり、**一部のサブエージェントがまだ実行中の可能性があります。** はいずれかが不完全であることを意味します。すべてのシェル/ツールの影響がキャンセルされたと想定せず、保持された結果を確認し、明示的な再試行を使用してください。再開時には一時停止または中断された内容がコーディネーターに通知され、作業の続け方はコーディネーターが判断します。
|
||||
|
||||
毎日の一時停止/停止は、スケジュールをキャンセルし、コーディネーターと独立した専門担当を含む登録済みルート担当の中断を要求しますが、**それらの会話や子孫が停止した証拠にはなりません**。ネイティブのファミリーを確認してください。停止は終端です。停止済みのスケジュールは、開始、再開、今すぐ実行を行えません。**実行を確認** は、再送せずに保留中の実行を照合します。一時的な定期確認の失敗は、最後の失敗を表示し自動再試行しますが、ユーザー制御の応答消失で制御を自動再送することはありません。
|
||||
ネイティブスケジュールの **一時停止** は今後の受付を閉じ、受け付け済み実行の中断を要求します。**再開** は既存の作業を照合してから再び有効にします。**停止…** は終端です。**今すぐ実行** はカレンダーを維持しながら、同じ保留防護と開始回数の上限を使います。**実行を確認** は元の実行を照合します。未確認の操作を保持し、**保存済みリクエストを更新** を使用してください。受動的な更新や再接続でもそのリクエストを保持します。開始済みのツールの影響は中断後も続く場合があります。
|
||||
|
||||
既存の日次項目は、日次の一時停止、停止、再開、今すぐ実行、実行を確認の操作を保持します。一時停止/停止は時計を解除し、独立した専門担当を含む登録済みルートの中断を要求します。ネイティブファミリーと中断の部分的な結果を確認してください。一時的な日次確認の失敗は、警告を表示して再試行します。ユーザー操作の応答が失われても元のリクエストに関連付けたままです。停止済みの項目は終端のままです。
|
||||
|
||||
レポートなしで待機している 1 回限りのコーディネーターは、主操作の ▶ で確認・復旧できる場合があります。タスクにも、対象条件を満たせば個別の不足レポート復旧があります。復旧は先にネイティブのアクティビティ、子孫、待機を確認し、アクティビティが不明なら受け付けを拒否します。割り当ての再実行や計画の置き換えはしません。会話を開いて、未解決の質問、権限、シェル、バックグラウンド作業を解消してください。
|
||||
|
||||
|
|
@ -192,12 +247,28 @@ CodeNomad の実行中は、ミッションのコーディネーターやタス
|
|||
|
||||
## 編集、削除、会話のクリーンアップ {#cleanup}
|
||||
|
||||
編集可能な 1 回限りのミッションで **選択したミッションを編集** を使用します。競合時は上書きせず、エディターを閉じて開き直し、現在のリビジョンを読み込みます。フォームの読み込み中に目的やメモが変わった場合は、入力せずに開いた後にミッションが変更されたことを表示します。現在の版を編集するには **開き直す** を選択します。毎日のスケジュールには編集/削除操作はありません。
|
||||
フォームの読み込み中に目的やメモが変わった場合は、入力せずに開いた後にミッションが変更されたことを表示します。現在の版を編集するには **開き直す** を選択します。
|
||||
|
||||
実行の合間には、一時停止中または稼働中のネイティブスケジュールで、実行や未解決の操作が対象を保持していなければ、タイトル、指示、メモ、要求プロファイル、タスクセッションのモード、ソース、カレンダー、上限を編集できます。一時停止中のスケジュールはそのまま、有効なスケジュールはカレンダーを照合してから続行します。保存時には過去の結果、消費済みソース、開始回数、最初の再生を起点とする基準を保持します。変更していない終了上限は絶対的な終了予定を保持します。
|
||||
|
||||
受け付け済みの実行中は、指示とメモだけが実行中のテキスト編集の契約に従います。変更は履歴と今後の実行用設定に保存されます。既存のタスク、依頼内容、プロファイル、実行の識別情報は受付時の値を保持します。構造に関わる項目は、一時停止後も含め、実行が確定するまでロックされます。プレイブックは完了・取り下げ済みを含むタスクが 1 件でも作成されるとロックされたままです。受付/操作が不確か、または整合性の根拠が不十分な場合、構造編集は読み取り専用です。作成と編集はカレンダーの検証を共有し、プレビューは編集した最後の許可予定を保存済みの次回起動に適用します。遅れた起動も対象です。
|
||||
|
||||
ローカルブランチの選択にはworktreeを持たないブランチも含まれ、ローカル参照だけを読み取ります。最大200ブランチに対応し、それを超える場合はソースが利用不可と表示されます。**開始**と**再開**は有効化が可能な場合に正確な設定パスを示して同意を求めます。ネイティブスケジュールが無効な間、**今すぐ実行**は理由を表示して無効になります。OpenCodeの設定で `experimental.schedules` を有効にしてから更新してください。有効化の応答が失われた場合、元のリクエストを保持して結果を照合します。
|
||||
|
||||
最初の Play 前なら、**オプション**の追跡中の会話を保持したまま、条件なしの即時起動に戻せます。Play は完全で安定したメッセージの連続部分を読み取り固定します(ソースごとに最大32件、起動入力全体で16,384文字以内)。ソースの欠落、移動、容量超過は説明とともに起動をブロックします。再試行でも固定されたコンテキストと元の起動 ID を維持します。
|
||||
|
||||
受領記録の読み取りで準備が部分的に完了していると確認されると、**再試行**が表示されます。クリックすると同じ記録を再確認し、ミッションを一時停止したまま元の保存済み変更だけを再開します。
|
||||
|
||||
新しく準備したミッションの初回起動前には、タイトル、指示、メモ、プレイブック、要求するエージェント・モデル・思考設定、タスクセッションのモード、**スケジュール**を編集できます。同じミッションを保ったまま、即時・遅延・繰り返し・条件付きの起動を切り替えられます。保存すると一時停止を維持したまま、新しい空のコーディネーターを準備し、元の会話を保持します。作業の開始やスケジュールの有効化は Play で行います。起動後の指示とメモは、実行中ミッションの編集規則に従います。保存が未確認の場合、**保存済みリクエストを更新**で元の結果を読み取ります。準備の一部が未完了で起動・テキスト編集・削除がブロックされている間は、同じリクエストを保持してください。
|
||||
|
||||
編集可能な単発ミッションまたはネイティブスケジュールで **選択したミッションを編集** を使用します。ロックされた項目の横の説明を読んでください。リビジョンが競合した場合は **現在の設定を再読み込み** を選び、**比較用の以前の下書き** を開いて保持された下書きを確認してから再度保存します。既存の日次スケジュールは専用ツールバーの操作を保持します。
|
||||
|
||||
ネイティブスケジュールの **選択したミッションを削除…** は、今後の実行を無効にしてから項目を削除します。会話と結果は保持され、照合待ちのクリーンアップは **技術詳細** に表示され続けます。未確認の削除を保持し、元のリクエストを更新してください。
|
||||
|
||||
1. 不要になった 1 回限りのミッションで、**選択したミッションを削除…** を選び、確認内容を読みます。
|
||||
2. 必要に応じて、確認前に **このミッション用に作成された専門担当の会話も削除する** を選びます。
|
||||
3. **会話のクリーンアップ** で削除/保持/保留の件数を確認します。再試行が提示される場合は、記録された残りのクリーンアップだけを再試行します。
|
||||
|
||||
ミッションの削除と停止は同じではありません。先に停止を使用して残った作業を確認してください。コーディネーターと再利用された会話は保持されます。共有、移動済み、子を持つ、所有関係を確認できない専門担当も保持される場合があります。部分的なクリーンアップは巻き戻されず、バックグラウンドで無断で完了されることもありません。概要リーダーには、完了したクリーンアップのうち最新の 20 件が残ります。ミッションを削除すると、プロジェクト内のその保存領域が解放されます。古い削除済みミッションは自動的に消去されます。
|
||||
ミッションの削除と停止は同じではありません。先に停止を使用して残った作業を確認してください。コーディネーターと再利用された会話は保持されます。共有、移動済み、子を持つ、所有関係を確認できない専門担当も保持される場合があります。部分的なクリーンアップは巻き戻されず、バックグラウンドで無断で完了されることもありません。会話のクリーンアップが保留 0 件でも、削除の確認が保留中のままミッションカードが消える場合があります。削除が確認されるまで、元の **再試行** 操作を使ってください。同じ削除要求とオプションが保持されます。概要リーダーには、完了したクリーンアップのうち最新の 20 件が残ります。ミッションを削除すると、プロジェクト内のその保存領域が解放されます。古い削除済みミッションは自動的に消去されます。
|
||||
|
||||
ネイティブの作業とサービス制御については、[権限](permissions.md)、[会話](conversations.md)、[OpenCode のセットアップ](opencode.md)を参照してください。
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ PATH の先頭に古い OpenCode がある場合、提示される操作はそ
|
|||
| 推奨 | OpenCode **2.2.5** | 検証済みの対象。技術的な最低要件とは別です。 |
|
||||
| 未検証 | 完全には検証していない、新しいまたはカスタムのバージョン | 未テストという理由だけで拒否せず、接続先サービスの実際の API 契約を確認します。 |
|
||||
|
||||
必要なセッション、権限、Form、受信箱、セッション環境の契約も利用可能である必要があります。実行ファイルのバージョンだけでは、接続先サービスの対応を証明できません。セットアップが実行中のサービスを未対応と報告した場合は、対応する CLI を選択/インストールし、準備ができたら明示的に共有サービスを再起動します。
|
||||
接続先サービスは、必要なセッション、権限、フォーム、受信箱、セッション環境の契約を提供する必要があります。ネイティブのミッションスケジュールは、スケジュール API、プラグイン/プロバイダーのバンドル、有効な `experimental.schedules` 設定も確認します。利用可否はこれらの使用する機能で決まり、互換性のある将来のバージョンにも適用されます。セットアップが実行中のサービスを未対応と報告した場合は、対応する CLI を選択/インストールし、準備ができたら明示的に共有サービスを再起動します。
|
||||
|
||||
## ホスト、WSL、共有サービスの範囲 {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ Windows で WSL の実行ファイルを選ぶと、選択したディストリ
|
|||
|
||||
グローバルなサービス登録、セッション、メッセージ、データベースは OpenCode が管理します。CodeNomad は、ウィンドウやプロジェクトごとに専用の OpenCode データベースを作りません。プロジェクトタブを閉じると CodeNomad 内でそのプロジェクトが閉じられますが、セッションは OpenCode で続き、プロジェクトを開き直すと再び表示されます。CodeNomad がネイティブの Location を解放することはありません。ウィンドウ全体を閉じる、終了する、再読み込みする場合は、復元のためにプロジェクト接続が保持されます。これらの操作も、バックエンドの終了も、共有デーモンを停止しません。[プロジェクト](projects.md)を参照してください。
|
||||
|
||||
## OpenCode の起動を維持 {#service-supervision}
|
||||
|
||||
最初の Mission の再生前に、**最初の Mission 再生時もオフを維持** で、監視を先にインストールせずに明示的なオフを保存できます。
|
||||
|
||||
**設定 → OpenCode** の **OpenCode の起動を維持** は、実行ホストに OS の監視をインストールし、そのサービスを使うすべての CodeNomad プロファイルで共有します。Missions とは独立して動作し、CodeNomad を閉じても有効です。事前に明示的に無効にしなければ、最初の Mission の再生で有効になります。一時停止状態の Mission の作成は選択を維持します。Mission の一時停止、停止、削除でもこの設定は維持されます。
|
||||
|
||||
無効にすると監視を削除し、OpenCode は動作を続けますが再起動されなくなります。継続的に停止するには、先に無効にしてから OpenCode を明示的に停止してください。監視が有効な場合、`opencode service stop` の後に再起動されます。
|
||||
|
||||
**稼働中 · 監視有効** には、OS 監視プロセスの稼働と OpenCode サービスの正常性が必要です。**インストール済み · サービス起動中**、**監視をインストールまたは削除中…**、**監視に失敗** は異なる状態です。サービスの応答だけでは監視は未確認のままです。詳細には、分かっている場合に原因、最終試行、監視プロセスの再試行、OS 復旧の安全確認が表示されます。**監視状態を確認** は読み取りのみ、**監視を再試行** は要求した設定の明示的な修復です。未確認の操作は元の要求を保持し、**元の操作を再試行** は同じ操作だけを再送します。設定リビジョンが変更された場合は上書きせず、確認のため更新します。
|
||||
|
||||
Windows、macOS、WSL はログイン後に起動します。WSL は選択したディストリビューションの localhost 転送が必要です。ネイティブ Linux の linger は、**有効なすべてのユーザーサービス** をログイン前に起動し、ログアウト後も動作させます。無効化時は CodeNomad が有効化し、他の監視が必要としない場合のみ linger を復元し、個人設定は維持します。ユーザーセッションが開かれていない状態で linger を復元すると監視から解放された OpenCode プロセスが停止するおそれがある場合、復元は保留され、**監視を再試行** とともに表示されます。ユーザーセッションが開かれるか、監視から解放された OpenCode プロセスが終了し、安全に復元できる状態になってから再試行してください。Linux が権限を求める場合は実行ホストで許可し、再試行してください。要求した設定は保存されたままです。スリープと休止状態は OS が制御します。利用できない、または未対応のホストでは、説明付きで操作を無効にします。
|
||||
|
||||
## 更新、再起動、再読み込み {#maintenance}
|
||||
|
||||
1. **設定 → OpenCode** で、**状態と更新を確認** を選びます。
|
||||
|
|
@ -41,7 +53,7 @@ Windows で WSL の実行ファイルを選ぶと、選択したディストリ
|
|||
| 操作 | 範囲と効果 | 安全な復旧 |
|
||||
| --- | --- | --- |
|
||||
| CLI の更新 | 対応するインストール済み実行ファイルを変更します。実行中のデーモンは暗黙に再起動しません。 | 失敗後はインストール済みと稼働中の両バージョンを確認し、エラー解決後にのみ明示的に再試行します。 |
|
||||
| サービスの再起動 | 共有プロセスを置き換え、接続中の全 OpenCode クライアントの作業を中断します。セッション履歴は残ります。 | 再接続し、会話とスケジュール済みミッションを確認します。再起動は自動続行ではありません。 |
|
||||
| サービスの再起動 | 共有プロセスを置き換え、接続中の全 OpenCode クライアントの作業を中断します。セッション履歴は残ります。 | 正常に稼働中のネイティブミッションスケジュールは自動復旧します。保留中の実行と、一時停止/エラー/不確かな状態を確認してください。既存の日次項目には再開が必要です。単発の会話と復旧操作は別に確認してください。[ミッション](missions.md#lifetime)を参照してください。 |
|
||||
| OpenCode の設定を再読み込み | プロセスは維持しますが、全クライアントの **読み込み済みのすべての Location** を再構築します。保留中の権限/Form をキャンセルし、ターミナル/バックグラウンドコマンドを閉じます。進行中の作業が中断される場合があります。 | 再読み込み後にリソースと保留中の作業を再確認します。応答が失われたという理由だけで、プロンプトや変更操作を繰り返さないでください。 |
|
||||
|
||||
保存した設定やインストール済みプラグインがまだ利用できず、中断を許容できる場合に、明示的に **OpenCode の設定を再読み込み**(実行中のサービスが対応していれば **トラブルシューティング** の下にあります)を使用します。無害な表示更新ではありません。CodeNomad は、古い選択済み CLI を使って、新しい実行中デーモンをダウングレードしません。
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
### あなたへの質問 {#questions}
|
||||
|
||||
あなたに質問するのはコーディネーターだけです。タスクとその補助エージェントは、自分の質問を起動元の会話へ返します。選択肢、根拠、影響、可逆性、推奨を添え、回答に依存しない作業は続けます。コーディネーターはまず自分で決められることを決め、残った質問を **対応が必要です** の 1 つのフォームにまとめます。何を質問するか、判断がどう表示されるかは [質問と判断](missions.md#questions) で説明しています。このルールは、1 回限りか毎日かを問わずすべてのコーディネーターと、すべてのタスク割り当てに指示として含まれます。探索では [後述](#wayfinder-checks) の判断の確認が加わります。
|
||||
あなたに質問するのはコーディネーターだけです。タスクとその補助エージェントは、自分の質問を起動元の会話へ返します。選択肢、根拠、影響、可逆性、推奨を添え、回答に依存しない作業は続けます。コーディネーターはまず自分で決められることを決め、残った質問を **対応が必要です** の 1 つのフォームにまとめます。何を質問するか、判断がどう表示されるかは [質問と判断](missions.md#questions) で説明しています。このルールは、1 回限りかスケジュール実行かを問わずすべてのコーディネーターと、すべてのタスク割り当てに指示として含まれます。探索では [後述](#wayfinder-checks) の判断の確認が加わります。
|
||||
|
||||
回答は割り込みドック(**対応が必要です** の **回答する**)で行ってください。それだけがあなたの回答として扱われます。チャットに入力した返事は通常のメッセージです。
|
||||
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
|
||||
コーディネーターには、最初の計画の後、タスクを確定するたび、あなたの回答を待ち始めるたび、そして最終レポートの前に状況報告を公開するよう指示されています。[状況報告と更新の依頼](missions.md#briefings) を参照してください。状況報告、タスクの要約、最終レポートはあなた向けに書かれます。あなたの言語で、短い文をいくつか、結果を先に書き、識別子やコマンドは根拠に残します。
|
||||
|
||||
最終レポートには、届けたもの、残っていること、あなたの判断が必要なことが書かれます。1 回限りのミッションは完了としてのみ終わります。あなたなしでは何も進められないとき、コーディネーターは失敗にせずにあなたに質問して待ちます。ミッションを停止できるのはあなただけです。毎日の実行は失敗として終わることもあります。
|
||||
最終レポートには、届けたもの、残っていること、あなたの判断が必要なことが書かれます。1 回限りのミッションは完了としてのみ終わります。あなたなしでは何も進められないとき、コーディネーターは失敗にせずにあなたに質問して待ちます。ミッションを停止できるのはあなただけです。スケジュールされた実行は失敗として終わることもあります。
|
||||
|
||||
### すべてのミッションで CodeNomad が確認すること {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
- 探索の 5 つの役割だけが受け付けられます。
|
||||
- 判断タスクは、コーディネーター自身の判断かあなたの回答でのみ完了します。コーディネーターの判断には質問、判断、理由が記され、あなたの回答として記録されることはありません。
|
||||
- あなたの回答は、CodeNomad のインターフェースで行った場合にのみ有効です。CodeNomad は質問のタグによって回答をタスクに結び付けます。質問がコーディネーターの会話から出されたこと、その本文がそのタスクのタグで終わり、他の判断を示していないこと、タスクの宣言後に回答されたこと、他の判断をまだ確定していないことが条件です。まとめたフォームの各質問は個別に結び付けられ、2 つの判断を示す質問はどちらも確定しません。判断タスク自身の会話から出された質問への回答も受け付けられます。
|
||||
- タスク自身のレポートで誰が決めたかを主張することはできません。ミッションまたは毎日の実行が成功として終了する前に、CodeNomad は完了したすべての判断をタグを含めて再確認します。
|
||||
- タスク自身のレポートで誰が決めたかを主張することはできません。ミッションまたはスケジュールされた実行が成功として終了する前に、CodeNomad は完了したすべての判断をタグを含めて再確認します。
|
||||
- これらの確認は **ネイティブのサブエージェント** の場合に適用されます。**独立したセッション** では、意思決定担当が自分で判断資料を報告し、コーディネーターが各結果を誰が決めたかとともにメモに記録します。CodeNomad はこれを検証しません。
|
||||
|
||||
指示にとどまるもの: どの選択を元に戻しやすいとみなすか、判断資料と調査の質、計画にとどまること。
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode ले तपाईंलाई इन्जिन दिन्छ। C
|
|||
|
||||
## लामो काम मिसनलाई सुम्पनुहोस् {#missions}
|
||||
|
||||
काम एकपटक वर्णन गर्नुहोस्। मिसनको coordinator ले यसलाई task tree का रूपमा योजना गर्छ, सीमित कार्यहरू नेटिभ OpenCode कुराकानीलाई प्रत्यायोजन गर्छ र नतिजा तिनका प्रमाणसहित रिपोर्ट गर्छ। मिसन एकपटक वा हरेक दिन चलाउनुहोस्; सिर्जना गर्दा तपाईंले नरोजेसम्म काम कहिल्यै सुरु हुँदैन। प्लेबुकले debugging, product delivery र अन्ध मूल्याङ्कनकर्तासहित दुई मोडेलको तुलना समेट्छन्। [मिसन](missions.md) हेर्नुहोस्।
|
||||
काम एकपटक वर्णन गर्नुहोस्। मिसनको coordinator ले यसलाई task tree का रूपमा योजना गर्छ, सीमित कार्यहरू नेटिभ OpenCode कुराकानीलाई प्रत्यायोजन गर्छ र नतिजा तिनका प्रमाणसहित रिपोर्ट गर्छ। एकपटक, छानिएका समय वा अन्तरालमा, वा परियोजनाको प्रमाणित परिवर्तनपछि चलाउनुहोस्; सिर्जनाले तपाईंको स्पष्ट सुरुवातका लागि विराममा रहेको मिसन सुरक्षित गर्छ। प्लेबुकले debugging, product delivery र अन्ध मूल्याङ्कनकर्तासहित दुई मोडेलको तुलना समेट्छन्। [मिसन](missions.md) हेर्नुहोस्।
|
||||
|
||||
## फोनबाट काम जारी राख्नुहोस् {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ npx @neuralnomads/codenomad --generate-token --launch
|
|||
| checkout र कुराकानीबीच काम गर्ने | [परियोजना](projects.md), [कुराकानी](conversations.md), [फाइल](files.md) |
|
||||
| इतिहास खोज्ने वा सुरक्षित सफाइ गर्ने | [इतिहास](history.md) |
|
||||
| नेटिभ प्रश्न र अनुमति उत्तर दिने | [अनुमति](permissions.md) |
|
||||
| सीमित वा दैनिक काम समन्वय गर्ने | [मिसन](missions.md) |
|
||||
| सीमित वा निर्धारित काम समन्वय गर्ने | [मिसन](missions.md) |
|
||||
| कार्यान्वयन र एकीकरण कन्फिगर गर्ने | [OpenCode](opencode.md), [प्रदायक](providers.md), [एकीकरण](integrations.md) |
|
||||
| अर्को उपकरण जोड्ने | [Remote Control](remote-control.md), [सुरक्षा](security.md), [मोबाइल](mobile.md) |
|
||||
| कार्यस्थान अनुकूल बनाउने | [सेटिङ](settings.md), [सर्टकट](shortcuts.md), [आवाज](voice.md), [विस्तार](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
1. दायाँ प्यानलको **मिसनहरू** खोली **मिसन सिर्जना गर्नुहोस्** रोज्नुहोस्। तपाईंले सिर्जना वा रद्द नगरेसम्म फारमले मिसन सूची र चयन गरिएको मिसनका कार्यहरूको ठाउँ लिन्छ।
|
||||
2. **मिसनले के गर्नुपर्छ?** भर्नुहोस् र पहिलो वाक्यबाट पूर्वभरिएको **शीर्षक** (60 character सम्म) जाँच्नुहोस्।
|
||||
3. **कहिले** मा **एक पटक** वा **हरेक दिन** रोज्नुहोस्। दैनिक काममा देखाइएको time zone जाँच्नुहोस्; **परिवर्तन** ले IANA zone दिन्छ।
|
||||
3. **एक पटक, तुरुन्तै** र **सर्तबिना** का लागि **समयतालिका** खुम्च्याएकै राख्नुहोस्, वा आवृत्ति, सर्त र ऐच्छिक सीमा छान्न विस्तार गर्नुहोस्। निर्धारित काम सिर्जना गर्नुअघि **बचत गरिएको समय क्षेत्र (IANA)** र पूर्वावलोकन जाँच्नुहोस्।
|
||||
4. **विकल्पहरू** विस्तार गरी **प्लेबुक**, **टिप्पणीहरू**, **एजेन्ट र मोडेलहरू**, **कार्य सत्रहरू**, project को **उप-सत्र नेस्टिङ गहिराइ**, सुरक्षित brief र ऐच्छिक followed conversation हेर्नुहोस्। **दुई मोडेल तुलना गर्नुहोस्** मा **मोडेल A** र **मोडेल B** selector मुख्य फारममै देखिन्छन्।
|
||||
5. **बनाउनुहोस्** रोज्नुहोस्। नयाँ प्रविष्टि चयन गरी तयार हुँदा छुट्टै ▶ प्रयोग गर्नुहोस्: एकपटकको काममा **मिसन सुरु गर्नुहोस्**, वा दैनिक schedule मा **तालिका {id} सुरु गर्नुहोस्**।
|
||||
5. **बनाउनुहोस्** रोज्नुहोस्। विराममा रहेको प्रविष्टि छानेर समीक्षा गर्नुहोस्, अनि तयार हुँदा ▶ **सुरु गर्नुहोस्** प्रयोग गर्नुहोस्। तत्काल सर्तरहित कामले एउटा मिसन सुरु गर्छ; ढिलो, दोहोरिने वा सर्तयुक्त नीतिले नेटिभ समयतालिका सक्रिय गर्छ।
|
||||
|
||||
**सिर्जनाले काम कहिल्यै सुरु गर्दैन।** एकपटकको मिसन तयार हुन्छ; दैनिक schedule paused हुन्छ। सिर्जना वा brief बचतले timer सक्रिय गर्दैन। सिर्जना चयनित project/Location मा सीमित हुन्छ र requested profile तथा task-session नीति स्थिर गर्छ। validation प्रभावित field छेउ हुन्छ; केही पठाइएन भन्ने अस्वीकृतिले सुधारका लागि मस्यौदा राख्छ। एकपटकको मिसनको coordinator ले तपाईंका लागि follow-up जस्तो छुट्टै मिसन पनि तयार गर्न सक्छ: त्यो सूचीमा **तयार** का रूपमा देखिन्छ र, अरू मिसनझैं, आफ्नो ▶ कार्यबाट मात्र सुरु हुन्छ।
|
||||
**सिर्जनाले विराममा रहेको मिसन बचत गर्छ।** चलाउने कार्यले मात्र काम र मूल पात्रो सक्रिय गर्छ; सिर्जना वा संक्षिप्त विवरण सुरक्षित गर्दा साझा सेवा निगरानीको प्राथमिकता कायम रहन्छ। सिर्जना चयनित परियोजना/Location सँग सम्बन्धित हुन्छ र अनुरोध गरिएका प्रोफाइल तथा कार्य-सत्र नीति अभिलेख गर्छ। प्रमाणीकरण सम्बन्धित क्षेत्रछेउ देखिन्छ; केही पठाइएन भन्ने अस्वीकृतिले सुधारका लागि मस्यौदा राख्छ। एकपटकको समन्वयकर्ताले छुट्टै अनुवर्ती मिसन पनि तयार गर्न सक्छ: त्यो **तयार** का रूपमा देखिन्छ र आफ्नै ▶ कार्यबाट सुरु हुन्छ।
|
||||
|
||||
सुरक्षित गरिएका एकपटकका मिशनहरूले कार्यान्वयन नियन्त्रण, कार्य वृक्ष, कुराकानी र अन्तिम प्रतिवेदन कायम राख्छन्। विद्यमान दैनिक तालिकाहरू दैनिक नियन्त्रण र [दैनिक समयका नियम](#existing-daily-schedules) सहित सूचीमा रहन्छन्। प्रविष्टि छानेर त्यस वस्तुमा उपलब्ध नियन्त्रण प्रयोग गर्नुहोस्।
|
||||
|
||||
### प्लेबुक {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@ mission map मिसनको साझा, टिकाउ स्मृति
|
|||
|
||||
पछिको task ले दुई तरिकाले अघिल्लो कामलाई आधार बनाउँछ। dependency ले कामको क्रम तय गर्छ: coordinator ले कुनै task त्यो निर्भर रहेका task पूरा भएपछि सुरु गर्छ, र सान्दर्भिक नतिजा brief मा वा assignment को साथमा राख्छ; independent session ले ती report map मा पनि पढ्न सक्छन्। सकिएको task को आफ्नै कुराकानी जारी राख्न, coordinator ले त्यसलाई पुनःप्रयोग गर्ने follow-up task घोषणा गर्छ: follow-up त्यही कुराकानीको हालको context सहित त्यहीँ जारी रहन्छ, जुन OpenCode ले [संक्षिप्त](history.md#compact-session) गरेको हुन सक्छ, र पहिलेका विवरण खोजका लागि यसको सुरक्षित इतिहासमा रहन्छन्। Debugging playbook ले यसरी दुवै review implementer लाई फर्काउँछ। **स्वतन्त्र सत्रहरू** मा coordinator ले task लाई विद्यमान मुख्य कुराकानीमा पनि तोक्न सक्छ, जसले असम्बन्धित अघिल्लो कामसहित आफ्नै context राख्छ। अन्यथा task आफ्नो assignment बाट नयाँ कुराकानीमा सुरु हुन्छ, र अर्को task को transcript यसलाई coordinator वा खोजमार्फत पुग्छ। **दुई मोडेल तुलना गर्नुहोस्** मा मूल्याङ्कनकर्ताको assignment र context ले objective र टिप्पणी छोड्छन्; यसको brief मा anonymized output हुन्छन्।
|
||||
|
||||
दैनिक schedule मा हरेक run आफ्नै coordinator र map सँग सुरु हुन्छ। यसको पहिलो सन्देशमा schedule का निर्देशन र अन्तिम completed run पछि यसका [पछ्याइएका कुराकानीहरू](#daily-schedules) मा थपिएका नयाँ user र assistant पाठ हुन्छन्; अघिल्ला run का नतिजा schedule history मा रहन्छन्। run भित्र सुरु गरिएका कुराकानीले त्यो run को map पढ्न सक्छन्। नयाँ एकपटकको मिसन पनि नयाँ map सँग सुरु हुन्छ: नतिजा अघि सार्न तिनलाई यसको objective वा **टिप्पणीहरू** मा लेख्नुहोस्, वा coordinator लाई follow-up मिसन तयार गर्न दिनुहोस्, जसको objective र टिप्पणी उसैले लेख्छ।
|
||||
हरेक निर्धारित पालो आफ्नै समन्वयकर्ता र मिसन नक्सासहित सुरु हुन्छ र सुरक्षित निर्देशन तथा स्थिर गरिएका इनपुट प्रयोग गर्छ। पछ्याइएका कुराकानीले प्रयोगकर्ता र सहायकको सीमित नयाँ पाठ दिन्छन्; सफल पालोपछि तिनका खपत सूचक अघि बढ्छन्। अघिल्ला पालाहरू तालिकाको इतिहासमा रहन्छन्। पालोभित्र सुरु गरिएका कुराकानीले त्यसको नक्सा पढ्न सक्छन्। नयाँ एकपटकको मिसनको छुट्टै नक्सा हुन्छ: अघि लैजानुपर्ने नतिजा त्यसको उद्देश्य वा **टिप्पणीहरू** मा राख्नुहोस्, वा त्यो सन्दर्भसहित अनुवर्ती मिसन तयार गर्न समन्वयकर्तालाई भन्नुहोस्।
|
||||
|
||||
### अघिल्ला कुराकानी खोज्नुहोस् {#context-search}
|
||||
|
||||
CodeNomad चलिरहँदा, project का हरेक agent कुराकानी, मिसन coordinator र task समेत, सँग CodeNomad को session search tool (`codenomad.session_search`, OpenCode को Code Mode मार्फत प्रयोग हुने) हुन्छ। यसले कुराकानीको project folder का सुरक्षित कुराकानीहरू **पूरै कार्यस्थान** [खोज](history.md#full-history-search) जस्तै ranking सहित खोज्छ, र हरेक कुराकानीको शीर्षक र identifier सहित छोटा अंश, साथै पूरै कुराकानी पढ्ने तरिका फर्काउँछ। native tool permission लागू हुन्छन्। agent ले आफैं निर्णय गर्दा वा objective, टिप्पणी वा तपाईंको सन्देशले भन्दा खोज्छ, उदाहरणका लागि अघिल्लो मिसन वा task ले के निष्कर्ष निकाल्यो पत्ता लगाउन। CodeNomad बन्द हुँदा चल्ने दैनिक run यो tool बिना काम गर्छ।
|
||||
CodeNomad चलिरहँदा, project का हरेक agent कुराकानी, मिसन coordinator र task समेत, सँग CodeNomad को session search tool (`codenomad.session_search`, OpenCode को Code Mode मार्फत प्रयोग हुने) हुन्छ। यसले कुराकानीको project folder का सुरक्षित कुराकानीहरू **पूरै कार्यस्थान** [खोज](history.md#full-history-search) जस्तै ranking सहित खोज्छ, र हरेक कुराकानीको शीर्षक र identifier सहित छोटा अंश, साथै पूरै कुराकानी पढ्ने तरिका फर्काउँछ। native tool permission लागू हुन्छन्। agent ले आफैं निर्णय गर्दा वा objective, टिप्पणी वा तपाईंको सन्देशले भन्दा खोज्छ, उदाहरणका लागि अघिल्लो मिसन वा task ले के निष्कर्ष निकाल्यो पत्ता लगाउन। CodeNomad बन्द हुँदा चल्ने निर्धारित पालो यो उपकरणबिना काम गर्छ।
|
||||
|
||||
## Profile, प्रत्यायोजन र depth {#profiles}
|
||||
|
||||
गहिराइ सुरक्षित गर्दा परियोजनाको कन्फिगरेसनमा सक्रिय नेटिभ समयतालिका कायम रहन्छन्। परियोजनामा स्पष्ट `schedules:false` भए बन्द नै रहन्छ; गहिराइ हटाउँदा समयतालिका सेटिङ कायम रहन्छ। नेटिभ सेटिङ सुरक्षित गर्नुअघि एकै तहका JSON र JSONC फाइलको द्वन्द्व समाधान गर्नुपर्छ।
|
||||
|
||||
1. Missions gear वा **सेटिङहरू → मिसनहरू** बाट **नयाँ मिसनका पूर्वनिर्धारित मान** सम्पादन गर्नुहोस्।
|
||||
2. **लागू हुने क्षेत्र** मा **सबै मिसनहरू** वा एउटा playbook रोज्नुहोस्, त्यसपछि **कार्य सत्रहरू** र **एजेन्ट र मोडेलहरू** अन्तर्गत हरेक भूमिकाका लागि एउटा row सेट गर्नुहोस् (coordinator, त्यसपछि playbook का भूमिका)। हरेक चयन तुरुन्तै सुरक्षित हुन्छ; Save बटन छैन।
|
||||
3. playbook दायरामा row ले **विश्वव्यापी** चिन्ह लागेका global मान देखाउँछन्। कुनै field बदल्दा ती मानबाट सुरु गरी त्यो भूमिका अनुकूलित हुन्छ; यसको revert icon ले फेरि global मान प्रयोग गर्छ। **रिसेट** (पुष्टिपछि) ले देखाइएको दायराका agent र model मात्र खाली गर्छ।
|
||||
|
|
@ -118,7 +122,7 @@ CodeNomad चलिरहँदा, project का हरेक agent कुर
|
|||
| सेटिङ | दायरा र असर |
|
||||
| --- | --- |
|
||||
| नयाँ मिसन default | **नयाँ** मिसनका लागि मात्र configuration-भरिका default; playbook exception ले सामान्य default override गर्छ। |
|
||||
| Agent / Model / Thinking | सिर्जनासँग अभिलेख गरिएका requested coordinator/role चयन; पछिको काम मिलाउन busy actor चुपचाप switch हुँदैन। daily run ले frozen requested profile पुनःप्रयोग गर्छन्। |
|
||||
| Agent / Model / Thinking | सिर्जनासँग अभिलेख गरिएका requested coordinator/role चयन; पछिको काम मिलाउन busy actor चुपचाप switch हुँदैन। निर्धारित पालाहरूले सुरक्षित अनुरोध गरिएका प्रोफाइल प्रयोग गर्छन्; सम्पादन भावी पालाहरूमा लागू हुन्छ। |
|
||||
| Native subagent | task session ले coordinator अन्तर्गत native subagent/all agent प्रयोग गर्छन्। |
|
||||
| Independent session | घोषित task session ले primary/all agent र स्वतन्त्र कुराकानी प्रयोग गर्छन्; internal native helper अक्षम हुँदैनन्। |
|
||||
| उप-सत्र नेस्टिङ गहिराइ | त्यहाँका **सबै** OpenCode कुराकानीको project कन्फिगरेसन, Missions मात्र होइन। खालीले inherit गर्छ: **उत्तराधिकार** ले project override र त्यसले खाली छोड्ने वा global **छविहरू एक पटक मात्र भण्डारण गर्नुहोस्** मान मात्र बाँकी रहने `experimental` block हटाउँछ। OpenCode ले project को `experimental` block लाई समग्र रूपमा पढ्छ, त्यसैले त्यहाँ राखिएका अन्य experimental सेटिङहरूले global गहिराइलाई अझै लुकाउँछन्। OpenCode को built-in **General** उप-एजेन्टले पूर्वनिर्धारित रूपमा उप-एजेन्ट सुरु गर्न सक्दैन, त्यसैले यसका कुराकानी कहिल्यै नेस्ट हुँदैनन्: जुनसुकै परियोजना गहिराइ सुरक्षित गर्दा पनि उप-एजेन्टलाई त्यो गहिराइसम्म आफ्नै उप-एजेन्ट सुरु गर्न दिने र अन्य कन्फिगरेसन फाइलका General प्रतिबन्धहरूभन्दा प्राथमिकता पाउने नियम उही फाइलमा थपिन्छ (गहिराइ १ मा OpenCode को गहिराइ सीमाले अझै नेस्टिङ रोक्छ), र **उत्तराधिकार** ले यसलाई हटाउँछ। **OpenCode को प्रभावकारी गहिराइ** मुनि, General उप-एजेन्ट नेस्ट हुन सक्छन् कि सक्दैनन् भनेर सम्पादकले जनाउँछ। यदि परियोजनाले General लाई पुरानो ढाँचामा एकल `permission` मानसहित, वा `task` र `subagent` दुवै पहिले नै सेट गरेर परिभाषित गर्छ भने, गहिराइ मात्र सुरक्षित हुन्छ र सम्पादकले त्यो जनाउँछ: त्यो फाइलमा General का लागि उप-एजेन्टलाई आफैँ अनुमति दिनुहोस्। सुरक्षित गर्दा OpenCode reload वा चलिरहेका कुराकानी बदलिँदैनन्। |
|
||||
|
|
@ -137,9 +141,51 @@ CodeNomad चलिरहँदा, project का हरेक agent कुर
|
|||
|
||||
brief ले निर्देशन र requested चयन राख्छ, task योजना, नतिजा, कुराकानी, schedule clock वा execution authority होइन। लोड गर्दा सामान्य creation draft भरिन्छ; केही start वा schedule हुँदैन। brief हटाउँदा विद्यमान मिसन/कुराकानी रहन्छन्। **सेटिङहरू → मिसनहरू → सुरक्षित संक्षिप्त विवरणहरू** ले library व्यवस्थापन गर्छ; library पढ्न नसकिने भए **सुरक्षित सङ्ग्रह रिसेट गर्नुहोस्** (पुष्टिपछि) ले सुरक्षित brief हटाउँछ, execution होइन। schedule मा save-as-brief कार्य हुँदैन।
|
||||
|
||||
## दैनिक समय, catch-up र followed conversation {#daily-schedules}
|
||||
## समयतालिका, छुटेको पालो र पछ्याइएका कुराकानी {#daily-schedules}
|
||||
|
||||
daily schedule ले सुरक्षित IANA time zone र civil time प्रयोग गर्छन्, बितेको 24-घण्टा interval होइन। समय schedule कै zone मा देखिन्छ, viewer भन्दा फरक हुँदा label सहित। हरेक run को आफ्नै coordinator र isolated mission map हुन्छ; चयनित schedule ले current task tree र date/outcome history देखाउँछ। केन्द्रीय reader मा पढ्न पुरानो run चयन गर्नुहोस्। इतिहास 30 result मा सीमित छ।
|
||||
### नेटिभ समयतालिका {#native-scheduling}
|
||||
|
||||
लेबलछेउका **i** चिह्नले विकल्प बुझाउँछन्: माउस राख्नुहोस्, किबोर्डबाट फोकस गर्नुहोस् वा छुनुहोस्।
|
||||
|
||||
सिर्जना वा सम्पादनमा **समयतालिका** विस्तार गर्नुहोस्। **आवृत्ति** मा यी विकल्प छन्:
|
||||
|
||||
| विकल्प | समय |
|
||||
| --- | --- |
|
||||
| एक पटक, तुरुन्तै | चलाउँदा एकपटक सुरु हुन्छ; ऐच्छिक सर्त तल वर्णन गरेअनुसार जाँचिन्छ। |
|
||||
| एक पटक, तोकिएको मितिमा | IANA क्षेत्रमा सुरक्षित एउटा मिति र समय; चलाउँदासम्म बितिसकेको मिति अस्वीकार हुन्छ। |
|
||||
| एक पटक, केही समयपछि | मिनेट वा घण्टामा धनात्मक पूर्ण अवधि, जुन चलाउँदा सुरु हुन्छ। |
|
||||
| निश्चित अन्तरालमा | कम्तीमा १५ मिनेट; पहिलो जागरण चलाएको एक अन्तरालपछि हुन्छ। |
|
||||
| हरेक दिन | सुरक्षित क्षेत्रको एउटा स्थानीय घडी समय। |
|
||||
| छानिएका बारहरूमा | चिनो लगाइएका बारमा एउटा समय। |
|
||||
| दिनमा धेरै पटक | 09:15 र 17:45 जस्ता बढीमा आठ स्वतन्त्र समय; दोहोरिएका समय एउटै पालोमा जोडिन्छन्। |
|
||||
| हरेक महिनाको तोकिएको दिन | छानिएको समयमा १–३१ मध्ये दिन; त्यो दिन नभएका महिना छोडिन्छन्। |
|
||||
| उन्नत (cron) | नेटिभ cron अभिव्यक्ति, बढीमा मिनेटमा एकपटक। |
|
||||
|
||||
**आगामी समयहरू (बढीमा पाँच)** ले सुरक्षित क्षेत्र र चलाउने अनुमानित समयको पूर्वावलोकन दिन्छ। सर्त भए **आगामी जाँचहरू (बढीमा पाँच)** ले जाँचहरूको पूर्वावलोकन दिन्छ; पालो चल्न प्रमाणित परिवर्तन चाहिन्छ। नबदलिएको सुरक्षित अन्तरालले आफ्नो मूल समयचक्र कायम राख्छ। विराममा रहेको वा अज्ञात समयचक्र मूल अवस्था पढ्न नसक्दासम्म अनुपलब्ध देखिन्छ। ढिलाइ र अन्तरालले बितेको अवधि प्रयोग गर्छन्; स्थानीय पात्रोले सुरक्षित क्षेत्र र OpenCode का डेलाइट सेभिङ नियम मान्छन्। देखाइएका मिति सेवा र होस्टको उपलब्धतामा निर्भर अनुमान हुन्।
|
||||
|
||||
अवरोधपछि नेटिभ समयतालिकाले भावी समयमा फर्कनुअघि बढीमा एउटा छुटेको पालो चलाउँछ। यसले अभिलेखित मूल तोकिएको समय प्रयोग गर्छ, जुन पछि छुटेका अन्य समयभन्दा अगाडिको हुन सक्छ। दैनिक धेरै समयले यही छुटेको पालो र एउटै दोहोरो सञ्चालन रोक्ने सुरक्षा साझा गर्छन्। अर्को पालो सुरु हुनुअघि अनिर्णीत पालोलाई यसको मूल कुराकानी र सन्देश पहिचानबाट मिलाइन्छ। स्पष्ट रूपमा विराममा राखिएको पुनरावृत्ति पुनः सुरु गर्दा अर्को भावी समय प्रयोग हुन्छ; विराममा रहेको एकपटकको पालो ढिलो गरी एकपटक आउन सक्छ।
|
||||
|
||||
### सर्त र स्रोत {#scheduling-conditions}
|
||||
|
||||
एउटा ऐच्छिक **सर्त** र त्यसको **यस परियोजनाको स्रोत** छान्नुहोस्: **अर्को समयतालिका सफल हुँदा**, **स्थानीय शाखामा नयाँ कमिटहरू**, **एकपटकको मिसन सफलतापूर्वक सकिँदा**, **पछ्याइएको कुराकानीमा नयाँ सन्देश** वा **तोकिएका फाइलहरू बदलिँदा**। सर्त मात्र भए **एक पटक, तुरुन्तै** राख्नुहोस्: एउटा पालो सुरु नभएसम्म नेटिभ जाँच हरेक १५ मिनेटमा चल्छ। पात्रोसहित भए सर्त त्यस पात्रोका समयमा जाँचिन्छ। दोहोरिने सर्तले नयाँ योग्य परिवर्तनपछि फेरि सुरु गर्न सक्छ।
|
||||
|
||||
पहिलो चलाइले प्रारम्भिक सन्दर्भ स्थापित गर्छ। पुनः सुरु गर्दा विरामपछि देखिएका परिवर्तनसमेत त्यसैमा सुरक्षित रहन्छन्। सफलता स्रोतलाई प्रमाणित सफल नतिजा र टुङ्गिएको मूल परिवार चाहिन्छ। शाखा जाँचले बाहिरबाट नल्याई स्थानीय कमिट र तिनको वंश सम्बन्ध प्रयोग गर्छ। कुराकानी जाँचले पूरा र स्थिर नयाँ सन्देश पर्खन्छ र प्राविधिक जागरण तथा पालोका आफ्नै कुराकानी छोड्छ। फाइलले परियोजनासापेक्ष बढीमा १२८ तोकिएका पथ, हरेक पङ्क्तिमा एउटा, प्रयोग गर्छन्; पुनरावर्ती ढाँचा र लिङ्क गरिएका पथ अस्वीकार हुन्छन्। हराएको, सारिएको, अस्पष्ट वा क्षमताभन्दा ठूलो स्रोतले कारणसहित प्रवेश रोक्छ। दुई जाँचबीचका परिवर्तन पत्ता लगाउन स्रोतमा टिकाउ चिनो चाहिन्छ।
|
||||
|
||||
सँगै देखिएका परिवर्तन समूहबद्ध हुन्छन्। पालोले अवलोकित स्रोत प्रमाण स्थिर गर्छ; सफलतापूर्वक टुङ्गिँदा त्यही अवस्थाको प्रतिलिपि खपत हुन्छ र पछिका परिवर्तन भावी जाँचका लागि बाँकी रहन्छन्। असफल पालोले स्रोतको जाँचबिन्दु कायम राख्छ र हस्तक्षेप माग्छ। **विकल्पहरू** अन्तर्गत **पछ्याइएका कुराकानीहरू (ऐच्छिक)** ले कुराकानी-सन्देश सर्त छान्ने कार्यभन्दा छुट्टै सन्दर्भ दिन्छ।
|
||||
|
||||
### अन्त्य र गणना {#scheduling-limits}
|
||||
|
||||
निरन्तर पुनरावृत्तिका लागि सीमा खाली राख्नुहोस्। **अन्तिम अनुमति प्राप्त समय** र **पहिलो चलाइदेखि यति घण्टापछिको अन्तिम समय (ऐच्छिक)** ले योग्य समय सीमित गर्छन्; पहिले आउने सीमा लागू हुन्छ। अवधि पहिलो चलाइमा सुरु हुन्छ र विराममा पनि चलिरहन्छ। अन्त्यअघि अभिलेख भएको समय लामो अवरोधपछि ढिलो आउन सक्छ। पहिले नै स्वीकार गरिएको पालोको नतिजासम्म अवलोकन गर्न सकिन्छ।
|
||||
|
||||
**सुरु गरिएका कार्यहरूको अधिकतम संख्या** सबै समय र **अहिले चलाउनुहोस्** ले साझा गर्ने कुल हो। समन्वयकर्ताको पहिलो इनपुट स्वीकार हुँदा पालो एकपटक गनिन्छ, पछि असफल भएको पालोसमेत। जाँच, नबदलिएका सर्त र अस्वीकार भएका जागरणले स्थान खर्च गर्दैनन्; अनिश्चित स्वीकृतिले मिलान नभएसम्म आफ्नो स्थान राख्छ। एकपटकको नीतिले बढीमा एउटा पालो अनुमति दिन्छ। खपत स्थान सम्पादन र पुनःसुरुवातपछि पनि रहन्छन्; नबदलिएका अन्त्य सीमाले पहिलो चलाइको आधार र निश्चित अन्तिम समय कायम राख्छन्। अधिकतम संख्या बदल्दा सम्पूर्ण जीवनकालको कुल सीमा बदलिन्छ; खपत गणना बराबर वा त्योभन्दा कम कुलले तालिका समाप्त गर्छ। सम्पादकले व्याख्या गरेअनुसार आवृत्ति बदल्दा सुरक्षित गर्ने बेलामा नयाँ अन्तराल समयचक्र सुरु हुन सक्छ।
|
||||
|
||||
चयन गरिएको नेटिभ तालिकाले **चाहिएको अवस्था**, **मूल पात्रो**, **अर्को जागरण** वा **अर्को जाँच**, **जाँचहरू** सँग अभिलेख सुरु भएको मिति, **सुरु भएका चरणहरू**, **सफल चरणहरू**, यसको अन्त्य र **पहिलेका पालाहरू** देखाउँछ। पुरानो पालो **कुराकानी खोल्नुहोस्** बाट पढ्नुहोस्। **तालिका समाप्त** ले तालिकाको अन्त्य जनाउँछ; पालोको नतिजा छुट्टै जाँच्नुहोस्। नतिजामा पछिल्ला ३० प्रविष्टि रहन्छन्; कुल जीवनकालको गणना त्यो सीमापछि पनि रहन्छ। **थप Missions लोड गर्नुहोस्** ले सुरक्षित वस्तुहरूको अर्को पृष्ठ लोड गर्छ।
|
||||
|
||||
सूचीले तोकिएका सर्त र अन्त्य मात्र देखाउँछ; **जाँचहरू** सर्त भएको बेला मात्र देखिन्छ।
|
||||
|
||||
### विद्यमान दैनिक तालिका {#existing-daily-schedules}
|
||||
|
||||
विद्यमान दैनिक तालिकाले सुरक्षित IANA क्षेत्र र स्थानीय घडी समय प्रयोग गर्छन्। हरेक सञ्चालनको आफ्नै समन्वयकर्ता र मिसन नक्सा हुन्छ; नतिजा केन्द्रीय रूपमा पढ्न हालको कार्य वृक्ष वा पुरानो मिति/परिणाम छान्नुहोस्। सुरक्षित इतिहासमा ३० नतिजा रहन्छन्। यी प्रविष्टिमा तलका दैनिक नियम लागू हुन्छन्:
|
||||
|
||||
| परिस्थिति | scheduling व्यवहार |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@ daily schedule ले सुरक्षित IANA time zone र civil time प
|
|||
| घडी पछाडि सर्छ | पहिले processed दिन फेरि due बनाउँदैन। |
|
||||
| विद्यमान run अझै pending | नयाँ daily run सँग overlap हुँदैन। |
|
||||
|
||||
creation time ले पनि eligible दैनिक काम सीमित गर्छ; नयाँ schedule सुरु गर्नु अस्तित्वअघिका मिति process गर्ने अनुमति होइन। उपलब्ध हुँदा **अहिले चलाउनुहोस्** ले छुट्टै manual run सुरु गर्छ; daily clock बदल्दैन वा pending run छल्दैन। देखाइएको next time projection हो, ठ्याक्कै-समय execution प्रत्याभूति होइन।
|
||||
सिर्जना समयले पनि योग्य दैनिक काम सीमित गर्छ। विद्यमान दैनिक प्रविष्टिमा उपलब्ध हुँदा **अहिले चलाउनुहोस्** ले बाँकी सञ्चालनको सुरक्षा लागू गर्दै छुट्टै हातैले सुरु गरिएको सञ्चालन चलाउँछ र दैनिक घडी कायम राख्छ। देखाइएको अर्को समय सेवा र होस्टको उपलब्धतामा निर्भर अनुमान हो।
|
||||
|
||||
**पछ्याइएका कुराकानीहरू (ऐच्छिक)** ले चयनित कुराकानीबाट bounded context थप्छ। completed run ले मात्र processed-message cursor अघि बढाउँछ; failed/interrupted run ले चुपचाप context खपत गर्दैन। instruction र context क्षमता नाघे instruction छोट्याउनुहोस् वा followed conversation हटाउनुहोस्, असीमित इतिहास पढिन्छ नमान्नुहोस्।
|
||||
|
||||
## आयु र service restart {#lifetime}
|
||||
|
||||
**प्राविधिक विवरण** छेउको **i** ले प्राविधिक गतिविधि र लागत बुझाउँछ। **सेटिङहरू → OpenCode** मा निगरानीको अवस्था र त्रुटि देखिइरहन्छन्; सामान्य व्याख्या **i** मा छन्।
|
||||
|
||||
मूल तालिकाका लागि **प्राविधिक विवरण** विस्तार गरेर यसको **मुख्य वाहक** र **मिलान वाहक**, तिनका मूल पात्रो र कुनै **सफाइ मिलानको प्रतीक्षामा** अवस्था पहिचान गर्नुहोस्। यी प्राविधिक संवादमा सन्देश लेख्ने कार्य हुँदैन र तिनीहरू सामान्य संवाद तथा स्रोत चयनमा देखिँदैनन्। प्राविधिक जागरण, शीर्षक र कम्प्याक्सनले प्रदायकलाई HTTP अनुरोध नगरी स्थानीय मोडेल प्रयोग गर्छन्; मूल घटना र तथ्याङ्कमा तिनीहरू अझै समावेश हुन सक्छन्, जबकि वास्तविक चरणहरूमा सामान्य मोडेल लागत लाग्छ। कम्प्याक्सनले संवाद र पात्रोको उही पहिचान कायम राख्दै प्रशोधित सन्दर्भ सीमित गर्छ। समाप्त भएको तालिका स्पष्ट रूपमा सफा नगरिएसम्म भण्डारित मूल सन्देश र घटनाहरू बढ्न सक्छन्; सुरक्षित राखिएका ३० Mission परिणामहरू छुट्टै हुन्। गहिराइ नियन्त्रण र **समयतालिका** ले उपलब्ध हुँदा **OpenCode कन्फिगरेसन फाइल:** अन्तर्गत पथ देखाउँछन्; गहिराइ नियन्त्रणले आफ्नो **परियोजनाको लेखन लक्ष्य** छुट्टै देखाउँछ।
|
||||
|
||||
| भाग | बन्द गर्दा अर्थ |
|
||||
| --- | --- |
|
||||
| CodeNomad परियोजना ट्याब | त्यो परियोजना CodeNomad मा बन्द हुन्छ; native काम वा OpenCode सेवा रोकिँदैन। फेरि खोल्दा सत्र भेटिन्छन्। |
|
||||
| पूरै विन्डो, एप बन्द वा renderer reload | पुनर्स्थापनाका लागि परियोजना जडान राख्छ; native काम वा OpenCode सेवा रोकिँदैन। |
|
||||
| CodeNomad backend | OpenCode रोकिँदैन। चलिरहेको एकपटकको मिसन जारी रहन्छ: यसको coordinator, native subagent task र मिसन tool OpenCode मा चल्छन्। Independent session मा task तोक्ने र तिनका report coordinator लाई पुर्याउने काम, coordinator ले तयार गरेका मिसन, retired task का कुराकानी रोक्ने काम, recovery र मिसन नियन्त्रण backend को प्रतीक्षा गर्छन्; OpenCode service restart पछि एकपटकको मिसन जारी राख्न पनि त्यस्तै हो। |
|
||||
| चलिरहेको daily schedule | clock र passage execution OpenCode plugin/service मा चल्छन्, CodeNomad UI र backend बन्द हुँदासमेत। execution host र OpenCode सेवा चलिरहनुपर्छ। |
|
||||
| OpenCode service restart | process-local schedule job हराउँछन्। चल्नुपर्ने schedule **अवरुद्ध** हुन्छ र स्पष्ट **पुनः सुरु गर्नुहोस्** नगरेसम्म त्यस्तै रहन्छ; CodeNomad फेरि खोल्दा rearm हुँदैन। |
|
||||
| सक्रिय नेटिभ तालिका | स्थायी रूपमा सुरक्षित OpenCode पात्रोले परियोजना जगाउँछन् र कार्यान्वयन होस्ट तथा सेवा उपलब्ध हुँदा CodeNomad को प्रयोगकर्ता इन्टरफेस र ब्याकएन्ड बन्द भए पनि पालाहरू चलाउँछन्। |
|
||||
| OpenCode सेवा पुनःसुरुवात — स्वस्थ नेटिभ तालिका | चाहिएको अवस्था चलिरहेको भएको स्वस्थ तालिकाले आफ्नो पात्रो स्वतः पुनः सुरु गर्छ। नयाँ प्रवेशअघि बाँकी काम यसको मूल पहिचानबाट मिलाइन्छ। |
|
||||
| विद्यमान दैनिक तालिका | सेवा जीवित हुँदासम्म CodeNomad बन्द भए पनि यसको प्लगइन घडी चल्छ। सेवा पुनः सुरु हुँदा यो **अवरुद्ध** रहन्छ; स्पष्ट **पुनः सुरु गर्नुहोस्** ले बाँकी सञ्चालन मिलाएर दैनिक घडी फेरि सक्रिय गर्छ। |
|
||||
|
||||
restart पछि pending run जाँचेर तयार हुँदा **पुनः सुरु गर्नुहोस्** रोज्नुहोस्। Resume ले coordinator input दोहोर्याएर नपठाई विद्यमान run reconcile गर्छ; interrupted native turn जारी हुने वाचा गर्दैन। restart ले काटेको turn **पुनः सुरुले अवरुद्ध** सहित report बिना सकिएकोमा settle हुन सक्छ। tool side effect retry गर्न restart प्रयोग नगर्नुहोस्।
|
||||
नेटिभ तालिकामा स्पष्ट विराम, स्थायी बन्द, असफल काम र अनिश्चित परिणामका प्रवेश अवरोध पुनःसुरुवातपछि पनि कायम रहन्छन्। सुरक्षित पालो र मूल अनुरोध जाँच्नुहोस्; उपलब्ध भए **पालो जाँच गर्नुहोस्** वा **सुरक्षित अनुरोध ताजा गर्नुहोस्** प्रयोग गर्नुहोस्, अनि अवस्थाले अनुमति दिए मात्र **पुनः सुरु गर्नुहोस्** प्रयोग गर्नुहोस्। मूल निरन्तरता विद्यमान पहिचानमै अवलोकन गरिन्छ। प्रमाणित असफलता वा प्रतिवेदनबिनाको अन्त्यलाई मानव पुनःप्राप्ति चाहिन्छ; अपूर्ण प्रमाणले पालो अनिश्चित राख्छ। सुरु भइसकेका उपकरण प्रभाव फेरि प्रयास गर्न पुनःसुरुवात कहिल्यै प्रयोग नगर्नुहोस्।
|
||||
|
||||
[OpenCode चलाइराख्नुहोस्](opencode.md#service-supervision) ले कार्यान्वयन होस्टको साझा सेवा छुट्टै निगरानी गर्छ। बन्द गर्ने छनोट सुरक्षित नगरेमा पहिलो मिसन चलाइले यसलाई सक्रिय गर्छ; विराममा रहेको मिसन सिर्जना गर्दा त्यो छनोट कायम रहन्छ। यसलाई बन्द गर्दा OpenCode चलिरहन्छ तर स्वतः फेरि सुरु हुँदैन। Windows, macOS र WSL ले लगइन सीमा प्रयोग गर्छन्; नेटिभ Linux ले अनुमतिअनुसार प्रयोगकर्ता सेवा निरन्तर चलाइराख्ने सुविधा प्रयोग गर्छ। स्लीप, हाइबरनेसन, निष्क्रिय WSL को आयु र बाह्य होस्टको नियन्त्रणले उपलब्धता सीमित गर्छन्; निगरानीको स्थिति तालिकाभन्दा छुट्टै जाँच्नुहोस्।
|
||||
|
||||
मिसन डेस्कटप एप र स्वतन्त्र CodeNomad [सर्भर](server.md) दुवैसँग काम गर्छन्। प्यानलले **मिसन नियन्त्रण उपलब्ध छैन** देखाउँदा यसको सन्देशले कारण बताउँछ:
|
||||
|
||||
|
|
@ -184,7 +237,9 @@ restart पछि pending run जाँचेर तयार हुँदा **
|
|||
|
||||
one-time Pause ले चलिरहेको काम सकिन दिन्छ: coordinator र यसका उप-एजेन्टहरूले हालको चरण पूरा गर्छन्, नयाँ केही सुरु गर्दैनन् र रोकिन्छन्; त्यस बेला panel ले देखाउँछ: **रोकिँदै: चलिरहेका चरणहरू सकिँदैछन्।** **अहिले नै रोक्नुहोस्** ले भने तिनलाई तुरुन्तै रोक्छ: native mission परिवार interrupt गरी coordination रोक्छ। Stop ले पनि interrupt गर्छ, queued family input/delivery रद्द गर्छ र terminal हो। interruption हरू bounded कार्य हुन्: **केही उप-एजेन्टहरू अझै चलिरहेका हुन सक्छन्।** को अर्थ कुनै एउटा अधुरो छ। सबै shell/tool प्रभाव रद्द भए नठानी retained result जाँचेर explicit retry प्रयोग गर्नुहोस्। Resume ले के pause वा interrupt भयो भनेर coordinator लाई बताउँछ; कसरी जारी राख्ने उसैले निर्णय गर्छ।
|
||||
|
||||
daily Pause/Stop ले scheduling रद्द गरी coordinator र independent specialist समेत registered root actor को interruption माग्छन्, तर **ती कुराकानी वा तिनका descendant रोकिएको प्रमाण दिँदैनन्**। native परिवार जाँच्नुहोस्। Stop terminal हो: stopped schedule ले Play, Resume वा Run now गर्न सक्दैन। **पालो जाँच गर्नुहोस्** ले pending run फेरि नपठाई reconcile गर्छ। अस्थायी scheduled-check failure ले अन्तिम त्रुटि देखाउँछन् र स्वतः retry गर्छन्; user-control reply हराउँदा automatic control resend हुँदैन।
|
||||
नेटिभ तालिकाको **रोक्नुहोस्** ले भावी प्रवेश बन्द गरी स्वीकार गरिएको पालोको अवरोध माग्छ; **पुनः सुरु गर्नुहोस्** ले फेरि सक्रिय गर्नुअघि विद्यमान काम मिलाउँछ। **बन्द गर्नुहोस्…** अन्तिम हो। **अहिले चलाउनुहोस्** ले पात्रो कायम राख्दै उही बाँकी-पालो सुरक्षा र सुरु-पालो सीमा प्रयोग गर्छ। **पालो जाँच गर्नुहोस्** ले मूल पालो मिलाउँछ। अपुष्ट कार्य राखेर **सुरक्षित अनुरोध ताजा गर्नुहोस्** प्रयोग गर्नुहोस्; निष्क्रिय ताजाकरण र पुनःजडानले त्यही अनुरोध कायम राख्छन्। सुरु भइसकेका उपकरण प्रभाव अवरोधपछि पनि चलिरहन सक्छन्।
|
||||
|
||||
विद्यमान दैनिक प्रविष्टिले आफ्ना दैनिक रोक्नुहोस्, बन्द गर्नुहोस्, पुनः सुरु गर्नुहोस्, अहिले चलाउनुहोस् र पालो जाँच गर्नुहोस् नियन्त्रण कायम राख्छन्। विराम/बन्दले तिनको घडी रद्द गर्छ र स्वतन्त्र विशेषज्ञसमेत दर्ता गरिएका मूल सञ्चालकको अवरोध माग्छ। तिनका मूल परिवार र अवरोधको कुनै आंशिक नतिजा जाँच्नुहोस्। अस्थायी दैनिक-जाँच असफलताले देखाइएको चेतावनीसहित फेरि प्रयास गर्छन्; प्रयोगकर्ता नियन्त्रणको उत्तर हराए पनि मूल अनुरोधसँग जोडिएकै रहन्छ। बन्द प्रविष्टि अन्तिम अवस्थामै रहन्छ।
|
||||
|
||||
report बिना idle one-time coordinator मा primary ▶ ले verify र recover गर्न सक्छ; योग्य task मा targeted missing-report recovery हुन्छ। recovery ले पहिले native activity, descendant र wait जाँच्छ; unknown activity मा admission अस्वीकार हुन्छ। assignment replay वा योजना प्रतिस्थापन गर्दैन। बाँकी question, permission, shell वा background काम सुल्झाउन कुराकानी खोल्नुहोस्।
|
||||
|
||||
|
|
@ -192,12 +247,28 @@ report बिना idle one-time coordinator मा primary ▶ ले verify
|
|||
|
||||
## सम्पादन, मेटाइ र कुराकानी सफाइ {#cleanup}
|
||||
|
||||
सम्पादनयोग्य एकपटकको मिसनमा **चयन गरिएको मिसन सम्पादन गर्नुहोस्** प्रयोग गर्नुहोस्; conflict भए overwrite नगरी current revision लोड गर्न editor बन्द/फेरि खोल्नुहोस्। form लोड हुँदै गर्दा उद्देश्य वा नोट परिवर्तन भए, यसले ती भर्नुको सट्टा तपाईंले खोलेपछि मिसन परिवर्तन भएको बताउँछ; हालको संस्करण सम्पादन गर्न **फेरि खोल्नुहोस्** छान्नुहोस्। daily schedule मा edit/delete नियन्त्रण हुँदैनन्।
|
||||
form लोड हुँदै गर्दा उद्देश्य वा नोट परिवर्तन भए, यसले ती भर्नुको सट्टा तपाईंले खोलेपछि मिसन परिवर्तन भएको बताउँछ; हालको संस्करण सम्पादन गर्न **फेरि खोल्नुहोस्** छान्नुहोस्।
|
||||
|
||||
पालाहरूबीच, कुनै पालो वा अनिर्णीत नियन्त्रणले वस्तु रोकेको नभए विराममा वा चलिरहेको नेटिभ तालिकाको शीर्षक, निर्देशन, टिप्पणी, अनुरोध गरिएका प्रोफाइल, कार्य-सत्र मोड, स्रोत, पात्रो र सीमा सम्पादन गर्न सकिन्छ। विराममा रहेको तालिका विराममै रहन्छ; सक्रिय तालिकाले जारी राख्नुअघि पात्रो मिलाउँछ। सुरक्षित गर्दा पुराना नतिजा, खपत स्रोत, सुरु भएको गणना र पहिलो चलाइको आधार कायम रहन्छन्; नबदलिएका अन्त्य सीमाले आफ्नो निश्चित अन्तिम समय कायम राख्छन्।
|
||||
|
||||
स्वीकार गरिएको पालो चल्दा निर्देशन र टिप्पणीले मात्र सक्रिय पाठ-सम्पादन सम्झौता पछ्याउँछन्: परिवर्तन त्यसको इतिहासमा अभिलेख हुन्छन् र भावी पालाका लागि सुरक्षित हुन्छन्। विद्यमान कार्य, संक्षिप्त विवरण, प्रोफाइल र कार्यान्वयन पहिचानले स्वीकार गरिएका मान कायम राख्छन्। विरामपछि समेत पालो नटुङ्गिएसम्म संरचनात्मक क्षेत्र बन्द रहन्छन्। सम्पन्न वा हटाइएकोसमेत कुनै कार्य सिर्जना भएपछि प्लेबुक बन्द नै रहन्छ। अनिश्चित प्रवेश/नियन्त्रण वा अपूर्ण अखण्डता प्रमाणले संरचनात्मक सम्पादन पढ्न मात्र मिल्ने राख्छ। सिर्जना र सम्पादनले एउटै पात्रो प्रमाणीकरण प्रयोग गर्छन्; पूर्वावलोकनले सम्पादित अन्तिम अनुमति प्राप्त समय सुरक्षित अर्को जागरणमा लागू गर्छ, ढिलो जागरणसमेत।
|
||||
|
||||
स्थानीय शाखा स्रोतमा worktree नभएका शाखा पनि पर्छन् र स्थानीय सन्दर्भ मात्र पढिन्छन्। चयनले बढीमा 200 स्थानीय शाखा समर्थन गर्छ; ठूलो सूचीमा स्रोत अनुपलब्ध देखिन्छ। सक्रियता उपलब्ध हुँदा **सुरु गर्नुहोस्** र **पुनः सुरु गर्नुहोस्** ले ठ्याक्कै कन्फिगरेसन बाटोसहित सहमति माग्छन्। नेटिभ समयतालिका निष्क्रिय हुँदा **अहिले चलाउनुहोस्** कारणसहित निष्क्रिय रहन्छ; OpenCode कन्फिगरेसनमा `experimental.schedules` सक्रिय गरेर ताजा गर्नुहोस्। सक्रियताको उत्तर हराएमा परिणाम जाँच्न मूल अनुरोध सुरक्षित रहन्छ।
|
||||
|
||||
पहिलो Play अघि **विकल्पहरू** मा पछ्याइएका कुराकानी राखेरै तत्काल, सर्तरहित सुरुवातमा फर्कन सकिन्छ। Play ले पूर्ण र स्थिर सन्देशको क्रम पढेर स्थिर गर्छ (प्रति स्रोत बढीमा 32 सन्देश, कुल सुरुवात इनपुट 16,384 अक्षरभित्र)। हराएको, सारिएको वा क्षमताभन्दा ठूलो स्रोतले कारणसहित सुरुवात रोक्छ। पुनः प्रयासले मूल स्थिर सन्दर्भ र सुरुवातको पहिचान कायम राख्छ।
|
||||
|
||||
रसीद पढ्दा आंशिक तयारी पुष्टि भए **फेरि प्रयास गर्नुहोस्** देखिन्छ। क्लिक गर्दा त्यही रसीद फेरि जाँचिन्छ र मूल सुरक्षित परिवर्तन मात्र जारी हुन्छ; मिसन रोकिएकै रहन्छ।
|
||||
|
||||
नयाँ तयार गरिएको मिसनको पहिलो सुरुवातअघि शीर्षक, निर्देशन, टिप्पणी, प्लेबुक, अनुरोध गरिएका एजेन्ट/मोडेल/सोच, कार्य सत्रको मोड र **समयतालिका** सम्पादन गर्न सकिन्छ। तत्काल, ढिलो, दोहोरिने र सर्तयुक्त सुरुवातबीच बदल्दा त्यही मिसन कायम रहन्छ। सुरक्षित गर्दा मिसन रोकिएकै रहन्छ र नयाँ खाली संयोजक तयार हुन्छ; सुरुको कुराकानी सुरक्षित रहन्छ। Play ले मात्र काम सुरु गर्छ वा समयतालिका सक्रिय गर्छ। सुरुवातपछि निर्देशन र टिप्पणीमा सक्रिय मिसनका सम्पादन नियम लागू हुन्छन्। सुरक्षित गर्ने नतिजा अपुष्ट भए **सुरक्षित अनुरोध ताजा गर्नुहोस्** ले मूल नतिजा पढ्छ; आंशिक तयारीले सुरु, पाठ सम्पादन र मेटाउने कार्य रोकेको बेलासम्म त्यही अनुरोध राख्नुहोस्।
|
||||
|
||||
सम्पादनयोग्य एकपटकको मिसन वा नेटिभ तालिकामा **चयन गरिएको मिसन सम्पादन गर्नुहोस्** प्रयोग गर्नुहोस्। बन्द क्षेत्रछेउका व्याख्या पढ्नुहोस्। संशोधन द्वन्द्व भए **हालको सेटिङ फेरि लोड गर्नुहोस्** रोज्नुहोस्, त्यसपछि फेरि सुरक्षित गर्नुअघि राखिएको मस्यौदा जाँच्न **तुलनाका लागि अघिल्लो मस्यौदा** खोल्नुहोस्। विद्यमान दैनिक तालिकाले आफ्नै उपकरणपट्टीका नियन्त्रण कायम राख्छन्।
|
||||
|
||||
नेटिभ तालिकामा **चयन गरिएको मिसन मेटाउनुहोस्…** ले प्रविष्टि हटाउनुअघि भावी पालाहरू निष्क्रिय गर्छ। कुराकानी र नतिजा राखिन्छन्; मिलान पर्खिरहेको सफाइ **प्राविधिक विवरण** मा देखिइरहन्छ। अपुष्ट मेटाइ राखेर त्यसको मूल अनुरोध ताजा गर्नुहोस्।
|
||||
|
||||
1. नचाहिने one-time mission मा **चयन गरिएको मिसन मेटाउनुहोस्…** रोजी confirmation समीक्षा गर्नुहोस्।
|
||||
2. चाहनुहुन्छ भने पुष्टि अघि **यस मिसनका लागि सिर्जना गरिएका विशेषज्ञ कुराकानीहरू पनि मेटाउनुहोस्** चयन गर्नुहोस्।
|
||||
3. removed/kept/pending count का लागि **कुराकानी सफाइ** जाँच्नुहोस्। उपलब्ध भए बाँकी recorded cleanup मात्र retry गर्नुहोस्।
|
||||
|
||||
मिसन मेट्नु रोक्नुजस्तो होइन; पहिले Stop गरी बाँकी काम जाँच्नुहोस्। coordinator र reused कुराकानी राखिन्छन्। shared, moved, child भएका वा ownership प्रमाणित नसकिने specialist पनि राखिन सक्छन्। partial cleanup rollback हुँदैन र चुपचाप background मा पूरा गरिँदैन। overview reader ले पछिल्ला २० पूरा cleanup राख्छ। मिसन मेट्दा project मा तिनको storage खाली हुन्छ: पुराना मेटिएका मिसन आफैँ हटाइन्छन्।
|
||||
मिसन मेट्नु रोक्नुजस्तो होइन; पहिले Stop गरी बाँकी काम जाँच्नुहोस्। coordinator र reused कुराकानी राखिन्छन्। shared, moved, child भएका वा ownership प्रमाणित नसकिने specialist पनि राखिन सक्छन्। partial cleanup rollback हुँदैन र चुपचाप background मा पूरा गरिँदैन। कुराकानी सफाइमा ० बाँकी देखिए पनि मेटाउने कामको पुष्टि बाँकी रहँदै मिसन कार्ड हराउन सक्छ। मेटाउने काम पुष्टि नभएसम्म मूल **फेरि प्रयास गर्नुहोस्** कार्य प्रयोग गरिरहनुहोस्; यसले उही मेटाउने अनुरोध र विकल्पहरू राख्छ। overview reader ले पछिल्ला २० पूरा cleanup राख्छ। मिसन मेट्दा project मा तिनको storage खाली हुन्छ: पुराना मेटिएका मिसन आफैँ हटाइन्छन्।
|
||||
|
||||
native काम र service नियन्त्रणका लागि [अनुमति](permissions.md), [कुराकानी](conversations.md) र [OpenCode सेटअप](opencode.md) हेर्नुहोस्।
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ PATH मा पुरानो OpenCode पहिले आउँदा दि
|
|||
| सिफारिस | OpenCode **2.2.5** | योग्य ठहरिएको लक्ष्य, प्राविधिक न्यूनतमभन्दा फरक। |
|
||||
| अप्रमाणित | पूर्ण जाँच नभएको नयाँ वा custom संस्करण | परीक्षण नभएकै कारण अस्वीकार हुँदैन; जोडिएको सेवाका वास्तविक API contract जाँचिन्छन्। |
|
||||
|
||||
आवश्यक session, permission, Form, inbox र session-environment contract पनि उपलब्ध हुनुपर्छ। executable संस्करणले मात्र जोडिएको सेवाको समर्थन प्रमाणित गर्दैन। सेटअपले चलिरहेको सेवा असमर्थित भने समर्थित CLI चयन/स्थापना गरी तयार हुँदा स्पष्ट साझा-सेवा पुनःसुरुवात प्रयोग गर्नुहोस्।
|
||||
जडित सेवाले आवश्यक सत्र, अनुमति, फारम, आगमन-पेटिका र सत्र-वातावरण सम्झौता दिनुपर्छ। नेटिभ मिसन समयतालिकाले तालिका API, प्लगइन/प्रदायक प्याकेज र प्रभावकारी `experimental.schedules` सेटिङ पनि जाँच्छ। मिल्ने भावी संस्करणमा समेत उपलब्धता यी प्रयोग गरिने क्षमतामा निर्भर हुन्छ। सेटअपले चलिरहेको सेवा असमर्थित भने समर्थित CLI छानेर/स्थापना गरेर तयार हुँदा साझा सेवा स्पष्ट रूपमा पुनः सुरु गर्नुहोस्।
|
||||
|
||||
## होस्ट, WSL र साझा-सेवा दायरा {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ Windows मा चयनित WSL executable रोजिएको वितर
|
|||
|
||||
OpenCode को global service registration, सत्र, सन्देश र database OpenCode कै हुन्। CodeNomad ले विन्डो वा परियोजनापिच्छे निजी OpenCode database बनाउँदैन। परियोजना ट्याब बन्द गर्दा त्यो परियोजना CodeNomad मा बन्द हुन्छ; सत्र OpenCode मा चलिरहन्छन् र परियोजना फेरि खोल्दा देखिन्छन्। CodeNomad ले नेटिभ Location कहिल्यै unload गर्दैन। पूरै विन्डो बन्द गर्दा, एप बन्द गर्दा वा reload गर्दा परियोजना जडान पुनर्स्थापनाका लागि राखिन्छन्; ती कार्य वा ब्याकएन्ड बन्द हुँदा पनि साझा daemon रोकिँदैन। [परियोजना](projects.md) हेर्नुहोस्।
|
||||
|
||||
## OpenCode चलाइराख्नुहोस् {#service-supervision}
|
||||
|
||||
पहिलो Mission Play अघि **पहिलो Mission Play मा पनि बन्द राख्नुहोस्** ले पहिले निगरानी स्थापना नगरी स्पष्ट OFF बचत गर्छ।
|
||||
|
||||
**सेटिङहरू → OpenCode** मा **OpenCode चलाइराख्नुहोस्** ले कार्यान्वयन होस्टमा OS निगरानी स्थापना गर्छ, जुन त्यो सेवा प्रयोग गर्ने सबै CodeNomad प्रोफाइलहरूमा साझा हुन्छ। यो Missions बाट स्वतन्त्र छ र CodeNomad बन्द हुँदा पनि चल्छ। पहिले स्पष्ट रूपमा बन्द नगरेमा पहिलो Mission Play ले सक्षम गर्छ; रोकिएको Mission बनाउँदा छनौट जोगिन्छ। Mission रोक्ने, स्थायी रूपमा रोक्ने वा मेट्ने कार्यले यो प्राथमिकता बदल्दैन।
|
||||
|
||||
बन्द गर्दा निगरानी हट्छ तर OpenCode चलिरहन्छ र फेरि सुरु गरिँदैन। लामो समय सेवा रोक्न, पहिले यो बन्द गरेर OpenCode स्पष्ट रूपमा रोक्नुहोस्। निगरानी सक्रिय हुँदा `opencode service stop` पछि पुनः सुरु हुन्छ।
|
||||
|
||||
**चलिरहेको · निगरानी सक्रिय** का लागि सक्रिय OS सुपरभाइजर र स्वस्थ OpenCode सेवा आवश्यक हुन्छ। **स्थापित · सेवा सुरु हुँदै**, **निगरानी स्थापना वा हटाउँदै…** र **निगरानी असफल** फरक अवस्थाहरू हुन्। सेवाले प्रतिक्रिया दिएको मात्रले निगरानी पुष्टि हुँदैन। थाहा भएको अवस्थामा कारण, पछिल्लो प्रयास, सुपरभाइजरको पुनः प्रयास र OS पुनःप्राप्ति सुरक्षा जाँच देखाइन्छ। **निगरानी स्थिति जाँच्नुहोस्** ले पढ्छ मात्र; **निगरानी फेरि प्रयास गर्नुहोस्** ले अनुरोध गरिएको सेटअप स्पष्ट रूपमा मर्मत गर्छ। अपुष्ट कार्यले मूल अनुरोध जोगाउँछ; **मूल कार्य फेरि प्रयास गर्नुहोस्** ले उही कार्य मात्र पुनः पठाउँछ। सेटिङ संशोधन बदलिएमा मिच्नुको सट्टा समीक्षाका लागि ताजा गरिन्छ।
|
||||
|
||||
Windows, macOS र WSL लगइनपछि सुरु हुन्छन्। WSL का लागि चयन गरिएको वितरणको localhost forwarding चाहिन्छ। नेटिभ Linux मा linger ले **सबै सक्षम प्रयोगकर्ता सेवाहरू** लगइनअघि सुरु गर्छ र लगआउटपछि पनि चलाउँछ। बन्द गर्दा CodeNomad ले सक्षम गरेको र अर्को निगरानीलाई आवश्यक नभएको अवस्थामा मात्र linger फर्काइन्छ; व्यक्तिगत सेटिङ जोगिन्छ। कुनै प्रयोगकर्ता सत्र नखुलेको बेला linger पुनर्स्थापना गर्दा निगरानीबाट मुक्त OpenCode प्रक्रियाहरू रोकिन सक्ने भएमा पुनर्स्थापना स्थगित हुन्छ र **निगरानी फेरि प्रयास गर्नुहोस्** सहित देखिन्छ। प्रयोगकर्ता सत्र खुलेपछि वा निगरानीबाट मुक्त OpenCode प्रक्रियाहरू समाप्त भई सुरक्षित पुनर्स्थापना सम्भव भएपछि फेरि प्रयास गर्नुहोस्। Linux ले अनुमति मागेमा कार्यान्वयन होस्टमा अनुमति दिएर फेरि प्रयास गर्नुहोस्; अनुरोध गरिएको प्राथमिकता जोगिन्छ। स्लीप र हाइबरनेसन OS को नियन्त्रणमा रहन्छन्। अनुपलब्ध वा असमर्थित होस्टमा व्याख्यासहित निष्क्रिय नियन्त्रण देखिन्छ।
|
||||
|
||||
## अपडेट, पुनःसुरुवात वा reload {#maintenance}
|
||||
|
||||
1. **सेटिङहरू → OpenCode** मा **स्थिति र अपडेट जाँच गर्नुहोस्** रोज्नुहोस्।
|
||||
|
|
@ -41,7 +53,7 @@ OpenCode को global service registration, सत्र, सन्देश
|
|||
| कार्य | दायरा र असर | सुरक्षित पुनःप्राप्ति |
|
||||
| --- | --- | --- |
|
||||
| CLI अपडेट | समर्थित स्थापित executable बदल्छ; चलिरहेको daemon स्वतः पुनः सुरु गर्दैन। | असफलतापछि स्थापित र चलिरहेको दुवै संस्करण जाँच्नुहोस्; त्रुटि समाधानपछि मात्र स्पष्ट पुनःप्रयास गर्नुहोस्। |
|
||||
| सेवा पुनःसुरुवात | साझा प्रक्रिया बदल्छ र सबै जोडिएका OpenCode क्लाइन्टको सक्रिय काम रोक्छ; सत्र इतिहास रहन्छ। | पुनःजोडी कुराकानी र निर्धारित Mission जाँच्नुहोस्; restart स्वतः continuation होइन। |
|
||||
| सेवा पुनःसुरुवात | साझा प्रक्रिया बदल्छ र सबै जोडिएका OpenCode क्लाइन्टको सक्रिय काम रोक्छ; सत्र इतिहास रहन्छ। | स्वस्थ चलिरहेका नेटिभ मिसन तालिका स्वतः पुनःप्राप्त हुन्छन्; बाँकी पालाहरू र कुनै विराम/त्रुटि/अनिश्चितता जाँच्नुहोस्। विद्यमान दैनिक प्रविष्टिलाई पुनः सुरु गर्नुपर्छ। एकपटकका कुराकानी र तिनका पुनःप्राप्ति नियन्त्रण छुट्टै जाँच्नुहोस्; [मिसन](missions.md#lifetime) हेर्नुहोस्। |
|
||||
| OpenCode कन्फिगरेसन reload | प्रक्रिया राख्छ तर सबै क्लाइन्टका **लोड भएका हरेक Location** पुनर्निर्माण गर्छ। pending अनुमति/Form रद्द र terminal/background command बन्द गर्छ; सक्रिय काम रोकिन सक्छ। | reload पछि स्रोत र pending काम पुनःजाँच गर्नुहोस्। जवाफ हरायो भन्दै prompt वा परिवर्तन नदोहोर्याउनुहोस्। |
|
||||
|
||||
सुरक्षित कन्फिगरेसन वा स्थापित plugin अझै अनुपलब्ध हुँदा र अवरोध स्वीकार्य हुँदा **OpenCode कन्फिगरेसन पुनः लोड गर्नुहोस्** (चलिरहेको सेवाले समर्थन गर्दा **समस्या समाधान** अन्तर्गत) स्पष्ट प्रयोग गर्नुहोस्। यो हानिरहित प्रदर्शन refresh होइन। CodeNomad ले पुरानो चयनित CLI बाट नयाँ चलिरहेको daemon downgrade गर्दैन।
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
### तपाईंलाई सोधिने प्रश्न {#questions}
|
||||
|
||||
तपाईंलाई प्रश्न समन्वयकर्ताले मात्र सोध्छ। कार्यहरू र तिनका सहायकले आफ्ना प्रश्न आफूलाई सुरु गर्ने कुराकानीमा विकल्प, प्रमाण, परिणाम, उल्टाउन मिल्ने/नमिल्ने र सिफारिससहित फर्काउँछन्, र जवाफमा निर्भर नहुने काम जारी राख्छन्। समन्वयकर्ताले पहिले आफूले सक्ने कुरा टुङ्ग्याउँछ र बाँकी प्रश्न **तपाईंको जवाफ चाहिन्छ** अन्तर्गत एउटै form मा समेट्छ; उसले के सोध्छ र निर्णयलाई कसरी लेबल गरिन्छ भन्ने कुरा [प्रश्न र निर्णय](missions.md#questions) ले व्याख्या गर्छ। एकपटके वा दैनिक, हरेक समन्वयकर्ता र हरेक कार्य assignment ले यो नियम निर्देशनका रूपमा पाउँछन्; खोजले [तल](#wayfinder-checks) निर्णयका जाँच थप्छ।
|
||||
तपाईंलाई प्रश्न समन्वयकर्ताले मात्र सोध्छ। कार्यहरू र तिनका सहायकले आफ्ना प्रश्न आफूलाई सुरु गर्ने कुराकानीमा विकल्प, प्रमाण, परिणाम, उल्टाउन मिल्ने/नमिल्ने र सिफारिससहित फर्काउँछन्, र जवाफमा निर्भर नहुने काम जारी राख्छन्। समन्वयकर्ताले पहिले आफूले सक्ने कुरा टुङ्ग्याउँछ र बाँकी प्रश्न **तपाईंको जवाफ चाहिन्छ** अन्तर्गत एउटै form मा समेट्छ; उसले के सोध्छ र निर्णयलाई कसरी लेबल गरिन्छ भन्ने कुरा [प्रश्न र निर्णय](missions.md#questions) ले व्याख्या गर्छ। एकपटके वा निर्धारित, हरेक समन्वयकर्ता र हरेक कार्य जिम्मेवारीले यो नियम निर्देशनका रूपमा पाउँछन्; खोजले [तल](#wayfinder-checks) निर्णयका जाँच थप्छ।
|
||||
|
||||
interruption dock मा उत्तर दिनुहोस् (**तपाईंको जवाफ चाहिन्छ** मा **जवाफ दिनुहोस्**): त्यो मात्र तपाईंको उत्तर मानिन्छ। chat मा टाइप गरिएको जवाफ साधारण सन्देश हो।
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ interruption dock मा उत्तर दिनुहोस् (**तपा
|
|||
|
||||
समन्वयकर्तालाई प्रारम्भिक योजनापछि, प्रत्येक कार्य टुङ्ग्याएपछि, तपाईंलाई पर्खन थाल्दा र अन्तिम प्रतिवेदनअघि briefing प्रकाशित गर्ने निर्देशन छ; [Briefing र update अनुरोध](missions.md#briefings) हेर्नुहोस्। Briefing, कार्य सारांश र अन्तिम प्रतिवेदन तपाईंका लागि लेखिन्छन्: तपाईंको भाषामा, केही छोटा वाक्यमा, नतिजा पहिले, पहिचानकर्ता र command चाहिँ evidence मा।
|
||||
|
||||
अन्तिम प्रतिवेदनले के दिइयो, के बाँकी छ र के कुरामा तपाईंको निर्णय चाहिन्छ भन्छ। एकपटके मिसन पूरा भएका रूपमा मात्र सकिन्छ: तपाईंबिना केही अघि बढ्न नसक्दा समन्वयकर्ता असफल हुनुको सट्टा तपाईंलाई सोधेर पर्खन्छ, र मिसन तपाईंले मात्र रोक्न सक्नुहुन्छ। दैनिक run असफल भएर पनि सकिन सक्छ।
|
||||
अन्तिम प्रतिवेदनले के दिइयो, के बाँकी छ र के कुरामा तपाईंको निर्णय चाहिन्छ भन्छ। एकपटके मिसन पूरा भएका रूपमा मात्र सकिन्छ: तपाईंबिना केही अघि बढ्न नसक्दा समन्वयकर्ता असफल हुनुको सट्टा तपाईंलाई सोधेर पर्खन्छ, र मिसन तपाईंले मात्र रोक्न सक्नुहुन्छ। निर्धारित पालो असफल भएर पनि सकिन सक्छ।
|
||||
|
||||
### हरेक मिसनमा CodeNomad ले जाँच्ने कुरा {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ interruption dock मा उत्तर दिनुहोस् (**तपा
|
|||
- खोजका पाँचवटा भूमिका मात्र स्वीकार हुन्छन्।
|
||||
- निर्णय कार्य समन्वयकर्ताको आफ्नै निर्णय वा तपाईंको उत्तरले मात्र पूरा हुन्छ। समन्वयकर्ताको निर्णयले प्रश्न, निर्णय र कारण बताउँछ, र कहिल्यै तपाईंको उत्तरका रूपमा अभिलेख हुँदैन।
|
||||
- तपाईंले CodeNomad interface मा दिएको उत्तर मात्र मानिन्छ। CodeNomad ले प्रश्नको tag बाट उत्तरलाई कार्यसँग जोड्छ: प्रश्न समन्वयकर्ताको कुराकानीबाट सोधिएको, त्यसको पाठ त्यही कार्यको tag ले अन्त्य भएको र अर्को निर्णय नजनाएको, कार्य घोषणा भएपछि उत्तर दिइएको, र अर्को निर्णय टुङ्ग्याइसकेको नहुनुपर्छ। समेटिएको form को हरेक प्रश्न छुट्टै जोडिन्छ; दुई निर्णय जनाउने प्रश्नले कुनै पनि टुङ्ग्याउँदैन। निर्णय कार्यकै कुराकानीबाट सोधिएको प्रश्नको उत्तर पनि स्वीकार हुन्छ।
|
||||
- कार्यको आफ्नै प्रतिवेदनले कसले निर्णय गर्यो भनी दाबी गर्न सक्दैन। मिसन वा दैनिक run सफल रूपमा सकिनुअघि CodeNomad ले पूरा भएका सबै निर्णय tag सहित फेरि जाँच्छ।
|
||||
- कार्यको आफ्नै प्रतिवेदनले कसले निर्णय गर्यो भनी दाबी गर्न सक्दैन। मिसन वा निर्धारित पालो सफल रूपमा सकिनुअघि CodeNomad ले पूरा भएका सबै निर्णय tag सहित फेरि जाँच्छ।
|
||||
- यी जाँच **नेटिभ उपएजेन्टहरू** सँग लागू हुन्छन्। **स्वतन्त्र सत्रहरू** सँग, निर्णय कार्यकर्ताले आफ्ना brief आफैं रिपोर्ट गर्छन् र समन्वयकर्ताले प्रत्येक नतिजा कसले निर्णय गर्यो भनी notes मा लेख्छ; CodeNomad ले यो प्रमाणित गर्दैन।
|
||||
|
||||
निर्देशन मात्र: कुन छनोट उल्टाउन सस्तो मानिन्छ, brief र अनुसन्धानको गुणस्तर, र योजनामै सीमित रहने कुरा।
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode даёт вам двигатель. CodeNomad даёт вам кокп
|
|||
|
||||
## Передайте долгую работу миссиям {#missions}
|
||||
|
||||
Опишите работу один раз. Координатор миссии планирует её как дерево задач, делегирует ограниченные задачи нативным разговорам OpenCode и сообщает результаты вместе с доказательствами. Запускайте миссию один раз или каждый день; создание миссии никогда не запускает работу, пока вы сами этого не решите. Сценарии охватывают отладку, реализацию продукта и сравнение двух моделей со слепым судьёй. См. [Миссии](missions.md).
|
||||
Опишите работу один раз. Координатор миссии планирует её как дерево задач, делегирует ограниченные задачи нативным разговорам OpenCode и сообщает результаты вместе с доказательствами. Запускайте один раз, в выбранные времена или через интервалы либо после подтверждённого изменения проекта; создание сохраняет приостановленную Миссию для вашего явного запуска. Сценарии охватывают отладку, реализацию продукта и сравнение двух моделей со слепым судьёй. См. [Миссии](missions.md).
|
||||
|
||||
## Продолжайте работу с телефона {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ npx @neuralnomads/codenomad --generate-token --launch
|
|||
| Работать с несколькими рабочими копиями и разговорами | [Проекты](projects.md), [Разговоры](conversations.md), [Файлы](files.md) |
|
||||
| Найти историю или безопасно очистить её | [История](history.md) |
|
||||
| Ответить на нативные вопросы и запросы разрешений | [Разрешения](permissions.md) |
|
||||
| Координировать ограниченную или ежедневную работу | [Миссии](missions.md) |
|
||||
| Координировать ограниченную работу или работу по расписанию | [Миссии](missions.md) |
|
||||
| Настроить выполнение и интеграции | [OpenCode](opencode.md), [Провайдеры](providers.md), [Интеграции](integrations.md) |
|
||||
| Подключить другое устройство | [Удалённое управление](remote-control.md), [Безопасность](security.md), [Мобильный доступ](mobile.md) |
|
||||
| Настроить рабочее пространство | [Настройки](settings.md), [Горячие клавиши](shortcuts.md), [Голос](voice.md), [Расширения](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
1. Откройте **Миссии** на правой панели и выберите **Создать миссию**. Форма заменяет список миссий и задачи выбранной миссии, пока вы не создадите миссию или не отмените создание.
|
||||
2. Заполните **Что должна сделать миссия?** и проверьте **Название**, предзаполненное из первого предложения (до 60 символов).
|
||||
3. В **Когда** выберите **Один раз** или **Каждый день в**. Для ежедневной работы проверьте показанный часовой пояс; **Изменить** позволяет указать зону IANA.
|
||||
3. Оставьте **Расписание** свёрнутым для **Один раз, немедленно** и **Без условия** или раскройте его, чтобы выбрать частоту, условие и необязательные ограничения. Перед созданием работы по расписанию проверьте **Сохранённый часовой пояс (IANA)** и предпросмотр.
|
||||
4. Раскройте **Параметры** для **Сценарий**, **Заметки**, **Агенты и модели**, **Сессии задач**, **Глубина вложенности подсессий** проекта, сохранённых описаний и необязательных отслеживаемых разговоров. При **Сравнить две модели** переключатели **Модель A** и **Модель B** появляются в основной форме.
|
||||
5. Нажмите **Создать**. Выберите новую запись, затем отдельное действие ▶, когда готовы: **Запустить миссию** для разовой работы или **Запустить расписание {id}** для ежедневного расписания.
|
||||
5. Нажмите **Создать**. Выберите приостановленную запись, проверьте её и, когда будете готовы, нажмите ▶ **Запустить**. Немедленная работа без условия запускает одну миссию; отложенная, повторяющаяся или условная политика активирует нативное расписание.
|
||||
|
||||
**Создание никогда не запускает работу.** Разовая миссия подготовлена; ежедневное расписание приостановлено. Ни создание, ни сохранение описания не включает таймер. Создание ограничено выбранным проектом/Location и фиксирует запрошенные профили и политику сессий задач. Проверка находится у соответствующего поля; отказ с указанием, что ничего не отправлено, оставляет черновик для исправления. Координатор разовой миссии также может подготовить для вас отдельную миссию, например продолжение: она появляется в списке как **Подготовлена** и, как любая миссия, запускается только своим действием ▶.
|
||||
**Создание сохраняет приостановленную Миссию.** Работа и нативные календари активируются только кнопкой запуска; создание или сохранение описания сохраняет общую настройку надзора за службой. Создание относится к выбранному проекту/Location и записывает запрошенные профили и политику сессий задач. Проверка отображается рядом с соответствующим полем; отказ с указанием, что ничего не отправлено, сохраняет черновик для исправления. Координатор разовой миссии также может подготовить отдельное продолжение: оно появляется как **Подготовлена** и запускается собственным действием ▶.
|
||||
|
||||
Сохранённые разовые миссии сохраняют управление выполнением, дерево задач, разговоры и итоговые отчёты. Существующие ежедневные расписания остаются в списке со своими ежедневными элементами управления и [правилами ежедневного времени](#existing-daily-schedules). Выберите запись и используйте доступные для этого объекта элементы управления.
|
||||
|
||||
### Сценарии {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@
|
|||
|
||||
Последующая задача опирается на прежнюю работу двумя способами. Зависимости задают порядок: координатор запускает задачу, когда завершены задачи, от которых она зависит, и помещает нужные результаты в задание или рядом с назначением; независимые сессии могут также читать эти отчёты в карте. Чтобы продолжить собственный разговор завершённой задачи, координатор объявляет задачу-продолжение, которая повторно использует этот разговор: продолжение идёт там с текущим контекстом разговора, который OpenCode мог [компактировать](history.md#compact-session), а прежние подробности остаются в его сохранённой истории для поиска. Так сценарий «Отладка» возвращает обе проверки исполнителю. В режиме **Независимые сессии** координатор может также назначить задачу существующему основному разговору, который сохраняет свой контекст, включая прежнюю несвязанную работу. В остальных случаях задача начинается в новом разговоре со своего назначения, а переписка другой задачи доходит до неё через координатора или поиск. В **Сравнить две модели** назначение и контекст судьи не содержат цели и заметок; его задание содержит анонимизированные результаты.
|
||||
|
||||
В ежедневном расписании каждый запуск начинается с собственным координатором и собственной картой. Его первое сообщение содержит инструкции расписания и новый текст пользователя и ассистента из его [отслеживаемых разговоров](#daily-schedules) после последнего завершённого запуска; результаты прежних запусков остаются в истории расписания. Разговоры, запущенные внутри запуска, могут читать его карту. Новая разовая миссия тоже начинается с новой карты: чтобы перенести результаты, впишите их в её цель или **Заметки** либо позвольте координатору подготовить миссию-продолжение, цель и заметки которой он пишет сам.
|
||||
Каждый запланированный проход начинается с собственного координатора и карты миссии, используя сохранённые инструкции и зафиксированные входные данные. Отслеживаемые разговоры добавляют ограниченный объём нового текста пользователя и ассистента; их курсоры обработанных сообщений продвигаются после успешного прохода. Предыдущие проходы остаются в истории расписания. Разговоры, запущенные внутри прохода, могут читать его карту. У новой разовой миссии отдельная карта: впишите переносимые результаты в её цель или **Заметки** либо попросите координатора подготовить продолжение с этим контекстом.
|
||||
|
||||
### Поиск в прежних разговорах {#context-search}
|
||||
|
||||
Пока CodeNomad работает, каждый разговор агента в проекте, включая координаторов и задачи миссий, располагает инструментом поиска сессий CodeNomad (`codenomad.session_search`, используется через Code Mode OpenCode). Он ищет в сохранённых разговорах папки проекта этого разговора с тем же ранжированием, что и [поиск](history.md#full-history-search) **Всё рабочее пространство**, и возвращает короткие фрагменты с названием и идентификатором каждого разговора, а также способ прочитать разговор целиком. Действуют нативные разрешения инструментов. Агент ищет, когда сам так решает или когда цель, заметки или ваше сообщение просят об этом, например чтобы узнать, к чему пришла прежняя миссия или задача. Ежедневный запуск, выполняемый при закрытом CodeNomad, работает без этого инструмента.
|
||||
Пока CodeNomad работает, каждый разговор агента в проекте, включая координаторов и задачи миссий, располагает инструментом поиска сессий CodeNomad (`codenomad.session_search`, используется через Code Mode OpenCode). Он ищет в сохранённых разговорах папки проекта этого разговора с тем же ранжированием, что и [поиск](history.md#full-history-search) **Всё рабочее пространство**, и возвращает короткие фрагменты с названием и идентификатором каждого разговора, а также способ прочитать разговор целиком. Действуют нативные разрешения инструментов. Агент ищет, когда сам так решает или когда цель, заметки или ваше сообщение просят об этом, например чтобы узнать, к чему пришла прежняя миссия или задача. Запланированный проход, выполняемый при закрытом CodeNomad, работает без этого инструмента.
|
||||
|
||||
## Профили, делегирование и глубина {#profiles}
|
||||
|
||||
Сохранение глубины сохраняет включённые нативные расписания в конфигурации проекта. Явное `schedules:false` в проекте остаётся выключенным; удаление глубины сохраняет настройку расписаний. Перед сохранением нативных настроек нужно устранить конфликт файлов JSON и JSONC в одном слое.
|
||||
|
||||
1. Используйте шестерёнку миссий или **Настройки → Миссии** для изменения **Настройки новых миссий**.
|
||||
2. В **Применять к** выберите **Все миссии** или один сценарий, затем задайте **Сессии задач** и в **Агенты и модели** по одной строке на роль (координатор, затем роли сценария). Каждый выбор сохраняется сразу; кнопки «Сохранить» нет.
|
||||
3. В области сценария строки показывают глобальные значения с пометкой **Глобально**. Изменение поля настраивает эту роль, начиная с этих значений; её значок возврата снова использует глобальные значения. **Сбросить** (после подтверждения) очищает только агентов и модели показанной области.
|
||||
|
|
@ -118,7 +122,7 @@
|
|||
| Настройка | Область и эффект |
|
||||
| --- | --- |
|
||||
| Настройки новых миссий | Значения всей конфигурации только для **новых** миссий; исключение сценария переопределяет общее значение. |
|
||||
| Агент / Модель / Мышление | Запрошенный выбор координатора/ролей фиксируется при создании; занятого участника не переключают незаметно ради последующей работы. Ежедневные запуски повторно используют фиксированные запрошенные профили. |
|
||||
| Агент / Модель / Мышление | Запрошенный выбор координатора/ролей фиксируется при создании; занятого участника не переключают незаметно ради последующей работы. Запланированные проходы используют сохранённые запрошенные профили; изменения применяются к будущим проходам. |
|
||||
| Нативные субагенты | Сессии задач используют нативных агентов subagent/all под координатором. |
|
||||
| Независимые сессии | Объявленные сессии задач используют агентов primary/all и независимые разговоры; это не отключает внутренних нативных помощников. |
|
||||
| Глубина вложенности подсессий | Конфигурация проекта для **всех** разговоров OpenCode там, не только миссий. Пустое значение наследуется: **Наследовать** удаляет переопределение проекта и блок `experimental`, который после этого остаётся пустым или содержит только глобальное значение **Хранить изображения один раз**. OpenCode читает блок `experimental` проекта целиком, поэтому другие экспериментальные настройки, оставшиеся в нём, по-прежнему скрывают глобальную глубину. Встроенный субагент OpenCode **General** по умолчанию не может запускать субагентов, поэтому его разговоры никогда не вкладываются: сохранение любой глубины проекта также добавляет в тот же файл правило, которое позволяет субагентам запускать собственных субагентов до этой глубины и имеет приоритет над ограничениями General из других файлов конфигурации (при глубине 1 ограничение глубины OpenCode по-прежнему не допускает вложенности), а **Наследовать** удаляет его. Под строкой **Действующая глубина OpenCode** редактор сообщает, могут ли субагенты General вкладываться. Если проект задаёт General в устаревшем формате с единственным значением `permission` или с уже заданными `task` и `subagent`, сохраняется только глубина, и редактор сообщает об этом: разрешите субагентов для General в этом файле самостоятельно. Сохранение не перезагружает OpenCode и не меняет работающие разговоры. |
|
||||
|
|
@ -137,9 +141,51 @@
|
|||
|
||||
Описания хранят инструкции и запрошенные варианты, не планы задач, результаты, разговоры, часы расписания или полномочия выполнения. Загрузка заполняет обычный черновик создания; она ничего не запускает и не планирует. Удаление описания сохраняет существующие миссии/разговоры. **Настройки → Миссии → Сохранённые описания** управляет библиотекой; если библиотека становится нечитаемой, **Сбросить сохранённую библиотеку** (после подтверждения) удаляет сохранённые описания, не исполнения. У самих расписаний нет сохранения как описания.
|
||||
|
||||
## Ежедневное время, наверстывание и отслеживаемые разговоры {#daily-schedules}
|
||||
## Расписание, наверстывание и отслеживаемые разговоры {#daily-schedules}
|
||||
|
||||
Ежедневные расписания используют сохранённый часовой пояс IANA и гражданское время, не истекший интервал 24 часа. Время показывается в зоне расписания с подписью, если отличается от зоны читателя. Каждый запуск получает собственного координатора и изолированную карту миссии; выбранное расписание показывает текущее дерево и историю дат/исходов. Выберите прошлый запуск для чтения в центре. История ограничена 30 результатами.
|
||||
### Нативное расписание {#native-scheduling}
|
||||
|
||||
Значки **i** рядом с подписями объясняют варианты: наведите указатель, переведите фокус клавиатурой или коснитесь значка.
|
||||
|
||||
Раскройте **Расписание** при создании или редактировании. **Частота** предлагает:
|
||||
|
||||
| Вариант | Время выполнения |
|
||||
| --- | --- |
|
||||
| Один раз, немедленно | Один запуск по кнопке запуска с необязательным условием, проверяемым по правилам ниже. |
|
||||
| Один раз, в заданную дату | Одна сохранённая дата и время в зоне IANA; дата, уже прошедшая на момент запуска, отклоняется. |
|
||||
| Один раз, с задержкой | Положительная целая длительность в минутах или часах, отсчитываемая от нажатия кнопки запуска. |
|
||||
| Через интервал | Не менее 15 минут; первое пробуждение — через один интервал после нажатия кнопки запуска. |
|
||||
| Каждый день | Одно местное гражданское время в сохранённом часовом поясе. |
|
||||
| Выбранные дни недели | Одно время в отмеченные дни недели. |
|
||||
| Несколько раз в день | До восьми независимых времён, например 09:15 и 17:45; совпадающие времена объединяются в один проход. |
|
||||
| В день каждого месяца | День 1–31 в выбранное время; месяцы без этого дня пропускаются. |
|
||||
| Расширенное (cron) | Нативное выражение cron, не чаще одного раза в минуту. |
|
||||
|
||||
**Следующие времена (до пяти)** показывает предпросмотр с сохранённым часовым поясом и предполагаемым временем нажатия кнопки запуска. При наличии условия **Следующие проверки (до пяти)** показывает проверки; проход зависит от подтверждённого изменения. Неизменённый сохранённый интервал сохраняет нативную фазу. Приостановленная или неизвестная фаза показывается как недоступная, пока не удастся прочитать её нативное состояние. Задержки и интервалы используют истекшее время; гражданские календари следуют сохранённому поясу и правилам перехода на летнее время OpenCode. Показанные даты — расчётные и зависят от доступности службы и хоста.
|
||||
|
||||
После сбоя нативное расписание доставляет не более одного наверстывающего прохода, затем возвращается к будущим временам. Оно использует записанный нативный срок, который может предшествовать более поздним пропущенным временам. Несколько времён в день совместно используют это наверстывание и одну защиту от перекрытия. Неразрешённый проход сверяется с исходными идентификаторами разговора и сообщения до запуска следующего прохода. Возобновление явно приостановленного повторения использует следующее будущее время; приостановленный разовый запуск может поступить один раз с опозданием.
|
||||
|
||||
### Условия и источники {#scheduling-conditions}
|
||||
|
||||
Выберите одно необязательное **Условие** и его **Источник в этом проекте**: **Успех другого расписания**, **Новые коммиты в локальной ветке**, **Успешное завершение разовой Миссии**, **Новое сообщение в отслеживаемом разговоре** или **Изменение явно указанных файлов**. Для одного условия оставьте **Один раз, немедленно**: нативные проверки выполняются каждые 15 минут, пока не запустится его единственный проход. С календарём условие проверяется в назначенные им времена. Повторяющиеся условия могут запускаться снова после новых подходящих изменений.
|
||||
|
||||
Первое нажатие кнопки запуска устанавливает исходную точку отсчёта. Возобновление сохраняет её, включая изменения, наблюдавшиеся после паузы. Источники успеха требуют проверенного успешного результата и завершённого нативного семейства. Проверки веток используют локальные коммиты и их происхождение без загрузки из удалённого репозитория. Проверки разговоров ждут полных стабильных новых сообщений и исключают технические пробуждения и собственные разговоры прохода. Для файлов используется до 128 явно указанных путей относительно проекта, по одному в строке; рекурсивные шаблоны и пути через ссылки отклоняются. Отсутствующий, перемещённый, неоднозначный или превышающий вместимость источник блокирует допуск с объяснением. Чтобы обнаружить изменения между проверками, в источнике должен оставаться устойчивый след.
|
||||
|
||||
Изменения, наблюдаемые вместе, группируются. Проход фиксирует наблюдаемые свидетельства источника; успешное завершение помечает этот снимок обработанным, оставляя последующие изменения для будущей проверки. Неудачный проход сохраняет контрольную точку источника и требует вмешательства. **Отслеживаемые разговоры (необязательно)** в разделе **Параметры** добавляют контекст отдельно от выбора условия нового сообщения в разговоре.
|
||||
|
||||
### Окончание и счётчики {#scheduling-limits}
|
||||
|
||||
Для неограниченного повторения оставьте ограничения пустыми. **Последнее разрешённое время** и **Последнее время через указанное число часов после первого запуска (необязательно)** ограничивают допустимые времена; действует самая ранняя граница. Длительность отсчитывается от первого нажатия кнопки запуска и продолжает идти во время паузы. Время, записанное до окончания, может поступить с опозданием после долгого сбоя. Уже допущенный проход остаётся наблюдаемым вплоть до результата.
|
||||
|
||||
**Максимум начатых запусков** — общий итог для всех времён и **Запустить сейчас**. Проход учитывается один раз при допуске первого ввода его координатору, включая проход, который позднее завершится сбоем. Проверки, неизменившиеся условия и отклонённые пробуждения не расходуют места; неподтверждённый допуск удерживает место до сверки. Разовые политики допускают не более одного прохода. Использованные места сохраняются при редактировании и перезапуске; неизменённые ограничения окончания сохраняют отсчёт от первого запуска и абсолютное окончание допустимых времён. Изменение максимума меняет общий лимит за всё время; лимит, равный числу использованных мест или меньший него, завершает расписание. Изменение частоты может начать новую фазу интервала при сохранении, как объясняет редактор.
|
||||
|
||||
Выбранное нативное расписание показывает **Желаемое состояние**, **Нативный календарь**, **Следующее пробуждение** или **Следующая проверка**, **Проверки** с отметкой **Учёт с**, **Запущенные проходы**, **Успешные проходы**, окончание и **Прошлые запуски**. Прошлый проход можно прочитать через **Открыть разговор**. **Расписание завершено** описывает окончание расписания; результат прохода проверяйте отдельно. Сохраняются последние 30 результатов; общие счётчики сохраняются за пределами этого окна. **Загрузить ещё Missions** загружает следующую страницу сохранённых объектов.
|
||||
|
||||
Список показывает условие и окончание только при их настройке, а **Проверки** — только при наличии условия.
|
||||
|
||||
### Существующие ежедневные расписания {#existing-daily-schedules}
|
||||
|
||||
Существующие ежедневные расписания используют сохранённую зону IANA и гражданское время. Каждый запуск имеет собственного координатора и карту миссии; выберите текущее дерево задач или прошлую дату/исход, чтобы прочитать результат в центре. Их история хранит 30 результатов. К этим записям применяются следующие ежедневные правила:
|
||||
|
||||
| Ситуация | Поведение расписания |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@
|
|||
| Часы переведены назад | Уже обработанный день не становится назначенным снова. |
|
||||
| Имеющийся запуск ещё ожидает | Не совмещает его с новым ежедневным запуском. |
|
||||
|
||||
Время создания тоже ограничивает допустимую ежедневную работу; старт нового расписания не разрешает обрабатывать даты до его существования. **Запустить сейчас** начинает отдельный ручной запуск, когда доступно; не заменяет ежедневные часы и не обходит ожидающий запуск. Показанное следующее время — проекция, не гарантия точного выполнения.
|
||||
Время создания тоже ограничивает допустимую ежедневную работу. **Запустить сейчас**, когда доступно для существующей ежедневной записи, начинает отдельный ручной запуск с защитой от перекрытия ожидающего запуска и сохраняет ежедневные часы. Показанное следующее время — расчётное и зависит от доступности службы и хоста.
|
||||
|
||||
**Отслеживаемые разговоры (необязательно)** добавляет ограниченный контекст выбранных разговоров. Только завершённый запуск продвигает курсоры обработанных сообщений; неудачный/прерванный запуск не поглощает этот контекст незаметно. Если инструкции с контекстом превышают вместимость, сократите инструкции или уберите отслеживаемые разговоры, не предполагая чтение неограниченной истории.
|
||||
|
||||
## Время жизни и перезапуск службы {#lifetime}
|
||||
|
||||
Значок **i** рядом с **Технические детали** объясняет техническую активность и затраты. В **Настройки → OpenCode** состояние надзора и ошибки остаются видимыми; общие объяснения находятся в **i**.
|
||||
|
||||
Для нативного расписания раскройте **Технические детали**, чтобы определить его **Основной носитель** и **Носитель сверки**, их нативные календари и наличие состояния **Очистка ожидает сверки**. В этих технических беседах нет действия для ввода сообщения, и они не отображаются в обычных списках выбора бесед и источников. Технические пробуждения, заголовки и сжатие контекста используют локальную модель без HTTP-запросов к провайдерам; нативные события и статистика всё же могут их включать, тогда как фактические проходы сохраняют обычные затраты на модели. Сжатие ограничивает обрабатываемый контекст, сохраняя идентичность беседы и календаря. Сохранённые нативные сообщения и события могут накапливаться до явной очистки завершённого расписания; 30 сохраняемых результатов Mission учитываются отдельно. Настройка глубины и **Расписание** показывают путь в поле **Файл конфигурации OpenCode:**, когда он доступен; настройка глубины отдельно указывает **Целевой файл проекта для записи**.
|
||||
|
||||
| Компонент | Что означает закрытие |
|
||||
| --- | --- |
|
||||
| Вкладка проекта CodeNomad | Закрывает проект в CodeNomad; нативные сессии продолжаются в OpenCode. Откройте проект снова, чтобы найти их. |
|
||||
| Всё окно, выход из приложения или перезагрузка интерфейса | Сохраняет подключения проектов для восстановления; не останавливает нативную работу или службу OpenCode. |
|
||||
| Серверная часть CodeNomad | Не останавливает OpenCode. Выполняемая разовая миссия продолжается: её координатор, задачи в нативных субагентах и инструменты миссии работают в OpenCode. Назначение задач независимым сессиям и доставка их отчётов координатору, миссии, которые готовит координатор, остановка разговоров выведенной из плана задачи, восстановление и Центр миссии ждут серверную часть, как и продолжение разовой миссии после перезапуска службы OpenCode. |
|
||||
| Работающее ежедневное расписание | Часы и выполнение проходов работают в плагине/службе OpenCode, в том числе при закрытых интерфейсе и серверной части CodeNomad. Хост выполнения и служба OpenCode должны оставаться запущенными. |
|
||||
| Перезапуск службы OpenCode | Теряет задания расписания в памяти процесса. Расписание с желаемым состоянием работы становится **Прервано** до явного **Возобновить**; повторное открытие CodeNomad не включает его заново. |
|
||||
| Активированное нативное расписание | Сохранённые календари OpenCode пробуждают проект и выполняют проходы при закрытых интерфейсе и серверной части CodeNomad, пока хост выполнения и служба доступны. |
|
||||
| Перезапуск службы OpenCode — исправное нативное расписание | Исправное расписание, желаемое состояние которого — работа, автоматически возобновляет календарь. Ожидающая работа сверяется с исходными идентификаторами до нового допуска. |
|
||||
| Существующее ежедневное расписание | Часы плагина работают при закрытом CodeNomad, пока служба жива. Перезапуск службы оставляет его в состоянии **Прервано**; явное **Возобновить** сверяет ожидающий запуск и вновь активирует ежедневные часы. |
|
||||
|
||||
После перезапуска проверьте ожидающий запуск и выберите **Возобновить**, когда готовы. Возобновление согласует существующий запуск без двойной отправки ввода координатору; не обещает продолжение прерванного нативного хода. Оборванный перезапуском ход может завершиться без отчёта с **Прервано перезапуском**. Не используйте перезапуск для повтора побочных эффектов инструментов.
|
||||
Для нативных расписаний явная приостановка, остановка, неудачная работа и неопределённые исходы сохраняют барьеры допуска после перезапуска. Проверьте сохранённый проход и исходный запрос; используйте **Проверить прогон** или **Обновить сохранённый запрос**, когда доступно, затем **Возобновить** только при допустимом состоянии. Нативное продолжение наблюдается с существующим идентификатором. Подтверждённый сбой или окончание без отчёта требует восстановления человеком; неполные доказательства оставляют проход неопределённым. Никогда не используйте перезапуск для повтора уже начатых эффектов инструментов.
|
||||
|
||||
[Поддерживать OpenCode запущенным](opencode.md#service-supervision) отдельно контролирует общую службу на её хосте выполнения. Первый запуск Миссии включает надзор, если вы не сохранили явное отключение; создание приостановленной Миссии сохраняет этот выбор. Отключение оставляет OpenCode работающим без автоматического перезапуска. Windows, macOS и WSL зависят от входа пользователя; нативный Linux использует linger пользователя с учётом разрешений. Сон, гибернация, срок жизни простаивающего WSL и управление внешним хостом ограничивают доступность; проверяйте состояние надзора отдельно от расписания.
|
||||
|
||||
Миссии работают в настольных приложениях и с отдельным [сервером](server.md) CodeNomad. Когда панель показывает **Центр миссии недоступен**, её сообщение называет причину:
|
||||
|
||||
|
|
@ -184,7 +237,9 @@
|
|||
|
||||
Разовая пауза даёт завершить текущую работу: координатор и его субагенты заканчивают текущий шаг, не начинают ничего нового и останавливаются, а панель показывает **Приостановка: текущие шаги завершаются.** **Прервать сейчас** вместо этого останавливает их сразу: прерывает нативное семейство миссии и приостанавливает координацию. Остановка тоже прерывает, отменяет семейные входящие/доставки в очереди и окончательна. Прерывания ограничены: **Некоторые субагенты, возможно, ещё работают.** означает, что одно из них неполное. Проверьте сохранённый результат и используйте явный повтор, а не предполагайте отмену всех последствий оболочки/инструментов. Возобновление сообщает координатору, что было приостановлено или прервано; он решает, как продолжать.
|
||||
|
||||
Ежедневные пауза/остановка отменяют расписание и запрашивают прерывание зарегистрированных корневых участников, включая координатора и независимых специалистов, но **не доказывают, что эти разговоры или их потомки остановились**. Проверьте нативные семейства. Остановка окончательна: остановленное расписание нельзя запустить, возобновить или запустить сейчас. **Проверить прогон** согласует ожидающий запуск без повторной отправки. Временные сбои плановой проверки показывают последнюю ошибку и повторяются автоматически; потерянный ответ пользовательского управления не вызывает автоматического повтора управления.
|
||||
**Приостановить** у нативного расписания закрывает будущий допуск и запрашивает прерывание допущенного прохода; **Возобновить** сверяет существующую работу перед повторной активацией. **Остановить…** действует окончательно. **Запустить сейчас** использует ту же защиту от ожидающего прохода и квоту запусков, сохраняя календарь. **Проверить прогон** сверяет исходный проход. Сохраняйте неподтверждённое действие и используйте **Обновить сохранённый запрос**; пассивное обновление и переподключение сохраняют этот запрос. Уже начатые эффекты инструментов могут продолжаться после прерывания.
|
||||
|
||||
Существующие ежедневные записи сохраняют свои действия **Приостановить**, **Остановить…**, **Возобновить**, **Запустить сейчас** и **Проверить прогон**. Приостановка и остановка отменяют их часы и запрашивают прерывание зарегистрированных корневых разговоров, включая независимых специалистов. Проверьте их нативные семейства и любой результат частичного прерывания. Временные сбои ежедневной проверки повторяются с показанным предупреждением; потерянный ответ на пользовательское действие остаётся связанным с исходным запросом. Остановленная запись остаётся окончательно остановленной.
|
||||
|
||||
Для разового координатора, простаивающего без отчёта, основное действие ▶ может проверить и восстановить его; задачи имеют целевое восстановление пропавшего отчёта, когда допустимо. Сначала восстановление проверяет нативную активность, потомков и ожидания; неизвестная активность не допускает ввод. Оно не повторяет назначения и не заменяет план. Откройте разговоры для разрешения оставшихся вопросов, разрешений, оболочек или фоновой работы.
|
||||
|
||||
|
|
@ -192,12 +247,28 @@
|
|||
|
||||
## Изменение, удаление и очистка разговоров {#cleanup}
|
||||
|
||||
Используйте **Изменить выбранную миссию** для редактируемой разовой миссии; при конфликте закройте/откройте редактор для загрузки текущей ревизии, а не перезаписи. Если цель или заметки меняются, пока форма ещё загружается, она сообщает, что миссия изменилась после открытия, и не заполняет их; выберите **Открыть заново**, чтобы изменить текущую версию. Ежедневные расписания не предлагают изменение/удаление.
|
||||
Если цель или заметки меняются, пока форма ещё загружается, она сообщает, что миссия изменилась после открытия, и не заполняет их; выберите **Открыть заново**, чтобы изменить текущую версию.
|
||||
|
||||
Между проходами у приостановленного или работающего нативного расписания можно изменить название, инструкции, заметки, запрошенные профили, режим сессий задач, источники, календарь и ограничения, если объект не удерживается проходом или неразрешённым действием управления. Приостановленное расписание остаётся приостановленным; активированное сверяет календарь перед продолжением. Сохранение сохраняет прошлые результаты, обработанные источники, число запусков и точку отсчёта первого запуска; неизменённые ограничения окончания сохраняют абсолютное окончание допустимых времён.
|
||||
|
||||
Во время допущенного прохода только инструкции и заметки следуют действующим правилам редактирования текста: изменения записываются в его историю и сохраняются для будущих проходов. Существующие задачи, задания, профили и идентификаторы выполнения сохраняют значения, зафиксированные при допуске. Структурные поля остаются заблокированными до завершения прохода, в том числе после приостановки. Сценарий остаётся заблокированным, как только создана хотя бы одна задача, включая завершённые и снятые. Неподтверждённый допуск или управление либо неполные доказательства целостности оставляют структурные настройки доступными только для чтения. Создание и редактирование используют общую проверку календаря; предпросмотр применяет изменённое последнее допустимое время к сохранённому следующему пробуждению, включая запоздалое.
|
||||
|
||||
Источник локальных веток включает ветки без worktree и читает только локальные ссылки. Поддерживается до 200 локальных веток; при большем списке источник отображается недоступным. **Запустить** и **Возобновить** запрашивают согласие с точными путями конфигурации, когда активация доступна. **Запустить сейчас** остаётся недоступным с объяснением, пока нативное расписание выключено; включите `experimental.schedules` в конфигурации OpenCode, затем обновите данные. При потере ответа активации исходный запрос сохраняется для проверки квитанции.
|
||||
|
||||
До первого Play можно вернуться к немедленному запуску без условия, сохранив отслеживаемые разговоры в **Параметрах**. Play читает и фиксирует их полный стабильный префикс сообщений (до 32 сообщений на источник, в пределах 16 384 символов входа запуска). Отсутствующий, перемещённый или слишком большой источник блокирует запуск с объяснением. Повторная попытка сохраняет исходный зафиксированный контекст и идентичность запуска.
|
||||
|
||||
Если чтение квитанции подтверждает частичную подготовку, появляется **Повторить**. Нажатие ещё раз проверяет ту же квитанцию и продолжает только исходное сохранённое изменение, оставляя миссию на паузе.
|
||||
|
||||
До первого запуска новой подготовленной миссии можно изменить название, инструкции, заметки, сценарий, запрошенных агентов/модели/режим размышления, режим сессий задач и **Расписание**. Можно переключаться между немедленным, отложенным, повторяющимся и условным запуском, сохраняя ту же Миссию. Сохранение оставляет миссию на паузе и подготавливает нового пустого координатора; исходный разговор сохраняется. Только Play начинает работу или активирует расписание. После запуска инструкции и заметки изменяются по правилам активной миссии. Если сохранение не подтверждено, **Обновить сохранённый запрос** читает исходный результат; сохраняйте этот запрос, пока частичная подготовка блокирует запуск, редактирование текстов и удаление.
|
||||
|
||||
Используйте **Изменить выбранную миссию** для редактируемой разовой миссии или нативного расписания. Прочитайте пояснения рядом с заблокированными полями. При конфликте ревизий выберите **Загрузить текущую конфигурацию**, затем откройте **Предыдущий черновик для сравнения**, чтобы проверить сохранённый черновик перед повторным сохранением. Существующие ежедневные расписания сохраняют элементы собственной панели инструментов.
|
||||
|
||||
Для нативного расписания **Удалить выбранную миссию…** отключает будущие проходы до удаления записи. Разговоры и результаты сохраняются; ожидающая сверки очистка остаётся видимой в **Технические детали**. Сохраняйте неподтверждённое удаление и обновляйте его исходный запрос.
|
||||
|
||||
1. Для ненужной разовой миссии выберите **Удалить выбранную миссию…** и проверьте подтверждение.
|
||||
2. При желании выберите **Также удалить разговоры специалистов, созданные для этой миссии** перед подтверждением.
|
||||
3. Проверьте **Очистка разговоров** для количества удалённых/сохранённых/ожидающих. Повторяйте только оставшуюся записанную очистку, если предложено.
|
||||
|
||||
Удаление миссии не равно остановке; сначала остановите и проверьте оставшуюся работу. Координаторы и повторно используемые разговоры сохраняются. Общие, перемещённые, имеющие детей специалисты или специалисты с непроверяемой принадлежностью тоже могут сохраняться. Частичная очистка не откатывается и не завершается незаметно в фоне. Просмотрщик обзора хранит 20 последних завершённых очисток. Удаление миссий освобождает их хранилище в проекте: более старые удалённые миссии стираются автоматически.
|
||||
Удаление миссии не равно остановке; сначала остановите и проверьте оставшуюся работу. Координаторы и повторно используемые разговоры сохраняются. Общие, перемещённые, имеющие детей специалисты или специалисты с непроверяемой принадлежностью тоже могут сохраняться. Частичная очистка не откатывается и не завершается незаметно в фоне. Карточка миссии может исчезнуть, пока подтверждение удаления ещё ожидается, даже если очистка разговоров показывает 0 ожидающих. Используйте исходное действие **Повторить** до подтверждения удаления; оно сохраняет тот же запрос удаления и те же параметры. Просмотрщик обзора хранит 20 последних завершённых очисток. Удаление миссий освобождает их хранилище в проекте: более старые удалённые миссии стираются автоматически.
|
||||
|
||||
Нативная работа и управление службой описаны в разделах [Разрешения](permissions.md), [Разговоры](conversations.md) и [Настройка OpenCode](opencode.md).
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ CodeNomad подключается к общей службе OpenCode на хо
|
|||
| Рекомендуемая | OpenCode **2.2.5** | Проверенная цель, отличная от технического минимума. |
|
||||
| Непроверенная | Новая или пользовательская версия, не прошедшая полную проверку | Не отклоняется лишь из-за отсутствия тестирования; проверяются реальные API-контракты подключённой службы. |
|
||||
|
||||
Необходимые контракты сессий, разрешений, форм, входящих и окружения сессий тоже должны быть доступны. Одна версия исполняемого файла не доказывает их поддержку подключённой службой. Если настройка сообщает о неподдерживаемой работающей службе, выберите/установите поддерживаемый CLI и выполните явный перезапуск общей службы, когда будете готовы.
|
||||
Подключённая служба должна предоставлять необходимые контракты сессий, разрешений, форм, входящих и окружения сессий. Нативное расписание Миссий также проверяет API расписаний, комплект плагина/провайдера и действующую настройку `experimental.schedules`. Доступность определяется этими используемыми возможностями, включая совместимые будущие версии. Если настройка сообщает о неподдерживаемой работающей службе, выберите/установите поддерживаемый CLI и выполните явный перезапуск общей службы, когда будете готовы.
|
||||
|
||||
## Хост, WSL и общая служба {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ CodeNomad подключается к общей службе OpenCode на хо
|
|||
|
||||
OpenCode владеет глобальной регистрацией службы, сессиями, сообщениями и базой данных. CodeNomad не создаёт отдельную базу OpenCode для окна или проекта. Закрытие вкладки проекта закрывает этот проект в CodeNomad; сессии продолжают работать в OpenCode и снова появляются при повторном открытии проекта. CodeNomad никогда не выгружает нативные Location. Закрытие всего окна, выход или перезагрузка сохраняют подключения проектов для восстановления; ни эти действия, ни завершение серверной части не останавливают общий демон. См. [проекты](projects.md).
|
||||
|
||||
## Поддерживать OpenCode запущенным {#service-supervision}
|
||||
|
||||
До первого запуска Mission кнопка **Оставить выключенным при первом запуске Mission** сохраняет явное отключение без предварительной установки надзора.
|
||||
|
||||
В **Настройки → OpenCode** параметр **Поддерживать OpenCode запущенным** устанавливает системный надзор на узле выполнения, общий для всех профилей CodeNomad этой службы. Он работает независимо от Missions, в том числе при закрытом CodeNomad. Первый запуск Mission включает его, если вы заранее явно не отключили; создание Mission на паузе сохраняет выбор. Пауза, остановка или удаление Mission оставляют настройку неизменной.
|
||||
|
||||
При отключении надзор удаляется, а OpenCode продолжает работать и больше не перезапускается. Для длительной остановки сначала отключите надзор, затем явно остановите OpenCode. При включённом надзоре после `opencode service stop` происходит перезапуск.
|
||||
|
||||
**Работает · надзор активен** требует активного системного надзирателя и здоровой службы OpenCode. **Установлено · служба запускается**, **Установка или удаление надзора…** и **Ошибка надзора** обозначают разные состояния. Один лишь ответ службы оставляет надзор неподтверждённым. Подробности показывают причину, последнюю попытку, повтор надзирателя и системную проверку восстановления, когда они известны. **Проверить надзор** только читает состояние; **Повторить надзор** явно восстанавливает запрошенную установку. Неподтверждённое действие сохраняет исходный запрос; **Повторить исходное действие** отправляет только то же точное действие. Изменившаяся версия настроек обновляется для проверки вместо перезаписи.
|
||||
|
||||
Windows, macOS и WSL запускаются после входа. WSL требует перенаправления localhost выбранного дистрибутива. В нативном Linux linger запускает **все включённые службы пользователя** до входа и сохраняет их после выхода. При отключении linger восстанавливается только если его включил CodeNomad и он не нужен другому надзору; личные настройки сохраняются. Если восстановление linger может остановить освобождённые от надзора процессы OpenCode при отсутствии открытого пользовательского сеанса, оно откладывается и остаётся видимым с **Повторить надзор**. Повторите попытку, когда открыт пользовательский сеанс или освобождённые от надзора процессы OpenCode завершились и восстановление стало безопасным. Если Linux требует разрешения, предоставьте его на узле выполнения и повторите; запрошенная настройка остаётся сохранённой. Сном и гибернацией управляет ОС. Недоступные или неподдерживаемые узлы показывают отключённый элемент управления с объяснением.
|
||||
|
||||
## Обновление, перезапуск и перезагрузка {#maintenance}
|
||||
|
||||
1. В **Настройки → OpenCode** выберите **Проверить состояние и обновления**.
|
||||
|
|
@ -41,7 +53,7 @@ OpenCode владеет глобальной регистрацией служб
|
|||
| Действие | Область и эффект | Безопасное восстановление |
|
||||
| --- | --- | --- |
|
||||
| Обновить CLI | Меняет поддерживаемый установленный исполняемый файл; не перезапускает работающий демон неявно. | После сбоя проверьте установленную и работающую версии; повторяйте явно только после устранения ошибки. |
|
||||
| Перезапустить службу | Заменяет общий процесс и прерывает активную работу всех подключённых клиентов OpenCode; история сессий остаётся. | Переподключитесь и проверьте разговоры и миссии по расписанию; перезапуск не означает автоматического продолжения. |
|
||||
| Перезапустить службу | Заменяет общий процесс и прерывает активную работу всех подключённых клиентов OpenCode; история сессий остаётся. | Исправные работающие нативные расписания Миссий восстанавливаются автоматически; проверьте ожидающие проходы и любую паузу, ошибку или неопределённость. Существующие ежедневные записи требуют действия **Возобновить**. Разовые разговоры и их элементы восстановления проверяйте отдельно; см. [Миссии](missions.md#lifetime). |
|
||||
| Перезагрузить конфигурацию OpenCode | Сохраняет процесс, но пересоздаёт **все загруженные Location** для всех клиентов. Отменяет ожидающие разрешения/формы и закрывает терминалы/фоновые команды; активная работа может прерваться. | Перепроверьте ресурсы и ожидающую работу после перезагрузки. Не повторяйте запрос или изменение лишь из-за потерянного ответа. |
|
||||
|
||||
Используйте **Перезагрузить конфигурацию OpenCode** (в **Устранение неполадок**, если работающая служба это поддерживает) явно, если сохранённая конфигурация или установленный плагин остаются недоступны и прерывание допустимо. Это не безобидное обновление отображения. CodeNomad не использует старый выбранный CLI для понижения версии нового работающего демона.
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
### Вопросы к вам {#questions}
|
||||
|
||||
Вопросы вам задаёт только координатор. Задачи и их помощники возвращают свои вопросы разговору, который их запустил, с вариантами, доводами, последствиями, обратимостью и рекомендацией, и продолжают работу, не зависящую от ответа. Координатор сначала решает то, что может, и объединяет оставшиеся вопросы в одну форму в разделе **Нужен ваш ответ**; раздел [Вопросы и решения](missions.md#questions) объясняет, о чём он спрашивает и как помечаются решения. Каждый координатор, разовый или ежедневный, и каждое назначение задачи получают это правило как инструкцию; Исследование добавляет проверки решений [ниже](#wayfinder-checks).
|
||||
Вопросы вам задаёт только координатор. Задачи и их помощники возвращают свои вопросы разговору, который их запустил, с вариантами, доводами, последствиями, обратимостью и рекомендацией, и продолжают работу, не зависящую от ответа. Координатор сначала решает то, что может, и объединяет оставшиеся вопросы в одну форму в разделе **Нужен ваш ответ**; раздел [Вопросы и решения](missions.md#questions) объясняет, о чём он спрашивает и как помечаются решения. Каждый координатор, разовый или работающий по расписанию, и каждое назначение задачи получают это правило как инструкцию; Исследование добавляет проверки решений [ниже](#wayfinder-checks).
|
||||
|
||||
Отвечайте в панели прерываний (**Ответить** в разделе **Нужен ваш ответ**): только такой ответ считается вашим. Ответ, набранный в чате, — обычное сообщение.
|
||||
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
|
||||
Координатору предписано публиковать сводку после первоначального плана, после каждой завершённой им задачи, всякий раз, когда он начинает ждать вас, и перед итоговым отчётом; см. [Сводки и запросы обновлений](missions.md#briefings). Сводки, резюме задач и итоговый отчёт пишутся для вас: на вашем языке, несколькими короткими предложениями, результат в начале, а идентификаторы и команды остаются в доказательствах.
|
||||
|
||||
Итоговый отчёт сообщает, что сделано, что осталось и что требует вашего решения. Разовая миссия завершается только как выполненная: когда без вас ничего не может продвинуться, координатор спрашивает вас и ждёт, а не завершается сбоем, и только вы останавливаете миссию. Ежедневный запуск может завершиться и сбоем.
|
||||
Итоговый отчёт сообщает, что сделано, что осталось и что требует вашего решения. Разовая миссия завершается только как выполненная: когда без вас ничего не может продвинуться, координатор спрашивает вас и ждёт, а не завершается сбоем, и только вы останавливаете миссию. Запланированный проход может завершиться и сбоем.
|
||||
|
||||
### Что CodeNomad проверяет в каждой миссии {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
- Принимаются только пять ролей Исследования.
|
||||
- Задача-решение завершается только собственным решением координатора или вашим ответом. Решение координатора содержит вопрос, решение и обоснование и никогда не записывается как ваш ответ.
|
||||
- Ваш ответ засчитывается, только если вы дали его в интерфейсе CodeNomad. CodeNomad привязывает его к задаче по метке вопроса: вопрос задан из разговора координатора, его текст заканчивается меткой этой задачи и не называет другого решения, ответ дан после объявления задачи и не закрыл другое решение. Каждый вопрос объединённой формы привязывается отдельно; вопрос, называющий два решения, не закрывает ни одного. Ответ на вопрос, заданный из собственного разговора задачи-решения, тоже принимается.
|
||||
- Собственный отчёт задачи не может указывать, кто принял решение. Перед успешным завершением миссии или ежедневного запуска CodeNomad снова проверяет каждое выполненное решение, включая метки.
|
||||
- Собственный отчёт задачи не может указывать, кто принял решение. Перед успешным завершением миссии или запланированного прохода CodeNomad снова проверяет каждое выполненное решение, включая метки.
|
||||
- Эти проверки действуют в режиме **Нативные субагенты**. В режиме **Независимые сессии** ответственные сами отправляют сводки, а координатор записывает каждый итог в заметки, указывая, кто решил; CodeNomad это не проверяет.
|
||||
|
||||
Только инструкции: когда выбор считается легко отменяемым, качество сводок и исследований и ограничение планированием.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode size motoru verir. CodeNomad size kokpiti verir. OpenCode'un paylaşıl
|
|||
|
||||
## Uzun işleri Görevlere devredin {#missions}
|
||||
|
||||
İşi bir kez tanımlayın. Görevin koordinatörü onu bir görev ağacı olarak planlar, sınırlı işleri yerel OpenCode konuşmalarına devreder ve sonuçları kanıtlarıyla birlikte raporlar. Bir görevi bir kez veya her gün çalıştırın; bir görev oluşturmak, siz seçene kadar hiçbir işi başlatmaz. İş akışı rehberleri hata ayıklamayı, ürün teslimini ve iki modelin kör bir hakemle karşılaştırılmasını kapsar. Bkz. [Görevler](missions.md).
|
||||
İşi bir kez tanımlayın. Görevin koordinatörü onu bir görev ağacı olarak planlar, sınırlı işleri yerel OpenCode konuşmalarına devreder ve sonuçları kanıtlarıyla birlikte raporlar. Bir kez, seçilen saatlerde veya aralıklarla ya da doğrulanmış bir proje değişikliğinden sonra çalıştırın; oluşturma, açıkça başlatmanız için duraklatılmış bir Görev kaydeder. İş akışı rehberleri hata ayıklamayı, ürün teslimini ve iki modelin kör bir hakemle karşılaştırılmasını kapsar. Bkz. [Görevler](missions.md).
|
||||
|
||||
## Telefonunuzdan çalışmaya devam edin {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ Kesin sınırlar için, bkz. [tarayıcı önizlemeleri ve SideCars](browser-side
|
|||
| Çalışma kopyaları ve konuşmalar arasında çalışın | [Projeler](projects.md), [Konuşmalar](conversations.md), [Dosyalar](files.md) |
|
||||
| Geçmişi bulun veya güvenle temizleyin | [Geçmiş](history.md) |
|
||||
| Yerel soruları ve izinleri yanıtlayın | [İzinler](permissions.md) |
|
||||
| Sınırlı veya günlük işleri koordine edin | [Görevler](missions.md) |
|
||||
| Sınırlı veya zamanlanmış işleri koordine edin | [Görevler](missions.md) |
|
||||
| Yürütme ve entegrasyonları yapılandırma | [OpenCode](opencode.md), [Sağlayıcılar](providers.md), [Entegrasyonlar](integrations.md) |
|
||||
| Başka bir cihaz bağlayın | [Uzaktan Kontrol](remote-control.md), [Güvenlik](security.md), [Mobil](mobile.md) |
|
||||
| Çalışma alanını uyarlayın | [Ayarlar](settings.md), [Kısayollar](shortcuts.md), [Ses](voice.md), [Uzantılar](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@ Görevler, kalıcı bir proje planını OpenCode'un kendi konuşmalarıyla birle
|
|||
|
||||
1. Sağ panelde **Görevler** bölümünü açıp **Görev oluştur** eylemini seçin. Form, oluşturana veya iptal edene kadar görev listesinin ve seçili görevin görev düğümlerinin yerini alır.
|
||||
2. **Görev ne yapmalı?** alanını doldurun ve ilk cümleden otomatik doldurulan **Başlık** alanını kontrol edin (en fazla 60 karakter).
|
||||
3. **Ne zaman** altında **Bir kez** veya **Her gün saat** seçeneğini belirleyin. Günlük işler için gösterilen saat dilimini doğrulayın; **Değiştir** ile bir IANA saat dilimi girebilirsiniz.
|
||||
3. **Bir kez, hemen** ve **Koşulsuz** için **Zamanlama** bölümünü kapalı bırakın veya sıklık, koşul ve isteğe bağlı sınırlar seçmek için genişletin. Zamanlanmış iş oluşturmadan önce **Kayıtlı saat dilimi (IANA)** değerini ve önizlemeyi doğrulayın.
|
||||
4. **Rehber**, **Notlar**, **Ajanlar ve modeller**, **Görev oturumları**, projenin **Alt oturum iç içe geçme derinliği** ayarı, kayıtlı özetler ve isteğe bağlı takip edilen konuşmalar için **Seçenekler** bölümünü genişletin. **İki modeli karşılaştır** seçildiğinde **Model A** ve **Model B** seçicileri ana formda görünür.
|
||||
5. **Oluştur** eylemini seçin. Yeni kaydı seçip hazır olduğunuzda ayrı ▶ eylemini kullanın: tek seferlik işler için **Görevi başlat**, günlük zamanlama için **{id} zamanlamasını başlat**.
|
||||
5. **Oluştur** eylemini seçin. Duraklatılmış kaydı seçip inceleyin, ardından hazır olduğunuzda ▶ **Başlat** eylemini kullanın. Hemen başlayan koşulsuz iş tek bir görev başlatır; gecikmeli, yinelenen veya koşullu bir ilke yerel zamanlamayı etkinleştirir.
|
||||
|
||||
**Oluşturmak hiçbir zaman işi başlatmaz.** Tek seferlik görev hazırlanır; günlük zamanlama duraklatılmış durumdadır. Ne oluşturma ne de özeti kaydetme bir zamanlayıcıyı devreye alır. Oluşturma seçili proje/Location kapsamındadır ve istenen profillerle görev oturumu ilkesini sabitler. Doğrulama mesajları ilgili alanın yanındadır; hiçbir şey gönderilmediğini belirten bir ret durumunda taslak düzeltilebilmesi için korunur. Tek seferlik bir görevin koordinatörü sizin için takip işi gibi ayrı bir görev de hazırlayabilir: bu görev listede **Hazır** olarak görünür ve diğer her görev gibi yalnızca ▶ eylemiyle başlar.
|
||||
**Oluştur, duraklatılmış bir Görev kaydeder.** İş ve yerel takvimler yalnızca başlatma eylemiyle etkinleştirilir; oluşturmak veya özet kaydetmek, paylaşılan hizmet gözetimi tercihini korur. Oluşturma seçili proje/Location kapsamındadır ve istenen profillerle görev oturumu ilkesini kaydeder. Doğrulama ilgili alanın yanında görünür; hiçbir şey gönderilmediğini belirten bir ret, taslağı düzeltme için korur. Tek seferlik koordinatör ayrı bir devam görevi de hazırlayabilir: **Hazır** olarak görünür ve kendi ▶ eylemiyle başlar.
|
||||
|
||||
Kayıtlı tek seferlik görevler yürütme kontrollerini, görev ağacını, konuşmalarını ve nihai raporlarını korur. Mevcut günlük zamanlamalar günlük kontrolleri ve [günlük zamanlama kurallarıyla](#existing-daily-schedules) listede kalır. Kaydı seçip o nesne için sunulan kontrolleri kullanın.
|
||||
|
||||
### İş akışı rehberleri {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@ Sonuçlar aynı yoldan geri döner. Yerel alt ajan sonucunu kendisini başlatan
|
|||
|
||||
Sonraki bir iş, önceki çalışmaya iki yolla dayanır. Bağımlılıklar çalışmanın sırasını belirler: koordinatör bir işi, bağlı olduğu işler tamamlandığında başlatır ve ilgili sonuçları özete ya da atamanın yanına koyar; bağımsız oturumlar bu raporları haritada da okuyabilir. Tamamlanmış bir işin kendi konuşmasını sürdürmek için koordinatör, o konuşmayı yeniden kullanan bir devam işi bildirir: devam işi orada, OpenCode'un [sıkıştırmış](history.md#compact-session) olabileceği o konuşmanın güncel bağlamıyla sürer; daha önceki ayrıntılar arama için saklanan geçmişte kalır. Hata ayıklama rehberi iki incelemeyi de uygulayıcıya bu şekilde geri verir. **Bağımsız oturumlar** ile koordinatör bir işi mevcut bir ana konuşmaya da atayabilir; bu konuşma, ilgisiz önceki çalışmalar dahil kendi bağlamını korur. Aksi halde iş kendi atamasıyla yeni bir konuşmada başlar ve başka bir işin konuşma dökümü ona koordinatör ya da bir arama aracılığıyla ulaşır. **İki modeli karşılaştır** rehberinde hakemin ataması ve bağlamı hedefi ve notları içermez; özeti anonimleştirilmiş çıktıları taşır.
|
||||
|
||||
Günlük bir zamanlamada her çalıştırma kendi koordinatörü ve haritasıyla başlar. İlk mesajı, zamanlamanın talimatlarını ve son tamamlanan çalıştırmadan bu yana [takip edilen konuşmalarına](#daily-schedules) eklenen yeni kullanıcı ve asistan metnini içerir; önceki çalıştırmaların sonuçları zamanlamanın geçmişinde kalır. Bir çalıştırma içinde başlatılan konuşmalar o çalıştırmanın haritasını okuyabilir. Yeni bir tek seferlik görev de yeni bir haritayla başlar: sonuçları aktarmak için onları hedefine veya **Notlar** bölümüne yazın ya da koordinatörün, hedefini ve notlarını kendisinin yazdığı bir devam görevi hazırlamasına izin verin.
|
||||
Her zamanlanmış geçiş, kayıtlı talimatlarını ve sabitlenmiş girdilerini kullanarak kendi koordinatörü ve görev haritasıyla başlar. Takip edilen konuşmalar sınırlı yeni kullanıcı ve asistan metni sağlar; işlenen mesaj imleçleri başarılı bir geçişten sonra ilerler. Önceki geçişler zamanlama geçmişinde kalır. Bir geçiş içinde başlatılan konuşmalar o geçişin haritasını okuyabilir. Yeni bir tek seferlik görevin ayrı bir haritası vardır: aktarılacak sonuçları hedefine veya **Notlar** bölümüne yazın ya da koordinatörden bu bağlamla bir devam görevi hazırlamasını isteyin.
|
||||
|
||||
### Önceki konuşmalarda arayın {#context-search}
|
||||
|
||||
CodeNomad çalıştığı sürece projedeki her ajan konuşması, görev koordinatörleri ve işler dahil, CodeNomad'in oturum arama aracına (`codenomad.session_search`, OpenCode'un Code Mode özelliği üzerinden kullanılır) sahiptir. Araç, konuşmanın proje klasöründeki kayıtlı konuşmalarda **Tüm çalışma alanı** [aramasıyla](history.md#full-history-search) aynı sıralamayla arar ve her konuşmanın başlığı ve kimliğiyle kısa alıntılar, ayrıca bir konuşmanın tamamını okumanın yolunu döndürür. OpenCode araç izinleri geçerlidir. Ajan, kendisi karar verdiğinde veya hedef, notlar ya da mesajınız bunu istediğinde arar; örneğin önceki bir görevin veya işin vardığı sonucu bulmak için. CodeNomad kapalıyken yürüyen günlük bir çalıştırma bu araç olmadan çalışır.
|
||||
CodeNomad çalıştığı sürece projedeki her ajan konuşması, görev koordinatörleri ve işler dahil, CodeNomad'in oturum arama aracına (`codenomad.session_search`, OpenCode'un Code Mode özelliği üzerinden kullanılır) sahiptir. Araç, konuşmanın proje klasöründeki kayıtlı konuşmalarda **Tüm çalışma alanı** [aramasıyla](history.md#full-history-search) aynı sıralamayla arar ve her konuşmanın başlığı ve kimliğiyle kısa alıntılar, ayrıca bir konuşmanın tamamını okumanın yolunu döndürür. OpenCode araç izinleri geçerlidir. Ajan, kendisi karar verdiğinde veya hedef, notlar ya da mesajınız bunu istediğinde arar; örneğin önceki bir görevin veya işin vardığı sonucu bulmak için. CodeNomad kapalıyken yürüyen zamanlanmış bir geçiş bu araç olmadan çalışır.
|
||||
|
||||
## Profiller, iş devri ve derinlik {#profiles}
|
||||
|
||||
Derinliği kaydetmek, proje yapılandırmasında etkin yerel zamanlamaları korur. Projedeki açık bir `schedules:false` kapalı kalır; derinliği kaldırmak zamanlama ayarını korur. Yerel ayarlar kaydedilmeden önce aynı katmandaki JSON ve JSONC dosyalarının çakışması giderilmelidir.
|
||||
|
||||
1. **Yeni görev varsayılanları** ayarını düzenlemek için Görevler dişli simgesini veya **Ayarlar → Görevler** yolunu kullanın.
|
||||
2. **Uygulanacak kapsam** altında **Tüm görevler** veya bir rehber seçin; ardından **Görev oturumları** ayarını ve **Ajanlar ve modeller** altında her rol için bir satırı (önce koordinatör, sonra rehberin rolleri) belirleyin. Her seçim hemen kaydedilir; Kaydet düğmesi yoktur.
|
||||
3. Bir rehber kapsamında satırlar, **Genel** olarak işaretlenmiş genel değerleri gösterir. Bir alanı değiştirmek o rolü bu değerlerden başlayarak özelleştirir; geri alma simgesi yeniden genel değerleri kullanır. **Sıfırla** (onaydan sonra) yalnızca gösterilen kapsamın ajanlarını ve modellerini temizler.
|
||||
|
|
@ -118,7 +122,7 @@ CodeNomad çalıştığı sürece projedeki her ajan konuşması, görev koordin
|
|||
| Ayar | Kapsam ve etki |
|
||||
| --- | --- |
|
||||
| Yeni görev varsayılanları | Yapılandırma genelinde yalnızca **yeni** görevleri etkileyen varsayılanlar; rehber istisnası genel varsayılanı geçersiz kılar. |
|
||||
| Ajan / Model / Düşünme | Oluşturmayla kaydedilen koordinatör/rol tercihleri; meşgul bir aktör sonraki işlere uyması için sessizce değiştirilmez. Günlük çalıştırmalar sabitlenmiş profil tercihlerini yeniden kullanır. |
|
||||
| Ajan / Model / Düşünme | Oluşturmayla kaydedilen koordinatör/rol tercihleri; meşgul bir aktör sonraki işlere uyması için sessizce değiştirilmez. Zamanlanmış geçişler kayıtlı profil tercihlerini kullanır; düzenlemeler gelecek geçişlere uygulanır. |
|
||||
| OpenCode alt ajanları | Görev oturumları koordinatörün altında subagent/all ajanlarını kullanır. |
|
||||
| Bağımsız oturumlar | Bildirilen görev oturumları primary/all ajanlarını ve bağımsız konuşmaları kullanır; bu, dahili OpenCode yardımcılarını devre dışı bırakmaz. |
|
||||
| Alt oturum iç içe geçme derinliği | Yalnızca Görevler için değil, oradaki **tüm** OpenCode konuşmaları için proje yapılandırması. Boş bırakılırsa devralınır: **Devral**, proje geçersiz kılmasını ve boş bıraktığı ya da yalnızca genel **Görselleri bir kez sakla** değerini içeren `experimental` bloğunu kaldırır. OpenCode projenin `experimental` bloğunu bir bütün olarak okur; bu nedenle orada tutulan diğer deneysel ayarlar genel derinliği gizlemeye devam eder. OpenCode'un yerleşik **General** alt ajanı varsayılan olarak alt ajan başlatamaz, bu yüzden konuşmaları hiç iç içe geçmez: herhangi bir proje derinliğini kaydetmek, alt ajanların o derinliğe kadar kendi alt ajanlarını başlatmasına izin veren ve diğer yapılandırma dosyalarındaki General kısıtlamalarından öncelikli olan bir kuralı da aynı dosyaya ekler (1 derinliğinde OpenCode'un derinlik sınırı iç içe geçmeyi yine engeller); **Devral** onu kaldırır. **Etkin OpenCode derinliği** altında düzenleyici, General alt ajanlarının iç içe geçip geçemeyeceğini belirtir. Proje General'ı eski biçimde tek bir `permission` değeriyle ya da hem `task` hem `subagent` zaten ayarlanmış olarak tanımlıyorsa yalnızca derinlik kaydedilir ve düzenleyici bunu belirtir: General için alt ajanlara o dosyada kendiniz izin verin. Kaydetme OpenCode'u yeniden yüklemez veya çalışan konuşmaları değiştirmez. |
|
||||
|
|
@ -137,9 +141,51 @@ Reddedilen veya doğrulanmayan bir seçim asla yeniden gönderilmez: kayıtlı d
|
|||
|
||||
Özetler talimatları ve istenen tercihleri saklar; görev planlarını, sonuçları, konuşmaları, zamanlama saatlerini veya yürütme yetkisini saklamaz. Bir özeti yüklemek sıradan oluşturma taslağını doldurur; hiçbir şeyi başlatmaz veya zamanlamaz. Özeti kaldırmak mevcut görevleri/konuşmaları korur. **Ayarlar → Görevler → Kayıtlı özetler** kitaplığı yönetir; kitaplık okunamaz hale gelirse **Kaydedilmiş kitaplığı sıfırla** (onaydan sonra) yürütmeleri değil, kayıtlı özetleri kaldırır. Zamanlamalarda özeti kaydetme eylemi yoktur.
|
||||
|
||||
## Günlük saat, kaçırılan günler ve takip edilen konuşmalar {#daily-schedules}
|
||||
## Zamanlama, kaçırılan zamanlar ve takip edilen konuşmalar {#daily-schedules}
|
||||
|
||||
Günlük zamanlamalar geçen 24 saatlik aralığı değil, kayıtlı IANA saat dilimini ve takvim saatini kullanır. Saatler zamanlamanın saat diliminde gösterilir; görüntüleyenin saat diliminden farklıysa belirtilir. Her çalıştırmanın kendi koordinatörü ve yalıtılmış görev haritası vardır; seçili zamanlama güncel görev ağacını ve tarih/sonuç geçmişini gösterir. Geçmiş bir çalıştırmayı seçerek merkezi alanda okuyabilirsiniz. Geçmiş 30 sonuçla sınırlıdır.
|
||||
### Yerel zamanlama {#native-scheduling}
|
||||
|
||||
Etiketlerin yanındaki **i** simgeleri seçenekleri açıklar: üzerine gelin, klavyeyle odaklayın veya dokunun.
|
||||
|
||||
Oluşturma veya düzenleme ekranında **Zamanlama** bölümünü genişletin. **Sıklık** şu seçenekleri sunar:
|
||||
|
||||
| Seçim | Zamanlama |
|
||||
| --- | --- |
|
||||
| Bir kez, hemen | Başlatma eyleminde tek bir çalıştırma; isteğe bağlı koşul aşağıda açıklandığı gibi denetlenir. |
|
||||
| Bir kez, belirli tarihte | IANA saat diliminde kayıtlı tek tarih ve saat; başlatma anında geçmiş olan tarih reddedilir. |
|
||||
| Bir kez, gecikmeyle | Başlatma eyleminde başlayan, dakika veya saat cinsinden pozitif tam sayı süre. |
|
||||
| Belirli aralıklarla | En az 15 dakika; ilk uyanma başlatmadan bir aralık sonradır. |
|
||||
| Her gün | Kayıtlı saat diliminde tek bir yerel takvim saati. |
|
||||
| Seçilen hafta günleri | İşaretlenen hafta günlerinde tek bir saat. |
|
||||
| Günde birkaç kez | 09:15 ve 17:45 gibi en fazla sekiz bağımsız saat; yinelenen saatler tek bir geçişi paylaşır. |
|
||||
| Her ayın belirli günü | Seçilen saatte ayın 1–31. günü; o günün bulunmadığı aylar atlanır. |
|
||||
| Gelişmiş (cron) | Dakikada en fazla bir kez çalışan yerel cron ifadesi. |
|
||||
|
||||
**Sonraki zamanlar (en fazla beş)**, kayıtlı saat dilimine ve varsayılan başlatma anına göre önizleme sunar. Koşul varsa **Sonraki denetimler (en fazla beş)** denetimleri gösterir; bir geçiş doğrulanmış bir değişikliğe bağlıdır. Değiştirilmeyen kayıtlı aralık, yerel aşamasını korur. Duraklatılmış veya bilinmeyen aşama, yerel durumu okunana kadar kullanılamaz olarak gösterilir. Gecikmeler ve aralıklar geçen süreyi kullanır; takvimler kayıtlı saat dilimini ve OpenCode'un yaz saati kurallarını izler. Gösterilen tarihler hizmetin ve ana bilgisayarın kullanılabilirliğine bağlı hesaplamalardır.
|
||||
|
||||
Bir kesintiden sonra yerel zamanlama, gelecek zamanlara dönmeden önce kaçırılan işi telafi etmek için en fazla bir geçiş sunar. Kaydedilmiş yerel planlı zamanı kullanır; bu zaman, daha sonra kaçırılan zamanlardan önce olabilir. Günde birkaç saat seçeneği aynı telafi hakkını ve çakışma korumasını paylaşır. Başka bir geçiş başlamadan önce çözülmemiş geçiş, özgün konuşma ve mesaj kimlikleri altında uzlaştırılır. Açıkça duraklatılmış yineleme sürdürüldüğünde sonraki gelecek zaman kullanılır; duraklatılmış tek seferlik iş bir kez gecikmeli gelebilir.
|
||||
|
||||
### Koşullar ve kaynaklar {#scheduling-conditions}
|
||||
|
||||
İsteğe bağlı tek bir **Koşul** ve **Bu projedeki kaynak** seçin: **Başka bir zamanlama başarılı olur**, **Yerel dalda yeni commit’ler**, **Tek seferlik Görev başarıyla tamamlanır**, **Takip edilen konuşmada yeni mesaj** veya **Belirtilen dosyalar değişir**. Yalnızca koşul için **Bir kez, hemen** seçimini koruyun: yerel denetimler tek geçiş başlatılana kadar 15 dakikada bir çalışır. Takvimle birlikte koşul yalnızca o takvimin zamanlarında denetlenir. Yinelenen koşullar, yeni uygun değişikliklerden sonra yeniden çalıştırma başlatabilir.
|
||||
|
||||
İlk başlatma başlangıç referansını belirler. Sürdürme, duraklatmadan sonra gözlemlenen değişiklikler dahil bu referansı korur. Başarı kaynakları doğrulanmış başarılı sonuç ve sonuçlandırılmış bir yerel konuşma ailesi gerektirir. Dal denetimleri, uzaktan veri getirmeden yerel commit'leri ve ata ilişkilerini kullanır. Konuşma denetimleri tam ve kararlı yeni mesajları bekler; teknik uyanmaları ve geçişin kendi konuşmalarını dışarıda bırakır. Dosyalar için satır başına bir tane olmak üzere projeye göre en fazla 128 açık yol kullanılır; özyinelemeli kalıplar ve bağlantılı yollar reddedilir. Eksik, taşınmış, belirsiz veya kapasiteyi aşan kaynak kabulü açıklamayla engeller. Denetimler arasındaki değişikliklerin algılanabilmesi için kaynakta kalıcı bir iz bırakması gerekir.
|
||||
|
||||
Birlikte gözlemlenen değişiklikler gruplanır. Geçiş, gözlemlenen kaynak kanıtlarını sabitler; başarıyla sonuçlandırma bu anlık görüntüyü işlenmiş sayar ve sonraki değişiklikleri gelecekteki denetime bırakır. Başarısız geçiş kaynak kontrol noktasını korur ve müdahale gerektirir. **Seçenekler** altındaki **Takip edilen konuşmalar (isteğe bağlı)**, konuşma mesajı koşulu seçmekten bağımsız olarak bağlam sağlar.
|
||||
|
||||
### Bitişler ve sayılar {#scheduling-limits}
|
||||
|
||||
Sürekli yineleme için sınırları boş bırakın. **İzin verilen son zaman** ve **İlk Oynat’tan bu kadar saat sonraki son zaman (isteğe bağlı)** uygun zamanları sınırlar; en erken sınır geçerlidir. Süre ilk başlatmada başlar ve duraklatma sırasında da ilerler. Bitişten önce kaydedilmiş bir zaman uzun bir kesintiden sonra geç ulaşabilir. Kabul edilmiş bir geçişin sonucu alınana kadar gözlemlenmesi sürer.
|
||||
|
||||
**Başlatılan en fazla çalıştırma sayısı**, tüm saatler ve **Şimdi çalıştır** için ortak toplamdır. Koordinatörünün ilk girdisi kabul edildiğinde geçiş bir kez sayılır; daha sonra başarısız olan geçiş de dahildir. Denetimler, değişmeyen koşullar ve reddedilen uyanmalar yer tüketmez; belirsiz kabul, uzlaştırılana kadar yerini tutar. Tek seferlik ilkeler en fazla bir geçişe izin verir. Kullanılan yerler düzenleme ve yeniden başlatmalarda korunur; değiştirilmeyen bitiş sınırları ilk başlatma referansını ve mutlak son zamanı korur. Üst sınırı değiştirmek, tüm çalışma ömrü için toplam sınırı değiştirir; kullanılan sayıya eşit veya daha düşük bir toplam sınır zamanlamayı bitirir. Düzenleyicinin açıkladığı gibi sıklığı değiştirmek, kaydetme sırasında yeni bir aralık aşaması başlatabilir.
|
||||
|
||||
Seçili yerel zamanlama **İstenen durum**, **Yerel takvim**, **Sonraki uyanma** veya **Sonraki kontrol**, **… tarihinden beri izleniyor** bilgisiyle **Kontroller**, **Başlatılan geçişler**, **Başarılı geçişler**, bitişi ve **Önceki çalıştırmalar** bilgilerini gösterir. Geçmiş bir geçişi **Konuşmayı aç** üzerinden okuyun. **Zamanlama tamamlandı**, zamanlamanın bitişini açıklar; geçişin sonucunu ayrıca inceleyin. Son 30 sonuç korunur; ömür boyu sayılar bu pencerenin dışında da kalır. **Daha fazla Mission yükle**, kayıtlı nesnelerin sonraki sayfasını yükler.
|
||||
|
||||
Liste, koşul ve bitişi yalnızca tanımlandığında; **Kontroller** bilgisini ise yalnızca koşul varsa gösterir.
|
||||
|
||||
### Mevcut günlük zamanlamalar {#existing-daily-schedules}
|
||||
|
||||
Mevcut günlük zamanlamalar kayıtlı IANA saat dilimini ve yerel takvim saatini kullanır. Her çalıştırmanın kendi koordinatörü ve görev haritası vardır; sonucu merkezi alanda okumak için güncel görev ağacını veya geçmiş bir tarih/sonucu seçin. Saklanan geçmişleri 30 sonuç tutar. Bu kayıtlar için aşağıdaki günlük kurallar geçerlidir:
|
||||
|
||||
| Durum | Zamanlama davranışı |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@ Günlük zamanlamalar geçen 24 saatlik aralığı değil, kayıtlı IANA saat d
|
|||
| Saatin geri alınması | Daha önce işlenen bir günü yeniden zamanı gelmiş saymaz. |
|
||||
| Mevcut çalıştırma hâlâ bekliyor | Yeni günlük çalıştırmayı bununla çakıştırmaz. |
|
||||
|
||||
Oluşturulma zamanı da uygun günlük işleri sınırlar; yeni bir zamanlamayı başlatmak, var olmadan önceki tarihleri işleme izni değildir. Sunulduğunda **Şimdi çalıştır** ayrı bir manuel çalıştırma başlatır; günlük saatin yerini almaz veya bekleyen çalıştırmayı aşmaz. Gösterilen sonraki zaman bir hesaplamadır, tam o anda yürütme garantisi değildir.
|
||||
Oluşturulma zamanı da uygun günlük işleri sınırlar. Mevcut günlük kayıtta sunulduğunda **Şimdi çalıştır**, bekleyen çalıştırma koruması altında ayrı bir manuel çalıştırma başlatır ve günlük saati korur. Gösterilen sonraki zaman, hizmetin ve ana bilgisayarın kullanılabilirliğine bağlı bir hesaplamadır.
|
||||
|
||||
**Takip edilen konuşmalar (isteğe bağlı)** seçili konuşmalardan sınırlı bağlam ekler. İşlenen mesaj imleçlerini yalnızca tamamlanan çalıştırma ilerletir; başarısız/kesintiye uğrayan çalıştırma bu bağlamı sessizce tüketmez. Talimatlar ve bağlam kapasiteyi aşarsa sınırsız geçmiş okunduğunu varsaymak yerine talimatları kısaltın veya takip edilen konuşmaları kaldırın.
|
||||
|
||||
## Çalışma ömrü ve hizmet yeniden başlatmaları {#lifetime}
|
||||
|
||||
**Teknik ayrıntılar** yanındaki **i**, teknik etkinliği ve maliyetleri açıklar. **Ayarlar → OpenCode** altında gözetim durumu ve hatalar görünür kalır; genel açıklamalar **i** içindedir.
|
||||
|
||||
Yerel bir zamanlamada **Teknik ayrıntılar** bölümünü genişleterek **Ana taşıyıcı** ve **Uzlaştırma taşıyıcısı** bilgilerini, bunların yerel takvimlerini ve varsa **Temizlik uzlaştırma bekliyor** durumunu görebilirsiniz. Bu teknik konuşmalarda mesaj yazma eylemi bulunmaz ve bunlar olağan konuşma ve kaynak seçicilerinde görünmez. Teknik uyanmalar, başlıklar ve bağlam sıkıştırma, sağlayıcılara HTTP isteği göndermeden yerel modeli kullanır; yerel olaylar ve istatistikler bunları yine de içerebilir, ancak gerçek geçişlerin olağan model maliyetleri korunur. Bağlam sıkıştırma, konuşma ve takvim kimliğini koruyarak işlenen bağlamı sınırlar. Saklanan yerel mesajlar ve olaylar, tamamlanmış bir zamanlama açıkça temizlenene kadar birikebilir; saklanan 30 Mission sonucu ayrıdır. Derinlik denetimi ve **Zamanlama**, kullanılabilir olduğunda yolu **OpenCode yapılandırma dosyası:** altında gösterir; derinlik denetimi **Projenin yazma hedefi** bilgisini ayrıca belirtir.
|
||||
|
||||
| Bileşen | Kapatılması ne anlama gelir |
|
||||
| --- | --- |
|
||||
| CodeNomad proje sekmesi | Projeyi CodeNomad'de kapatır; yerel işler OpenCode'da devam eder ve projeyi yeniden açtığınızda konuşmalar bulunur. |
|
||||
| Pencerenin tamamı, uygulamadan çıkış veya arayüzün yeniden yüklenmesi | Proje bağlantılarını geri yükleme için korur; OpenCode işlerini veya hizmetini durdurmaz. |
|
||||
| CodeNomad arka ucu | OpenCode'u durdurmaz. Çalışan tek seferlik bir görev sürer: koordinatörü, OpenCode alt ajanı olarak yürüyen görevleri ve görev araçları OpenCode içinde çalışır. Bağımsız oturumlara görev atama ve raporlarını koordinatöre iletme, koordinatörün hazırladığı görevler, plandan çıkarılan bir görevin konuşmalarını durdurma, kurtarma ve Görev Kontrolü arka ucu bekler; OpenCode hizmetinin yeniden başlatılmasından sonra tek seferlik bir görevi sürdürmek de öyle. |
|
||||
| Çalışan günlük zamanlama | Saat ve çalıştırma yürütmesi, CodeNomad arayüzü ve arka ucu kapalıyken de OpenCode eklentisinde/hizmetinde sürer. Yürütme ana bilgisayarı ve OpenCode hizmeti çalışır durumda kalmalıdır. |
|
||||
| OpenCode hizmetinin yeniden başlatılması | Sürece bağlı zamanlama işleri kaybolur. Çalışması istenen zamanlama, açıkça **Sürdür** seçilene kadar **Kesintiye uğradı** durumuna geçer; CodeNomad'i yeniden açmak onu devreye almaz. |
|
||||
| Etkinleştirilmiş yerel zamanlama | Kalıcı OpenCode takvimleri, yürütme ana bilgisayarı ve hizmet kullanılabilirken CodeNomad arayüzü ve arka ucu kapalı olsa da projeyi uyandırır ve geçişleri çalıştırır. |
|
||||
| OpenCode hizmetinin yeniden başlatılması — sağlıklı yerel zamanlama | İstenen durumu çalışıyor olan sağlıklı zamanlama takvimini otomatik sürdürür. Yeni kabulden önce bekleyen iş özgün kimlikleri altında uzlaştırılır. |
|
||||
| Mevcut günlük zamanlama | Hizmet yaşadığı sürece eklenti saati CodeNomad kapalıyken çalışır. Hizmet yeniden başlatılırsa **Kesintiye uğradı** durumunda kalır; açık **Sürdür** eylemi bekleyen çalıştırmayı uzlaştırır ve günlük saati yeniden etkinleştirir. |
|
||||
|
||||
Yeniden başlatmadan sonra bekleyen çalıştırmayı inceleyip hazır olduğunuzda **Sürdür** eylemini seçin. Sürdürme mevcut çalıştırmayı, koordinatör girdisini iki kez göndermeden uzlaştırır; kesilen OpenCode turunun devam edeceğini vaat etmez. Yeniden başlatmanın kestiği bir tur, **Yeniden başlatmayla kesildi** durumuyla raporsuz sonlanmış olarak sonuçlandırılabilir. Yeniden başlatmayı araçların yan etkilerini yeniden deneme yöntemi olarak kullanmayın.
|
||||
Yerel zamanlamalarda açık duraklatma, durdurma, başarısız iş ve belirsiz sonuçlar, yeniden başlatmadan sonra kabul engellerini korur. Korunan geçişi ve özgün isteği inceleyin; sunulduğunda **Çalıştırmayı denetle** veya **Kayıtlı isteği yenile** eylemini kullanın, ardından yalnızca durum izin veriyorsa **Sürdür** eylemini seçin. Yerel devam, mevcut kimliği altında gözlemlenir. Kanıtlanmış başarısızlık veya raporsuz sonlanma insan müdahalesiyle kurtarma gerektirir; eksik kanıt geçişi belirsiz bırakır. Başlatılmış araç etkilerini tekrarlamak için asla yeniden başlatma kullanmayın.
|
||||
|
||||
[OpenCode'u çalışır tut](opencode.md#service-supervision), paylaşılan hizmeti yürütme ana bilgisayarında ayrıca gözetir. KAPALI tercihini kaydetmediyseniz ilk Görev başlatma bunu etkinleştirir; duraklatılmış Görev oluşturmak bu seçimi korur. Kapatmak, OpenCode'u otomatik yeniden başlatma olmadan çalışır bırakır. Windows, macOS ve WSL oturum açma sınırını kullanır; yerel Linux izinlere bağlı kullanıcı linger özelliğini kullanır. Uyku, hazırda bekletme, WSL'nin boşta kalma ömrü ve harici ana bilgisayarın denetimi kullanılabilirliği sınırlar; gözetim durumunu zamanlamadan bağımsız inceleyin.
|
||||
|
||||
Görevler masaüstü uygulamalarıyla ve bağımsız bir CodeNomad [sunucusuyla](server.md) çalışır. Panel **Görev Kontrolü kullanılamıyor** gösterdiğinde mesajı nedeni belirtir:
|
||||
|
||||
|
|
@ -184,7 +237,9 @@ Görevler masaüstü uygulamalarıyla ve bağımsız bir CodeNomad [sunucusuyla]
|
|||
|
||||
Tek seferlik Duraklat devam eden işin bitmesine izin verir: koordinatör ve alt ajanları bulundukları adımı tamamlar, yeni bir şey başlatmaz ve durur; bu sırada panelde şu görünür: **Duraklatılıyor: devam eden adımlar tamamlanıyor.** **Şimdi kes** ise onları hemen durdurur: görevin OpenCode konuşma ailesini kesintiye uğratıp koordinasyonu bekletir. Durdur da kesintiye uğratır, ailede sıradaki girdileri/teslimatları iptal eder ve kalıcıdır. Kesintiler sınırlı işlemlerdir: **Bazı alt ajanlar hâlâ çalışıyor olabilir.** birinin tamamlanmadığını gösterir. Tüm kabuk/araç etkilerinin iptal edildiğini varsaymak yerine korunan sonucu inceleyip açık yeniden deneme eylemini kullanın. Sürdürme, koordinatöre neyin duraklatıldığını veya kesildiğini bildirir; işin nasıl devam edeceğine koordinatör karar verir.
|
||||
|
||||
Günlük Duraklat/Durdur zamanlamayı iptal eder ve koordinatörle bağımsız uzmanlar dahil kayıtlı kök aktörlerin kesilmesini ister; ancak **bu konuşmaların veya alt konuşmalarının durduğunu kanıtlamaz**. OpenCode konuşma ailelerini inceleyin. Durdurma kalıcıdır: durdurulmuş zamanlama Başlat, Sürdür veya Şimdi çalıştır eylemlerini kullanamaz. **Çalıştırmayı denetle** bekleyen çalıştırmayı tekrar göndermeden uzlaştırır. Geçici zamanlanmış denetim hataları son hatayı gösterir ve otomatik yeniden denenir; kullanıcı kontrolüne ait yanıtın kaybolması kontrolün otomatik yeniden gönderilmesine yol açmaz.
|
||||
Yerel zamanlamada **Duraklat**, gelecek kabulleri kapatır ve kabul edilmiş geçişin kesilmesini ister; **Sürdür**, yeniden etkinleştirmeden önce mevcut işi uzlaştırır. **Durdur…** kalıcıdır. **Şimdi çalıştır**, takvimi korurken aynı bekleyen geçiş korumasını ve başlatılan geçiş kotasını kullanır. **Çalıştırmayı denetle** özgün geçişi uzlaştırır. Doğrulanmamış eylemi koruyup **Kayıtlı isteği yenile** kullanın; pasif yenileme ve yeniden bağlanma bu isteği korur. Başlatılmış araç etkileri kesintiden sonra sürebilir.
|
||||
|
||||
Mevcut günlük kayıtlar günlük **Duraklat**, **Durdur…**, **Sürdür**, **Şimdi çalıştır** ve **Çalıştırmayı denetle** kontrollerini korur. Duraklatma ve durdurma, saatlerini iptal eder ve bağımsız uzmanlar dahil kayıtlı kök konuşmaların kesilmesini ister. Yerel konuşma ailelerini ve varsa kısmi kesinti sonucunu inceleyin. Geçici günlük denetim hataları gösterilen uyarıyla yeniden denenir; kaybolan kullanıcı kontrol yanıtı özgün isteğine bağlı kalır. Durdurulmuş kayıt kalıcı olarak durdurulmuş kalır.
|
||||
|
||||
Raporunu vermeden boşta kalan tek seferlik koordinatör için ana ▶ eylemi doğrulama ve kurtarma yapabilir; uygun görevlerde eksik rapora yönelik hedefli kurtarma bulunur. Kurtarma önce OpenCode etkinliğini, alt konuşmaları ve beklemeleri kontrol eder; etkinlik bilinmiyorsa girdiyi kabul etmez. Atamaları yeniden göndermez veya planı değiştirmez. Çözülmemiş sorular, izinler, kabuklar veya arka plan işleri için konuşmaları açın.
|
||||
|
||||
|
|
@ -192,12 +247,28 @@ Raporunu vermeden boşta kalan tek seferlik koordinatör için ana ▶ eylemi do
|
|||
|
||||
## Konuşmaları düzenleyin, silin ve temizleyin {#cleanup}
|
||||
|
||||
Düzenlenebilir tek seferlik görevde **Seçili görevi düzenle** eylemini kullanın; çakışma olursa üzerine yazmak yerine güncel revizyonu yüklemek için düzenleyiciyi kapatıp açın. Form hâlâ yüklenirken amaç veya notlar değişirse, form bunları doldurmak yerine görevin siz açtıktan sonra değiştiğini belirtir; güncel sürümü düzenlemek için **Yeniden aç** seçeneğini kullanın. Günlük zamanlamalarda düzenleme/silme kontrolleri yoktur.
|
||||
Form hâlâ yüklenirken amaç veya notlar değişirse, form bunları doldurmak yerine görevin siz açtıktan sonra değiştiğini belirtir; güncel sürümü düzenlemek için **Yeniden aç** seçeneğini kullanın.
|
||||
|
||||
Geçişler arasında, nesneyi tutan bir geçiş veya çözülmemiş kontrol yoksa duraklatılmış ya da çalışan yerel zamanlamanın başlığı, talimatları, notları, istenen profilleri, görev oturumu modu, kaynakları, takvimi ve sınırları düzenlenebilir. Duraklatılmış zamanlama duraklatılmış kalır; etkinleştirilmiş zamanlama devam etmeden önce takvimini uzlaştırır. Kaydetme geçmiş sonuçları, işlenmiş kaynakları, başlatma sayısını ve ilk başlatma referansını korur; değiştirilmeyen bitiş sınırları mutlak son zamanlarını korur.
|
||||
|
||||
Kabul edilmiş geçiş sırasında yalnızca talimatlar ve notlar etkin metin düzenleme kurallarına uyar: değişiklikler geçişin geçmişine kaydedilir ve gelecek geçişler için saklanır. Mevcut işler, özetler, profiller ve yürütme kimlikleri kabul anındaki değerlerini korur. Yapısal alanlar, duraklatmadan sonra da geçiş sonuçlanana kadar kilitli kalır. Tamamlanmış veya geri çekilmiş işler dahil herhangi bir iş oluşturulduktan sonra rehber kilitli kalır. Belirsiz kabul/kontrol veya eksik bütünlük kanıtı yapısal düzenlemeyi salt okunur tutar. Oluşturma ve düzenleme aynı takvim doğrulamasını kullanır; önizleme, düzenlenen son izinli zamanı gecikmiş olsa bile kayıtlı sonraki uyanışa uygular.
|
||||
|
||||
Yerel dal kaynağı worktree bulunmayan dalları da içerir ve yalnızca yerel referansları okur. Seçici en fazla 200 yerel dalı destekler; daha büyük listelerde kaynak kullanılamaz olarak gösterilir. Etkinleştirme mümkün olduğunda **Başlat** ve **Sürdür** tam yapılandırma yollarıyla onay ister. Yerel zamanlama etkin değilken **Şimdi çalıştır** bir açıklamayla devre dışı kalır; OpenCode yapılandırmasında `experimental.schedules` ayarını etkinleştirip yenileyin. Etkinleştirme yanıtı kaybolursa sonuç denetimi için özgün istek korunur.
|
||||
|
||||
İlk Play öncesinde **Seçenekler** altındaki takip edilen konuşmaları koruyarak anlık ve koşulsuz başlatmaya dönebilirsiniz. Play, tamamlanmış ve kararlı mesaj önekini okuyup sabitler (kaynak başına en fazla 32 mesaj, toplam başlatma girdisi 16.384 karakter içinde). Eksik, taşınmış veya kapasiteyi aşan kaynaklar açıklamayla başlatmayı engeller. Yeniden deneme, özgün sabit bağlamı ve başlatma kimliğini korur.
|
||||
|
||||
Makbuz okuması kısmi hazırlığı doğruladığında **Tekrar dene** görünür. Tıklama aynı makbuzu yeniden denetler ve yalnızca ilk kaydedilen değişikliği sürdürür; görev duraklatılmış kalır.
|
||||
|
||||
Yeni hazırlanmış bir görevin ilk başlatılmasından önce başlık, talimatlar, notlar, çalışma kitabı, istenen ajan/model/düşünme, görev oturumu modu ve **Zamanlama** düzenlenebilir. Aynı Görevi koruyarak anlık, gecikmeli, yinelenen ve koşullu başlatmalar arasında geçiş yapabilirsiniz. Kaydetme görevi duraklatılmış tutar ve yeni, boş bir koordinatör hazırlar; ilk konuşma korunur. Yalnızca Play işi başlatır veya zamanlamayı etkinleştirir. Başlatmadan sonra talimatlar ve notlar etkin görevin düzenleme kurallarına uyar. Kaydetme onaylanmazsa **Kayıtlı isteği yenile** ilk sonucu okur; kısmi hazırlık başlatmayı, metin düzenlemeyi ve silmeyi engellediği sürece aynı isteği koruyun.
|
||||
|
||||
Düzenlenebilir tek seferlik görev veya yerel zamanlama için **Seçili görevi düzenle** eylemini kullanın. Kilitli alanların yanındaki açıklamaları okuyun. Revizyon çakışmasında **Güncel yapılandırmayı yeniden yükle** eylemini seçin, ardından yeniden kaydetmeden önce korunan taslağı incelemek için **Karşılaştırma için önceki taslak** bölümünü açın. Mevcut günlük zamanlamalar kendi araç çubuklarındaki kontrolleri korur.
|
||||
|
||||
Yerel zamanlamada **Seçili görevi sil…**, kaydı kaldırmadan önce gelecek geçişleri devre dışı bırakır. Konuşmalar ve sonuçlar korunur; uzlaştırma bekleyen temizlik **Teknik ayrıntılar** içinde görünür kalır. Doğrulanmamış silme isteğini koruyup özgün isteği yenileyin.
|
||||
|
||||
1. Artık gerek duymadığınız tek seferlik görev için **Seçili görevi sil…** eylemini seçip onayı inceleyin.
|
||||
2. Onaylamadan önce isteğe bağlı olarak **Bu görev için oluşturulan uzman konuşmalarını da sil** seçeneğini işaretleyin.
|
||||
3. Kaldırılan/korunan/bekleyen sayılarını **Konuşma temizliği** bölümünde kontrol edin. Sunuluyorsa yalnızca kaydedilmiş, kalan temizliği yeniden deneyin.
|
||||
|
||||
Görevi silmek durdurmakla aynı şey değildir; önce Durdur'u kullanıp kalan işleri inceleyin. Koordinatörler ve yeniden kullanılan konuşmalar korunur. Paylaşılan, taşınan, alt konuşması bulunan veya sahipliği doğrulanamayan uzmanlar da korunabilir. Kısmi temizlik geri alınmaz ve arka planda sessizce tamamlanmaz. Genel bakış okuyucusu tamamlanan en son 20 temizliği tutar. Görevleri silmek projedeki depolama alanlarını boşaltır: daha eski silinmiş görevler otomatik olarak temizlenir.
|
||||
Görevi silmek durdurmakla aynı şey değildir; önce Durdur'u kullanıp kalan işleri inceleyin. Koordinatörler ve yeniden kullanılan konuşmalar korunur. Paylaşılan, taşınan, alt konuşması bulunan veya sahipliği doğrulanamayan uzmanlar da korunabilir. Kısmi temizlik geri alınmaz ve arka planda sessizce tamamlanmaz. Konuşma temizliği 0 bekleyen gösterse bile silme doğrulaması beklenirken görev kartı kaybolabilir. Silme doğrulanana kadar özgün **Tekrar dene** eylemini kullanmaya devam edin; aynı silme isteği ve seçenekler korunur. Genel bakış okuyucusu tamamlanan en son 20 temizliği tutar. Görevleri silmek projedeki depolama alanlarını boşaltır: daha eski silinmiş görevler otomatik olarak temizlenir.
|
||||
|
||||
OpenCode işleri ve hizmet kontrolleri için [İzinler](permissions.md), [Konuşmalar](conversations.md) ve [OpenCode kurulumu](opencode.md) sayfalarına bakın.
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ Yüklü yürütülebilir dosya ve çalışan hizmetin farklı sürümleri olabil
|
|||
| Önerilen | OpenCode **2.2.5** | Teknik alt sınırdan farklı, doğrulanan hedef. |
|
||||
| Doğrulanmamış | Tam olarak doğrulanmamış daha yeni veya özel bir sürüm | Yalnızca denenmemiş olduğu için reddedilmez; bağlı hizmetin gerçek API sözleşmeleri kontrol edilir. |
|
||||
|
||||
Gerekli oturum, izin, Form, gelen kutusu ve oturum-ortam sözleşmelerinin de mevcut olması gerekir. Yürütülebilir bir sürüm tek başına bağlı hizmetin bunları desteklediğini kanıtlamaz. Kurulum, desteklenmeyen bir çalışan hizmet bildirirse desteklenen bir CLI seçin/yükleyin ve hazır olduğunuzda açık paylaşılan hizmet yeniden başlatmayı kullanın.
|
||||
Bağlı hizmet gerekli oturum, izin, Form, gelen kutusu ve oturum ortamı sözleşmelerini sunmalıdır. Yerel Görev zamanlaması ayrıca zamanlama API'lerini, eklenti/sağlayıcı paketini ve etkin `experimental.schedules` ayarını denetler. Kullanılabilirlik, uyumlu gelecek sürümler dahil, kullanılan bu yeteneklere bağlıdır. Kurulum desteklenmeyen bir çalışan hizmet bildirirse desteklenen bir CLI seçin/yükleyin ve hazır olduğunuzda paylaşılan hizmeti açıkça yeniden başlatın.
|
||||
|
||||
## Ana bilgisayar, WSL ve paylaşılan hizmet kapsamı {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ Windows'ta seçilen bir WSL yürütülebilir dosyası, seçilen dağıtımın i
|
|||
|
||||
OpenCode, küresel hizmet kaydına, oturumlarına, mesajlarına ve veritabanına sahiptir. CodeNomad, pencere veya proje başına özel bir OpenCode veritabanı oluşturmaz. Proje sekmesini kapatmak projeyi CodeNomad'de kapatır; oturumlar OpenCode'da devam eder ve projeyi yeniden açtığınızda yeniden görünür. CodeNomad yerel Location bağlamlarını hiçbir zaman kaldırmaz. Bir pencerenin tamamını kapatmak, uygulamadan çıkmak veya yeniden yüklemek proje bağlantılarını geri yükleme için korur; ne bu eylemler ne de arka ucun kapanması paylaşılan arka plan programını durdurur. Bkz. [Projeler](projects.md).
|
||||
|
||||
## OpenCode'u çalışır tut {#service-supervision}
|
||||
|
||||
İlk Mission başlatılmadan önce **İlk Mission başlatılırken kapalı tut**, önce gözetim kurmadan açık bir KAPALI tercihi kaydeder.
|
||||
|
||||
**Ayarlar → OpenCode** içindeki **OpenCode'u çalışır tut**, yürütme sunucusuna işletim sistemi gözetimi kurar; bu hizmeti kullanan tüm CodeNomad profillerinde paylaşılır. Missions'tan bağımsızdır ve CodeNomad kapalıyken de çalışır. Önceden açıkça kapatmadıysanız ilk Mission başlatma işlemi etkinleştirir; duraklatılmış Mission oluşturmak seçimi korur. Mission duraklatmak, durdurmak veya silmek bu tercihi değiştirmez.
|
||||
|
||||
Kapatmak gözetimi kaldırır; OpenCode çalışmaya devam eder ancak yeniden başlatılmaz. Kalıcı durdurma için önce bunu kapatın, sonra OpenCode'u açıkça durdurun. Gözetim açıkken `opencode service stop` sonrasında yeniden başlatılır.
|
||||
|
||||
**Çalışıyor · gözetim etkin**, etkin bir sistem gözetmeni ve sağlıklı OpenCode hizmeti gerektirir. **Kurulu · hizmet başlatılıyor**, **Gözetim kuruluyor veya kaldırılıyor…** ve **Gözetim başarısız** farklı durumları belirtir. Yalnızca hizmetin yanıt vermesi gözetimi doğrulamaz. Biliniyorsa neden, son deneme, gözetmen yeniden denemesi ve sistem kurtarma güvenlik denetimi gösterilir. **Gözetim durumunu denetle** yalnızca okur; **Gözetimi yeniden dene** istenen kurulumu açıkça onarır. Onaylanmamış işlem özgün isteği korur; **Özgün işlemi yeniden dene** yalnızca aynı işlemi yeniden gönderir. Değişen ayar sürümü üzerine yazılmak yerine inceleme için yenilenir.
|
||||
|
||||
Windows, macOS ve WSL oturum açıldıktan sonra başlar. WSL seçili dağıtım için localhost yönlendirmesi gerektirir. Yerel Linux'ta linger, **etkin tüm kullanıcı hizmetlerini** oturum açılmadan başlatır ve oturum kapandıktan sonra çalışır tutar. Kapatılırken linger yalnızca CodeNomad etkinleştirmişse ve başka gözetim gerektirmiyorsa geri alınır; kişisel ayarlar korunur. Kullanıcı oturumu açık değilken linger ayarını geri almak gözetimden bırakılan OpenCode süreçlerini durdurabilecekse geri alma ertelenir ve **Gözetimi yeniden dene** ile görünür kalır. Bir kullanıcı oturumu açıldığında veya gözetimden bırakılan OpenCode süreçleri sonlandığında ve güvenli geri alma mümkün olduğunda yeniden deneyin. Linux izin isterse yürütme sunucusunda izin verip yeniden deneyin; istenen tercih kayıtlı kalır. Uyku ve hazırda bekletmeyi işletim sistemi yönetir. Kullanılamayan veya desteklenmeyen sunucularda açıklamalı devre dışı kontrol gösterilir.
|
||||
|
||||
## Güncelleme, yeniden başlatma veya yeniden yükleme {#maintenance}
|
||||
|
||||
1. **Ayarlar → OpenCode** bölümünde **Durumu ve güncellemeleri denetle** öğesini seçin.
|
||||
|
|
@ -41,7 +53,7 @@ OpenCode, küresel hizmet kaydına, oturumlarına, mesajlarına ve veritabanına
|
|||
| Eylem | Kapsam ve etki | Güvenli kurtarma |
|
||||
| --- | --- | --- |
|
||||
| CLI'yi güncelleyin | Desteklenen yüklü yürütülebilir dosyayı değiştirir; çalışan arka plan programını dolaylı olarak yeniden başlatmaz. | Bir arızadan sonra hem yüklü hem de çalışan sürümleri kontrol edin; yalnızca hatayı çözdükten sonra açıkça yeniden deneyin. |
|
||||
| Hizmeti yeniden başlat | Paylaşılan süreci değiştirir ve bağlı tüm OpenCode istemcilerindeki etkin işleri keser; oturum geçmişi kalır. | Yeniden bağlanıp konuşmaları ve zamanlanmış Görevleri inceleyin; yeniden başlatma otomatik devam anlamına gelmez. |
|
||||
| Hizmeti yeniden başlat | Paylaşılan süreci değiştirir ve bağlı tüm OpenCode istemcilerindeki etkin işleri keser; oturum geçmişi kalır. | Sağlıklı çalışan yerel Görev zamanlamaları otomatik kurtarılır; bekleyen geçişleri ve varsa duraklatma, hata veya belirsizliği inceleyin. Mevcut günlük kayıtlar **Sürdür** gerektirir. Tek seferlik konuşmaları ve kurtarma kontrollerini ayrıca inceleyin; bkz. [Görevler](missions.md#lifetime). |
|
||||
| OpenCode yapılandırmasını yeniden yükle | Süreci korur ancak tüm istemcilerdeki **yüklü her Location** bağlamını yeniden kurar. Bekleyen izinleri/Formları iptal eder, terminalleri/arka plan komutlarını kapatır; etkin işler kesilebilir. | Yeniden yükledikten sonra kaynakları ve bekleyen işleri kontrol edin. Yanıt kayboldu diye istemi veya değişiklik işlemini tekrarlamayın. |
|
||||
|
||||
Kaydedilmiş bir yapılandırma veya yüklü eklenti kullanılamadığında ve kesinti kabul edilebilir olduğunda, çalışan hizmet destekliyorsa **Sorun giderme** altında bulunan **OpenCode yapılandırmasını yeniden yükle** öğesini açıkça kullanın. Zararsız bir ekran yenileme değildir. CodeNomad, daha yeni çalışan bir arka plan programının sürümünü düşürmek için seçilen eski bir CLI'yi kullanmayacaktır.
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ Bunlar talimattır. Bir ajanın gerçekte ne yapabileceğini, ajanının ve proj
|
|||
|
||||
### Size sorulan sorular {#questions}
|
||||
|
||||
Size yalnızca koordinatör soru sorar. İşler ve yardımcıları kendi sorularını, onları başlatan konuşmaya seçenekler, kanıtlar, sonuçlar, geri alınabilirlik ve bir öneriyle geri gönderir ve yanıta bağlı olmayan işe devam eder. Koordinatör önce yapabildiğini çözer ve kalan soruları **Yanıtınız gerekiyor** altında tek bir formda toplar; neleri sorduğunu ve kararların nasıl etiketlendiğini [Sorular ve kararlar](missions.md#questions) açıklar. Tek seferlik veya günlük her koordinatör ve her iş ataması bu kuralı talimat olarak alır; Keşif [aşağıdaki](#wayfinder-checks) karar denetimlerini ekler.
|
||||
Size yalnızca koordinatör soru sorar. İşler ve yardımcıları kendi sorularını, onları başlatan konuşmaya seçenekler, kanıtlar, sonuçlar, geri alınabilirlik ve bir öneriyle geri gönderir ve yanıta bağlı olmayan işe devam eder. Koordinatör önce yapabildiğini çözer ve kalan soruları **Yanıtınız gerekiyor** altında tek bir formda toplar; neleri sorduğunu ve kararların nasıl etiketlendiğini [Sorular ve kararlar](missions.md#questions) açıklar. Tek seferlik veya zamanlanmış her koordinatör ve her iş ataması bu kuralı talimat olarak alır; Keşif [aşağıdaki](#wayfinder-checks) karar denetimlerini ekler.
|
||||
|
||||
İstek panelinde yanıtlayın (**Yanıtınız gerekiyor** altında **Yanıtla**): yalnızca bu sizin yanıtınız sayılır. Sohbete yazılan bir yanıt sıradan bir mesajdır.
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ Size yalnızca koordinatör soru sorar. İşler ve yardımcıları kendi sorular
|
|||
|
||||
Koordinatöre ilk planından sonra, sonuçlandırdığı her işten sonra, sizi beklemeye başladığı her an ve son raporundan önce bir durum özeti yayımlaması talimatı verilir; [Durum özetleri ve güncelleme istekleri](missions.md#briefings) bölümüne bakın. Durum özetleri, iş özetleri ve son rapor sizin için yazılır: sizin dilinizde, birkaç kısa cümleyle, önce sonuç; tanımlayıcılar ve komutlar kanıtlarda kalır.
|
||||
|
||||
Son rapor neyin teslim edildiğini, neyin kaldığını ve neyin sizin kararınızı gerektirdiğini belirtir. Tek seferlik bir görev yalnızca tamamlanmış olarak biter: siz olmadan hiçbir şey ilerleyemediğinde koordinatör başarısız olmak yerine size sorar ve bekler; bir görevi yalnızca siz durdurursunuz. Günlük bir çalıştırma başarısız olarak da bitebilir.
|
||||
Son rapor neyin teslim edildiğini, neyin kaldığını ve neyin sizin kararınızı gerektirdiğini belirtir. Tek seferlik bir görev yalnızca tamamlanmış olarak biter: siz olmadan hiçbir şey ilerleyemediğinde koordinatör başarısız olmak yerine size sorar ve bekler; bir görevi yalnızca siz durdurursunuz. Zamanlanmış bir geçiş başarısız olarak da bitebilir.
|
||||
|
||||
### CodeNomad'in her görevde denetledikleri {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@ Yanıtınızı asla kendisi yazmaz ve kendi kararını sizinki gibi sunmaz. Sonu
|
|||
- Yalnızca Keşif'in beş rolü kabul edilir.
|
||||
- Bir karar işi yalnızca koordinatörün kendi kararıyla veya sizin yanıtınızla tamamlanır. Koordinatör kararı soruyu, kararı ve gerekçesini belirtir ve asla sizin yanıtınız olarak kaydedilmez.
|
||||
- Yanıtınız yalnızca CodeNomad arayüzünde verdiyseniz sayılır. CodeNomad onu sorunun etiketiyle bir işe bağlar: soru koordinatörün konuşmasından sorulmuştur, metni o işin etiketiyle biter ve başka bir karar anmaz, iş bildirildikten sonra yanıtlanmıştır ve başka bir kararı sonuçlandırmamıştır. Toplu bir formun her sorusu ayrı bağlanır; iki kararı anan bir soru hiçbirini sonuçlandırmaz. Karar işinin kendi konuşmasından sorulan bir sorunun yanıtı da kabul edilir.
|
||||
- Bir işin kendi raporu kimin karar verdiğini iddia edemez. Bir görev veya günlük çalıştırma başarıyla bitmeden önce CodeNomad, tamamlanan her kararı etiketleri dahil yeniden denetler.
|
||||
- Bir işin kendi raporu kimin karar verdiğini iddia edemez. Bir görev veya zamanlanmış geçiş başarıyla bitmeden önce CodeNomad, tamamlanan her kararı etiketleri dahil yeniden denetler.
|
||||
- Bu denetimler **Yerel alt ajanlar** ile geçerlidir. **Bağımsız oturumlar** ile karar sorumluları özetlerini kendileri bildirir ve koordinatör her sonucu kimin karar verdiğini belirterek notlara kaydeder; CodeNomad bunu doğrulamaz.
|
||||
|
||||
Yalnızca talimat olanlar: bir seçimin ne zaman geri alınması ucuz sayılacağı, özetlerin ve araştırmanın kalitesi ve planlamayla sınırlı kalmak.
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ OpenCode 提供引擎,CodeNomad 提供驾驶舱。它连接到 OpenCode 的共
|
|||
|
||||
## 将长时间工作交给任务 {#missions}
|
||||
|
||||
只需描述一次工作。任务的协调者将其规划为任务树,把有界任务委派给原生 OpenCode 对话,并报告结果及其证据。任务可以运行一次或每天运行;创建任务后,在你选择启动之前绝不会开始工作。剧本涵盖调试、产品交付,以及由盲评裁判比较两个模型。参阅[任务](missions.md)。
|
||||
只需描述一次工作。任务的协调者将其规划为任务树,把有界任务委派给原生 OpenCode 对话,并报告结果及其证据。可以运行一次、按选定时间或间隔运行,或在核实项目变化后运行;创建会保存已暂停的任务,等待你显式启动。剧本涵盖调试、产品交付,以及由盲评裁判比较两个模型。参阅[任务](missions.md)。
|
||||
|
||||
## 用手机继续工作 {#phone-access}
|
||||
|
||||
|
|
@ -77,7 +77,7 @@ npx @neuralnomads/codenomad --generate-token --launch
|
|||
| 跨检出目录和对话工作 | [项目](projects.md)、[对话](conversations.md)、[文件](files.md) |
|
||||
| 查找历史记录或安全清理 | [历史记录](history.md) |
|
||||
| 回答原生问题和权限请求 | [权限](permissions.md) |
|
||||
| 协调有界任务或每日工作 | [任务](missions.md) |
|
||||
| 协调有界任务或计划工作 | [任务](missions.md) |
|
||||
| 配置执行和集成 | [OpenCode](opencode.md)、[提供商](providers.md)、[集成](integrations.md) |
|
||||
| 连接另一台设备 | [远程控制](remote-control.md)、[安全](security.md)、[移动设备](mobile.md) |
|
||||
| 调整工作区 | [设置](settings.md)、[快捷键](shortcuts.md)、[语音](voice.md)、[扩展](extensions.md) |
|
||||
|
|
|
|||
|
|
@ -6,11 +6,13 @@
|
|||
|
||||
1. 打开右侧面板的**任务**,选择**创建任务**。在你创建或取消之前,表单会替代任务列表和所选任务的任务节点。
|
||||
2. 填写**任务要做什么?**,检查从首句预填的**标题**(最多 60 个字符)。
|
||||
3. 在**时间**下选择**一次**或**每天**。对于每日工作,验证显示的时区;**更改**允许输入 IANA 时区。
|
||||
3. 对于**一次,立即执行**和**无条件**,保持**计划**折叠;也可展开以选择频率、条件和可选限制。创建计划工作前,请检查**保存的时区(IANA)**和预览。
|
||||
4. 展开**选项**,设置**剧本**、**备注**、**代理和模型**、**任务会话**、项目的**子会话嵌套深度**、已保存简述及可选关注对话。选择**比较两个模型**时,**模型 A** 和**模型 B** 选择器会显示在主表单中。
|
||||
5. 选择**创建**。选择新条目,准备好时再使用其独立 ▶ 操作:一次性工作为**启动任务**,每日日程为**启动日程 {id}**。
|
||||
5. 选择**创建**。选择暂停的条目并检查,准备好后使用 ▶ **开始**。立即无条件工作会启动一个任务;延迟、重复或有条件策略会启用原生计划。
|
||||
|
||||
**创建绝不启动工作。**一次性任务处于已准备状态;每日日程处于暂停状态。创建和保存简述都不会启用计时器。创建以所选项目/Location 为作用域,冻结请求的配置和任务会话策略。验证信息位于受影响字段旁;若拒绝信息明确说明未发送任何内容,草稿会保留供修改。一次性任务的协调者也可以为你准备一个单独的任务,例如后续任务:它会以**已准备**状态出现在列表中,并且与其他任务一样,只能通过其 ▶ 操作启动。
|
||||
**创建会保存已暂停的任务。**只有点击播放才会启动工作或启用原生日历;创建或保存简述会保留共享服务监管偏好。创建属于所选项目/Location,并记录请求的配置和任务会话策略。验证信息显示在相应字段旁;若拒绝信息说明未发送任何内容,草稿会保留供修改。一次性任务的协调者也可以准备单独的后续任务:它显示为**已准备**,通过自己的 ▶ 操作启动。
|
||||
|
||||
已保存的一次性任务保留执行控制、任务树、对话和最终报告。现有每日日程仍列出其每日控制,并遵循[每日时间规则](#existing-daily-schedules)。请选择条目并使用该对象可用的控件。
|
||||
|
||||
### 剧本 {#playbooks}
|
||||
|
||||
|
|
@ -101,14 +103,16 @@
|
|||
|
||||
后续任务节点通过两种方式利用先前的工作。依赖决定工作顺序:协调者在某个节点所依赖的任务节点完成后才启动它,并把相关结果写入简述或附在分配旁;独立会话也可以在任务图中读取这些报告。若要继续某个已完成任务节点自己的对话,协调者会声明一个复用该对话的后续任务节点:后续节点在那里以该对话的当前上下文继续,OpenCode 可能已对其进行[压缩](history.md#compact-session),而更早的细节仍保留在其已存储历史中,可供搜索。调试剧本就是这样把两份审查交回实现者的。使用**独立会话**时,协调者也可以把任务节点分配给现有的主对话,该对话保留自己的上下文,包括与之无关的先前工作。除此之外,任务节点从自己的分配开始于新对话,其他节点的对话记录通过协调者或搜索到达它。在**比较两个模型**中,裁判的分配和上下文不包含目标和备注;其简述包含匿名化的输出。
|
||||
|
||||
在每日日程中,每次运行都以自己的协调者和任务图开始。其第一条消息包含日程的指令,以及自上次完成的运行以来其[关注的对话](#daily-schedules)中新增的用户和助手文本;以往运行的结果保留在日程历史中。运行中启动的对话可以读取该次运行的任务图。新的一次性任务同样从新的任务图开始:要延续结果,请把它们写入其目标或**备注**,或让协调者准备一个后续任务,由协调者撰写其目标和备注。
|
||||
每个计划轮次都以自己的协调者和任务图开始,使用已保存的指令和冻结的输入。关注的对话提供有界的新增用户和助手文本;成功轮次结束后,其已处理消息游标才会推进。先前轮次保留在计划历史中。轮次内启动的对话可以读取该轮次的任务图。新的一次性任务拥有单独的任务图:请将需要延续的结果写入其目标或**备注**,或请协调者用这些上下文准备后续任务。
|
||||
|
||||
### 搜索以往对话 {#context-search}
|
||||
|
||||
CodeNomad 运行期间,项目中的每个智能体对话(包括任务协调者和任务节点)都可以使用 CodeNomad 的会话搜索工具(`codenomad.session_search`,通过 OpenCode 的 Code Mode 使用)。它在该对话所属项目文件夹的已保存对话中搜索,排序方式与**整个工作区**[搜索](history.md#full-history-search)相同,返回带有每个对话标题和标识符的简短摘录,以及读取完整对话的方法。原生工具权限照常适用。智能体会在自行决定时,或在目标、备注或你的消息提出要求时进行搜索,例如查找以往任务或任务节点得出的结论。CodeNomad 关闭期间执行的每日运行在没有此工具的情况下工作。
|
||||
CodeNomad 运行期间,项目中的每个智能体对话(包括任务协调者和任务节点)都可以使用 CodeNomad 的会话搜索工具(`codenomad.session_search`,通过 OpenCode 的 Code Mode 使用)。它在该对话所属项目文件夹的已保存对话中搜索,排序方式与**整个工作区**[搜索](history.md#full-history-search)相同,返回带有每个对话标题和标识符的简短摘录,以及读取完整对话的方法。原生工具权限照常适用。智能体会在自行决定时,或在目标、备注或你的消息提出要求时进行搜索,例如查找以往任务或任务节点得出的结论。CodeNomad 关闭期间执行的计划轮次在没有此工具的情况下工作。
|
||||
|
||||
## 配置、委派和深度 {#profiles}
|
||||
|
||||
保存深度时会在项目配置中保留已启用的原生计划。项目中明确设置的 `schedules:false` 仍保持关闭;移除深度会保留计划设置。保存原生设置前,必须先解决 JSON 与 JSONC 文件并存的冲突。
|
||||
|
||||
1. 使用任务齿轮或**设置 → 任务**编辑**新任务默认设置**。
|
||||
2. 在**应用范围**下选择**所有任务**或某个剧本,然后设置**任务会话**,并在**代理和模型**下为每个角色设置一行(先是协调者,然后是该剧本的各个角色)。每项选择都会立即保存;没有保存按钮。
|
||||
3. 在剧本范围中,各行显示标有**全局**的全局值。更改某个字段会以这些值为起点自定义该角色;其还原图标会重新使用全局值。**重置**(需确认)只清除所显示范围的代理和模型。
|
||||
|
|
@ -118,7 +122,7 @@ CodeNomad 运行期间,项目中的每个智能体对话(包括任务协调
|
|||
| 设置 | 范围与影响 |
|
||||
| --- | --- |
|
||||
| 新任务默认设置 | 配置范围的默认值,仅适用于**新建**任务;剧本例外覆盖通用默认值。 |
|
||||
| 智能体 / 模型 / 思考 | 随创建记录的协调者/角色请求选择;不会为满足后续工作静默切换繁忙参与者。每日运行复用冻结的请求配置。 |
|
||||
| 智能体 / 模型 / 思考 | 随创建记录的协调者/角色请求选择;不会为满足后续工作静默切换繁忙参与者。计划轮次使用已保存的请求配置;编辑会应用于后续轮次。 |
|
||||
| 原生子代理 | 任务会话在协调者下使用原生 subagent/all 智能体。 |
|
||||
| 独立会话 | 声明的任务会话使用 primary/all 智能体和独立对话;不禁用内部原生辅助智能体。 |
|
||||
| 子会话嵌套深度 | 该处**全部** OpenCode 对话的项目配置,而非仅任务。留空为继承:**继承**会移除项目覆盖设置,以及因此变空或只剩全局**图像只存储一次**值的 `experimental` 块。OpenCode 将项目的 `experimental` 块作为整体读取,因此其中保留的其他实验性设置仍会遮蔽全局深度。OpenCode 内置的 **General** 子代理默认无法启动子代理,因此其对话从不嵌套:保存任意项目深度时,还会在同一文件中添加一条规则,允许子代理在该深度内启动自己的子代理,并优先于其他配置文件中对 General 的限制(深度为 1 时,OpenCode 的深度上限仍会阻止嵌套);**继承**会将其移除。在 **OpenCode 有效深度** 下方,编辑器会说明 General 子代理能否嵌套。如果项目以旧格式定义 General,且 `permission` 为单个值,或已同时设置 `task` 和 `subagent`,则只保存深度,编辑器会予以提示:请自行在该文件中允许 General 启动子代理。保存不会重新加载 OpenCode 或更改运行中的对话。 |
|
||||
|
|
@ -137,9 +141,51 @@ CodeNomad 运行期间,项目中的每个智能体对话(包括任务协调
|
|||
|
||||
简述保存指令和请求选择,不保存任务计划、结果、对话、日程时钟或执行权限。加载只填入普通创建草稿;不会启动或安排任何工作。移除简述会保留现有任务/对话。**设置 → 任务 → 已保存的简述**管理资料库;如果资料库无法读取,**重置已保存的模板库**(需确认)移除的是已保存简述,而非执行。日程本身没有另存为简述操作。
|
||||
|
||||
## 每日时间、补跑和关注对话 {#daily-schedules}
|
||||
## 计划、补跑和关注对话 {#daily-schedules}
|
||||
|
||||
每日日程使用已保存 IANA 时区和当地民用时间,而非经过 24 小时的间隔。时间以日程时区显示,与查看者时区不同时会标注。每次运行拥有自己的协调者和隔离任务图;所选日程显示当前任务树及日期/结果历史。选择过去运行可在中央阅读。历史最多保留 30 个结果。
|
||||
### 原生计划 {#native-scheduling}
|
||||
|
||||
标签旁的 **i** 图标提供选项说明:悬停、用键盘聚焦或轻触即可查看。
|
||||
|
||||
在创建或编辑时展开**计划**。**频率**提供以下选项:
|
||||
|
||||
| 选项 | 时间安排 |
|
||||
| --- | --- |
|
||||
| 一次,立即执行 | 点击播放时启动一次,可选条件按下文说明检查。 |
|
||||
| 一次,指定日期 | 保存的 IANA 时区中的一个日期和时间;点击播放时已过去的日期会被拒绝。 |
|
||||
| 一次,延迟执行 | 以分钟或小时为单位的正整数时长,从点击播放时开始。 |
|
||||
| 固定间隔 | 至少 15 分钟;首次唤醒在点击播放一个间隔后。 |
|
||||
| 每天 | 保存时区中的一个当地民用时间。 |
|
||||
| 指定星期几 | 在勾选的星期几的同一时间。 |
|
||||
| 每天多次 | 最多八个独立时间点,例如 09:15 和 17:45;重复时间共用一个轮次。 |
|
||||
| 每月指定日 | 在每月 1–31 日的选定时间;没有该日期的月份会跳过。 |
|
||||
| 高级(cron) | 原生 cron 表达式,最多每分钟一次。 |
|
||||
|
||||
**后续时段(最多5个)**根据已保存时区和假定的播放时间提供预览。有条件时,**后续检查(最多5个)**预览检查时间;轮次取决于已核实的变化。未更改的已保存间隔保留其原生相位。已暂停或未知的相位会显示为不可用,直到可以读取其原生状态。延迟和间隔使用经过时长;民用日历遵循已保存时区和 OpenCode 的夏令时规则。显示的日期是推算值,受服务和主机可用性限制。
|
||||
|
||||
故障后,原生计划最多补跑一个轮次,然后回到未来时段。它使用已记录的原生到期时段,该时段可能早于后来错过的时段。每天多个时间点共用这一次补跑和同一防重叠保护。另一个轮次启动前,未解决的轮次会以其原始对话和消息标识进行核对。恢复显式暂停的重复计划时,使用下一个未来时段;暂停的一次性计划可以延迟到达一次。
|
||||
|
||||
### 条件和来源 {#scheduling-conditions}
|
||||
|
||||
选择一个可选**条件**及其**此项目中的来源**:**另一计划执行成功**、**本地分支有新提交**、**一次性任务成功完成**、**关注的对话有新消息**或**指定文件发生变化**。只使用条件时,保留**一次,立即执行**:原生检查每 15 分钟运行一次,直到其唯一轮次启动。结合日历时,仅在该日历的时段检查条件。重复条件可在出现新的合格变化后再次启动轮次。
|
||||
|
||||
首次播放建立起始参照。继续会保留此参照,包括暂停后观察到的变化。成功来源要求经过验证的成功结果和已结算的原生会话族。分支检查使用本地提交及其祖先关系,不从远程获取。对话检查等待完整、稳定的新消息,并排除技术唤醒及该轮次自己的对话。文件使用最多 128 个明确指定的项目相对路径,每行一个;递归模式和链接路径会被拒绝。来源缺失、已移动、不明确或超出容量时,会说明原因并阻止准入。检查之间的变化必须在来源中留下持久痕迹才能被检测到。
|
||||
|
||||
同时观察到的变化会合并。轮次会冻结已观察到的来源证据;成功结算会将该快照标记为已处理,后续变化留待下一次检查。失败轮次会保留来源检查点,并要求人工干预。**选项**下的**关注的对话(可选)**单独提供上下文,与选择对话消息条件相互独立。
|
||||
|
||||
### 结束限制和计数 {#scheduling-limits}
|
||||
|
||||
持续重复时将限制留空。**最后允许的时段**和**首次播放后多少小时为最后时段(可选)**限定合格时段;最早的限制生效。时长从首次播放开始,暂停期间继续计时。结束前记录的时段可能在长时间故障后延迟到达。已接纳的轮次可以持续观察,直到获得结果。
|
||||
|
||||
**最多启动次数**是所有时间点和**立即运行**共用的总数。协调者的首次输入被接纳时,轮次计数一次,包括后来失败的轮次。检查、未变化的条件和被拒绝的唤醒不占名额;准入不确定时会占住名额,等待核对。一次性策略最多允许一个轮次。已用名额在编辑和重启后仍会保留;未更改的结束限制保留首次播放的起算点和绝对结束时段。更改上限会改变整个生命周期的总额度;总额度等于或低于已用次数时,计划结束。编辑器会说明,更改频率可能在保存时开始新的间隔相位。
|
||||
|
||||
选中的原生计划显示**期望状态**、**原生日历**、**下次唤醒**或**下次检查**、带有**自 … 起记录**的**检查次数**、**已启动轮次**、**成功轮次**、结束限制和**历史运行**。通过**打开对话**阅读过去的轮次。**计划已结束**描述计划的结束;请单独检查轮次结果。结果保留最新 30 条;累计计数在此窗口之外仍会保留。**加载更多 Mission**加载下一页已保存对象。
|
||||
|
||||
列表仅在已配置时显示条件和结束限制,仅在有条件时显示**检查次数**。
|
||||
|
||||
### 现有每日日程 {#existing-daily-schedules}
|
||||
|
||||
现有每日日程使用已保存的 IANA 时区和当地民用时间。每次运行都有自己的协调者和任务图;选择当前任务树或过去的日期/结果,即可在中央阅读结果。其历史保留 30 个结果。以下每日规则适用于这些条目:
|
||||
|
||||
| 情况 | 调度行为 |
|
||||
| --- | --- |
|
||||
|
|
@ -150,21 +196,28 @@ CodeNomad 运行期间,项目中的每个智能体对话(包括任务协调
|
|||
| 时钟后拨 | 不会让已处理日期再次到期。 |
|
||||
| 现有运行仍待处理 | 不会与新的每日运行重叠。 |
|
||||
|
||||
创建时间也限制符合条件的每日工作;启动新日程不授权处理其存在之前的日期。提供时,**立即运行**会启动独立手动运行;不会替换每日时钟或绕过待处理运行。显示的下次时间只是推算,不保证准时执行。
|
||||
创建时间也限制符合条件的每日工作。现有每日条目提供**立即运行**时,它会在待处理运行保护下启动单独的手动运行,并保留每日时钟。显示的下次时间是推算值,受服务和主机可用性限制。
|
||||
|
||||
**关注的对话(可选)**从选定对话添加有界上下文。只有完成的运行才推进已处理消息游标;失败/中断运行不会静默消费该上下文。若指令加上下文超出容量,请缩短指令或移除关注对话,而非假定会读取无限历史。
|
||||
|
||||
## 生命周期与服务重启 {#lifetime}
|
||||
|
||||
**技术详情**旁的 **i** 说明技术活动及费用。在**设置 → OpenCode**中,监管状态和错误保持可见,常规说明收纳在 **i** 中。
|
||||
|
||||
对于原生计划,展开**技术详情**可查看其**主载体**和**协调载体**、各自的原生日历,以及任何**清理等待协调**状态。这些技术会话不提供消息输入操作,也不会出现在普通会话或来源选择器中。技术唤醒、标题和上下文压缩使用本地模型,不向提供商发送 HTTP 请求;原生事件和统计仍可能包含这些活动,而实际轮次仍产生正常的模型费用。上下文压缩在保持会话和日历标识不变的同时,限制所处理的上下文。已存储的原生消息和事件可能持续增长,直到显式清理已结束的计划;保留的 30 条 Mission 结果与之分开。深度控件和**计划**会在路径可用时通过**OpenCode 配置文件:**显示该路径;深度控件会另行标明其**项目写入目标**。
|
||||
|
||||
| 组件 | 关闭意味着什么 |
|
||||
| --- | --- |
|
||||
| CodeNomad 项目标签页 | 在 CodeNomad 中关闭该项目;不停止原生工作或 OpenCode 服务,重新打开即可再次找到会话。 |
|
||||
| 整个窗口、退出应用或重新加载渲染器 | 保留项目连接以便恢复;不停止原生工作或 OpenCode 服务。 |
|
||||
| CodeNomad 后端 | 不停止 OpenCode。运行中的一次性任务会继续:其协调者、原生子代理任务和任务工具都在 OpenCode 中运行。向独立会话分配任务并将其报告送达协调者、协调者准备的任务、停止已撤回任务的对话、恢复以及任务控制台需要等待后端;OpenCode 服务重启后继续一次性任务也是如此。 |
|
||||
| 运行中的每日日程 | 时钟和运行执行在 OpenCode 插件/服务中进行,包括 CodeNomad 界面和后端都关闭时。执行主机和 OpenCode 服务必须持续运行。 |
|
||||
| OpenCode 服务重启 | 丢失进程内日程作业。期望运行的日程变为**已中断**,直到显式**继续**;重新打开 CodeNomad 不会重新启用它。 |
|
||||
| 已启用的原生计划 | 在执行主机和服务可用时,持久化的 OpenCode 日历会唤醒其项目并执行轮次,即使 CodeNomad 界面和后端都已关闭。 |
|
||||
| OpenCode 服务重启 — 健康的原生计划 | 健康且期望状态为运行的计划会自动恢复其日历。新准入之前,会以原始标识核对待处理工作。 |
|
||||
| 现有每日日程 | 服务存活期间,即使 CodeNomad 关闭,其插件时钟也会运行。服务重启后,它处于**已中断**;显式**继续**会核对待处理运行并重新启用每日时钟。 |
|
||||
|
||||
重启后检查待处理运行,准备好后选择**继续**。继续会协调现有运行,不会将协调者输入发送两次;不保证继续被中断的原生轮次。被重启切断的轮次可能以无报告结束,并显示**因重启而中断**。不要用重启来重试工具副作用。
|
||||
对于原生计划,显式暂停、停止、失败工作和不确定结果会在重启后保留准入屏障。检查保留的轮次和原始请求;提供时使用**检查本次运行**或**刷新已保存的请求**,然后仅在状态允许时**继续**。原生继续执行会以其现有标识进行观察。已证实的失败或无报告结束需要人工恢复;证据不完整时,轮次仍不确定。绝不要通过重启来重复已开始的工具效果。
|
||||
|
||||
[保持 OpenCode 运行](opencode.md#service-supervision)会单独监管执行主机上的共享服务。除非已保存关闭偏好,否则首次播放任务会启用此功能;创建暂停的任务会保留此选择。关闭监管后,OpenCode 继续运行,但不会自动重新启动。Windows、macOS 和 WSL 受登录边界限制;原生 Linux 使用用户 linger,并受权限限制。睡眠、休眠、WSL 空闲生命周期和外部主机控制都会限制可用性;请独立于计划检查监管状态。
|
||||
|
||||
任务可在桌面应用和独立的 CodeNomad [服务器](server.md)上运行。面板显示**任务控制台不可用**时,其消息会指出原因:
|
||||
|
||||
|
|
@ -184,7 +237,9 @@ CodeNomad 运行期间,项目中的每个智能体对话(包括任务协调
|
|||
|
||||
一次性暂停会让进行中的工作先完成:协调者及其子代理完成当前步骤、不再开始新工作然后停止,期间面板显示**正在暂停:进行中的步骤正在完成。** **立即中断**则会立即停止它们:中断任务原生会话族并挂起协调。停止同样会中断,还会取消排队的会话族输入/交付,且为终止状态。中断有边界:**部分子代理可能仍在运行。**表示其中有一项不完整。检查保留结果并使用其显式重试,而非假定所有 Shell/工具影响已取消。继续会告知协调者哪些内容被暂停或中断;由它决定如何继续工作。
|
||||
|
||||
每日暂停/停止会取消调度并请求中断已注册根参与者,包括协调者和独立专家,但**不能证明这些对话或其后代已停止**。请检查原生会话族。停止是终止状态:停止的日程不能启动、继续或立即运行。**检查本次运行**会协调待处理运行,不会重新发送。临时计划检查失败会显示上次失败并自动重试;用户控制回复丢失不会引发自动控制重发。
|
||||
原生计划的**暂停**会关闭未来准入,并请求中断已接纳的轮次;**继续**会在重新启用前核对现有工作。**停止…**为终止操作。**立即运行**使用相同的待处理保护和已启动轮次额度,同时保留日历。**检查本次运行**核对原始轮次。请保留未确认的操作并使用**刷新已保存的请求**;被动刷新和重新连接会保留此请求。已开始的工具效果可能在中断后继续。
|
||||
|
||||
现有每日条目保留每日**暂停**、**停止…**、**继续**、**立即运行**和**检查本次运行**控件。暂停和停止会取消其时钟,并请求中断已注册的根对话,包括独立专家。请检查其原生会话族以及任何部分中断结果。临时每日检查失败会显示警告并重试;丢失的用户控制回复仍关联其原始请求。已停止的条目保持终止状态。
|
||||
|
||||
对于空闲但无报告的一次性协调者,主 ▶ 操作可验证并恢复;任务符合条件时有针对性的缺失报告恢复。恢复先检查原生活动、后代和等待;活动未知时拒绝接纳。不会重放分配或替换计划。打开对话以解决未完成问题、权限、Shell 或后台工作。
|
||||
|
||||
|
|
@ -192,12 +247,28 @@ CodeNomad 运行期间,项目中的每个智能体对话(包括任务协调
|
|||
|
||||
## 编辑、删除和清理对话 {#cleanup}
|
||||
|
||||
对可编辑的一次性任务使用**编辑所选任务**;冲突时关闭/重新打开编辑器以加载当前修订,不要覆盖。如果表单仍在加载时目标或备注发生变化,它会提示任务在你打开后已更改,而不会填入它们;选择**重新打开**以编辑当前版本。每日日程不提供编辑/删除控件。
|
||||
如果表单仍在加载时目标或备注发生变化,它会提示任务在你打开后已更改,而不会填入它们;选择**重新打开**以编辑当前版本。
|
||||
|
||||
轮次之间,若没有轮次或未解决的控制请求占用对象,暂停或运行中的原生计划可编辑标题、指令、备注、请求的配置、任务会话模式、来源、日历和限制。暂停的计划仍保持暂停;已启用的计划会在继续之前核对日历。保存会保留过去的结果、已处理的来源、启动计数和首次播放的起算点;未更改的结束限制保留其绝对结束时段。
|
||||
|
||||
已接纳的轮次期间,只有指令和备注遵循活动文本编辑契约:更改记录在该轮次历史中,并保存供未来轮次使用。现有子任务、简述、配置和执行标识保持准入时的值。结构字段在轮次结算前保持锁定,暂停后也是如此。只要创建过任何子任务,剧本就保持锁定,包括已完成或已撤回的子任务。准入或控制不确定,或完整性证据不足时,结构编辑保持只读。创建与编辑使用同一套日历校验;预览会将修改后的最后允许时段应用到已保存的下次唤醒,包括延迟到达的唤醒。
|
||||
|
||||
本地分支来源包含没有worktree的分支,仅读取本地引用。选择器最多支持200个本地分支;列表更大时来源会显示为不可用。可启用原生计划时,**开始**和**继续**会显示准确的配置路径并请求同意。原生计划未启用时,**立即运行**会禁用并显示原因;请在 OpenCode 配置中启用 `experimental.schedules`,然后刷新。启用响应丢失时,会保留原始请求以核对回执。
|
||||
|
||||
首次 Play 前,可以保留**选项**中的关注对话并改回立即无条件启动。Play 会读取并冻结完整、稳定的消息前缀(每个来源最多32条消息,启动输入总计不超过16,384个字符)。来源缺失、已移动或超出容量时,将说明原因并阻止启动。重试会保留原始冻结上下文和启动标识。
|
||||
|
||||
读取回执确认准备工作仅部分完成后,会显示**重试**。点击会再次检查同一回执,仅继续原始保存的更改,并保持任务暂停。
|
||||
|
||||
新准备的任务首次启动前,可以编辑标题、指令、备注、工作流程、请求的智能体/模型/思考设置、任务会话模式和**计划**。可以在即时、延迟、重复和有条件启动之间切换,并保留同一个任务。保存会保持暂停并准备一个新的空协调者,同时保留初始对话。只有 Play 才会开始工作或启用计划。启动后,指令和备注遵循活动任务的编辑规则。如果保存尚未确认,**刷新已保存的请求**只读取原始结果;部分准备工作阻止启动、文本编辑和删除时,请保留同一请求。
|
||||
|
||||
对可编辑的一次性任务或原生计划使用**编辑所选任务**。阅读锁定字段旁的说明。发生修订冲突时,选择**重新加载当前配置**,再打开**用于比较的旧草稿**,检查保留的草稿后再保存。现有每日日程保留其专属工具栏控件。
|
||||
|
||||
对于原生计划,**删除所选任务…**会先禁用未来轮次,再移除条目。对话和结果会保留;等待核对的清理仍显示在**技术详情**中。请保留未确认的删除,并刷新其原始请求。
|
||||
|
||||
1. 对不再需要的一次性任务,选择**删除所选任务…**并检查确认内容。
|
||||
2. 确认前可选中**同时删除为此任务创建的专家会话**。
|
||||
3. 查看**会话清理**中的已移除/保留/待处理数量。提供重试时,只重试剩余的已记录清理。
|
||||
|
||||
删除任务不同于停止;请先停止并检查剩余工作。协调者和复用对话会保留。共享、移动、有子会话或无法验证归属的专家也可能保留。部分清理不会回滚,也不会在后台静默完成。概览阅读器保留最近 20 次已完成的清理。删除任务会释放其在项目中的存储:较早删除的任务会自动清除。
|
||||
删除任务不同于停止;请先停止并检查剩余工作。协调者和复用对话会保留。共享、移动、有子会话或无法验证归属的专家也可能保留。部分清理不会回滚,也不会在后台静默完成。即使会话清理显示待处理 0,任务卡片也可能在删除仍待确认时消失。请继续使用原来的**重试**操作,直到删除得到确认;它会保留相同的删除请求和选项。概览阅读器保留最近 20 次已完成的清理。删除任务会释放其在项目中的存储:较早删除的任务会自动清除。
|
||||
|
||||
原生工作和服务控件请参阅[权限](permissions.md)、[对话](conversations.md)和 [OpenCode 设置](opencode.md)。
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ CodeNomad 连接到执行主机上的共享 OpenCode 服务。选择 CLI、更
|
|||
| 推荐 | OpenCode **2.2.5** | 已验证目标,与技术最低要求不同。 |
|
||||
| 未验证 | 尚未完整验证的更新版本或自定义版本 | 不会仅因未经测试而拒绝;会检查所连接服务的实际 API 契约。 |
|
||||
|
||||
所需的会话、权限、Form、收件箱和会话环境契约也必须可用。仅有可执行文件版本并不能证明所连接服务支持它们。如果设置报告运行中的服务不受支持,请选择/安装受支持 CLI,并在准备好后显式重启共享服务。
|
||||
所连接的服务必须提供所需的会话、权限、Form、收件箱和会话环境契约。原生任务计划还会检查计划 API、插件/提供商包和实际生效的 `experimental.schedules` 设置。可用性取决于这些实际使用的能力,也适用于兼容的未来版本。如果设置报告运行中的服务不受支持,请选择/安装受支持的 CLI,并在准备好后显式重启共享服务。
|
||||
|
||||
## 主机、WSL 和共享服务范围 {#execution-host}
|
||||
|
||||
|
|
@ -31,6 +31,18 @@ Windows 上,所选 WSL 可执行文件在其指定发行版中运行。请在
|
|||
|
||||
OpenCode 管理其全局服务注册、会话、消息和数据库。CodeNomad 不会按窗口或项目创建私有 OpenCode 数据库。关闭项目标签页会在 CodeNomad 中关闭该项目;会话继续在 OpenCode 中运行,重新打开项目时会再次出现。CodeNomad 绝不会卸载原生 Location。关闭整个窗口、退出或重新加载会保留项目连接以便恢复;这些操作以及关闭后端都不会停止共享守护进程。参阅[项目](projects.md)。
|
||||
|
||||
## 保持 OpenCode 运行 {#service-supervision}
|
||||
|
||||
在首次启动 Mission 前,**首次启动 Mission 时保持关闭** 可保存显式关闭的选择,无需先安装监管。
|
||||
|
||||
在 **设置 → OpenCode** 中,**保持 OpenCode 运行** 会在执行主机上安装系统监管,由使用此服务的所有 CodeNomad 配置共享。它独立于 Missions 运行,即使 CodeNomad 已关闭也有效。若未提前显式关闭,首次启动 Mission 时会启用;创建暂停的 Mission 会保留此选择。暂停、停止或删除 Mission 均保留此偏好。
|
||||
|
||||
关闭会移除监管,OpenCode 继续运行但不再被自动重启。如需持续停止,请先关闭此项,再显式停止 OpenCode。监管启用时,`opencode service stop` 后会重新启动。
|
||||
|
||||
**运行中 · 监管已生效** 要求系统监管进程活跃且 OpenCode 服务健康。**已安装 · 服务启动中**、**正在安装或移除监管…** 和 **监管失败** 表示不同状态。仅服务有响应,监管仍属未确认。详情会在已知时显示原因、上次尝试、监管进程重试和系统恢复安全检查。**检查监管状态** 只读取;**重试监管** 显式修复请求的安装。未确认的操作会保留原始请求;**重试原始操作** 仅重新发送完全相同的操作。设置版本发生更改时,会刷新以供查看,而非覆盖。
|
||||
|
||||
Windows、macOS 和 WSL 在登录后启动。WSL 需要为所选发行版启用 localhost 转发。在原生 Linux 上,linger 会在登录前启动 **所有已启用的用户服务**,并在注销后保持运行。关闭时,仅当 linger 由 CodeNomad 启用且其他监管不再需要时才会恢复;个人设置会保留。如果在没有用户会话打开时恢复 linger 可能导致脱离监管的 OpenCode 进程停止,恢复操作会暂缓,并继续显示 **重试监管**。请在用户会话打开,或脱离监管的 OpenCode 进程退出、可以安全恢复后重试。如果 Linux 需要权限,请在执行主机上授权后重试;请求的偏好仍会保存。睡眠和休眠由操作系统控制。不可用或不受支持的执行主机会显示禁用的控件及说明。
|
||||
|
||||
## 更新、重启或重新加载 {#maintenance}
|
||||
|
||||
1. 在**设置 → OpenCode**中选择**检查状态和更新**。
|
||||
|
|
@ -41,7 +53,7 @@ OpenCode 管理其全局服务注册、会话、消息和数据库。CodeNomad
|
|||
| 操作 | 范围与影响 | 安全恢复 |
|
||||
| --- | --- | --- |
|
||||
| 更新 CLI | 更改受支持的已安装可执行文件;不会隐式重启运行中的守护进程。 | 失败后检查已安装和运行中两个版本;解决错误后才显式重试。 |
|
||||
| 重启服务 | 替换共享进程,中断所有已连接 OpenCode 客户端中的活动工作;保留会话历史。 | 重新连接并检查对话和计划任务;重启不代表自动继续。 |
|
||||
| 重启服务 | 替换共享进程,中断所有已连接 OpenCode 客户端中的活动工作;保留会话历史。 | 健康运行中的原生任务计划会自动恢复;请检查待处理轮次及任何暂停、错误或不确定状态。现有每日条目需要**继续**。请单独检查一次性对话及其恢复控件;参见[任务](missions.md#lifetime)。 |
|
||||
| 重新加载 OpenCode 配置 | 保留进程,但为所有客户端重建**每个已加载 Location**。取消待处理权限/Form,关闭终端/后台命令;可能中断活动工作。 | 重新加载后检查资源和待处理工作。不要仅因回复丢失而重复提示词或修改操作。 |
|
||||
|
||||
已保存配置或已安装插件仍不可用,且可以接受中断时,请显式使用**重新加载 OpenCode 配置**(运行中的服务支持时位于**故障排查**下)。它不是无害的显示刷新。CodeNomad 不会用所选的较旧 CLI 降级正在运行的较新守护进程。
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@
|
|||
|
||||
### 向你提出的问题 {#questions}
|
||||
|
||||
只有协调者会向你提问。子任务及其辅助代理会把自己的问题返回给启动它们的对话,附上选项、证据、后果、可撤销性和建议,并继续进行不依赖答复的工作。协调者先解决自己能解决的问题,再把剩余问题合并到**需要你处理**下的一个表单中;它会问什么、决定如何标注,见[问题与决定](missions.md#questions)。无论一次性还是每日运行,每个协调者和每个子任务分配都会以指令形式收到这条规则;探索还增加了[下文](#wayfinder-checks)的决策检查。
|
||||
只有协调者会向你提问。子任务及其辅助代理会把自己的问题返回给启动它们的对话,附上选项、证据、后果、可撤销性和建议,并继续进行不依赖答复的工作。协调者先解决自己能解决的问题,再把剩余问题合并到**需要你处理**下的一个表单中;它会问什么、决定如何标注,见[问题与决定](missions.md#questions)。无论一次性还是按计划运行,每个协调者和每个子任务分配都会以指令形式收到这条规则;探索还增加了[下文](#wayfinder-checks)的决策检查。
|
||||
|
||||
请在中断停靠区作答(**需要你处理**下的**回答**):只有这样才算你的回答。在聊天中输入的回复只是普通消息。
|
||||
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
|
||||
协调者被要求在初始计划之后、每结束一个子任务之后、每当开始等待你时以及最终报告之前发布简报;参见[简报与更新请求](missions.md#briefings)。简报、子任务摘要和最终报告都是写给你的:使用你的语言,几句简短的话,先说结果,标识符和命令放在证据中。
|
||||
|
||||
最终报告说明交付了什么、还剩什么以及需要你决定什么。一次性任务只能以完成结束:当没有你就无法推进时,协调者会向你提问并等待,而不是以失败结束;只有你可以停止任务。每日运行也可能以失败结束。
|
||||
最终报告说明交付了什么、还剩什么以及需要你决定什么。一次性任务只能以完成结束:当没有你就无法推进时,协调者会向你提问并等待,而不是以失败结束;只有你可以停止任务。计划轮次也可能以失败结束。
|
||||
|
||||
### CodeNomad 在每个任务中检查的内容 {#common-checks}
|
||||
|
||||
|
|
@ -177,7 +177,7 @@
|
|||
- 只接受探索的五个角色。
|
||||
- 决策子任务只能以协调者自己的决定或你的回答完成。协调者的决定会写明问题、决定和理由,绝不会被记录为你的回答。
|
||||
- 只有你在 CodeNomad 界面中给出的回答才算数。CodeNomad 通过问题的标签把回答关联到子任务:问题是从协调者的对话中提出的,其文本以该子任务的标签结尾且不提及其他决定,回答发生在该子任务声明之后,并且尚未解决其他决定。合并表单中的每个问题分别关联;同时提到两个决定的问题哪一个都不解决。对从决策子任务自身对话中提出的问题的回答也会被接受。
|
||||
- 子任务自己的报告不能声称由谁决定。在任务或每日运行以成功结束之前,CodeNomad 会再次检查每个已完成的决策,包括标签。
|
||||
- 子任务自己的报告不能声称由谁决定。在任务或计划轮次以成功结束之前,CodeNomad 会再次检查每个已完成的决策,包括标签。
|
||||
- 这些检查适用于**原生子代理**。使用**独立会话**时,决策执行者自行提交简报,协调者把每个结果记录在备注中并注明由谁决定;CodeNomad 不验证这一点。
|
||||
|
||||
仅为指令的内容:何时认为一个选择易于撤销、简报和调研的质量,以及只做规划。
|
||||
|
|
|
|||
891
package-lock.json
generated
891
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -18,24 +18,25 @@
|
|||
},
|
||||
"scripts": {
|
||||
"clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"",
|
||||
"build": "npm run clean && npm run build:ui && npm run prepare-ui && tsc -p tsconfig.json && node ./scripts/copy-auth-pages.mjs && npm run build:pruning && npm run build:automation && npm run build:missions && npm run build:remote-tunnel",
|
||||
"build": "npm run clean && npm run build:ui && npm run prepare-ui && tsc -p tsconfig.json && node ./scripts/copy-auth-pages.mjs && npm run build:pruning && npm run build:automation && npm run build:missions && npm run build:supervision && npm run build:remote-tunnel",
|
||||
"build:pruning": "node ./scripts/build-session-pruning.mjs",
|
||||
"build:automation": "node ./scripts/build-automation-plugin.mjs",
|
||||
"build:missions": "node ./scripts/build-missions-plugin.mjs",
|
||||
"build:supervision": "node ./scripts/build-service-supervision.mjs",
|
||||
"build:remote-tunnel": "node ./scripts/build-remote-tunnel.mjs",
|
||||
"build:ui": "npm run build --prefix ../ui",
|
||||
"prepare-ui": "node ./scripts/copy-ui-dist.mjs",
|
||||
"dev": "npm run build:pruning && npm run build:automation && npm run build:missions && cross-env CODENOMAD_DEV=1 CODENOMAD_SERVER_PASSWORD=codenomad-dev CLI_UI_DEV_SERVER=http://localhost:3000 CLI_HTTPS=false CLI_HTTP=true tsx src/index.ts",
|
||||
"dev": "npm run build:pruning && npm run build:automation && npm run build:missions && npm run build:supervision && cross-env CODENOMAD_DEV=1 CODENOMAD_SERVER_PASSWORD=codenomad-dev CLI_UI_DEV_SERVER=http://localhost:3000 CLI_HTTPS=false CLI_HTTP=true tsx src/index.ts",
|
||||
"typecheck": "tsc --noEmit -p tsconfig.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fastify/cors": "^8.5.0",
|
||||
"@fastify/reply-from": "^9.8.0",
|
||||
"@fastify/static": "^7.0.4",
|
||||
"@opencode/client": "2.1.0",
|
||||
"@opencode/schema": "2.1.0",
|
||||
"@opencode/client": "2.2.3",
|
||||
"@opencode/schema": "2.2.3",
|
||||
"commander": "^12.1.0",
|
||||
"effect": "4.0.1",
|
||||
"effect": "4.0.6",
|
||||
"fastify": "^4.28.1",
|
||||
"fuzzysort": "^2.0.4",
|
||||
"jsonc-parser": "^3.3.1",
|
||||
|
|
@ -50,7 +51,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@codenomad/remote-tunnel": "file:../remote-tunnel",
|
||||
"@opencode/plugin": "2.1.0",
|
||||
"@opencode/plugin": "2.2.3",
|
||||
"@types/node-forge": "^1.3.14",
|
||||
"@types/npm-package-arg": "^6.1.4",
|
||||
"@types/yauzl": "^2.10.0",
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import { nativePluginBuild } from "./native-plugin-build.mjs"
|
|||
|
||||
await build({
|
||||
...nativePluginBuild,
|
||||
entryPoints: [fileURLToPath(new URL("../src/opencode/missions/managed-owner-plugin.ts", import.meta.url))],
|
||||
outfile: fileURLToPath(new URL("../dist/plugins/missions/plugin.mjs", import.meta.url)),
|
||||
entryPoints: {
|
||||
plugin: fileURLToPath(new URL("../src/opencode/missions/managed-owner-plugin.ts", import.meta.url)),
|
||||
provider: fileURLToPath(new URL("../src/opencode/missions/native-schedule-carrier-provider.ts", import.meta.url)),
|
||||
},
|
||||
outdir: fileURLToPath(new URL("../dist/plugins/missions", import.meta.url)),
|
||||
outExtension: { ".js": ".mjs" },
|
||||
})
|
||||
|
|
|
|||
30
packages/server/scripts/build-native-schedule-foundation.mjs
Normal file
30
packages/server/scripts/build-native-schedule-foundation.mjs
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
import path from "node:path"
|
||||
import { readFile, writeFile } from "node:fs/promises"
|
||||
import { createHash } from "node:crypto"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { build } from "esbuild"
|
||||
import { nativePluginBuild } from "./native-plugin-build.mjs"
|
||||
|
||||
/** Caller-owned packaging directory. Qualification entry is opt-in and separate
|
||||
* from the product plugin; the durable mapping owner controls installation. */
|
||||
export async function buildNativeScheduleFoundation(directory, { qualification = false } = {}) {
|
||||
if (!path.isAbsolute(directory)) throw new Error("Absolute native artifact directory required")
|
||||
const entries = {
|
||||
provider: "native-schedule-carrier-provider.ts",
|
||||
runtime: "native-schedule-carrier-runtime.ts",
|
||||
...(qualification ? { qualification: "native-schedule-carrier-native-fixture.ts", observer: "native-schedule-carrier-clock-fixture.ts" } : {}),
|
||||
}
|
||||
const result = await build({ ...nativePluginBuild, outdir: directory, outExtension: { ".js": ".mjs" }, metafile: true,
|
||||
entryPoints: Object.fromEntries(Object.entries(entries).map(([key, file]) => [["qualification", "observer"].includes(key) ? `${key}/index` : key,
|
||||
fileURLToPath(new URL(`../src/opencode/missions/${file}`, import.meta.url))])) })
|
||||
if (Object.keys(result.metafile.inputs).some(file => /node_modules[/\\](effect|@opencode[/\\]plugin)[/\\]/.test(file)))
|
||||
throw new Error("Native Effect/plugin runtime embedded")
|
||||
if (qualification) for (const part of ["qualification", "observer"]) await writeFile(path.join(directory, part, "package.json"), JSON.stringify({
|
||||
name: `codenomad-native-schedule-${part}`, version: "1.0.0", type: "module", exports: { ".": "./index.mjs" },
|
||||
peerDependencies: { effect: "^4.0.0" },
|
||||
}))
|
||||
return Object.fromEntries(await Promise.all(Object.keys(entries).map(async key => {
|
||||
const file = path.join(directory, ["qualification", "observer"].includes(key) ? `${key}/index.mjs` : `${key}.mjs`)
|
||||
return [key, { file, sha256: createHash("sha256").update(await readFile(file)).digest("hex") }]
|
||||
})))
|
||||
}
|
||||
20
packages/server/scripts/build-service-supervision.mjs
Normal file
20
packages/server/scripts/build-service-supervision.mjs
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import { build } from "esbuild"
|
||||
import { copyFile, mkdir } from "node:fs/promises"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { execFileSync } from "node:child_process"
|
||||
const root = new URL("../", import.meta.url)
|
||||
const destination = new URL("dist/host-lifetime/", root)
|
||||
await mkdir(destination, { recursive: true })
|
||||
const options = { bundle: true, platform: "node", format: "cjs", target: "node22",
|
||||
define: { "import.meta.url": "__moduleURL" },
|
||||
banner: { js: 'const __moduleURL = require("node:url").pathToFileURL(__filename).href;' } }
|
||||
await build({ entryPoints: [fileURLToPath(new URL("src/host-lifetime/service-supervision-probe.ts", root))],
|
||||
outfile: fileURLToPath(new URL("service-supervision-probe.cjs", destination)), ...options })
|
||||
await copyFile(new URL("src/host-lifetime/service-supervision-windows.ps1", root), new URL("service-supervision-windows.ps1", destination))
|
||||
await build({ entryPoints: [fileURLToPath(new URL("src/host-lifetime/service-supervision-linux-guard.ts", root))],
|
||||
outfile: fileURLToPath(new URL("service-supervision-linux-guard.cjs", destination)), ...options })
|
||||
if (process.platform === "darwin") {
|
||||
execFileSync("/usr/bin/clang", ["-std=c11", "-O2", "-Wall", "-Wextra", "-Werror", "-arch", "x86_64", "-arch", "arm64", "-mmacosx-version-min=13.5",
|
||||
fileURLToPath(new URL("src/host-lifetime/service-supervision-macos-native.c", root)),
|
||||
"-o", fileURLToPath(new URL("service-supervision-macos-native", destination))])
|
||||
}
|
||||
|
|
@ -11,6 +11,16 @@ export type { PanelExtensionManifest, PanelExtensionSummary, PanelExtensionConte
|
|||
export type { ForgeRepository, ForgeWorkItem, ForgeWorkItemKind, ForgeWorkItemsResult } from "./panel-extensions/contract"
|
||||
export type { GitHistoryCommit, GitHistoryPage, GitCommitFile, GitCommitDetails, GitCommitDiff } from "./git-history-types"
|
||||
export type { MissionRecurrenceReadPage } from "./missions/recurrence-reader-contract"
|
||||
export type { MissionScheduleList } from "./missions/mission-schedule-list"
|
||||
export type { MissionScheduleRead } from "./missions/mission-schedule-view"
|
||||
export interface MissionScheduleCapability {
|
||||
location: PluginControlLocation
|
||||
api: { available: true } | { available: false; reason: "missing-api" | "incompatible-contract" }
|
||||
enabled: boolean
|
||||
origin: string | null
|
||||
activation: { available: true; targets: Array<{ path: string; scope: "global" | "project" }> }
|
||||
| { available: false; reason: string; targets: [] }
|
||||
}
|
||||
|
||||
export type {
|
||||
MissionActor,
|
||||
|
|
@ -480,6 +490,8 @@ export interface SubagentDepthSnapshot {
|
|||
/** OpenCode's General subagent per its resolved rules: `effect` of launching another General subagent,
|
||||
* and `hidden` when OpenCode removes the subagent tool from General (a blanket denial last); `null` when unknown. */
|
||||
nesting: { effect: SubagentNestingEffect; hidden?: true } | null
|
||||
/** Document selecting the whole effective `experimental` block, including when depth uses its native default. */
|
||||
origin: string | null
|
||||
/** `emptyExperimental`: the project holds an `experimental` object without properties, which
|
||||
* hides every inherited experimental setting; Inherit removes it. `nestingSkipped`: the override saved only
|
||||
* the depth, because the project's General block cannot take the nesting rule without rewriting its value. */
|
||||
|
|
@ -946,3 +958,4 @@ export interface ServiceUsageSnapshot {
|
|||
connection?: string
|
||||
stats: import("@opencode/client").SessionStatsInfo
|
||||
}
|
||||
export type { SupervisionSettingsResponse, SupervisionSettingsControl } from "./host-lifetime/service-supervision-service"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { lookupMacosProcess } from "./service-supervision-macos-native"
|
||||
import { validateOwner, type Owner } from "./protocol"
|
||||
|
||||
export type ProcessState = { state: "live"; startIdentity: string } | { state: "dead" | "unknown" }
|
||||
|
|
@ -19,9 +21,10 @@ export const lookupProcess: ProcessLookup = async (pid) => {
|
|||
// Absence is a specific native lookup result, not any suppressed lookup
|
||||
// error. Permission/host failures cannot authorize replacing an owner.
|
||||
const script = `try { $p = [System.Diagnostics.Process]::GetProcessById(${pid}); try { 'LIVE:' + $p.StartTime.ToUniversalTime().Ticks } catch { 'UNKNOWN' } } catch [System.ArgumentException] { 'DEAD' } catch { 'UNKNOWN' }`
|
||||
return command("powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], "win32")
|
||||
return command(path.join(process.env.SystemRoot || "C:/Windows", "System32/WindowsPowerShell/v1.0/powershell.exe"),
|
||||
["-NoProfile", "-NonInteractive", "-Command", script], "win32")
|
||||
}
|
||||
// ps lstart has only second resolution. It is not a production identity gate.
|
||||
if (process.platform === "darwin") return lookupMacosProcess(pid)
|
||||
return { state: "unknown" }
|
||||
}
|
||||
function command(file: string, args: string[], prefix: string): Promise<ProcessState> {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,179 @@
|
|||
import { createHash } from "node:crypto"
|
||||
import path from "node:path"
|
||||
import { z } from "zod"
|
||||
|
||||
export const SUPERVISION_MANIFEST_BYTES = 64 * 1024
|
||||
const text = z.string().min(1).max(4096).regex(/^[^\x00-\x1f\x7f]+$/)
|
||||
const counter = z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER - 1)
|
||||
const requestID = z.string().uuid()
|
||||
const platform = z.enum(["win32", "darwin", "linux", "wsl"])
|
||||
const nativePath = (value: string, windows: boolean) => {
|
||||
const api = windows ? path.win32 : path.posix
|
||||
return api.isAbsolute(value) && api.normalize(value) === value
|
||||
&& !value.split(/[\\/]/).some(part => part === "." || part === "..")
|
||||
&& (windows ? /^[A-Za-z]:\\/.test(value) && !value.slice(2).includes(":") : !value.includes("\\"))
|
||||
}
|
||||
|
||||
/** Values come from the acquired Settings execution host, after native realpath
|
||||
* and UID/SID verification. Profiles, binaries, URLs and PIDs never enter the key. */
|
||||
export const supervisionNamespaceSchema = z.object({
|
||||
platform, host: text, user: text,
|
||||
distro: text.optional(),
|
||||
configDirectory: text, stateDirectory: text,
|
||||
registrationName: z.string().max(240).regex(/^service(?:-[a-zA-Z0-9._-]+)?\.json$/),
|
||||
}).strict().superRefine((value, ctx) => {
|
||||
const windows = value.platform === "win32"
|
||||
if (!nativePath(value.configDirectory, windows) || !nativePath(value.stateDirectory, windows)
|
||||
|| (value.platform === "wsl") !== Boolean(value.distro)
|
||||
|| (windows ? !/^S-1-\d+(?:-\d+)+$/.test(value.user) : !/^(?:0|[1-9]\d*)$/.test(value.user)))
|
||||
ctx.addIssue({ code: "custom", message: "supervision-namespace" })
|
||||
})
|
||||
export type SupervisionNamespace = z.infer<typeof supervisionNamespaceSchema>
|
||||
|
||||
export function supervisionKey(input: SupervisionNamespace): string {
|
||||
const value = supervisionNamespaceSchema.parse(input)
|
||||
// Case folding is limited to Windows paths/channel spelling. POSIX
|
||||
// namespace spelling is the canonical realpath supplied by its execution host.
|
||||
const fold = (entry: string) => value.platform === "win32" ? entry.toLowerCase() : entry
|
||||
return createHash("sha256").update(JSON.stringify([value.platform, value.host, value.user,
|
||||
value.distro ?? null, fold(value.configDirectory), fold(value.stateDirectory), fold(value.registrationName)])).digest("hex")
|
||||
}
|
||||
|
||||
/** The owning runtime resolver validates this selection. No arbitrary argv,
|
||||
* command string, profile environment or credentials can be persisted here. */
|
||||
const digest = z.string().regex(/^[a-f0-9]{64}$/)
|
||||
export const supervisionTargetSchema = z.object({ binary: text, provenance: z.enum(["npm", "standalone", "custom"]),
|
||||
sha256: digest.optional(),
|
||||
package: z.object({ file: text, sha256: digest, lock: text }).strict().optional(),
|
||||
}).strict()
|
||||
export type SupervisionTarget = z.infer<typeof supervisionTargetSchema>
|
||||
const controlSchema = z.object({
|
||||
requestID, expectedRevision: counter,
|
||||
action: z.enum(["on", "off", "first-play", "retry", "select-binary"]),
|
||||
target: supervisionTargetSchema.optional(),
|
||||
}).strict().superRefine((value, ctx) => {
|
||||
if ((value.action === "select-binary") !== Boolean(value.target))
|
||||
ctx.addIssue({ code: "custom", message: "supervision-target" })
|
||||
})
|
||||
export type SupervisionControl = z.infer<typeof controlSchema>
|
||||
const receiptSchema = z.object({ control: controlSchema, appliedRevision: counter }).strict()
|
||||
const timestamp = z.string().datetime({ offset: true })
|
||||
const attemptSchema = z.object({ revision: counter, at: timestamp, cause: z.literal("adapter-failed").nullable() }).strict()
|
||||
export const supervisionManifestSchema = z.object({
|
||||
v: z.literal(1), key: z.string().regex(/^[a-f0-9]{64}$/), namespace: supervisionNamespaceSchema,
|
||||
revision: counter, desired: z.enum(["unset", "on", "off"]), target: supervisionTargetSchema,
|
||||
// Acknowledgement requires a fresh read of the exact owned OS resources. This
|
||||
// revision is separate from the preference write and never inferred from time.
|
||||
reconciledRevision: counter.nullable(),
|
||||
receipt: receiptSchema.nullable(),
|
||||
attempt: attemptSchema.optional(),
|
||||
}).strict().superRefine((value, ctx) => {
|
||||
if (value.key !== supervisionKey(value.namespace)
|
||||
|| !nativePath(value.target.binary, value.namespace.platform === "win32")
|
||||
|| value.target.package && (!nativePath(value.target.package.file, value.namespace.platform === "win32")
|
||||
|| !nativePath(value.target.package.lock, value.namespace.platform === "win32"))
|
||||
|| value.reconciledRevision !== null && value.reconciledRevision > value.revision
|
||||
|| value.receipt !== null && value.receipt.appliedRevision !== value.revision
|
||||
|| value.attempt !== undefined && value.attempt.revision > value.revision)
|
||||
ctx.addIssue({ code: "custom", message: "supervision-manifest" })
|
||||
})
|
||||
export type SupervisionManifest = z.infer<typeof supervisionManifestSchema>
|
||||
|
||||
export class SupervisionError extends Error {
|
||||
constructor(readonly code: "invalid" | "conflict" | "busy" | "unknown-request" | "unsafe-storage") { super(`supervision-${code}`) }
|
||||
}
|
||||
export function parseSupervisionManifest(input: unknown): SupervisionManifest {
|
||||
try {
|
||||
if (Buffer.byteLength(JSON.stringify(input) ?? "", "utf8") > SUPERVISION_MANIFEST_BYTES) throw new Error()
|
||||
return supervisionManifestSchema.parse(input)
|
||||
} catch { throw new SupervisionError("invalid") }
|
||||
}
|
||||
export function initialSupervisionManifest(namespace: SupervisionNamespace, target: SupervisionTarget): SupervisionManifest {
|
||||
return parseSupervisionManifest({ v: 1, key: supervisionKey(namespace), namespace, target,
|
||||
revision: 0, desired: "unset", reconciledRevision: null, receipt: null })
|
||||
}
|
||||
|
||||
/** One bounded current receipt. A missing/evicted receipt is unknown; stale
|
||||
* revisions never authorize an old ON to overwrite a later OFF. */
|
||||
export function applySupervisionControl(input: SupervisionManifest, raw: SupervisionControl): SupervisionManifest {
|
||||
const current = parseSupervisionManifest(input)
|
||||
const parsed = controlSchema.safeParse(raw)
|
||||
if (!parsed.success) throw new SupervisionError("invalid")
|
||||
const control = parsed.data
|
||||
if (current.receipt?.control.requestID === control.requestID) {
|
||||
if (JSON.stringify(current.receipt.control) !== JSON.stringify(control)) throw new SupervisionError("conflict")
|
||||
return current
|
||||
}
|
||||
if (control.expectedRevision !== current.revision) throw new SupervisionError("conflict")
|
||||
if (control.action === "first-play" && current.desired !== "unset") return current
|
||||
// Preserve an unresolved original operation, including an unacknowledged binary
|
||||
// selection. OFF alone may fence it; retry reconciles that same revision.
|
||||
if (current.receipt && current.reconciledRevision !== current.revision && !["off", "retry"].includes(control.action))
|
||||
throw new SupervisionError("busy")
|
||||
if (control.action === "retry") return current
|
||||
return parseSupervisionManifest({ ...current, revision: current.revision + 1,
|
||||
desired: control.action === "on" || control.action === "first-play" ? "on" : control.action === "off" ? "off" : current.desired,
|
||||
target: control.target ?? current.target,
|
||||
receipt: { control, appliedRevision: current.revision + 1 } })
|
||||
}
|
||||
|
||||
export type SupervisionHealth =
|
||||
| { state: "healthy"; pid: number; id: string; startIdentity: string }
|
||||
| { state: "absent" | "unknown" | "starting" | "unhealthy" }
|
||||
export interface SupervisionObservation {
|
||||
revision: number
|
||||
resources: "absent" | "installed" | "unknown"
|
||||
guard: "active" | "absent" | "unknown"
|
||||
health: SupervisionHealth
|
||||
recoveryNextTickAt?: string
|
||||
artifactMissing?: boolean
|
||||
linuxLinger?: "enabled" | "external" | "disabled" | "deferred" | "uncertain" | "permission-required"
|
||||
detail?: {
|
||||
cause: "native-service-disabled-or-unknown" | "start-failed" | "guard-failed" | "service-unknown" | "wsl-forwarding-unavailable" | null
|
||||
lastAttemptAt?: string
|
||||
retryAt?: string
|
||||
}
|
||||
}
|
||||
export type SupervisionStatus = {
|
||||
desired: SupervisionManifest["desired"]; revision: number
|
||||
state: "off" | "pending" | "installed" | "running" | "failing"
|
||||
cause: "reconciliation-pending" | "observation-stale" | "resources-unknown" | "guard-absent"
|
||||
| "guard-unknown" | "service-absent" | "service-unknown" | "service-starting" | "service-unhealthy"
|
||||
| "adapter-failed" | "native-service-disabled-or-unknown" | "start-failed" | "guard-failed" | "artifact-missing" | "wsl-forwarding-unavailable"
|
||||
| "linux-linger-deferred" | "linux-linger-uncertain" | "linux-linger-permission-required" | "linux-linger-disabled" | null
|
||||
serviceResponding: boolean
|
||||
lastAttemptAt?: string
|
||||
retryAt?: string
|
||||
recoveryNextTickAt?: string
|
||||
}
|
||||
export function supervisionStatus(manifest: SupervisionManifest, observation: SupervisionObservation): SupervisionStatus {
|
||||
const value = parseSupervisionManifest(manifest)
|
||||
const result = (state: SupervisionStatus["state"], cause: SupervisionStatus["cause"]): SupervisionStatus => ({
|
||||
desired: value.desired, revision: value.revision, state, cause,
|
||||
serviceResponding: observation.revision === value.revision && observation.health.state === "healthy",
|
||||
...(value.attempt?.revision === value.revision ? { lastAttemptAt: value.attempt.at } : {}),
|
||||
...(value.desired === "on" && observation.revision === value.revision && observation.recoveryNextTickAt
|
||||
? { recoveryNextTickAt: observation.recoveryNextTickAt } : {}),
|
||||
...(observation.revision === value.revision && observation.guard === "active" ? {
|
||||
...(observation.detail?.lastAttemptAt ? { lastAttemptAt: observation.detail.lastAttemptAt } : {}),
|
||||
...(observation.detail?.retryAt ? { retryAt: observation.detail.retryAt } : {}),
|
||||
} : {}),
|
||||
})
|
||||
if (observation.revision !== value.revision) return result("failing", "observation-stale")
|
||||
if (value.desired === "unset" && observation.resources === "absent" && observation.guard === "absent") return result("off", null)
|
||||
if (value.desired === "on" && observation.artifactMissing) return result("failing", "artifact-missing")
|
||||
if (observation.linuxLinger && ["uncertain", "permission-required"].includes(observation.linuxLinger))
|
||||
return result("failing",
|
||||
`linux-linger-${observation.linuxLinger}` as SupervisionStatus["cause"])
|
||||
if (value.desired === "on" && observation.linuxLinger === "disabled") return result("failing", "linux-linger-disabled")
|
||||
if (value.attempt?.revision === value.revision && value.attempt.cause) return result("failing", value.attempt.cause)
|
||||
if (value.reconciledRevision !== value.revision) return result("pending", "reconciliation-pending")
|
||||
if (value.desired !== "on") return observation.resources === "absent" && observation.guard === "absent"
|
||||
? result("off", observation.linuxLinger === "deferred" ? "linux-linger-deferred" : null) : result("failing", "resources-unknown")
|
||||
if (observation.resources !== "installed") return result("failing", "resources-unknown")
|
||||
if (observation.guard !== "active") return result("failing", observation.guard === "absent" ? "guard-absent" : "guard-unknown")
|
||||
if (observation.detail?.cause) return result("failing", observation.detail.cause)
|
||||
if (observation.health.state === "healthy") return result("running", null)
|
||||
if (observation.health.state === "starting") return result("installed", "service-starting")
|
||||
return result("failing", `service-${observation.health.state}`)
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { test } from "node:test"
|
||||
import { mkdtemp, writeFile, rm } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { readSupervisionSample, supervisionReleasePreserved, type SupervisionSample } from "./service-supervision-health"
|
||||
|
||||
test("OFF continuity survives HTTP failure only with exact registration and native process proof", async t => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "supervision-release-")), file = path.join(root, "service.json")
|
||||
t.after(() => rm(root, { recursive: true, force: true }))
|
||||
const record = { pid: 123, id: "instance", url: "http://127.0.0.1:12345", version: "test" }
|
||||
const reset = () => writeFile(file, JSON.stringify({ ...record, password: "synthetic-private" }))
|
||||
const live = { state: "live" as const, startIdentity: "linux:boot:1234" }
|
||||
const deps = { lookup: async () => live, discover: async () => undefined }
|
||||
await reset()
|
||||
const unhealthy = await readSupervisionSample(file, deps)
|
||||
assert.deepEqual(unhealthy, { health: { state: "unhealthy" }, identity: { ...record, startIdentity: live.startIdentity } })
|
||||
assert.equal(JSON.stringify(unhealthy).includes("synthetic-private"), false)
|
||||
const healthy = await readSupervisionSample(file, { ...deps, discover: async () => ({ url: record.url, auth: undefined }) })
|
||||
const unknownHealth = await readSupervisionSample(file, { ...deps, discover: async () => { throw new Error("private") } })
|
||||
for (const before of [healthy, unhealthy, unknownHealth]) {
|
||||
for (const after of [healthy, unhealthy, unknownHealth]) assert.equal(supervisionReleasePreserved(before, after), true)
|
||||
}
|
||||
for (const changed of [{ pid: 124 }, { id: "replacement" }, { url: "http://127.0.0.1:12346" }, { version: "other" }]) {
|
||||
await writeFile(file, JSON.stringify({ ...record, ...changed }))
|
||||
const after = await readSupervisionSample(file, deps)
|
||||
assert.equal(supervisionReleasePreserved(unhealthy, after), false)
|
||||
}
|
||||
for (const bytes of ["{bad", JSON.stringify({ ...record, id: undefined }), "x".repeat(32769)]) {
|
||||
await writeFile(file, bytes)
|
||||
const after = await readSupervisionSample(file, deps)
|
||||
assert.deepEqual(after, { health: { state: "unknown" } })
|
||||
assert.equal(supervisionReleasePreserved(unhealthy, after), false)
|
||||
}
|
||||
await rm(file)
|
||||
const absent = await readSupervisionSample(file, deps)
|
||||
assert.equal(supervisionReleasePreserved(unhealthy, absent), false, "lost registration never proves release")
|
||||
assert.equal(supervisionReleasePreserved(absent, absent), true)
|
||||
assert.equal(supervisionReleasePreserved(absent, unhealthy), false, "a replacement cannot appear during removal")
|
||||
await reset()
|
||||
for (const process of [{ state: "dead" as const }, { state: "unknown" as const }, { ...live, startIdentity: "linux:boot:reused" }]) {
|
||||
const after = await readSupervisionSample(file, { ...deps, lookup: async () => process })
|
||||
assert.equal(supervisionReleasePreserved(unhealthy, after), false)
|
||||
}
|
||||
for (const state of ["healthy", "unknown", "unhealthy", "starting"] as const) {
|
||||
const unproved = { health: { state } } as SupervisionSample
|
||||
assert.equal(supervisionReleasePreserved(unproved, unproved), false, "generic health has no release authority")
|
||||
}
|
||||
})
|
||||
|
||||
test("discovery races and incomplete native identity invalidate OFF proof", async t => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "supervision-race-")), file = path.join(root, "service.json")
|
||||
t.after(() => rm(root, { recursive: true, force: true }))
|
||||
const record = { pid: 123, id: "original", url: "http://127.0.0.1:12345" }
|
||||
for (const change of ["id", "url", "pid", "start", "dead", "unknown", "remove", "malformed"] as const) {
|
||||
await writeFile(file, JSON.stringify(record))
|
||||
let reads = 0
|
||||
const sample = await readSupervisionSample(file, {
|
||||
lookup: async () => {
|
||||
reads++
|
||||
if (reads === 2 && (change === "dead" || change === "unknown")) return { state: change }
|
||||
return { state: "live", startIdentity: reads === 2 && change === "start" ? "reused" : "original" }
|
||||
},
|
||||
discover: async () => {
|
||||
if (change === "remove") await rm(file)
|
||||
if (change === "malformed") await writeFile(file, "{}")
|
||||
if (change === "id") await writeFile(file, JSON.stringify({ ...record, id: "other" }))
|
||||
if (change === "url") await writeFile(file, JSON.stringify({ ...record, url: "http://127.0.0.1:12346" }))
|
||||
if (change === "pid") await writeFile(file, JSON.stringify({ ...record, pid: 124 }))
|
||||
return undefined
|
||||
},
|
||||
})
|
||||
assert.deepEqual(sample, { health: { state: "unknown" } }, change)
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
import { lstat, readFile } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { Service } from "@opencode/client/service"
|
||||
import { lookupProcess, type ProcessLookup } from "./process-identity"
|
||||
import { readNativeScheduleIncarnation, sameNativeScheduleIncarnation, type NativeScheduleIncarnation } from "../opencode/missions/native-schedule-transport-identity"
|
||||
import { type SupervisionHealth } from "./service-supervision-contract"
|
||||
|
||||
export async function nativeSupervisionStartPermitted(configFile: string): Promise<boolean> {
|
||||
try {
|
||||
const stat = await lstat(configFile).catch(error => { if (error.code !== "ENOENT") throw error; return null })
|
||||
if (!stat) return true
|
||||
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 32768) return false
|
||||
const bytes = await readFile(configFile)
|
||||
if (bytes.length > 32768) return false
|
||||
const config = JSON.parse(bytes.toString("utf8"))
|
||||
return config !== null && typeof config === "object" && !Array.isArray(config)
|
||||
&& (config.disabled === undefined || config.disabled === false)
|
||||
} catch { return false }
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
discover: typeof Service.discover
|
||||
lookup: ProcessLookup
|
||||
}
|
||||
|
||||
/** A health read has no lifecycle capability. A live but unresponsive process,
|
||||
* an unreadable registration, or PID reuse always stays unknown/unhealthy.
|
||||
* Absence is an observation, not permission to spawn: the guard must still hold
|
||||
* its Start exclusion and prove the acquired namespace vacant. */
|
||||
export async function readSupervisionHealth(registrationFile: string,
|
||||
overrides: Partial<Dependencies> = {}): Promise<SupervisionHealth> {
|
||||
return (await readSupervisionSample(registrationFile, overrides)).health
|
||||
}
|
||||
|
||||
export interface SupervisionSample {
|
||||
health: SupervisionHealth
|
||||
identity?: NativeScheduleIncarnation & { startIdentity: string }
|
||||
}
|
||||
|
||||
/** Process continuity is independent of HTTP health and grants only OFF release
|
||||
* acknowledgement. Start and Running still require their existing health gates. */
|
||||
export function supervisionReleasePreserved(before: SupervisionSample, after: SupervisionSample): boolean {
|
||||
return before.identity !== undefined && after.identity !== undefined
|
||||
? sameNativeScheduleIncarnation(before.identity, after.identity)
|
||||
&& before.identity.startIdentity === after.identity.startIdentity
|
||||
: !before.identity && !after.identity && before.health.state === "absent" && after.health.state === "absent"
|
||||
}
|
||||
|
||||
export async function readSupervisionSample(registrationFile: string,
|
||||
overrides: Partial<Dependencies> = {}): Promise<SupervisionSample> {
|
||||
if (!path.isAbsolute(registrationFile)) return { health: { state: "unknown" } }
|
||||
const deps: Dependencies = { discover: Service.discover, lookup: lookupProcess, ...overrides }
|
||||
try {
|
||||
// Missing path and an existing malformed/dangling entry are distinct.
|
||||
const stat = await lstat(registrationFile).catch(error => {
|
||||
if (error.code === "ENOENT") return undefined
|
||||
throw error
|
||||
})
|
||||
// Namespace absence alone cannot prove a previously observed process ended;
|
||||
// linger rollback separately checks its durable PID/start identity.
|
||||
if (!stat) return { health: { state: "absent" } }
|
||||
if (stat.isSymbolicLink() || !stat.isFile()) return { health: { state: "unknown" } }
|
||||
const before = readNativeScheduleIncarnation(registrationFile)
|
||||
const processBefore = await deps.lookup(before.pid)
|
||||
if (processBefore.state !== "live") return { health: { state: processBefore.state === "dead" ? "absent" : "unknown" } }
|
||||
// Official discovery authenticates its bounded health probe, with credentials
|
||||
// scoped to this invocation. It neither ensures a daemon nor spawns a CLI.
|
||||
let discoveryFailed = false
|
||||
const endpoint = await deps.discover({ file: registrationFile }).catch(() => { discoveryFailed = true; return undefined })
|
||||
const afterStat = await lstat(registrationFile)
|
||||
if (afterStat.isSymbolicLink() || !afterStat.isFile()) return { health: { state: "unknown" } }
|
||||
const after = readNativeScheduleIncarnation(registrationFile)
|
||||
const processAfter = await deps.lookup(after.pid)
|
||||
if (!sameNativeScheduleIncarnation(before, after) || processAfter.state !== "live"
|
||||
|| processBefore.startIdentity !== processAfter.startIdentity) return { health: { state: "unknown" } }
|
||||
const identity = { ...after, startIdentity: processAfter.startIdentity }
|
||||
if (!endpoint || endpoint.url !== after.url) return { health: { state: discoveryFailed ? "unknown" : "unhealthy" }, identity }
|
||||
return { health: { state: "healthy", pid: after.pid, id: after.id, startIdentity: processAfter.startIdentity }, identity }
|
||||
} catch {
|
||||
// Never propagate SDK/parser/native errors containing credentials or paths.
|
||||
return { health: { state: "unknown" } }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
import { execFile, spawn, type ChildProcess } from "node:child_process"
|
||||
import path from "node:path"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import { readLinuxPrivate, writeLinuxPrivate } from "./service-supervision-linux-native"
|
||||
import { linuxRuntimeSchema } from "./service-supervision-linux"
|
||||
import { parseSupervisionManifest, type SupervisionHealth } from "./service-supervision-contract"
|
||||
import { nativeSupervisionStartPermitted, readSupervisionHealth } from "./service-supervision-health"
|
||||
import { lookupProcess } from "./process-identity"
|
||||
import { withLinuxSupervisionLock } from "./service-supervision-linux-lock"
|
||||
import { macosRuntimeSchema } from "./service-supervision-macos"
|
||||
import { lookupMacosProcess, withMacosSupervisionLock } from "./service-supervision-macos-native"
|
||||
import { supervisionTargetReady } from "./service-supervision-target"
|
||||
|
||||
/** The OS owns this foreground process. SIGTERM/OFF releases its child without
|
||||
* signaling it. A live unhealthy child or incumbent is preserved indefinitely. */
|
||||
async function guard() {
|
||||
const [directory, key] = process.argv.slice(2)
|
||||
const macos = process.platform === "darwin"
|
||||
if ((!macos && process.platform !== "linux") || !directory || !path.isAbsolute(directory)) throw new Error()
|
||||
const initial = parseSupervisionManifest(await readLinuxPrivate(path.join(directory, "manifest.json")))
|
||||
if (initial.key !== key || initial.namespace.platform !== process.platform || initial.namespace.user !== String(process.getuid!())) throw new Error()
|
||||
const runtime = macos ? macosRuntimeSchema.parse(await readLinuxPrivate(path.join(directory, "macos.json")))
|
||||
: linuxRuntimeSchema.parse(await readLinuxPrivate(path.join(directory, "linux.json")))
|
||||
const nativeHelper = "nativeHelper" in runtime ? runtime.nativeHelper : undefined
|
||||
const lookup = macos ? (pid: number) => lookupMacosProcess(pid, nativeHelper) : lookupProcess
|
||||
const healthRead = (file: string) => readSupervisionHealth(file, { lookup })
|
||||
const lock = <T>(file: string, run: () => Promise<T>) => macos ? withMacosSupervisionLock(file, run, undefined, nativeHelper)
|
||||
: withLinuxSupervisionLock(file, run)
|
||||
if (path.join(runtime.environment.XDG_CONFIG_HOME, "opencode") !== initial.namespace.configDirectory
|
||||
|| path.join(runtime.environment.XDG_STATE_HOME, "opencode") !== initial.namespace.stateDirectory) throw new Error()
|
||||
const store = new ServiceSupervisionStore(path.dirname(directory), initial.namespace, initial.target, undefined, nativeHelper)
|
||||
if (store.directory !== directory) throw new Error()
|
||||
const owner = await lookup(process.pid)
|
||||
if (owner.state !== "live") throw new Error()
|
||||
let running = true, child: ChildProcess | undefined, nextAttempt = 0, failures = 0
|
||||
let health: SupervisionHealth = { state: "unknown" }, nextProbe = 0, probeFailures = 0
|
||||
let lastAttemptAt: string | undefined, lastStatus = ""
|
||||
let firstHealthy: { id: string; at: string; bootSeconds: number } | undefined
|
||||
process.on("SIGTERM", () => { running = false })
|
||||
process.on("SIGINT", () => { running = false })
|
||||
process.on("SIGHUP", () => { running = false })
|
||||
const registration = path.join(initial.namespace.stateDirectory, initial.namespace.registrationName)
|
||||
try {
|
||||
while (running) {
|
||||
const manifest = await store.read()
|
||||
if (manifest.desired !== "on") break
|
||||
if (Date.now() >= nextProbe) {
|
||||
health = await healthRead(registration)
|
||||
probeFailures = health.state === "healthy" ? 0 : Math.min(probeFailures + 1, 5)
|
||||
nextProbe = Date.now() + Math.min(16_000, 1000 * 2 ** Math.max(0, probeFailures - 1))
|
||||
}
|
||||
const permitted = await nativeSupervisionStartPermitted(path.join(manifest.namespace.configDirectory, manifest.namespace.registrationName))
|
||||
let cause: "native-service-disabled-or-unknown" | "start-failed" | "service-unknown" | null = permitted ? null : "native-service-disabled-or-unknown"
|
||||
if (child && (child.exitCode !== null || child.signalCode !== null)) child = undefined
|
||||
const childLive = child?.pid ? (await lookup(child.pid)).state !== "dead" : false
|
||||
if (permitted && health.state === "absent" && !childLive && Date.now() >= nextAttempt) {
|
||||
await lock(path.join(directory, "admission.lock"), async () => {
|
||||
const current = await store.read()
|
||||
if (!running || current.desired !== "on") return
|
||||
health = await healthRead(registration)
|
||||
if (health.state !== "absent") return
|
||||
if (!await nativeSupervisionStartPermitted(path.join(current.namespace.configDirectory, current.namespace.registrationName))) {
|
||||
cause = "native-service-disabled-or-unknown"; nextAttempt = Date.now() + 30_000; return
|
||||
}
|
||||
if (!running || (await store.read()).desired !== "on") return
|
||||
lastAttemptAt = new Date().toISOString()
|
||||
nextAttempt = Date.now() + Math.min(30_000, 2000 * 2 ** Math.min(failures++, 4))
|
||||
const run = (args: string[]) => new Promise<string>((resolve, reject) => execFile(current.target.binary, args,
|
||||
{ env: { ...runtime.environment, PATH: "/usr/bin:/bin", LANG: "C.UTF-8" }, timeout: 2000, maxBuffer: 65536 },
|
||||
(error, stdout) => error ? reject(new Error("supervision-target-unavailable")) : resolve(stdout.trim())))
|
||||
if (!await supervisionTargetReady(current.target, undefined, { namespace: current.namespace, run })) { cause = "start-failed"; return }
|
||||
if (!running || (await store.read()).revision !== current.revision) return
|
||||
// Independent /dev/null outputs survive guard exit. No detached launcher,
|
||||
// kill timer, service stop, profile environment or credential arguments.
|
||||
child = spawn(current.target.binary, ["serve", "--service"], { env: { ...runtime.environment, PATH: "/usr/bin:/bin", LANG: "C.UTF-8" },
|
||||
cwd: runtime.environment.HOME, stdio: "ignore" })
|
||||
await new Promise<void>(resolve => { child!.once("spawn", resolve); child!.once("error", () => { child = undefined; cause = "start-failed"; resolve() }) })
|
||||
}).catch(() => { cause = "service-unknown" })
|
||||
}
|
||||
if (health.state === "healthy") {
|
||||
failures = 0; nextAttempt = 0
|
||||
if (!macos && firstHealthy?.id !== health.id) firstHealthy = { id: health.id, at: new Date().toISOString(),
|
||||
bootSeconds: Number((await readFile("/proc/uptime", "utf8")).split(" ")[0]) }
|
||||
}
|
||||
else if (!cause) cause = health.state === "absent" && !childLive ? "start-failed" : "service-unknown"
|
||||
const status = { key, revision: manifest.revision, guardPid: process.pid, guardStart: owner.startIdentity, cause,
|
||||
mode: childLive ? "hosted" : health.state === "healthy" ? "observed" : "waiting", service: health,
|
||||
...(firstHealthy ? { firstHealthy } : {}),
|
||||
...(lastAttemptAt ? { lastAttemptAt } : {}), ...(nextAttempt > Date.now() ? { retryAt: new Date(nextAttempt).toISOString() } : {}) }
|
||||
const bytes = JSON.stringify(status)
|
||||
if (bytes !== lastStatus) { await writeLinuxPrivate(path.join(directory, macos ? "macos-status.json" : "linux-status.json"), status); lastStatus = bytes }
|
||||
if (running) await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
}
|
||||
} finally { child?.unref() }
|
||||
}
|
||||
guard().catch(() => { process.exitCode = 1 })
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
import path from "node:path"
|
||||
import { lstat } from "node:fs/promises"
|
||||
import { z } from "zod"
|
||||
import { SupervisionError, type SupervisionManifest } from "./service-supervision-contract"
|
||||
import { withLinuxSupervisionLock } from "./service-supervision-linux-lock"
|
||||
import { linuxCommand, readLinuxPrivate, writeLinuxPrivate, type LinuxCommand } from "./service-supervision-linux-native"
|
||||
import { readSupervisionHealth } from "./service-supervision-health"
|
||||
import { lookupProcess, type ProcessLookup } from "./process-identity"
|
||||
import { readNativeScheduleIncarnation, sameNativeScheduleIncarnation } from "../opencode/missions/native-schedule-transport-identity"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
|
||||
const key = z.string().regex(/^[a-f0-9]{64}$/)
|
||||
const registrySchema = z.object({ v: z.literal(1), user: z.string().regex(/^\d+$/), host: z.string(),
|
||||
phase: z.enum(["external", "enabling", "owned", "disabling", "uncertain", "disabled"]),
|
||||
fingerprint: z.string().nullable(), requests: z.array(key).max(256),
|
||||
released: z.array(z.object({ key, registration: z.string().max(4096),
|
||||
process: z.object({ pid: z.number().int().positive(), id: z.string().min(1), startIdentity: z.string().min(1) }).strict().nullable().default(null),
|
||||
}).strict()).max(256),
|
||||
deferred: z.boolean(),
|
||||
}).strict()
|
||||
type Registry = z.infer<typeof registrySchema>
|
||||
export type LinuxLingerState = "enabled" | "external" | "disabled" | "deferred" | "uncertain" | "permission-required"
|
||||
interface Dependencies {
|
||||
command: LinuxCommand
|
||||
fingerprint(): Promise<string | null>
|
||||
health: typeof readSupervisionHealth
|
||||
lookup: ProcessLookup
|
||||
identity: typeof releasedProcess
|
||||
lock<T>(file: string, run: () => Promise<T>): Promise<T>
|
||||
}
|
||||
|
||||
async function releasedProcess(registration: string, lookup: ProcessLookup) {
|
||||
try {
|
||||
const stat = await lstat(registration)
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) return null
|
||||
const before = readNativeScheduleIncarnation(registration), process = await lookup(before.pid)
|
||||
if (process.state === "live" && sameNativeScheduleIncarnation(before, readNativeScheduleIncarnation(registration)))
|
||||
return { pid: before.pid, id: before.id, startIdentity: process.startIdentity }
|
||||
} catch { /* Missing or damaged metadata leaves release ownership unknown. */ }
|
||||
return null
|
||||
}
|
||||
|
||||
/** A single registry per UID/host, across every native service namespace. An
|
||||
* interrupted enable/disable remains uncertain and never acquires ownership. */
|
||||
export class LinuxSupervisionLinger {
|
||||
private readonly deps: Dependencies
|
||||
constructor(private readonly root: string, private readonly user: string, private readonly host: string,
|
||||
username: string, private readonly policy: StoragePolicy = privateStorage, overrides: Partial<Dependencies> = {}) {
|
||||
if (!/^[A-Za-z0-9_.@-]+$/.test(username)) throw new SupervisionError("invalid")
|
||||
this.deps = { command: linuxCommand, health: readSupervisionHealth, lookup: lookupProcess, identity: releasedProcess,
|
||||
lock: (file, run) => withLinuxSupervisionLock(file, run, policy),
|
||||
fingerprint: async () => {
|
||||
try {
|
||||
const s = await lstat(`/var/lib/systemd/linger/${username}`, { bigint: true })
|
||||
if (!s.isFile() || s.isSymbolicLink() || s.uid !== 0n) throw new SupervisionError("unsafe-storage")
|
||||
return `${s.dev}:${s.ino}:${s.mtimeNs}:${s.ctimeNs}`
|
||||
} catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return null; throw error }
|
||||
}, ...overrides }
|
||||
}
|
||||
private async read(): Promise<Registry | undefined> {
|
||||
const raw = await readLinuxPrivate(path.join(this.root, "linux-linger.json"), this.policy)
|
||||
if (raw === undefined) return
|
||||
const r = registrySchema.parse(raw)
|
||||
if (r.user !== this.user || r.host !== this.host || new Set(r.requests).size !== r.requests.length)
|
||||
throw new SupervisionError("conflict")
|
||||
return r
|
||||
}
|
||||
private async enabled() {
|
||||
const value = await this.deps.command("/usr/bin/loginctl", ["show-user", this.user, "--property=Linger", "--value"])
|
||||
if (value !== "yes" && value !== "no") throw new SupervisionError("invalid")
|
||||
return value === "yes"
|
||||
}
|
||||
private save(r: Registry) { return writeLinuxPrivate(path.join(this.root, "linux-linger.json"), registrySchema.parse(r), this.policy) }
|
||||
|
||||
/** Persist the live process before stopping its guard. Health failure and a
|
||||
* later deleted registration must preserve this independent native identity. */
|
||||
async retainReleased(manifest: SupervisionManifest): Promise<void> {
|
||||
await this.deps.lock(path.join(this.root, "linux-linger.lock"), async () => {
|
||||
const r = await this.read()
|
||||
if (!r || r.released.some(entry => entry.key === manifest.key)) return
|
||||
const registration = path.posix.join(manifest.namespace.stateDirectory, manifest.namespace.registrationName)
|
||||
r.released.push({ key: manifest.key, registration, process: await this.deps.identity(registration, this.deps.lookup) })
|
||||
await this.save(r)
|
||||
})
|
||||
}
|
||||
|
||||
async observe(): Promise<LinuxLingerState> {
|
||||
await this.reconcileDeferred()
|
||||
const r = await this.read(), enabled = await this.enabled()
|
||||
if (!r) return enabled ? "external" : "disabled"
|
||||
if (r.phase === "enabling") return enabled ? "uncertain" : "permission-required"
|
||||
if (["disabling", "uncertain"].includes(r.phase)) return "uncertain"
|
||||
if (r.phase === "owned" && (!enabled || r.fingerprint !== await this.deps.fingerprint())) return "uncertain"
|
||||
return r.deferred ? "deferred" : enabled ? r.phase === "owned" ? "enabled" : "external" : "disabled"
|
||||
}
|
||||
|
||||
/** Complete an already requested rollback during startup/status reconciliation.
|
||||
* Recheck all account-wide consumers and provenance under the same lock as ON. */
|
||||
async reconcileDeferred(): Promise<void> {
|
||||
await this.deps.lock(path.join(this.root, "linux-linger.lock"), async () => {
|
||||
const r = await this.read()
|
||||
if (!r?.deferred || r.phase !== "owned" || r.requests.length) return
|
||||
if (!await this.enabled() || r.fingerprint !== await this.deps.fingerprint()) {
|
||||
r.phase = "uncertain"; await this.save(r); return
|
||||
}
|
||||
await this.rollback(r)
|
||||
})
|
||||
}
|
||||
|
||||
async reconcile(manifest: SupervisionManifest): Promise<LinuxLingerState> {
|
||||
return this.deps.lock(path.join(this.root, "linux-linger.lock"), async () => {
|
||||
let r = await this.read()
|
||||
let enabled = await this.enabled()
|
||||
if (!r) r = { v: 1, user: this.user, host: this.host, phase: enabled ? "external" : "disabled",
|
||||
fingerprint: null, requests: [], released: [], deferred: false }
|
||||
if (r.phase === "owned" && (!enabled || r.fingerprint !== await this.deps.fingerprint())) r.phase = "uncertain"
|
||||
if (r.phase === "disabling") r.phase = "uncertain"
|
||||
if (manifest.desired === "on") {
|
||||
if (!r.requests.includes(manifest.key)) r.requests.push(manifest.key)
|
||||
r.released = r.released.filter(value => value.key !== manifest.key)
|
||||
r.deferred = false
|
||||
if (r.phase === "enabling" && enabled) r.phase = "uncertain" // Lost ACK cannot prove who enabled it.
|
||||
await this.save(r)
|
||||
if (r.phase === "uncertain") return "uncertain"
|
||||
if (!enabled) {
|
||||
r.phase = "enabling"; r.fingerprint = null; await this.save(r)
|
||||
try { await this.deps.command("/usr/bin/loginctl", ["--no-ask-password", "enable-linger", this.user]) }
|
||||
catch { return "permission-required" }
|
||||
enabled = await this.enabled()
|
||||
if (!enabled || !(r.fingerprint = await this.deps.fingerprint())) return "uncertain"
|
||||
r.phase = "owned"; await this.save(r)
|
||||
}
|
||||
return r.phase === "owned" ? "enabled" : "external"
|
||||
}
|
||||
r.requests = r.requests.filter(value => value !== manifest.key)
|
||||
if (!r.released.some(value => value.key === manifest.key)) {
|
||||
const registration = path.posix.join(manifest.namespace.stateDirectory, manifest.namespace.registrationName)
|
||||
r.released.push({ key: manifest.key, registration, process: await this.deps.identity(registration, this.deps.lookup) })
|
||||
}
|
||||
await this.save(r)
|
||||
if (["enabling", "uncertain"].includes(r.phase)) return "uncertain"
|
||||
if (r.phase !== "owned" || r.requests.length) return enabled ? r.phase === "owned" ? "enabled" : "external" : "disabled"
|
||||
return this.rollback(r)
|
||||
})
|
||||
}
|
||||
|
||||
private async rollback(r: Registry): Promise<LinuxLingerState> {
|
||||
// A session must really maintain the user manager. Closing/lingering-only
|
||||
// sessions do not qualify. Otherwise every released service must be absent.
|
||||
const sessions = (await this.deps.command("/usr/bin/loginctl", ["show-user", this.user, "--property=Sessions", "--value"])).split(/\s+/).filter(Boolean)
|
||||
let safe = false
|
||||
for (const id of sessions) {
|
||||
if (!/^[A-Za-z0-9_-]+$/.test(id)) throw new SupervisionError("invalid")
|
||||
const state = await this.deps.command("/usr/bin/loginctl", ["show-session", id, "--property=State", "--value"])
|
||||
if (state === "active" || state === "online") safe = true
|
||||
}
|
||||
if (!safe) {
|
||||
safe = true
|
||||
for (const entry of r.released) {
|
||||
if (!entry.process) { safe = false; continue }
|
||||
const process = await this.deps.lookup(entry.process.pid)
|
||||
if (process.state === "unknown" || process.state === "live" && process.startIdentity === entry.process.startIdentity
|
||||
|| (await this.deps.health(entry.registration)).state !== "absent") safe = false
|
||||
}
|
||||
}
|
||||
if (!safe) { r.deferred = true; await this.save(r); return "deferred" }
|
||||
// Revalidate the owned marker immediately before rollback.
|
||||
if (r.fingerprint !== await this.deps.fingerprint()) { r.phase = "uncertain"; await this.save(r); return "uncertain" }
|
||||
r.phase = "disabling"; await this.save(r)
|
||||
try { await this.deps.command("/usr/bin/loginctl", ["--no-ask-password", "disable-linger", this.user]) }
|
||||
catch { r.phase = "owned"; r.deferred = true; await this.save(r); return "permission-required" }
|
||||
if (await this.enabled()) return "uncertain"
|
||||
r.phase = "disabled"; r.fingerprint = null; r.deferred = false; r.released = []; await this.save(r)
|
||||
return "disabled"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import { spawn } from "node:child_process"
|
||||
import { open } from "node:fs/promises"
|
||||
import { SupervisionError } from "./service-supervision-contract"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
|
||||
/** flock owns an advisory descriptor, released even when this process crashes.
|
||||
* stdin EOF releases the helper; no PID/time-based abandoned-lock stealing. */
|
||||
export async function withLinuxSupervisionLock<T>(file: string, run: () => Promise<T>, policy: StoragePolicy = privateStorage): Promise<T> {
|
||||
const handle = await open(file, "a", 0o600)
|
||||
await handle.close()
|
||||
await policy.verify(file, false)
|
||||
const child = spawn("/usr/bin/flock", ["--exclusive", "--nonblock", file, "/bin/sh", "-c", "printf ready; cat >/dev/null"],
|
||||
{ stdio: ["pipe", "pipe", "ignore"], env: { PATH: "/usr/bin:/bin", LANG: "C" } })
|
||||
child.stdin.on("error", () => {})
|
||||
const exited = new Promise<void>(resolve => { child.once("close", () => resolve()); child.once("error", () => resolve()) })
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => { child.stdin.end(); reject(new SupervisionError("busy")) }, 3000)
|
||||
child.once("error", () => { clearTimeout(timer); reject(new SupervisionError("busy")) })
|
||||
child.once("exit", () => { clearTimeout(timer); reject(new SupervisionError("busy")) })
|
||||
child.stdout.once("data", bytes => { clearTimeout(timer); bytes.toString() === "ready" ? resolve() : reject(new SupervisionError("busy")) })
|
||||
})
|
||||
return await run()
|
||||
} finally { child.stdin.end(); await exited }
|
||||
}
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import { lstat, mkdir, open, readFile, rename, rm } from "node:fs/promises"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import path from "node:path"
|
||||
import { SupervisionError } from "./service-supervision-contract"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
|
||||
export type LinuxCommand = (binary: "/usr/bin/systemctl" | "/usr/bin/loginctl", args: string[]) => Promise<string>
|
||||
export const linuxCommand: LinuxCommand = (binary, args) => new Promise((resolve, reject) => {
|
||||
execFile(binary, args, { env: { PATH: "/usr/bin:/bin", LANG: "C", HOME: process.env.HOME,
|
||||
XDG_RUNTIME_DIR: `/run/user/${process.getuid!()}`, DBUS_SESSION_BUS_ADDRESS: `unix:path=/run/user/${process.getuid!()}/bus` },
|
||||
timeout: 10_000, maxBuffer: 32768 }, (error, stdout) => error ? reject(new SupervisionError("busy")) : resolve(stdout.trim()))
|
||||
})
|
||||
|
||||
/** Check ancestors before creating a child; OS search directories may also hold
|
||||
* unrelated apps' public resources, so retain their existing permissions. */
|
||||
export async function preparePosixSupervisionDirectory(directory: string) {
|
||||
let current = path.parse(directory).root
|
||||
for (const part of directory.slice(current.length).split(path.sep)) {
|
||||
current = path.join(current, part)
|
||||
await mkdir(current, { mode: 0o700 }).catch(error => { if (error.code !== "EEXIST") throw error })
|
||||
const stat = await lstat(current)
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink() || ![0, process.getuid!()].includes(stat.uid) || (stat.mode & 0o022))
|
||||
throw new SupervisionError("unsafe-storage")
|
||||
}
|
||||
}
|
||||
|
||||
export async function readLinuxPrivate<T>(file: string, policy: StoragePolicy = privateStorage): Promise<T | undefined> {
|
||||
try { await policy.verify(file, false) } catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return; throw error }
|
||||
if ((await lstat(file)).size > 65536) throw new SupervisionError("invalid")
|
||||
const bytes = await readFile(file)
|
||||
if (bytes.length > 65536) throw new SupervisionError("invalid")
|
||||
return JSON.parse(bytes.toString("utf8")) as T
|
||||
}
|
||||
export async function writeLinuxPrivate(file: string, value: unknown, policy: StoragePolicy = privateStorage) {
|
||||
const bytes = JSON.stringify(value)
|
||||
if (Buffer.byteLength(bytes) > 65536) throw new SupervisionError("invalid")
|
||||
const temporary = path.join(path.dirname(file), `${randomUUID()}.tmp`)
|
||||
try {
|
||||
const handle = await open(temporary, "wx", 0o600)
|
||||
try { await handle.writeFile(bytes); await handle.sync() } finally { await handle.close() }
|
||||
await policy.verify(temporary, false)
|
||||
await rename(temporary, file)
|
||||
} finally { await rm(temporary, { force: true }) }
|
||||
}
|
||||
|
||||
/** systemd has its own quoting and specifier expansion, independent of a shell. */
|
||||
export function linuxUnitQuote(value: string): string {
|
||||
if (!value || /[\x00-\x1f\x7f]/.test(value)) throw new SupervisionError("invalid")
|
||||
return `"${value.replaceAll("\\", "\\\\").replaceAll('"', '\\"').replaceAll("%", "%%").replaceAll("$", () => "$$")}"`
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import { lstat, mkdir, realpath } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { runtimeIdentity } from "../opencode/compatibility/runtime"
|
||||
import { readNativeScheduleIncarnation } from "../opencode/missions/native-schedule-transport-identity"
|
||||
import { readNativeServiceChannel } from "../workspaces/native-service-registration"
|
||||
import type { ServiceConnection } from "../workspaces/opencode-service"
|
||||
import { SupervisionError, supervisionKey } from "./service-supervision-contract"
|
||||
import { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import { LinuxServiceSupervision, readLinuxSupervisionRuntime, type LinuxSupervisionRuntime } from "./service-supervision-linux"
|
||||
import { LinuxSupervisionLinger } from "./service-supervision-linux-linger"
|
||||
import { provisionLinuxSupervisionRuntime } from "./service-supervision-linux-runtime"
|
||||
import type { AcquiredSupervision } from "./service-supervision-service"
|
||||
import { privateStorage } from "./storage"
|
||||
import { MacosServiceSupervision, readMacosSupervisionRuntime, type MacosSupervisionRuntime } from "./service-supervision-macos"
|
||||
import { macosNativeHelper } from "./service-supervision-macos-native"
|
||||
import { acquireSupervisionTarget, supervisionTargetReady } from "./service-supervision-target"
|
||||
|
||||
interface Acquisition { selectedBinary(): string; connection?: ServiceConnection;
|
||||
environment?: NodeJS.ProcessEnv; root?: string; guardSource?: string; nativeHelperSource?: string; selection?: boolean }
|
||||
export async function acquireLinuxSupervision(input: Acquisition): Promise<AcquiredSupervision> {
|
||||
if (process.platform !== "linux") throw new Error("supervision-platform-unavailable")
|
||||
return acquirePosixSupervision(input)
|
||||
}
|
||||
export async function acquirePosixSupervision(input: Acquisition): Promise<AcquiredSupervision> {
|
||||
const macos = process.platform === "darwin"
|
||||
if ((!macos && process.platform !== "linux") || process.env.WSL_DISTRO_NAME || /microsoft/i.test(os.release())) throw new Error("supervision-platform-unavailable")
|
||||
const selected = input.selectedBinary(), env = { ...(input.environment ?? process.env) }
|
||||
if (!path.isAbsolute(selected)) throw new SupervisionError("invalid")
|
||||
const binary = await realpath(selected), stat = await lstat(binary)
|
||||
if (!stat.isFile() || stat.isSymbolicLink() || !(stat.mode & 0o111)) throw new SupervisionError("invalid")
|
||||
const selectedTarget = await acquireSupervisionTarget(binary)
|
||||
const current = () => { input.connection?.assertCurrent(); if (selected !== input.selectedBinary()) throw new SupervisionError("conflict") }
|
||||
const run = async (args: string[]) => {
|
||||
current()
|
||||
const result = await new Promise<string>((resolve, reject) => execFile(binary, args,
|
||||
{ env, timeout: 5000, maxBuffer: 65536 }, (error, stdout) => error ? reject(new SupervisionError("invalid")) : resolve(stdout.trim())))
|
||||
current()
|
||||
if (!result || /[\r\n\x00]/.test(result)) throw new SupervisionError("invalid")
|
||||
return result
|
||||
}
|
||||
const roots = {} as Record<"config" | "state" | "data" | "cache", string>
|
||||
for (const key of ["config", "state", "data", "cache"] as const) {
|
||||
const value = await run(["debug", "paths", key])
|
||||
if (!path.isAbsolute(value)) throw new SupervisionError("invalid")
|
||||
roots[key] = await realpath(value)
|
||||
if (path.basename(roots[key]) !== "opencode") throw new SupervisionError("invalid")
|
||||
}
|
||||
const password = await run(["service", "get", "password"]), channel = await readNativeServiceChannel(roots.config, password)
|
||||
if (input.connection) {
|
||||
const identity = runtimeIdentity(input.connection.endpoint), incarnation = readNativeScheduleIncarnation(path.join(roots.state, channel.name))
|
||||
if (!identity || identity.pid !== incarnation.pid || input.connection.endpoint.url !== incarnation.url
|
||||
|| input.connection.endpoint.auth?.type !== "basic" || input.connection.endpoint.auth.password !== password) throw new SupervisionError("conflict")
|
||||
}
|
||||
// The linger ownership registry lives outside XDG/profile/channel namespaces.
|
||||
const home = await realpath(os.userInfo().homedir), root = input.root ?? path.join(home, ".codenomad-service-supervision")
|
||||
const namespace = { platform: process.platform as "linux" | "darwin", host: os.hostname(), user: String(process.getuid!()),
|
||||
configDirectory: roots.config, stateDirectory: roots.state, registrationName: channel.name }
|
||||
if (input.selection) await lstat(path.join(root, supervisionKey(namespace), "manifest.json")).catch(error => {
|
||||
if (error.code === "ENOENT") throw new SupervisionError("unknown-request"); throw error
|
||||
})
|
||||
await mkdir(root, { mode: 0o700 }).catch(error => { if (error.code !== "EEXIST") throw error })
|
||||
await privateStorage.verify(root, true)
|
||||
let store = new ServiceSupervisionStore(root, namespace, selectedTarget)
|
||||
const manifest = await store.read()
|
||||
let runtime: LinuxSupervisionRuntime | MacosSupervisionRuntime | undefined = macos ? await readMacosSupervisionRuntime(store) : await readLinuxSupervisionRuntime(store)
|
||||
if (!runtime) {
|
||||
const environment = { HOME: await realpath(env.HOME ?? home) } as LinuxSupervisionRuntime["environment"]
|
||||
for (const key of ["config", "state", "data", "cache"] as const) environment[`XDG_${key.toUpperCase()}_HOME` as "XDG_CONFIG_HOME"] = path.dirname(roots[key])
|
||||
if (env.OPENCODE_TEST_HOME) environment.OPENCODE_TEST_HOME = await realpath(env.OPENCODE_TEST_HOME)
|
||||
const artifacts = await provisionLinuxSupervisionRuntime(store.directory, privateStorage, { node: process.execPath,
|
||||
guard: input.guardSource ?? fileURLToPath(new URL("../../dist/host-lifetime/service-supervision-linux-guard.cjs", import.meta.url)),
|
||||
...(macos ? { nativeHelper: input.nativeHelperSource ?? macosNativeHelper() } : {}) })
|
||||
runtime = macos ? { ...artifacts, nativeHelper: artifacts.nativeHelper!, agentDirectory: path.join(home, "Library/LaunchAgents"), environment }
|
||||
: { ...artifacts, unitDirectory: path.join(home, ".config/systemd/user"), environment }
|
||||
}
|
||||
current()
|
||||
if ("nativeHelper" in runtime) {
|
||||
store = new ServiceSupervisionStore(root, manifest.namespace, manifest.target, privateStorage, runtime.nativeHelper)
|
||||
const adapter = new MacosServiceSupervision(store, runtime)
|
||||
await adapter.prepareRuntime(); current()
|
||||
if (!await supervisionTargetReady(selectedTarget)) throw new SupervisionError("conflict")
|
||||
current()
|
||||
return { store, adapter, selectedTarget, assertCurrent: current }
|
||||
}
|
||||
const linger = new LinuxSupervisionLinger(root, String(process.getuid!()), os.hostname(), os.userInfo().username)
|
||||
await linger.reconcileDeferred()
|
||||
const adapter = new LinuxServiceSupervision(store, runtime, linger)
|
||||
await adapter.prepareRuntime(); current()
|
||||
if (!await supervisionTargetReady(selectedTarget)) throw new SupervisionError("conflict")
|
||||
current()
|
||||
return { store, adapter, selectedTarget, assertCurrent: current }
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import { createHash, randomUUID } from "node:crypto"
|
||||
import { lstat, mkdir, readFile, open, link, rm, chmod } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
import { SupervisionError } from "./service-supervision-contract"
|
||||
|
||||
/** Content-addressed executable copies survive backend/package replacement. */
|
||||
export async function provisionLinuxSupervisionRuntime(directory: string, policy: StoragePolicy = privateStorage,
|
||||
sources: { node: string; guard: string; nativeHelper?: string } = { node: process.execPath,
|
||||
guard: fileURLToPath(new URL("../../dist/host-lifetime/service-supervision-linux-guard.cjs", import.meta.url)) }) {
|
||||
const root = path.join(directory, "runtime")
|
||||
await mkdir(root, { mode: 0o700 }).catch(error => { if (error.code !== "EEXIST") throw error })
|
||||
await policy.verify(root, true)
|
||||
const result = {} as { node: string; guard: string; nativeHelper?: string }
|
||||
for (const key of ["node", "guard", "nativeHelper"] as const) {
|
||||
const source = sources[key]
|
||||
if (!source) continue
|
||||
const stat = await lstat(source)
|
||||
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024 * 1024) throw new SupervisionError("unsafe-storage")
|
||||
const bytes = await readFile(source)
|
||||
if (bytes.length !== stat.size) throw new SupervisionError("conflict")
|
||||
const hash = createHash("sha256").update(bytes).digest("hex"), file = path.join(root, hash + (key === "guard" ? ".cjs" : ""))
|
||||
try { await lstat(file) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
||||
const temporary = path.join(root, `${randomUUID()}.tmp`)
|
||||
try {
|
||||
const handle = await open(temporary, "wx", 0o600)
|
||||
try { await handle.writeFile(bytes); await handle.sync() } finally { await handle.close() }
|
||||
if (key !== "guard") await chmod(temporary, 0o700)
|
||||
await policy.verify(temporary, false)
|
||||
await link(temporary, file).catch(error => { if (error.code !== "EEXIST") throw error })
|
||||
} finally { await rm(temporary, { force: true }) }
|
||||
}
|
||||
await policy.verify(file, false)
|
||||
if (createHash("sha256").update(await readFile(file)).digest("hex") !== hash) throw new SupervisionError("conflict")
|
||||
result[key] = file
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -0,0 +1,210 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { test, type TestContext } from "node:test"
|
||||
import { lstat, mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { LinuxSupervisionLinger } from "./service-supervision-linux-linger"
|
||||
import { linuxSupervisionUnit } from "./service-supervision-linux"
|
||||
import { linuxUnitQuote, type LinuxCommand } from "./service-supervision-linux-native"
|
||||
import { initialSupervisionManifest, applySupervisionControl, supervisionStatus, type SupervisionManifest, type SupervisionHealth } from "./service-supervision-contract"
|
||||
import type { StoragePolicy } from "./storage"
|
||||
import { nativeSupervisionStartPermitted } from "./service-supervision-health"
|
||||
import type { ProcessState } from "./process-identity"
|
||||
|
||||
const policy: StoragePolicy = { async verify(file, directory) {
|
||||
const s = await lstat(file)
|
||||
assert.equal(s.isSymbolicLink(), false)
|
||||
assert.equal(directory ? s.isDirectory() : s.isFile() && s.nlink === 1, true)
|
||||
} }
|
||||
function manifest(channel = "service.json"): SupervisionManifest {
|
||||
return applySupervisionControl(initialSupervisionManifest({ platform: "linux", host: "host", user: "1000",
|
||||
configDirectory: "/home/u/config/opencode", stateDirectory: "/home/u/state/opencode", registrationName: channel },
|
||||
{ binary: "/home/u/Open Code/opencode", provenance: "custom" }), { action: "on", requestID: randomUUID(), expectedRevision: 0 })
|
||||
}
|
||||
const off = (m: SupervisionManifest) => applySupervisionControl(m, { action: "off", requestID: randomUUID(), expectedRevision: m.revision })
|
||||
async function fixture(t: TestContext, preexisting = false) {
|
||||
const parent = path.join(os.tmpdir(), "opencode")
|
||||
await mkdir(parent, { recursive: true })
|
||||
const root = await mkdtemp(path.join(parent, "linux-supervision-"))
|
||||
t.after(() => rm(root, { recursive: true, force: true }))
|
||||
const state = { enabled: preexisting, fingerprint: preexisting ? "personal" : null as string | null,
|
||||
sessions: "s1", sessionState: "active", health: { state: "healthy", pid: 12, id: "id", startIdentity: "start" } as SupervisionHealth,
|
||||
process: { state: "live", startIdentity: "start" } as ProcessState,
|
||||
identity: { pid: 12, id: "id", startIdentity: "start" } as { pid: number; id: string; startIdentity: string } | null,
|
||||
refuse: false, loseAck: false, commands: [] as string[] }
|
||||
const command: LinuxCommand = async (binary, args) => {
|
||||
assert.equal(binary, "/usr/bin/loginctl")
|
||||
state.commands.push(args.join(" "))
|
||||
if (args.includes("enable-linger")) {
|
||||
assert.equal(args[0], "--no-ask-password")
|
||||
if (state.refuse) throw new Error("private polkit error")
|
||||
state.enabled = true; state.fingerprint = "owned"
|
||||
if (state.loseAck) throw new Error("lost acknowledgement")
|
||||
return ""
|
||||
}
|
||||
if (args.includes("disable-linger")) { state.enabled = false; state.fingerprint = null; return "" }
|
||||
if (args.includes("--property=Linger")) return state.enabled ? "yes" : "no"
|
||||
if (args.includes("--property=Sessions")) return state.sessions
|
||||
if (args.includes("--property=State")) return state.sessionState
|
||||
throw new Error("unexpected command")
|
||||
}
|
||||
let active = false
|
||||
const reopen = () => new LinuxSupervisionLinger(root, "1000", "host", "u", policy, { command,
|
||||
fingerprint: async () => state.fingerprint, health: async () => state.health,
|
||||
lookup: async () => state.process, identity: async () => state.identity,
|
||||
lock: async (_file, run) => { if (active) throw new Error("busy"); active = true; try { return await run() } finally { active = false } } })
|
||||
return { root, state, linger: reopen(), reopen }
|
||||
}
|
||||
|
||||
test("Linux unit is foreground, restart bounded by systemd, KillMode process, with escaped fixed argv and no credentials", () => {
|
||||
const unit = linuxSupervisionUnit(manifest().key, "/home/u/Code Nomad/manifest", {
|
||||
node: "/home/u/runtime/node", guard: "/home/u/runtime/guard.cjs", unitDirectory: "/home/u/.config/systemd/user",
|
||||
environment: { HOME: "/home/u", XDG_CONFIG_HOME: "/config", XDG_STATE_HOME: "/state", XDG_DATA_HOME: "/data", XDG_CACHE_HOME: "/cache" } })
|
||||
for (const value of ["Restart=always", "RestartSec=2", "StartLimitIntervalSec=60", "StartLimitBurst=5", "KillMode=process", "WantedBy=default.target", '"/home/u/Code Nomad/manifest"']) assert.ok(unit.includes(value))
|
||||
assert.ok(!/password|token|OPENCODE_CONFIG_CONTENT|service start/i.test(unit))
|
||||
assert.equal(linuxUnitQuote('/a%u/$HOME/"b'), '"/a%%u/$$HOME/\\"b"')
|
||||
assert.throws(() => linuxUnitQuote("/a\nExecStart=/bad"))
|
||||
})
|
||||
test("linger ownership survives profiles, is deduplicated, and last OFF restores it", async t => {
|
||||
const { linger, state, root } = await fixture(t), a = manifest(), b = manifest("service-other.json")
|
||||
assert.equal(await linger.reconcile(a), "enabled")
|
||||
assert.equal(await linger.reconcile(a), "enabled")
|
||||
assert.equal(await linger.reconcile(b), "enabled")
|
||||
assert.equal(await linger.reconcile(off(a)), "enabled")
|
||||
assert.equal(state.enabled, true)
|
||||
assert.equal(await linger.reconcile(off(b)), "disabled")
|
||||
assert.equal(state.enabled, false)
|
||||
assert.equal(state.commands.filter(c => c.includes("enable-linger")).length, 1)
|
||||
assert.deepEqual(JSON.parse(await readFile(path.join(root, "linux-linger.json"), "utf8")).requests, [])
|
||||
})
|
||||
test("personal linger is never claimed or disabled", async t => {
|
||||
const { linger, state } = await fixture(t, true), m = manifest()
|
||||
assert.equal(await linger.reconcile(m), "external")
|
||||
assert.equal(await linger.reconcile(off(m)), "external")
|
||||
assert.equal(state.commands.some(c => c.includes("disable-linger")), false)
|
||||
})
|
||||
test("OFF without an open session preserves a released healthy/unknown process and visibly defers linger rollback", async t => {
|
||||
const { linger, state } = await fixture(t), m = manifest()
|
||||
await linger.reconcile(m); state.sessions = ""
|
||||
assert.equal(await linger.reconcile(off(m)), "deferred")
|
||||
assert.equal(await linger.observe(), "deferred")
|
||||
state.health = { state: "unknown" }
|
||||
assert.equal(await linger.reconcile(off(m)), "deferred")
|
||||
assert.equal(state.enabled, true)
|
||||
state.health = { state: "absent" }
|
||||
state.process = { state: "dead" }
|
||||
assert.equal(await linger.reconcile(off(m)), "disabled")
|
||||
})
|
||||
test("a new ON cancels deferred rollback; closing sessions do not authorize disable", async t => {
|
||||
const { linger, state } = await fixture(t), m = manifest()
|
||||
await linger.reconcile(m); state.sessionState = "closing"
|
||||
assert.equal(await linger.reconcile(off(m)), "deferred")
|
||||
assert.equal(await linger.reconcile(m), "enabled")
|
||||
assert.equal(state.commands.some(c => c.includes("disable-linger")), false)
|
||||
state.sessionState = "online"
|
||||
assert.equal(await linger.observe(), "enabled")
|
||||
assert.equal(state.commands.some(c => c.includes("disable-linger")), false)
|
||||
assert.equal(await linger.reconcile(off(m)), "disabled")
|
||||
})
|
||||
test("passive reconciliation completes deferred OFF when a session opens or the released service ends", async t => {
|
||||
for (const safe of ["session", "exit"] as const) {
|
||||
const { linger, state } = await fixture(t), m = manifest()
|
||||
await linger.reconcile(m); state.sessions = ""
|
||||
assert.equal(await linger.reconcile(off(m)), "deferred")
|
||||
assert.equal(await linger.observe(), "deferred")
|
||||
if (safe === "session") { state.sessions = "s1"; state.sessionState = "online" }
|
||||
else { state.health = { state: "absent" }; state.process = { state: "dead" } }
|
||||
// No Retry or second OFF: startup and status reconciliation own this cleanup.
|
||||
await linger.reconcileDeferred()
|
||||
assert.equal(await linger.observe(), "disabled")
|
||||
assert.equal(state.commands.filter(c => c.includes("disable-linger")).length, 1)
|
||||
}
|
||||
})
|
||||
test("deleted registration never proves a released process ended; durable PID/start identity fences deferred cleanup", async t => {
|
||||
const { linger, state, root, reopen } = await fixture(t), m = manifest()
|
||||
await linger.reconcile(m)
|
||||
await linger.retainReleased(off(m))
|
||||
const retained = JSON.parse(await readFile(path.join(root, "linux-linger.json"), "utf8")).released[0]
|
||||
assert.deepEqual(retained.process, state.identity)
|
||||
assert.equal(retained.registration, "/home/u/state/opencode/service.json")
|
||||
state.sessions = ""; state.health = { state: "absent" }; state.identity = null
|
||||
assert.equal(await linger.reconcile(off(m)), "deferred")
|
||||
const restarted = reopen()
|
||||
assert.equal(await restarted.observe(), "deferred", "missing registration + live retained PID stays deferred after restart")
|
||||
state.process = { state: "unknown" }
|
||||
assert.equal(await restarted.observe(), "deferred")
|
||||
state.process = { state: "live", startIdentity: "reused-pid" }
|
||||
assert.equal(await restarted.observe(), "disabled", "a different native start proves the retained incarnation ended")
|
||||
})
|
||||
test("missing release identity remains unknown even when registration is absent and sessions are closed", async t => {
|
||||
const { linger, state } = await fixture(t), m = manifest()
|
||||
await linger.reconcile(m)
|
||||
state.sessions = ""; state.health = { state: "absent" }; state.identity = null; state.process = { state: "dead" }
|
||||
assert.equal(await linger.reconcile(off(m)), "deferred")
|
||||
assert.equal(await linger.observe(), "deferred")
|
||||
assert.equal(state.commands.some(c => c.includes("disable-linger")), false)
|
||||
state.sessions = "s1"
|
||||
assert.equal(await linger.observe(), "disabled")
|
||||
})
|
||||
test("automatic deferred cleanup preserves new ON consumers and changed ownership", async t => {
|
||||
const { linger, state } = await fixture(t), m = manifest()
|
||||
await linger.reconcile(m); state.sessions = ""
|
||||
await linger.reconcile(off(m))
|
||||
state.fingerprint = "changed"; state.health = { state: "absent" }
|
||||
assert.equal(await linger.observe(), "uncertain")
|
||||
assert.equal(state.commands.some(c => c.includes("disable-linger")), false)
|
||||
})
|
||||
test("polkit refusal keeps requested ON and fixed diagnostic; Retry can enable without elevation", async t => {
|
||||
const { linger, state, root } = await fixture(t), m = manifest()
|
||||
state.refuse = true
|
||||
assert.equal(await linger.reconcile(m), "permission-required")
|
||||
assert.equal(await linger.observe(), "permission-required")
|
||||
const bytes = await readFile(path.join(root, "linux-linger.json"), "utf8")
|
||||
assert.ok(bytes.includes(m.key)); assert.ok(!bytes.includes("private polkit"))
|
||||
state.refuse = false
|
||||
assert.equal(await linger.reconcile(m), "enabled")
|
||||
assert.equal(state.commands.some(c => /sudo|pkexec/.test(c)), false)
|
||||
})
|
||||
test("lost enable ACK and externally changed owned marker preserve linger as uncertain", async t => {
|
||||
const { linger, state } = await fixture(t), m = manifest()
|
||||
state.loseAck = true
|
||||
assert.equal(await linger.reconcile(m), "permission-required")
|
||||
state.loseAck = false
|
||||
assert.equal(await linger.reconcile(m), "uncertain")
|
||||
assert.equal(await linger.reconcile(off(m)), "uncertain")
|
||||
assert.equal(state.enabled, true)
|
||||
assert.equal(state.commands.some(c => c.includes("disable-linger")), false)
|
||||
})
|
||||
test("external marker replacement cannot authorize rollback", async t => {
|
||||
const { linger, state } = await fixture(t), m = manifest()
|
||||
await linger.reconcile(m); state.fingerprint = "someone-else"
|
||||
assert.equal(await linger.observe(), "uncertain")
|
||||
assert.equal(await linger.reconcile(off(m)), "uncertain")
|
||||
assert.equal(state.enabled, true)
|
||||
})
|
||||
test("corrupt registry refuses all mutations", async t => {
|
||||
const { linger, root, state } = await fixture(t)
|
||||
await writeFile(path.join(root, "linux-linger.json"), '{"v":2}', { mode: 0o600 })
|
||||
await assert.rejects(linger.reconcile(manifest()))
|
||||
assert.equal(state.commands.length, 0)
|
||||
})
|
||||
test("status distinguishes OFF deferred rollback, unknown provenance and polkit refusal", () => {
|
||||
const m = manifest(), stopped = { ...off(m), reconciledRevision: 2 }
|
||||
const base = { revision: 2, resources: "absent" as const, guard: "absent" as const, health: { state: "absent" as const } }
|
||||
assert.deepEqual([supervisionStatus(stopped, { ...base, linuxLinger: "deferred" }).state,
|
||||
supervisionStatus(stopped, { ...base, linuxLinger: "deferred" }).cause], ["off", "linux-linger-deferred"])
|
||||
assert.equal(supervisionStatus(stopped, { ...base, linuxLinger: "uncertain" }).cause, "linux-linger-uncertain")
|
||||
assert.equal(supervisionStatus(m, { ...base, revision: 1, linuxLinger: "permission-required" }).cause, "linux-linger-permission-required")
|
||||
assert.equal(supervisionStatus(stopped, { ...base, resources: "installed", linuxLinger: "deferred" }).state, "failing")
|
||||
})
|
||||
test("native start gate admits absent/false config and refuses disabled, malformed and incomplete reads", async t => {
|
||||
const { root } = await fixture(t), file = path.join(root, "native-service.json")
|
||||
assert.equal(await nativeSupervisionStartPermitted(file), true)
|
||||
for (const [bytes, permitted] of [["{}", true], ['{"disabled":false}', true], ['{"disabled":true}', false],
|
||||
['{"disabled":"false"}', false], ["{", false], ["null", false]] as const) {
|
||||
await writeFile(file, bytes)
|
||||
assert.equal(await nativeSupervisionStartPermitted(file), permitted)
|
||||
}
|
||||
assert.equal(await nativeSupervisionStartPermitted(root), false)
|
||||
})
|
||||
153
packages/server/src/host-lifetime/service-supervision-linux.ts
Normal file
153
packages/server/src/host-lifetime/service-supervision-linux.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
import { lstat, readFile, realpath, readlink, unlink, open } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { z } from "zod"
|
||||
import { SupervisionError, type SupervisionObservation } from "./service-supervision-contract"
|
||||
import { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import { LinuxSupervisionLinger } from "./service-supervision-linux-linger"
|
||||
import { linuxCommand, linuxUnitQuote, preparePosixSupervisionDirectory, readLinuxPrivate, writeLinuxPrivate, type LinuxCommand } from "./service-supervision-linux-native"
|
||||
import { withLinuxSupervisionLock } from "./service-supervision-linux-lock"
|
||||
import { readSupervisionHealth } from "./service-supervision-health"
|
||||
import { lookupProcess } from "./process-identity"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
|
||||
const absolute = z.string().min(1).max(4096).refine(value => path.posix.isAbsolute(value)
|
||||
&& path.posix.normalize(value) === value && !/[\x00-\x1f\\]/.test(value))
|
||||
export const linuxRuntimeSchema = z.object({ node: absolute, guard: absolute, unitDirectory: absolute,
|
||||
environment: z.object({ HOME: absolute, XDG_CONFIG_HOME: absolute, XDG_STATE_HOME: absolute,
|
||||
XDG_DATA_HOME: absolute, XDG_CACHE_HOME: absolute, OPENCODE_TEST_HOME: absolute.optional() }).strict(),
|
||||
}).strict()
|
||||
export type LinuxSupervisionRuntime = z.infer<typeof linuxRuntimeSchema>
|
||||
|
||||
export function linuxSupervisionUnit(key: string, directory: string, runtime: LinuxSupervisionRuntime): string {
|
||||
if (!/^[a-f0-9]{64}$/.test(key)) throw new SupervisionError("invalid")
|
||||
const d = linuxRuntimeSchema.parse(runtime)
|
||||
return `[Unit]\nDescription=CodeNomad OpenCode ${key}\nStartLimitIntervalSec=60\nStartLimitBurst=5\n\n[Service]\nType=simple\nExecStart=${[d.node, d.guard, directory, key].map(linuxUnitQuote).join(" ")}\nRestart=always\nRestartSec=2\nKillMode=process\nTimeoutStopSec=15\nStandardInput=null\nStandardOutput=null\nStandardError=null\n\n[Install]\nWantedBy=default.target\n`
|
||||
}
|
||||
|
||||
export async function readLinuxSupervisionRuntime(store: ServiceSupervisionStore, policy: StoragePolicy = privateStorage) {
|
||||
const value = await readLinuxPrivate(path.join(store.directory, "linux.json"), policy)
|
||||
if (value === undefined) return
|
||||
return linuxRuntimeSchema.parse(value)
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
command: LinuxCommand
|
||||
health: typeof readSupervisionHealth
|
||||
lookup: typeof lookupProcess
|
||||
lock<T>(file: string, run: () => Promise<T>): Promise<T>
|
||||
}
|
||||
export class LinuxServiceSupervision {
|
||||
private readonly deps: Dependencies
|
||||
constructor(private readonly store: ServiceSupervisionStore, private readonly runtime: LinuxSupervisionRuntime,
|
||||
private readonly linger: Pick<LinuxSupervisionLinger, "reconcile" | "observe" | "retainReleased">,
|
||||
private readonly policy: StoragePolicy = privateStorage, overrides: Partial<Dependencies> = {}) {
|
||||
this.deps = { command: linuxCommand, health: readSupervisionHealth, lookup: lookupProcess,
|
||||
lock: (file, run) => withLinuxSupervisionLock(file, run, policy), ...overrides }
|
||||
}
|
||||
async prepareRuntime() {
|
||||
const d = linuxRuntimeSchema.parse(this.runtime), m = await this.store.read()
|
||||
if (m.namespace.platform !== "linux" || path.posix.join(d.environment.XDG_CONFIG_HOME, "opencode") !== m.namespace.configDirectory
|
||||
|| path.posix.join(d.environment.XDG_STATE_HOME, "opencode") !== m.namespace.stateDirectory) throw new SupervisionError("conflict")
|
||||
const existing = await readLinuxSupervisionRuntime(this.store, this.policy)
|
||||
if (existing && JSON.stringify(existing) !== JSON.stringify(d)) throw new SupervisionError("conflict")
|
||||
if (!existing) await writeLinuxPrivate(path.join(this.store.directory, "linux.json"), d, this.policy)
|
||||
}
|
||||
private async unit() {
|
||||
const m = await this.store.read(), name = `codenomad-opencode-${m.key}.service`
|
||||
return { name, file: path.join(this.runtime.unitDirectory, name), text: linuxSupervisionUnit(m.key, this.store.directory, this.runtime) }
|
||||
}
|
||||
private async ownedUnit() {
|
||||
const unit = await this.unit()
|
||||
const wanted = path.join(this.runtime.unitDirectory, "default.target.wants", unit.name)
|
||||
const link = await lstat(wanted).catch(error => { if (error.code !== "ENOENT") throw error; return null })
|
||||
if (link && (!link.isSymbolicLink() || path.resolve(path.dirname(wanted), await readlink(wanted)) !== unit.file))
|
||||
throw new SupervisionError("conflict")
|
||||
const stat = await lstat(unit.file).catch(error => { if (error.code !== "ENOENT") throw error; return null })
|
||||
if (!stat) return { ...unit, exists: false }
|
||||
await this.policy.verify(unit.file, false)
|
||||
if (stat.size > 32768 || await readFile(unit.file, "utf8") !== unit.text) throw new SupervisionError("conflict")
|
||||
return { ...unit, exists: true }
|
||||
}
|
||||
private async loaded(name: string): Promise<Record<string, string>> {
|
||||
const text = await this.deps.command("/usr/bin/systemctl", ["--user", "show", name,
|
||||
"--property=LoadState,ActiveState,MainPID,FragmentPath,KillMode,UnitFileState,DropInPaths"])
|
||||
return Object.fromEntries(text.split("\n").map(line => { const i = line.indexOf("="); return [line.slice(0, i), line.slice(i + 1)] }))
|
||||
}
|
||||
async reconcile(expectedRevision: number) {
|
||||
await this.prepareRuntime()
|
||||
await this.deps.lock(path.join(this.store.directory, "admission.lock"), async () => {
|
||||
const manifest = await this.store.read()
|
||||
if (manifest.revision !== expectedRevision) throw new SupervisionError("conflict")
|
||||
if (manifest.desired === "unset") return
|
||||
let unit = await this.ownedUnit()
|
||||
const state = await this.loaded(unit.name)
|
||||
if (state.LoadState !== "not-found" && (state.FragmentPath !== unit.file || state.KillMode !== "process" || state.DropInPaths)) throw new SupervisionError("conflict")
|
||||
if (manifest.desired === "on") {
|
||||
const linger = await this.linger.reconcile(manifest)
|
||||
if (!["enabled", "external"].includes(linger)) throw new SupervisionError("busy")
|
||||
await preparePosixSupervisionDirectory(this.runtime.unitDirectory)
|
||||
// systemd's user directory can contain unrelated public unit files. Only
|
||||
// our leaf is private; refuse redirected parent chains and foreign files.
|
||||
if (await realpath(this.runtime.unitDirectory) !== this.runtime.unitDirectory) throw new SupervisionError("unsafe-storage")
|
||||
if (!unit.exists) {
|
||||
const handle = await open(unit.file, "wx", 0o600)
|
||||
try { await handle.writeFile(unit.text); await handle.sync() } finally { await handle.close() }
|
||||
}
|
||||
await this.ownedUnit()
|
||||
await this.deps.command("/usr/bin/systemctl", ["--user", "daemon-reload"])
|
||||
await this.deps.command("/usr/bin/systemctl", ["--user", "enable", unit.name])
|
||||
await this.deps.command("/usr/bin/systemctl", ["--user", "reset-failed", unit.name])
|
||||
await this.deps.command("/usr/bin/systemctl", ["--user", "start", unit.name])
|
||||
const after = await this.loaded(unit.name)
|
||||
if (after.FragmentPath !== unit.file || after.KillMode !== "process" || after.DropInPaths || after.UnitFileState !== "enabled"
|
||||
|| !["active", "activating"].includes(after.ActiveState ?? "")) throw new SupervisionError("busy")
|
||||
} else {
|
||||
await this.linger.retainReleased(manifest)
|
||||
if (state.LoadState !== "not-found") {
|
||||
// Never disable/unlink before stop: systemd must retain KillMode=process
|
||||
// while stopping the foreground guard and cancelling Restart=always.
|
||||
await this.deps.command("/usr/bin/systemctl", ["--user", "stop", unit.name])
|
||||
if ((await this.loaded(unit.name)).ActiveState !== "inactive") throw new SupervisionError("busy")
|
||||
await this.deps.command("/usr/bin/systemctl", ["--user", "disable", unit.name])
|
||||
}
|
||||
unit = await this.ownedUnit()
|
||||
if (unit.exists) await unlink(unit.file)
|
||||
await this.deps.command("/usr/bin/systemctl", ["--user", "daemon-reload"])
|
||||
const after = await this.loaded(unit.name)
|
||||
if (after.LoadState !== "not-found" || after.ActiveState !== "inactive") throw new SupervisionError("busy")
|
||||
const linger = await this.linger.reconcile(manifest)
|
||||
if (["uncertain", "permission-required"].includes(linger)) throw new SupervisionError("busy")
|
||||
// Deferred rollback is an acknowledged OFF with a distinct visible cause.
|
||||
}
|
||||
})
|
||||
await this.store.acknowledge(expectedRevision)
|
||||
}
|
||||
|
||||
async observe(): Promise<SupervisionObservation> {
|
||||
const manifest = await this.store.read(), health = await this.deps.health(path.join(manifest.namespace.stateDirectory, manifest.namespace.registrationName))
|
||||
let resources: SupervisionObservation["resources"] = "unknown", guard: SupervisionObservation["guard"] = "unknown"
|
||||
let detail: SupervisionObservation["detail"], artifactMissing = false
|
||||
for (const file of [this.runtime.node, this.runtime.guard]) {
|
||||
try { await this.policy.verify(file, false) } catch { artifactMissing = true }
|
||||
}
|
||||
try {
|
||||
const unit = await this.ownedUnit(), state = await this.loaded(unit.name)
|
||||
resources = !unit.exists && state.LoadState === "not-found" ? "absent"
|
||||
: unit.exists && state.FragmentPath === unit.file && state.KillMode === "process" && !state.DropInPaths && state.UnitFileState === "enabled" ? "installed" : "unknown"
|
||||
guard = state.ActiveState === "inactive" || state.ActiveState === "failed" ? "absent" : "unknown"
|
||||
if (state.ActiveState === "active") {
|
||||
const status = z.object({ revision: z.literal(manifest.revision), key: z.literal(manifest.key),
|
||||
guardPid: z.number().int().positive(), guardStart: z.string(),
|
||||
cause: z.enum(["native-service-disabled-or-unknown", "start-failed", "guard-failed", "service-unknown"]).nullable(),
|
||||
lastAttemptAt: z.string().datetime().optional(), retryAt: z.string().datetime().optional(),
|
||||
}).passthrough().parse(await readLinuxPrivate(path.join(this.store.directory, "linux-status.json"), this.policy))
|
||||
const owner = await this.deps.lookup(status.guardPid)
|
||||
if (String(status.guardPid) === state.MainPID && owner.state === "live" && owner.startIdentity === status.guardStart) {
|
||||
guard = "active"; detail = { cause: status.cause, lastAttemptAt: status.lastAttemptAt, retryAt: status.retryAt }
|
||||
}
|
||||
}
|
||||
} catch { /* Partial ownership or status evidence stays unknown. */ }
|
||||
const linuxLinger = await this.linger.observe().catch(() => "uncertain" as const)
|
||||
return { revision: manifest.revision, resources, guard, health, artifactMissing, linuxLinger, ...(detail ? { detail } : {}) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
#include <errno.h>
|
||||
#include <fcntl.h>
|
||||
#include <libproc.h>
|
||||
#include <signal.h>
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
/* Built with the app, never compiled on the user's machine. No service lifecycle
|
||||
* or arbitrary execution: precise native process identity and a pipe-owned lock. */
|
||||
int main(int argc, char **argv) {
|
||||
if (argc != 3) return 2;
|
||||
if (strcmp(argv[1], "identity") == 0) {
|
||||
char *end;
|
||||
errno = 0;
|
||||
long value = strtol(argv[2], &end, 10);
|
||||
if (errno || *end || value <= 0 || value > 2147483647) return 2;
|
||||
pid_t pid = (pid_t)value;
|
||||
struct proc_bsdinfo info;
|
||||
int size = proc_pidinfo(pid, PROC_PIDTBSDINFO, 0, &info, sizeof(info));
|
||||
if (size == sizeof(info) && info.pbi_start_tvsec) {
|
||||
printf("{\"state\":\"live\",\"startIdentity\":\"darwin:%llu:%llu\"}\n",
|
||||
(unsigned long long)info.pbi_start_tvsec, (unsigned long long)info.pbi_start_tvusec);
|
||||
} else if (kill(pid, 0) == -1 && errno == ESRCH) {
|
||||
puts("{\"state\":\"dead\"}");
|
||||
} else {
|
||||
puts("{\"state\":\"unknown\"}");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
if (strcmp(argv[1], "lock") == 0 && strcmp(argv[2], "3") == 0) {
|
||||
struct stat st;
|
||||
if (fstat(3, &st) || !S_ISREG(st.st_mode) || st.st_uid != getuid()
|
||||
|| st.st_nlink != 1 || (st.st_mode & 077) || flock(3, LOCK_EX | LOCK_NB)) return 3;
|
||||
if (write(STDOUT_FILENO, "ready", 5) != 5) return 3;
|
||||
char buf[64];
|
||||
ssize_t n;
|
||||
do { n = read(STDIN_FILENO, buf, sizeof(buf)); } while (n > 0 || (n < 0 && errno == EINTR));
|
||||
return n == 0 ? 0 : 3;
|
||||
}
|
||||
return 2;
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
import { execFile, spawn } from "node:child_process"
|
||||
import { constants } from "node:fs"
|
||||
import { open } from "node:fs/promises"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { SupervisionError } from "./service-supervision-contract"
|
||||
import type { ProcessState } from "./process-identity"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
|
||||
export const macosNativeHelper = () => fileURLToPath(new URL("../../dist/host-lifetime/service-supervision-macos-native", import.meta.url))
|
||||
|
||||
export function lookupMacosProcess(pid: number, helper = macosNativeHelper()): Promise<ProcessState> {
|
||||
if (!Number.isSafeInteger(pid) || pid <= 0) return Promise.resolve({ state: "unknown" })
|
||||
return new Promise(resolve => execFile(helper, ["identity", String(pid)], { timeout: 3000, maxBuffer: 1024 }, (error, stdout) => {
|
||||
try {
|
||||
if (error) throw error
|
||||
const value = JSON.parse(stdout)
|
||||
if (value.state === "dead" || value.state === "unknown") return resolve({ state: value.state })
|
||||
if (value.state === "live" && /^darwin:\d+:\d+$/.test(value.startIdentity)) return resolve(value)
|
||||
} catch { /* Missing helper or incomplete native evidence stays unknown. */ }
|
||||
resolve({ state: "unknown" })
|
||||
}))
|
||||
}
|
||||
|
||||
/** Descriptor ownership is released by the kernel after helper/caller death.
|
||||
* The file descriptor crosses spawn, never an unverified pathname reopen. */
|
||||
export async function withMacosSupervisionLock<T>(file: string, run: () => Promise<T>, policy: StoragePolicy = privateStorage,
|
||||
helper = macosNativeHelper()): Promise<T> {
|
||||
const handle = await open(file, constants.O_RDWR | constants.O_CREAT | constants.O_NOFOLLOW, 0o600)
|
||||
try {
|
||||
await policy.verify(file, false)
|
||||
const child = spawn(helper, ["lock", "3"], { stdio: ["pipe", "pipe", "ignore", handle.fd], env: { PATH: "/usr/bin:/bin" } })
|
||||
child.stdin!.on("error", () => {})
|
||||
const exited = new Promise<void>(resolve => { child.once("close", () => resolve()); child.once("error", () => resolve()) })
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const timer = setTimeout(() => { child.stdin!.end(); reject(new SupervisionError("busy")) }, 3000)
|
||||
child.once("error", () => { clearTimeout(timer); reject(new SupervisionError("busy")) })
|
||||
child.once("exit", () => { clearTimeout(timer); reject(new SupervisionError("busy")) })
|
||||
child.stdout!.once("data", bytes => { clearTimeout(timer); bytes.toString() === "ready" ? resolve() : reject(new SupervisionError("busy")) })
|
||||
})
|
||||
return await run()
|
||||
} finally { child.stdin!.end(); await exited }
|
||||
} finally { await handle.close() }
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { test } from "node:test"
|
||||
import { macosLoadedAgent, macosRuntimeSchema, macosSupervisionPlist, type MacosSupervisionRuntime } from "./service-supervision-macos"
|
||||
import { lookupMacosProcess } from "./service-supervision-macos-native"
|
||||
|
||||
const runtime: MacosSupervisionRuntime = { node: "/Users/u/Code Nomad/node", guard: "/Users/u/runtime/guard.cjs",
|
||||
nativeHelper: "/Users/u/runtime/native", agentDirectory: "/Users/u/Library/LaunchAgents",
|
||||
environment: { HOME: "/Users/u", XDG_CONFIG_HOME: "/config", XDG_STATE_HOME: "/state", XDG_DATA_HOME: "/data", XDG_CACHE_HOME: "/cache" } }
|
||||
const key = "a".repeat(64), file = `${runtime.agentDirectory}/org.codenomad.opencode.${key}.plist`
|
||||
const argv = [runtime.node, runtime.guard, "/Users/u/private", key]
|
||||
const loaded = `gui/501/org.codenomad.opencode.${key} = {
|
||||
\tpath = ${file}
|
||||
\ttype = LaunchAgent
|
||||
\tstate = running
|
||||
\tprogram = ${runtime.node}
|
||||
\targuments = {
|
||||
${argv.map(arg => `\t\t${arg}\n`).join("")}\t}
|
||||
\tminimum runtime = 5
|
||||
\tpid = 42
|
||||
\tproperties = keepalive | runatload | abandon process group | inferred program
|
||||
}`
|
||||
|
||||
test("LaunchAgent hosts a foreground guard and explicitly abandons its service group at OFF/crash", () => {
|
||||
const plist = macosSupervisionPlist(key, '/Users/u/A&B/"<data>"', runtime)
|
||||
for (const value of ["<key>RunAtLoad</key><true/>", "<key>KeepAlive</key><true/>",
|
||||
"<key>ThrottleInterval</key><integer>5</integer>", "<key>AbandonProcessGroup</key><true/>",
|
||||
"<key>StandardInPath</key><string>/dev/null</string>", "A&B/"<data>"", runtime.node]) assert.ok(plist.includes(value))
|
||||
assert.doesNotMatch(plist, /password|token|OPENCODE_CONFIG_CONTENT|service start|service stop|StartCalendarInterval/i)
|
||||
assert.throws(() => macosSupervisionPlist("bad", "/private", runtime))
|
||||
assert.equal(macosRuntimeSchema.safeParse({ ...runtime, nativeHelper: "/bad\nhelper" }).success, false)
|
||||
assert.equal(macosRuntimeSchema.safeParse({ ...runtime, environment: { ...runtime.environment, TOKEN: "secret" } }).success, false)
|
||||
})
|
||||
test("loaded ownership requires complete fixed argv and the release/restart policy", () => {
|
||||
assert.deepEqual(macosLoadedAgent(loaded, file, argv), { state: "running", pid: 42 })
|
||||
for (const text of [loaded.replace(file, "/foreign.plist"), loaded.replace("abandon process group | ", ""),
|
||||
loaded.replace("keepalive | ", ""), loaded.replace("runatload | ", ""), loaded.replace("\tminimum runtime = 5", "\tminimum runtime = 1"),
|
||||
loaded.replace(`\t\t${key}`, "\t\tforeign"), loaded.replace("pid = 42", "pid = unknown")]) assert.throws(() => macosLoadedAgent(text, file, argv))
|
||||
assert.throws(() => macosLoadedAgent(loaded, file, [...argv, "extra"]))
|
||||
})
|
||||
test("a loaded agent waiting for launchd throttle is installed but has no active guard", () => {
|
||||
assert.deepEqual(macosLoadedAgent(loaded.replace("\tpid = 42\n", "").replace("state = running", "state = spawn scheduled"), file, argv),
|
||||
{ state: "spawn scheduled" })
|
||||
})
|
||||
test("invalid PID and missing native identity helper stay unknown", async () => {
|
||||
assert.deepEqual(await lookupMacosProcess(-1, "/missing"), { state: "unknown" })
|
||||
assert.deepEqual(await lookupMacosProcess(42, "/missing/native/helper"), { state: "unknown" })
|
||||
})
|
||||
161
packages/server/src/host-lifetime/service-supervision-macos.ts
Normal file
161
packages/server/src/host-lifetime/service-supervision-macos.ts
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import { lstat, open, readFile, realpath, unlink } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { z } from "zod"
|
||||
import { setTimeout as delay } from "node:timers/promises"
|
||||
import { SupervisionError, type SupervisionObservation } from "./service-supervision-contract"
|
||||
import { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import { linuxRuntimeSchema } from "./service-supervision-linux"
|
||||
import { preparePosixSupervisionDirectory, readLinuxPrivate as readPrivate, writeLinuxPrivate as writePrivate } from "./service-supervision-linux-native"
|
||||
import { lookupMacosProcess, withMacosSupervisionLock } from "./service-supervision-macos-native"
|
||||
import { readSupervisionHealth } from "./service-supervision-health"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
|
||||
const absolute = z.string().min(1).max(4096).refine(value => path.posix.isAbsolute(value)
|
||||
&& path.posix.normalize(value) === value && !/[\x00-\x1f\\]/.test(value))
|
||||
export const macosRuntimeSchema = linuxRuntimeSchema.omit({ unitDirectory: true }).extend({ nativeHelper: absolute, agentDirectory: absolute })
|
||||
export type MacosSupervisionRuntime = z.infer<typeof macosRuntimeSchema>
|
||||
export type MacosLaunchctl = (args: string[]) => Promise<{ code: number; text: string }>
|
||||
const launchctl: MacosLaunchctl = args => new Promise((resolve, reject) => execFile("/bin/launchctl", args,
|
||||
{ env: { PATH: "/usr/bin:/bin", HOME: process.env.HOME }, timeout: 20_000, maxBuffer: 65536 }, (error, stdout) => {
|
||||
if (error && typeof error.code !== "number") return reject(new SupervisionError("busy"))
|
||||
resolve({ code: error ? Number(error.code) : 0, text: stdout.trim() })
|
||||
}))
|
||||
const xml = (text: string) => text.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'")
|
||||
|
||||
export function macosSupervisionPlist(key: string, directory: string, raw: MacosSupervisionRuntime): string {
|
||||
if (!/^[a-f0-9]{64}$/.test(key)) throw new SupervisionError("invalid")
|
||||
const d = macosRuntimeSchema.parse(raw)
|
||||
absolute.parse(directory)
|
||||
return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict>
|
||||
<key>Label</key><string>org.codenomad.opencode.${key}</string>
|
||||
<key>ProgramArguments</key><array>${[d.node, d.guard, directory, key].map(value => `<string>${xml(value)}</string>`).join("")}</array>
|
||||
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
|
||||
<key>ThrottleInterval</key><integer>5</integer>
|
||||
<key>AbandonProcessGroup</key><true/>
|
||||
<key>StandardInPath</key><string>/dev/null</string>
|
||||
<key>StandardOutPath</key><string>/dev/null</string><key>StandardErrorPath</key><string>/dev/null</string>
|
||||
</dict></plist>\n`
|
||||
}
|
||||
|
||||
/** launchctl's bounded print output is independent evidence of the loaded action.
|
||||
* Verify its path, complete argv and release/restart policy before any mutation. */
|
||||
export function macosLoadedAgent(text: string, file: string, argv: string[]): { pid?: number; state: string } {
|
||||
const scalar = (name: string) => text.match(new RegExp(`^\\t${name} = (.+)$`, "m"))?.[1]
|
||||
const args = text.match(/^\targuments = \{\n([\s\S]*?)^\t\}/m)?.[1].split("\n").filter(Boolean).map(line => line.replace(/^\t\t/, ""))
|
||||
const properties = scalar("properties")?.split(/\s*\|\s*/)
|
||||
if (scalar("type") !== "LaunchAgent" || scalar("minimum runtime") !== "5" || scalar("path") !== file || scalar("program") !== argv[0] || JSON.stringify(args) !== JSON.stringify(argv)
|
||||
|| !["keepalive", "runatload", "abandon process group"].every(value => properties?.includes(value))) throw new SupervisionError("conflict")
|
||||
const pid = scalar("pid")
|
||||
if (pid !== undefined && !/^[1-9]\d*$/.test(pid)) throw new SupervisionError("invalid")
|
||||
return { state: scalar("state") ?? "unknown", ...(pid ? { pid: Number(pid) } : {}) }
|
||||
}
|
||||
|
||||
export async function readMacosSupervisionRuntime(store: ServiceSupervisionStore, policy: StoragePolicy = privateStorage) {
|
||||
const raw = await readPrivate(path.join(store.directory, "macos.json"), policy)
|
||||
return raw === undefined ? undefined : macosRuntimeSchema.parse(raw)
|
||||
}
|
||||
|
||||
export class MacosServiceSupervision {
|
||||
constructor(private readonly store: ServiceSupervisionStore, private readonly runtime: MacosSupervisionRuntime,
|
||||
private readonly policy: StoragePolicy = privateStorage, private readonly command: MacosLaunchctl = launchctl) {}
|
||||
|
||||
async prepareRuntime() {
|
||||
const d = macosRuntimeSchema.parse(this.runtime), m = await this.store.read()
|
||||
if (m.namespace.platform !== "darwin" || path.posix.join(d.environment.XDG_CONFIG_HOME, "opencode") !== m.namespace.configDirectory
|
||||
|| path.posix.join(d.environment.XDG_STATE_HOME, "opencode") !== m.namespace.stateDirectory) throw new SupervisionError("conflict")
|
||||
const existing = await readMacosSupervisionRuntime(this.store, this.policy)
|
||||
if (existing && JSON.stringify(existing) !== JSON.stringify(d)) throw new SupervisionError("conflict")
|
||||
if (!existing) await writePrivate(path.join(this.store.directory, "macos.json"), d, this.policy)
|
||||
}
|
||||
private async agent() {
|
||||
const m = await this.store.read(), label = `org.codenomad.opencode.${m.key}`, file = path.join(this.runtime.agentDirectory, `${label}.plist`)
|
||||
return { label, file, target: `gui/${m.namespace.user}/${label}`, text: macosSupervisionPlist(m.key, this.store.directory, this.runtime),
|
||||
argv: [this.runtime.node, this.runtime.guard, this.store.directory, m.key] }
|
||||
}
|
||||
private async ownedAgent() {
|
||||
const agent = await this.agent()
|
||||
const stat = await lstat(agent.file).catch(error => { if (error.code !== "ENOENT") throw error; return null })
|
||||
if (!stat) return { ...agent, exists: false }
|
||||
await this.policy.verify(agent.file, false)
|
||||
if (stat.size > 32768 || await readFile(agent.file, "utf8") !== agent.text) throw new SupervisionError("conflict")
|
||||
return { ...agent, exists: true }
|
||||
}
|
||||
private async loaded(agent: Awaited<ReturnType<MacosServiceSupervision["agent"]>>) {
|
||||
const result = await this.command(["print", agent.target])
|
||||
if (result.code === 113) return null // Exact missing service, not a missing GUI domain or permission error.
|
||||
if (result.code !== 0) throw new SupervisionError("busy")
|
||||
return macosLoadedAgent(result.text, agent.file, agent.argv)
|
||||
}
|
||||
async reconcile(expectedRevision: number) {
|
||||
await this.prepareRuntime()
|
||||
await withMacosSupervisionLock(path.join(this.store.directory, "admission.lock"), async () => {
|
||||
const m = await this.store.read()
|
||||
if (m.revision !== expectedRevision) throw new SupervisionError("conflict")
|
||||
if (m.desired === "unset") return
|
||||
let agent = await this.ownedAgent(), loaded = await this.loaded(agent)
|
||||
if (m.desired === "on") {
|
||||
// LaunchAgents is shared with unrelated apps. Check the real directory
|
||||
// chain without changing its permissions, then create only our leaf.
|
||||
await preparePosixSupervisionDirectory(this.runtime.agentDirectory)
|
||||
if (await realpath(this.runtime.agentDirectory) !== this.runtime.agentDirectory) throw new SupervisionError("unsafe-storage")
|
||||
if (!agent.exists) {
|
||||
const handle = await open(agent.file, "wx", 0o600)
|
||||
try { await handle.writeFile(agent.text); await handle.sync() } finally { await handle.close() }
|
||||
}
|
||||
agent = await this.ownedAgent()
|
||||
if (!loaded) {
|
||||
const result = await this.command(["bootstrap", `gui/${m.namespace.user}`, agent.file])
|
||||
if (result.code) throw new SupervisionError("busy")
|
||||
}
|
||||
if (!await this.loaded(agent)) throw new SupervisionError("busy")
|
||||
} else {
|
||||
if (loaded) {
|
||||
const result = await this.command(["bootout", agent.target])
|
||||
if (result.code) throw new SupervisionError("busy")
|
||||
}
|
||||
const deadline = Date.now() + 15_000
|
||||
while (await this.loaded(agent)) {
|
||||
if (Date.now() >= deadline) throw new SupervisionError("busy")
|
||||
await delay(100)
|
||||
}
|
||||
// AbandonProcessGroup leaves the released child intact, even if launchd
|
||||
// escalates termination of the foreground guard. Never signal OpenCode.
|
||||
if (loaded?.pid) {
|
||||
while ((await lookupMacosProcess(loaded.pid, this.runtime.nativeHelper)).state !== "dead") {
|
||||
if (Date.now() >= deadline) throw new SupervisionError("busy")
|
||||
await delay(100)
|
||||
}
|
||||
}
|
||||
agent = await this.ownedAgent()
|
||||
if (agent.exists) await unlink(agent.file)
|
||||
}
|
||||
}, this.policy, this.runtime.nativeHelper)
|
||||
await this.store.acknowledge(expectedRevision)
|
||||
}
|
||||
async observe(): Promise<SupervisionObservation> {
|
||||
const m = await this.store.read(), lookup = (pid: number) => lookupMacosProcess(pid, this.runtime.nativeHelper)
|
||||
const health = await readSupervisionHealth(path.join(m.namespace.stateDirectory, m.namespace.registrationName), { lookup })
|
||||
let resources: SupervisionObservation["resources"] = "unknown", guard: SupervisionObservation["guard"] = "unknown"
|
||||
let detail: SupervisionObservation["detail"], artifactMissing = false
|
||||
for (const file of [this.runtime.node, this.runtime.guard, this.runtime.nativeHelper]) {
|
||||
try { await this.policy.verify(file, false) } catch { artifactMissing = true }
|
||||
}
|
||||
try {
|
||||
const agent = await this.ownedAgent(), loaded = await this.loaded(agent)
|
||||
resources = !agent.exists && !loaded ? "absent" : agent.exists && loaded ? "installed" : "unknown"
|
||||
guard = !loaded || !loaded.pid ? "absent" : "unknown"
|
||||
if (loaded?.pid && loaded.state === "running") {
|
||||
const status = z.object({ key: z.literal(m.key), revision: z.literal(m.revision), guardPid: z.literal(loaded.pid), guardStart: z.string(),
|
||||
cause: z.enum(["native-service-disabled-or-unknown", "start-failed", "service-unknown"]).nullable(),
|
||||
lastAttemptAt: z.string().datetime().optional(), retryAt: z.string().datetime().optional(),
|
||||
}).passthrough().parse(await readPrivate(path.join(this.store.directory, "macos-status.json"), this.policy))
|
||||
const owner = await lookup(loaded.pid)
|
||||
if (owner.state === "live" && owner.startIdentity === status.guardStart) {
|
||||
guard = "active"; detail = { cause: status.cause, lastAttemptAt: status.lastAttemptAt, retryAt: status.retryAt }
|
||||
}
|
||||
}
|
||||
} catch { /* Missing/partial ownership and native status evidence stays unknown. */ }
|
||||
return { revision: m.revision, resources, guard, health, artifactMissing, ...(detail ? { detail } : {}) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
// Read-only executable used by the foreground OS guard. Bundle it with the
|
||||
// client; never resolve dependencies from an open project or a user's PATH.
|
||||
import { readFile, lstat } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { parseSupervisionManifest, SUPERVISION_MANIFEST_BYTES } from "./service-supervision-contract"
|
||||
import { readSupervisionHealth, nativeSupervisionStartPermitted } from "./service-supervision-health"
|
||||
import { wslRuntimeSchema, readWslSupervisionSample } from "./service-supervision-wsl"
|
||||
import { supervisionTargetReady } from "./service-supervision-target"
|
||||
import { execFile } from "node:child_process"
|
||||
|
||||
async function probe() {
|
||||
const [directory, key] = process.argv.slice(2)
|
||||
if (!directory || !path.isAbsolute(directory) || !/^[a-f0-9]{64}$/.test(key)) throw new Error()
|
||||
const file = path.join(directory, "manifest.json")
|
||||
const stat = await lstat(file)
|
||||
if (!stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1 || stat.size > SUPERVISION_MANIFEST_BYTES) throw new Error()
|
||||
const bytes = await readFile(file)
|
||||
if (bytes.length > SUPERVISION_MANIFEST_BYTES) throw new Error()
|
||||
const manifest = parseSupervisionManifest(JSON.parse(bytes.toString("utf8")))
|
||||
if (manifest.key !== key) throw new Error()
|
||||
if (manifest.namespace.platform === "wsl") {
|
||||
const descriptorFile = path.join(directory, "windows.json"), descriptorStat = await lstat(descriptorFile)
|
||||
if (!descriptorStat.isFile() || descriptorStat.isSymbolicLink() || descriptorStat.size > 32768) throw new Error()
|
||||
const descriptor = JSON.parse(await readFile(descriptorFile, "utf8"))
|
||||
if (descriptor.key !== key || descriptor.directory !== directory) throw new Error()
|
||||
return readWslSupervisionSample(manifest, wslRuntimeSchema.parse(descriptor.wsl), undefined, process.argv[4] === "--start")
|
||||
}
|
||||
if (manifest.namespace.platform !== "win32") throw new Error()
|
||||
const registration = path.join(manifest.namespace.stateDirectory, manifest.namespace.registrationName)
|
||||
const health = await readSupervisionHealth(registration)
|
||||
const permitted = await nativeSupervisionStartPermitted(path.join(manifest.namespace.configDirectory, manifest.namespace.registrationName))
|
||||
// Selection, preference and health are non-secret. No registration/auth bytes
|
||||
// or exception text cross into the wrapper or its status file.
|
||||
const run = (args: string[]) => new Promise<string>((resolve, reject) => execFile(manifest.target.binary, args,
|
||||
{ env: process.env, windowsHide: true, timeout: 2000, maxBuffer: 65536 },
|
||||
(error, stdout) => error ? reject(new Error("supervision-target-unavailable")) : resolve(stdout.trim())))
|
||||
return { revision: manifest.revision, desired: manifest.desired, binary: manifest.target.binary, health, permitted,
|
||||
...(process.argv[4] === "--start" ? { targetReady: await supervisionTargetReady(manifest.target, undefined,
|
||||
{ namespace: manifest.namespace, run }) } : {}) }
|
||||
}
|
||||
probe().then(result => console.log(JSON.stringify(result))).catch(() => {
|
||||
console.log(JSON.stringify({ health: { state: "unknown" } }))
|
||||
process.exitCode = 1
|
||||
})
|
||||
|
|
@ -0,0 +1,135 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import { lstat, readFile, realpath } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { BinaryResolver } from "../settings/binaries"
|
||||
import type { SettingsService } from "../settings/service"
|
||||
import type { WorkspaceManager } from "../workspaces/manager"
|
||||
import type { ServiceConnection } from "../workspaces/opencode-service"
|
||||
import { buildSpawnSpec, parseWslUncPath } from "../workspaces/spawn"
|
||||
import { readNativeServiceChannel } from "../workspaces/native-service-registration"
|
||||
import { runtimeIdentity } from "../opencode/compatibility/runtime"
|
||||
import { readNativeScheduleIncarnation } from "../opencode/missions/native-schedule-transport-identity"
|
||||
import { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import { ServiceSupervisionSettings, type AcquiredSupervision } from "./service-supervision-service"
|
||||
import { WindowsServiceSupervision, readWindowsSupervisionRuntime, type WindowsSupervisionRuntime } from "./service-supervision-windows"
|
||||
import { prepareSupervisionRoot, provisionSupervisionRuntime, supervisionArtifactPolicy } from "./service-supervision-runtime"
|
||||
import { SupervisionError, supervisionKey } from "./service-supervision-contract"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
import { acquirePosixSupervision } from "./service-supervision-linux-resolver"
|
||||
import { acquireWslSupervision } from "./service-supervision-wsl-resolver"
|
||||
import { acquireSupervisionTarget, supervisionTargetReady } from "./service-supervision-target"
|
||||
|
||||
/** Only the authorized runtime selection is consumed. No project/session list,
|
||||
* default registration path, startup profile environment or service lifecycle. */
|
||||
export function createServiceSupervisionSettings(settings: SettingsService,
|
||||
manager: Pick<WorkspaceManager, "resolveBinaryPath">) {
|
||||
const resolver = new BinaryResolver(settings)
|
||||
const acquire = (connection?: ServiceConnection, selection = false) => (process.platform === "linux" || process.platform === "darwin" ? acquirePosixSupervision : acquireWindowsSupervision)({
|
||||
selectedBinary: () => manager.resolveBinaryPath(resolver.resolveDefault().path), connection, selection,
|
||||
})
|
||||
const supervision = new ServiceSupervisionSettings(acquire, () => acquire(undefined, true))
|
||||
// Startup completes previously requested Linux cleanup even if Preferences is
|
||||
// never opened. This preserves intent and leaves installation repair explicit.
|
||||
if (process.platform === "linux") void supervision.read().catch(() => {})
|
||||
return supervision
|
||||
}
|
||||
|
||||
interface Acquisition {
|
||||
selectedBinary(): string
|
||||
connection?: ServiceConnection
|
||||
/** Native qualification supplies isolated roots; never accepted from HTTP. */
|
||||
environment?: NodeJS.ProcessEnv
|
||||
root?: string
|
||||
policy?: StoragePolicy
|
||||
artifactPolicy?: StoragePolicy
|
||||
selection?: boolean
|
||||
}
|
||||
|
||||
export async function acquireWindowsSupervision(input: Acquisition): Promise<AcquiredSupervision> {
|
||||
if (process.platform !== "win32") throw new Error("supervision-platform-unavailable")
|
||||
const selected = input.selectedBinary()
|
||||
if (parseWslUncPath(selected)) return acquireWslSupervision(input)
|
||||
if (!path.isAbsolute(selected)) throw new Error("supervision-host-unavailable")
|
||||
// Reuse the lifecycle's published npm-bin/shim resolution. Custom wrappers
|
||||
// remain unsupported: the manifest receives only the proven native image.
|
||||
const launch = buildSpawnSpec(selected, [], { env: input.environment ?? process.env })
|
||||
if (launch.args.length) throw new SupervisionError("invalid")
|
||||
const binary = await realpath(launch.command), binaryStat = await lstat(binary)
|
||||
// The foreground guard invokes the native image directly, never cmd/PowerShell
|
||||
// launchers or an executable found later on PATH.
|
||||
if (!binaryStat.isFile() || binaryStat.isSymbolicLink() || path.extname(binary).toLowerCase() !== ".exe")
|
||||
throw new SupervisionError("invalid")
|
||||
const selectedTarget = await acquireSupervisionTarget(binary)
|
||||
const env = { ...(input.environment ?? process.env) }, system = env.SystemRoot
|
||||
if (!system || !path.isAbsolute(system)) throw new SupervisionError("invalid")
|
||||
const powershell = path.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe")
|
||||
const current = () => {
|
||||
input.connection?.assertCurrent()
|
||||
if (input.selectedBinary() !== selected) throw new SupervisionError("conflict")
|
||||
}
|
||||
const run = async (file: string, args: string[]) => {
|
||||
current()
|
||||
const spec = buildSpawnSpec(file, args, { env })
|
||||
const text = await new Promise<string>((resolve, reject) => execFile(spec.command, spec.args,
|
||||
{ env, windowsHide: true, timeout: 5000, maxBuffer: 65536, encoding: "utf8" },
|
||||
(error, stdout) => error ? reject(new Error("supervision-namespace-unavailable")) : resolve(stdout.trim())))
|
||||
current()
|
||||
if (!text || /[\r\n\x00]/.test(text)) throw new Error("supervision-namespace-unavailable")
|
||||
return text
|
||||
}
|
||||
const roots = {} as Record<"config" | "state" | "data" | "cache", string>
|
||||
for (const key of ["config", "state", "data", "cache"] as const) {
|
||||
const native = await run(binary, ["debug", "paths", key])
|
||||
if (!path.isAbsolute(native)) throw new SupervisionError("invalid")
|
||||
roots[key] = await realpath(native)
|
||||
if (path.basename(roots[key]).toLowerCase() !== "opencode") throw new SupervisionError("invalid")
|
||||
}
|
||||
const password = await run(binary, ["service", "get", "password"])
|
||||
const channel = await readNativeServiceChannel(roots.config, password)
|
||||
const user = await run(powershell, ["-NoProfile", "-NonInteractive", "-Command",
|
||||
"[Security.Principal.WindowsIdentity]::GetCurrent().User.Value"])
|
||||
const registration = path.join(roots.state, channel.name)
|
||||
const connection = input.connection
|
||||
if (connection) {
|
||||
current()
|
||||
const incarnation = readNativeScheduleIncarnation(registration), identity = runtimeIdentity(connection.endpoint)
|
||||
if (!identity || identity.pid !== incarnation.pid || connection.endpoint.url !== incarnation.url
|
||||
|| connection.endpoint.auth?.type !== "basic" || connection.endpoint.auth.password !== password)
|
||||
throw new SupervisionError("conflict")
|
||||
}
|
||||
const policy = input.policy ?? privateStorage, artifactPolicy = input.artifactPolicy ?? supervisionArtifactPolicy
|
||||
if (!env.LOCALAPPDATA) throw new SupervisionError("invalid")
|
||||
const root = input.root ?? path.join(env.LOCALAPPDATA, "CodeNomadServiceSupervision")
|
||||
const namespace = { platform: "win32" as const, host: os.hostname(), user,
|
||||
configDirectory: roots.config, stateDirectory: roots.state, registrationName: channel.name }
|
||||
if (input.selection) await lstat(path.join(root, supervisionKey(namespace), "manifest.json")).catch(error => {
|
||||
if (error.code === "ENOENT") throw new SupervisionError("unknown-request"); throw error
|
||||
})
|
||||
current()
|
||||
await prepareSupervisionRoot(root, powershell, policy)
|
||||
const store = new ServiceSupervisionStore(root, namespace, selectedTarget, policy)
|
||||
await store.read()
|
||||
let runtime = await readWindowsSupervisionRuntime(store, policy)
|
||||
const preparing = !runtime
|
||||
if (!runtime) {
|
||||
const environment = { SystemRoot: system, WINDIR: system, COMSPEC: path.join(system, "System32/cmd.exe") } as WindowsSupervisionRuntime["environment"]
|
||||
for (const key of ["HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP"] as const) {
|
||||
const value = env[key] ?? (key === "HOME" ? env.USERPROFILE : undefined)
|
||||
if (!value || !path.isAbsolute(value)) throw new SupervisionError("invalid")
|
||||
environment[key] = await realpath(value)
|
||||
}
|
||||
for (const key of ["config", "state", "data", "cache"] as const)
|
||||
environment[`XDG_${key.toUpperCase()}_HOME` as "XDG_CONFIG_HOME"] = path.dirname(roots[key])
|
||||
// The native test-home override is a path, only from the acquired process.
|
||||
if (env.OPENCODE_TEST_HOME) environment.OPENCODE_TEST_HOME = await realpath(env.OPENCODE_TEST_HOME)
|
||||
current()
|
||||
runtime = { powershell, ...await provisionSupervisionRuntime(store.directory, artifactPolicy), environment }
|
||||
}
|
||||
current()
|
||||
const adapter = new WindowsServiceSupervision(store, runtime, policy, artifactPolicy)
|
||||
if (preparing) { current(); await adapter.prepareRuntime(); current() }
|
||||
if (!await supervisionTargetReady(selectedTarget)) throw new SupervisionError("conflict")
|
||||
current()
|
||||
return { store, adapter, selectedTarget, assertCurrent: current }
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import test from "node:test"
|
||||
import { execFile } from "node:child_process"
|
||||
import { promisify } from "node:util"
|
||||
import { prepareSupervisionRoot, provisionSupervisionRuntime } from "./service-supervision-runtime"
|
||||
import { windowsStorageScript, verifyWindowsStorageEvidence } from "./windows-storage"
|
||||
|
||||
test("durable runtime copies are immutable, shared and usable after package removal; altered artifacts fail closed", async t => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "supervision-runtime-"))
|
||||
t.after(() => rm(root, { recursive: true, force: true }))
|
||||
const source = path.join(root, "package"), owned = path.join(root, "owned")
|
||||
await mkdir(source); await mkdir(owned)
|
||||
const sources = { node: path.join(source, "node.exe"), script: path.join(source, "guard.ps1"), probe: path.join(source, "probe.cjs") }
|
||||
for (const [key, file] of Object.entries(sources)) await writeFile(file, `owned-${key}`)
|
||||
const policy = { async verify(file: string, directory: boolean) {
|
||||
const stat = await lstat(file)
|
||||
assert.equal(stat.isSymbolicLink(), false)
|
||||
assert.ok(directory ? stat.isDirectory() : stat.isFile() && stat.nlink === 1)
|
||||
} }
|
||||
const first = await provisionSupervisionRuntime(owned, policy, sources)
|
||||
const second = await provisionSupervisionRuntime(owned, policy, sources)
|
||||
assert.deepEqual(first, second)
|
||||
await writeFile(first.probe, "tampered")
|
||||
await assert.rejects(provisionSupervisionRuntime(owned, policy, sources), /conflict/)
|
||||
await writeFile(first.probe, "owned-probe")
|
||||
await rm(source, { recursive: true })
|
||||
for (const [key, file] of Object.entries(first)) assert.equal(await readFile(file, "utf8"), `owned-${key}`)
|
||||
})
|
||||
|
||||
test("new Windows roots receive a private DACL; existing ACLs are verified rather than rewritten", { skip: process.platform !== "win32" }, async t => {
|
||||
const parent = await mkdtemp(path.join(os.tmpdir(), "supervision-private-root-")), root = path.join(parent, "private")
|
||||
t.after(() => rm(parent, { recursive: true, force: true }))
|
||||
const powershell = path.join(process.env.SystemRoot!, "System32/WindowsPowerShell/v1.0/powershell.exe")
|
||||
const exec = promisify(execFile)
|
||||
let evidence = ""
|
||||
const policy = { async verify(file: string, directory: boolean) {
|
||||
const { script } = windowsStorageScript(file)
|
||||
const { stdout } = await exec(powershell, ["-NoProfile", "-NonInteractive", "-EncodedCommand",
|
||||
Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 5000, maxBuffer: 65536,
|
||||
env: { ...process.env, PSModulePath: path.join(path.dirname(powershell), "Modules") } })
|
||||
const parsed = JSON.parse(stdout)
|
||||
parsed.entries = parsed.entries.slice(0, 1)
|
||||
verifyWindowsStorageEvidence(JSON.stringify(parsed), directory, 0)
|
||||
evidence = JSON.stringify(parsed)
|
||||
} }
|
||||
await prepareSupervisionRoot(root, powershell, policy)
|
||||
const before = evidence
|
||||
await prepareSupervisionRoot(root, powershell, policy)
|
||||
assert.equal(evidence, before)
|
||||
const leaf = JSON.parse(evidence)
|
||||
assert.ok(leaf.entries[0].aces.every((ace: { sid: string }) => [leaf.currentSid, "S-1-5-18"].includes(ace.sid)))
|
||||
})
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import { createHash, randomUUID } from "node:crypto"
|
||||
import { lstat, mkdir, open, readFile, link, rm } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { fileURLToPath } from "node:url"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
import { verifyWindowsPrivateStorage } from "./windows-storage"
|
||||
import { SupervisionError } from "./service-supervision-contract"
|
||||
|
||||
export const supervisionArtifactPolicy: StoragePolicy = {
|
||||
async verify(file, directory) {
|
||||
const stat = await lstat(file)
|
||||
if (stat.isSymbolicLink() || (directory ? !stat.isDirectory() : !stat.isFile() || stat.nlink !== 1))
|
||||
throw new SupervisionError("unsafe-storage")
|
||||
await verifyWindowsPrivateStorage(file, directory, 256 * 1024 * 1024)
|
||||
},
|
||||
}
|
||||
|
||||
/** Creates only a new private leaf; existing directories/ACLs are verified and
|
||||
* preserved. Production always checks the complete ancestor chain afterward. */
|
||||
export async function prepareSupervisionRoot(root: string, powershell: string, policy: StoragePolicy = privateStorage) {
|
||||
if (!path.win32.isAbsolute(root) || /[\x00-\x1f]/.test(root)) throw new SupervisionError("unsafe-storage")
|
||||
try { await lstat(root) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
||||
const literal = root.replaceAll("'", "''")
|
||||
const script = `$ErrorActionPreference='Stop'; $u=[Security.Principal.WindowsIdentity]::GetCurrent().User;
|
||||
$a=[Security.AccessControl.DirectorySecurity]::new(); $a.SetOwner($u); $a.SetAccessRuleProtection($true,$false);
|
||||
foreach($s in @($u,[Security.Principal.SecurityIdentifier]::new('S-1-5-18'))){
|
||||
$a.AddAccessRule([Security.AccessControl.FileSystemAccessRule]::new($s,'FullControl','ContainerInherit,ObjectInherit','None','Allow'))
|
||||
}; [void][IO.Directory]::CreateDirectory('${literal}',$a)`
|
||||
await new Promise<void>((resolve, reject) => execFile(powershell, ["-NoProfile", "-NonInteractive", "-EncodedCommand",
|
||||
Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, timeout: 5000, maxBuffer: 4096 },
|
||||
error => error ? reject(new SupervisionError("unsafe-storage")) : resolve()))
|
||||
}
|
||||
await policy.verify(root, true)
|
||||
}
|
||||
|
||||
/** Immutable content-addressed copies outlive backend/package replacement.
|
||||
* A published descriptor pins its version; another profile never rewrites it. */
|
||||
export async function provisionSupervisionRuntime(directory: string, policy: StoragePolicy = supervisionArtifactPolicy,
|
||||
sources = { node: process.execPath,
|
||||
script: fileURLToPath(new URL("../../dist/host-lifetime/service-supervision-windows.ps1", import.meta.url)),
|
||||
probe: fileURLToPath(new URL("../../dist/host-lifetime/service-supervision-probe.cjs", import.meta.url)) }) {
|
||||
const destination = path.join(directory, "runtime")
|
||||
await mkdir(destination, { mode: 0o700 }).catch(error => { if (error.code !== "EEXIST") throw error })
|
||||
await policy.verify(destination, true)
|
||||
const result = {} as Record<keyof typeof sources, string>
|
||||
for (const key of ["node", "script", "probe"] as const) {
|
||||
const source = sources[key], stat = await lstat(source)
|
||||
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024 * 1024) throw new SupervisionError("unsafe-storage")
|
||||
const bytes = await readFile(source)
|
||||
if (bytes.length !== stat.size) throw new SupervisionError("conflict")
|
||||
const hash = createHash("sha256").update(bytes).digest("hex")
|
||||
const file = path.join(destination, `${hash}${key === "node" ? ".exe" : key === "script" ? ".ps1" : ".cjs"}`)
|
||||
try { await lstat(file) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error
|
||||
const temporary = path.join(destination, `${randomUUID()}.tmp`)
|
||||
try {
|
||||
const handle = await open(temporary, "wx", 0o600)
|
||||
try { await handle.writeFile(bytes); await handle.sync() } finally { await handle.close() }
|
||||
await policy.verify(temporary, false)
|
||||
await link(temporary, file).catch(error => { if (error.code !== "EEXIST") throw error })
|
||||
} finally { await rm(temporary, { force: true }) }
|
||||
}
|
||||
await policy.verify(file, false)
|
||||
if (createHash("sha256").update(await readFile(file)).digest("hex") !== hash) throw new SupervisionError("conflict")
|
||||
result[key] = file
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
@ -0,0 +1,256 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { lstat, mkdir, mkdtemp, readFile, rm } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { test, type TestContext } from "node:test"
|
||||
import Fastify from "fastify"
|
||||
import { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import { ServiceSupervisionSettings, type SupervisionSettingsControl } from "./service-supervision-service"
|
||||
import { supervisionStatus, type SupervisionNamespace, type SupervisionObservation } from "./service-supervision-contract"
|
||||
import { registerServiceSupervisionRoutes } from "../server/routes/service-supervision"
|
||||
|
||||
async function fixture(t: TestContext) {
|
||||
const temporary = path.join(os.tmpdir(), "opencode")
|
||||
await mkdir(temporary, { recursive: true })
|
||||
const root = await mkdtemp(path.join(temporary, "supervision-settings-"))
|
||||
t.after(() => rm(root, { force: true, recursive: true }))
|
||||
const namespace: SupervisionNamespace = { platform: "win32", host: "isolated-test", user: "S-1-5-21-1-2-3-1000",
|
||||
configDirectory: "C:\\isolated\\config\\opencode", stateDirectory: "C:\\isolated\\state\\opencode", registrationName: "service-test.json" }
|
||||
const policy = { async verify(file: string, directory: boolean) {
|
||||
const stat = await lstat(file)
|
||||
assert.equal(stat.isSymbolicLink(), false)
|
||||
assert.equal(directory ? stat.isDirectory() : stat.isFile() && stat.nlink === 1, true)
|
||||
} }
|
||||
const store = new ServiceSupervisionStore(root, namespace, { binary: "C:\\isolated\\OpenCode.exe", provenance: "custom" }, policy)
|
||||
const key = (await store.read()).key
|
||||
let current = true, failure = false, observations = 0, effects = 0, observing: (() => Promise<void>) | undefined
|
||||
const adapter = {
|
||||
async reconcile(revision: number) {
|
||||
effects++
|
||||
if (failure) throw new Error("private-password-and-environment")
|
||||
await store.acknowledge(revision)
|
||||
},
|
||||
async observe(): Promise<SupervisionObservation> {
|
||||
observations++
|
||||
const manifest = await store.read()
|
||||
await observing?.()
|
||||
return { revision: manifest.revision, resources: manifest.desired === "on" ? "installed" : "absent",
|
||||
guard: manifest.desired === "on" ? "active" : "absent",
|
||||
health: { state: "healthy", pid: 123, id: "incumbent", startIdentity: "same-start" } }
|
||||
},
|
||||
}
|
||||
const acquire = async () => ({ store, adapter, assertCurrent: () => { if (!current) throw new Error("changed") } })
|
||||
const service = new ServiceSupervisionSettings(acquire)
|
||||
const control = (action: SupervisionSettingsControl["action"], expectedRevision: number): SupervisionSettingsControl => ({
|
||||
serviceKey: key, requestID: randomUUID(), action, expectedRevision,
|
||||
})
|
||||
return { root, store, key, service, acquire, control, adapter,
|
||||
get effects() { return effects }, get observations() { return observations },
|
||||
change: () => { current = false }, fail: (value: boolean) => { failure = value },
|
||||
observe: (value: () => Promise<void>) => { observing = value },
|
||||
}
|
||||
}
|
||||
|
||||
test("Settings controls share durable intent; passive reads and acknowledged replays perform no OS effects", async t => {
|
||||
const f = await fixture(t), authorize = () => {}, on = f.control("on", 0)
|
||||
await f.service.read()
|
||||
assert.equal(f.effects, 0)
|
||||
assert.equal((await f.service.control(on, authorize)).state, "running")
|
||||
assert.equal(f.effects, 1)
|
||||
const other = new ServiceSupervisionSettings(f.acquire)
|
||||
const exact = await other.read({ serviceKey: f.key, requestID: on.requestID })
|
||||
assert.deepEqual(exact.request, { requestID: on.requestID, state: "recorded", action: "on", appliedRevision: 1 })
|
||||
await other.control(on, authorize)
|
||||
assert.equal(f.effects, 1)
|
||||
const off = await other.control(f.control("off", 1), authorize)
|
||||
assert.equal(off.state, "off")
|
||||
assert.equal(off.serviceResponding, true)
|
||||
assert.equal(f.effects, 2)
|
||||
assert.equal((await other.read({ requestID: on.requestID })).request?.state, "unknown")
|
||||
await assert.rejects(other.control(on, authorize), /conflict/)
|
||||
assert.doesNotMatch(JSON.stringify(exact), /binary|configDirectory|stateDirectory|target|password|incumbent|startIdentity/)
|
||||
})
|
||||
|
||||
test("explicit binary publication preserves the service/preference and fences competing revisions and OFF", async t => {
|
||||
const f = await fixture(t)
|
||||
await f.service.control(f.control("on", 0), () => {})
|
||||
let selectedTarget = { binary: "C:\\new path\\opencode.exe", provenance: "custom" as const, sha256: "a".repeat(64) }
|
||||
const acquire = async () => ({ ...await f.acquire(), selectedTarget })
|
||||
const service = new ServiceSupervisionSettings(acquire)
|
||||
assert.equal((await service.read()).desired, "on")
|
||||
assert.equal((await f.store.read()).target.binary, "C:\\isolated\\OpenCode.exe", "passive read preserves target")
|
||||
await service.selectBinary()
|
||||
const published = await f.store.read()
|
||||
assert.equal(published.key, f.key); assert.equal(published.desired, "on")
|
||||
assert.deepEqual(published.target, selectedTarget)
|
||||
assert.equal(published.reconciledRevision, 2)
|
||||
await f.store.control({ action: "off", requestID: randomUUID(), expectedRevision: 2 })
|
||||
selectedTarget = { ...selectedTarget, sha256: "b".repeat(64) }
|
||||
await assert.rejects(service.selectBinary(), /busy/)
|
||||
assert.equal((await f.store.read()).desired, "off")
|
||||
await f.store.acknowledge(3)
|
||||
let atWrite = 0
|
||||
await assert.rejects(service.selectBinary(() => { if (++atWrite === 2) throw new Error("selection-changed") }), /selection-changed/)
|
||||
assert.equal((await f.store.read()).revision, 3)
|
||||
const results = await Promise.allSettled([service.selectBinary(), service.selectBinary()])
|
||||
assert.ok(results.some(result => result.status === "fulfilled"))
|
||||
assert.equal((await f.store.read()).revision, 4)
|
||||
assert.equal((await f.store.read()).desired, "off")
|
||||
})
|
||||
|
||||
test("another profile can read and turn OFF a shared service without publishing its selected binary", async t => {
|
||||
const f = await fixture(t)
|
||||
await f.service.control(f.control("on", 0), () => {})
|
||||
const before = await f.store.read()
|
||||
const other = new ServiceSupervisionSettings(async () => ({ ...await f.acquire(),
|
||||
selectedTarget: { binary: "C:\\another-profile\\opencode.exe", provenance: "standalone" as const } }))
|
||||
assert.equal((await other.read({ serviceKey: f.key })).state, "running")
|
||||
await assert.rejects(other.read({ serviceKey: "0".repeat(64) }), /conflict/)
|
||||
assert.equal((await other.control(f.control("off", 1), () => {})).state, "off")
|
||||
assert.deepEqual((await f.store.read()).target, before.target)
|
||||
})
|
||||
|
||||
test("fixed failure survives refresh and Retry reconciles the same revision without exposing native errors", async t => {
|
||||
const f = await fixture(t), on = f.control("on", 0)
|
||||
f.fail(true)
|
||||
const failed = await f.service.control(on, () => {})
|
||||
assert.equal(failed.state, "failing"); assert.equal(failed.cause, "adapter-failed")
|
||||
assert.equal(failed.desired, "on"); assert.ok(failed.lastAttemptAt)
|
||||
const other = new ServiceSupervisionSettings(f.acquire)
|
||||
assert.equal((await other.read()).cause, "adapter-failed")
|
||||
assert.equal(f.effects, 1)
|
||||
assert.doesNotMatch(await readFile(path.join(f.store.directory, "manifest.json"), "utf8"), /private-password-and-environment/)
|
||||
f.fail(false)
|
||||
const retried = await other.control(f.control("retry", 1), () => {})
|
||||
assert.equal(retried.state, "running"); assert.equal(retried.revision, 1)
|
||||
assert.equal((await f.store.read()).receipt?.control.requestID, on.requestID)
|
||||
assert.equal(f.effects, 2)
|
||||
f.adapter.observe = async () => { throw new Error("private-observer-error") }
|
||||
const unknown = await other.read()
|
||||
assert.equal(unknown.state, "failing"); assert.equal(unknown.cause, "resources-unknown")
|
||||
assert.equal(unknown.serviceResponding, false)
|
||||
assert.doesNotMatch(JSON.stringify(unknown), /private-observer-error/)
|
||||
})
|
||||
|
||||
test("first Play defaults only unset to ON; an explicit OFF and another pending ON remain untouched", async t => {
|
||||
const f = await fixture(t)
|
||||
const first = await f.service.firstPlay({ serviceKey: f.key, requestID: randomUUID() }, () => {})
|
||||
assert.equal(first.desired, "on"); assert.equal(first.state, "running")
|
||||
await f.service.control(f.control("off", 1), () => {})
|
||||
const before = await f.store.read(), effects = f.effects
|
||||
await f.service.firstPlay({ serviceKey: f.key, requestID: randomUUID() }, () => {})
|
||||
assert.deepEqual(await f.store.read(), before)
|
||||
assert.equal(f.effects, effects)
|
||||
const on = await f.store.control({ requestID: randomUUID(), expectedRevision: 2, action: "on" })
|
||||
await f.service.firstPlay({ serviceKey: f.key, requestID: randomUUID() }, () => {})
|
||||
assert.deepEqual(await f.store.read(), on)
|
||||
assert.equal(f.effects, effects)
|
||||
})
|
||||
|
||||
test("key, context and human fences refuse mutations; schema refuses UI command strings and internal controls", async t => {
|
||||
const f = await fixture(t), on = f.control("on", 0)
|
||||
await assert.rejects(f.service.control({ ...on, serviceKey: "0".repeat(64) }, () => {}), /conflict/)
|
||||
await assert.rejects(f.service.control(on, () => { throw new Error("revoked") }), /revoked/)
|
||||
for (const change of [{ target: { binary: "C:\\other.exe" } }, { action: "first-play" }, { command: "anything" }, { expectedRevision: -1 }])
|
||||
await assert.rejects(f.service.control({ ...on, ...change } as SupervisionSettingsControl, () => {}), /invalid/)
|
||||
f.change()
|
||||
await assert.rejects(f.service.control(on, () => {}), /changed/)
|
||||
assert.equal((await f.store.read()).revision, 0)
|
||||
assert.equal(f.effects, 0)
|
||||
})
|
||||
|
||||
test("human loss at the serialized write and a revision change during observation are fenced", async t => {
|
||||
const f = await fixture(t)
|
||||
let checks = 0
|
||||
await assert.rejects(f.service.control(f.control("on", 0), () => { if (++checks === 3) throw new Error("revoked-at-write") }), /revoked-at-write/)
|
||||
assert.equal((await f.store.read()).revision, 0)
|
||||
await f.service.control(f.control("on", 0), () => {})
|
||||
f.observe(async () => { await f.store.control({ requestID: randomUUID(), expectedRevision: 1, action: "off" }) })
|
||||
const result = await f.service.read()
|
||||
assert.equal(result.desired, "off"); assert.equal(result.state, "failing")
|
||||
assert.equal(result.cause, "observation-stale"); assert.equal(result.serviceResponding, false)
|
||||
assert.equal(f.effects, 1)
|
||||
})
|
||||
|
||||
test("an older failed installation cannot overwrite a competing OFF or acknowledge its resources", async t => {
|
||||
const f = await fixture(t)
|
||||
f.adapter.reconcile = async () => {
|
||||
await f.store.control({ requestID: randomUUID(), expectedRevision: 1, action: "off" })
|
||||
throw new Error("old-installer-error")
|
||||
}
|
||||
const result = await f.service.control(f.control("on", 0), () => {})
|
||||
assert.equal(result.desired, "off"); assert.equal(result.state, "pending")
|
||||
assert.equal((await f.store.read()).attempt?.revision, 1)
|
||||
assert.equal(result.lastAttemptAt, undefined)
|
||||
})
|
||||
|
||||
test("guard diagnostics are revision-fenced, retain actual backoff and cannot conceal native disabled", async t => {
|
||||
const f = await fixture(t)
|
||||
await f.service.control(f.control("on", 0), () => {})
|
||||
const manifest = await f.store.read(), observation = await f.adapter.observe()
|
||||
const detail = { cause: "start-failed" as const, lastAttemptAt: "2026-10-30T12:00:00.000Z", retryAt: "2026-10-30T12:00:30.000Z" }
|
||||
const result = supervisionStatus(manifest, { ...observation, detail })
|
||||
assert.equal(result.state, "failing"); assert.equal(result.cause, "start-failed")
|
||||
assert.equal(result.retryAt, detail.retryAt); assert.equal(result.lastAttemptAt, detail.lastAttemptAt)
|
||||
assert.equal(supervisionStatus(manifest, { ...observation, detail: { cause: "native-service-disabled-or-unknown" } }).state, "failing")
|
||||
assert.equal(supervisionStatus(manifest, { ...observation, guard: "absent", detail }).retryAt, undefined)
|
||||
assert.equal(supervisionStatus(manifest, { ...observation, revision: 0, detail }).retryAt, undefined)
|
||||
const tick = "2026-10-30T12:01:00.000Z"
|
||||
const missing = supervisionStatus(manifest, { ...observation, guard: "absent", artifactMissing: true, recoveryNextTickAt: tick })
|
||||
assert.equal(missing.state, "failing"); assert.equal(missing.cause, "artifact-missing")
|
||||
assert.equal(missing.recoveryNextTickAt, tick); assert.equal(missing.serviceResponding, true)
|
||||
assert.equal(supervisionStatus(manifest, { ...observation, revision: 0, recoveryNextTickAt: tick }).recoveryNextTickAt, undefined)
|
||||
})
|
||||
|
||||
test("product Play pins the acquired service and uses one deterministic receipt while preserving OFF", async t => {
|
||||
const f = await fixture(t), connection = { assertCurrent() {} }
|
||||
const service = new ServiceSupervisionSettings(async expected => {
|
||||
assert.equal(expected, connection)
|
||||
return f.acquire()
|
||||
})
|
||||
await service.missionPlay(connection as never, "mission-play-original", () => {})
|
||||
const before = await f.store.read()
|
||||
assert.equal(before.receipt?.control.action, "first-play")
|
||||
assert.equal(before.desired, "on")
|
||||
await service.missionPlay(connection as never, "mission-play-original", () => {})
|
||||
assert.deepEqual(await f.store.read(), before)
|
||||
await f.service.control(f.control("off", 1), () => {})
|
||||
await service.missionPlay(connection as never, "another-play", () => {})
|
||||
assert.equal((await f.store.read()).desired, "off")
|
||||
const effects = f.effects
|
||||
await assert.rejects(service.missionPlay(connection as never, "revoked-play", () => { throw new Error("revoked") }), /revoked/)
|
||||
assert.equal(f.effects, effects)
|
||||
})
|
||||
|
||||
test("Settings HTTP requires a live human before acquisition; strict inputs and fixed errors preserve privacy", async t => {
|
||||
const f = await fixture(t), app = Fastify()
|
||||
t.after(() => app.close())
|
||||
let enabled = true, acquisitions = 0
|
||||
const service = new ServiceSupervisionSettings(async () => { acquisitions++; return f.acquire() })
|
||||
registerServiceSupervisionRoutes(app, { service, auth: { isAuthEnabled: () => enabled,
|
||||
getSessionFromRequest: request => request.headers.cookie === "human=valid" ? { username: "human", sessionId: "live-cookie" } : null } })
|
||||
for (const method of ["GET", "POST"] as const) {
|
||||
const reply = await app.inject({ method, url: "/api/opencode/supervision", ...(method === "POST" ? { payload: f.control("on", 0) } : {}) })
|
||||
assert.equal(reply.statusCode, 401)
|
||||
}
|
||||
assert.equal(acquisitions, 0)
|
||||
const invalid = await app.inject({ method: "POST", url: "/api/opencode/supervision", headers: { cookie: "human=valid" },
|
||||
payload: { ...f.control("on", 0), command: "private-command" } })
|
||||
assert.equal(invalid.statusCode, 400); assert.equal(acquisitions, 0)
|
||||
const ambiguous = await app.inject({ method: "POST", url: `/api/opencode/supervision?serviceKey=${"0".repeat(64)}`,
|
||||
headers: { cookie: "human=valid" }, payload: f.control("on", 0) })
|
||||
assert.equal(ambiguous.statusCode, 400); assert.equal(acquisitions, 0)
|
||||
enabled = false
|
||||
assert.equal((await app.inject({ method: "GET", url: "/api/opencode/supervision", headers: { cookie: "human=valid" } })).statusCode, 401)
|
||||
enabled = true
|
||||
const on = f.control("on", 0)
|
||||
assert.equal((await app.inject({ method: "POST", url: "/api/opencode/supervision", headers: { cookie: "human=valid" }, payload: on })).statusCode, 200)
|
||||
const read = await app.inject({ method: "GET", url: `/api/opencode/supervision?serviceKey=${f.key}&requestID=${on.requestID}`, headers: { cookie: "human=valid" } })
|
||||
assert.equal(read.json().request.state, "recorded"); assert.equal(f.effects, 1)
|
||||
const conflict = await app.inject({ method: "POST", url: "/api/opencode/supervision", headers: { cookie: "human=valid" }, payload: f.control("off", 0) })
|
||||
assert.equal(conflict.statusCode, 409); assert.deepEqual(conflict.json(), { error: "supervision-conflict" })
|
||||
f.change()
|
||||
const changed = await app.inject({ method: "GET", url: "/api/opencode/supervision", headers: { cookie: "human=valid" } })
|
||||
assert.equal(changed.statusCode, 503); assert.deepEqual(changed.json(), { error: "supervision-unavailable" })
|
||||
})
|
||||
145
packages/server/src/host-lifetime/service-supervision-service.ts
Normal file
145
packages/server/src/host-lifetime/service-supervision-service.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import { z } from "zod"
|
||||
import { SupervisionError, supervisionStatus, type SupervisionObservation, type SupervisionStatus, type SupervisionTarget } from "./service-supervision-contract"
|
||||
import type { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import type { ServiceConnection } from "../workspaces/opencode-service"
|
||||
import { createHash, randomUUID } from "node:crypto"
|
||||
|
||||
export const supervisionSettingsControlSchema = z.object({
|
||||
serviceKey: z.string().regex(/^[a-f0-9]{64}$/), requestID: z.string().uuid(),
|
||||
expectedRevision: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER - 1),
|
||||
action: z.enum(["on", "off", "retry"]),
|
||||
}).strict()
|
||||
export type SupervisionSettingsControl = z.infer<typeof supervisionSettingsControlSchema>
|
||||
export type SupervisionSettingsResponse = SupervisionStatus & {
|
||||
serviceKey: string
|
||||
request?: { requestID: string; state: "recorded" | "unknown"; action?: "on" | "off" | "first-play" | "retry" | "select-binary"; appliedRevision?: number }
|
||||
}
|
||||
export interface AcquiredSupervision {
|
||||
/** Acquired from the authorized Settings execution host, never from UI paths,
|
||||
* a workspace inventory or the currently selected conversation. */
|
||||
store: ServiceSupervisionStore
|
||||
adapter: { observe(): Promise<SupervisionObservation>; reconcile(revision: number): Promise<void> }
|
||||
assertCurrent(): void
|
||||
selectedTarget?: SupervisionTarget
|
||||
}
|
||||
|
||||
/** The resolver owns exact native namespace, executable and durable runtime
|
||||
* provisioning. This service owns intent/OS-effect sequencing and safe readout. */
|
||||
export class ServiceSupervisionSettings {
|
||||
constructor(private readonly acquire: (connection?: ServiceConnection) => Promise<AcquiredSupervision>,
|
||||
private readonly acquireSelection = () => acquire()) {}
|
||||
|
||||
/** Internal explicit selection/verified-install/restart seam shared by desktop
|
||||
* hosts. Passive acquisition never replaces another profile's chosen target. */
|
||||
async selectBinary(assertCurrent: () => void = () => {}): Promise<void> {
|
||||
let scope: AcquiredSupervision
|
||||
try { scope = await this.acquireSelection() }
|
||||
catch (error) { if (error instanceof SupervisionError && error.code === "unknown-request") return; throw error }
|
||||
const before = await scope.store.read()
|
||||
const target = scope.selectedTarget
|
||||
const check = () => { assertCurrent(); scope.assertCurrent() }
|
||||
check()
|
||||
if (!target) return
|
||||
const same = JSON.stringify(before.target) === JSON.stringify(target)
|
||||
if (same && (before.reconciledRevision === before.revision || before.receipt?.control.action !== "select-binary")) return
|
||||
const next = same ? before : await scope.store.control({ action: "select-binary", target, expectedRevision: before.revision,
|
||||
requestID: randomUUID() }, check)
|
||||
check()
|
||||
if (next.desired === "unset") await scope.store.acknowledge(next.revision)
|
||||
else await this.reconcile(scope, next.revision, check)
|
||||
check()
|
||||
if ((await scope.store.read()).reconciledRevision !== next.revision) throw new SupervisionError("busy")
|
||||
}
|
||||
|
||||
/** Product Play pins the already authorized connection. The resolver must prove
|
||||
* that its Settings namespace is this service before changing the preference. */
|
||||
async missionPlay(connection: ServiceConnection, requestID: string, authorize: () => void): Promise<SupervisionSettingsResponse> {
|
||||
authorize(); connection.assertCurrent()
|
||||
const scope = await this.acquire(connection), manifest = await scope.store.read()
|
||||
const hex = createHash("sha256").update(`${manifest.key}\0${requestID}`).digest("hex")
|
||||
const id = `${hex.slice(0, 8)}-${hex.slice(8, 12)}-4${hex.slice(13, 16)}-8${hex.slice(17, 20)}-${hex.slice(20, 32)}`
|
||||
return new ServiceSupervisionSettings(async () => scope).firstPlay({ serviceKey: manifest.key, requestID: id }, () => {
|
||||
authorize(); connection.assertCurrent()
|
||||
})
|
||||
}
|
||||
|
||||
async read(input: { serviceKey?: string; requestID?: string } = {}): Promise<SupervisionSettingsResponse> {
|
||||
const scope = await this.acquire()
|
||||
return this.snapshot(scope, input)
|
||||
}
|
||||
|
||||
async control(raw: SupervisionSettingsControl, authorize: () => void): Promise<SupervisionSettingsResponse> {
|
||||
const parsed = supervisionSettingsControlSchema.safeParse(raw)
|
||||
if (!parsed.success) throw new SupervisionError("invalid")
|
||||
authorize()
|
||||
const scope = await this.acquire(), input = parsed.data
|
||||
const check = () => { authorize(); scope.assertCurrent() }
|
||||
await this.checkKey(scope, input.serviceKey)
|
||||
check()
|
||||
const manifest = await scope.store.control({ action: input.action, requestID: input.requestID,
|
||||
expectedRevision: input.expectedRevision }, check)
|
||||
check()
|
||||
// Repeating an acknowledged request observes the installation. Only explicit
|
||||
// Retry repairs it at the same revision. Deferred Linux linger rollback is
|
||||
// reconciled separately by the adapter's status/startup acquisition.
|
||||
if (manifest.desired !== "unset" && (input.action === "retry" || manifest.reconciledRevision !== manifest.revision))
|
||||
await this.reconcile(scope, manifest.revision, check)
|
||||
check()
|
||||
return this.snapshot(scope, input)
|
||||
}
|
||||
|
||||
/** Internal first-Play seam. Call only after human Play admission on this exact
|
||||
* acquired service; creating/reading a Mission never calls it. Explicit OFF wins. */
|
||||
async firstPlay(input: { serviceKey: string; requestID: string }, authorize: () => void): Promise<SupervisionSettingsResponse> {
|
||||
if (!z.object({ serviceKey: z.string().regex(/^[a-f0-9]{64}$/), requestID: z.string().uuid() }).strict().safeParse(input).success)
|
||||
throw new SupervisionError("invalid")
|
||||
authorize()
|
||||
const scope = await this.acquire()
|
||||
const check = () => { authorize(); scope.assertCurrent() }
|
||||
const before = await this.checkKey(scope, input.serviceKey)
|
||||
check()
|
||||
if (before.desired !== "unset") return this.snapshot(scope, input)
|
||||
const manifest = await scope.store.control({ action: "first-play", requestID: input.requestID,
|
||||
expectedRevision: before.revision }, check)
|
||||
check()
|
||||
if (manifest.desired === "on" && manifest.reconciledRevision !== manifest.revision)
|
||||
await this.reconcile(scope, manifest.revision, check)
|
||||
check()
|
||||
return this.snapshot(scope, input)
|
||||
}
|
||||
|
||||
private async reconcile(scope: AcquiredSupervision, revision: number, check: () => void): Promise<void> {
|
||||
check()
|
||||
await scope.store.recordAttempt(revision, null)
|
||||
check()
|
||||
try { await scope.adapter.reconcile(revision) }
|
||||
catch {
|
||||
// A competing OFF may have won during the native operation. Its intent and
|
||||
// diagnostics must survive; never write this older failure over it.
|
||||
try { await scope.store.recordAttempt(revision, "adapter-failed") }
|
||||
catch (error) { if (!(error instanceof SupervisionError) || error.code !== "conflict") throw error }
|
||||
}
|
||||
}
|
||||
|
||||
private async checkKey(scope: AcquiredSupervision, serviceKey?: string) {
|
||||
scope.assertCurrent()
|
||||
const manifest = await scope.store.read()
|
||||
scope.assertCurrent()
|
||||
if (serviceKey !== undefined && manifest.key !== serviceKey) throw new SupervisionError("conflict")
|
||||
return manifest
|
||||
}
|
||||
|
||||
private async snapshot(scope: AcquiredSupervision, input: { serviceKey?: string; requestID?: string }): Promise<SupervisionSettingsResponse> {
|
||||
const before = await this.checkKey(scope, input.serviceKey)
|
||||
const observation = await scope.adapter.observe().catch((): SupervisionObservation => ({ revision: before.revision,
|
||||
resources: "unknown", guard: "unknown", health: { state: "unknown" } }))
|
||||
const manifest = await this.checkKey(scope, before.key)
|
||||
// A concurrent preference change is returned with stale observation rather
|
||||
// than being labelled protected using another revision's healthy service.
|
||||
const request = input.requestID ? manifest.receipt?.control.requestID === input.requestID
|
||||
? { requestID: input.requestID, state: "recorded" as const, action: manifest.receipt.control.action,
|
||||
appliedRevision: manifest.receipt.appliedRevision }
|
||||
: { requestID: input.requestID, state: "unknown" as const } : undefined
|
||||
return { serviceKey: manifest.key, ...supervisionStatus(manifest, observation), ...(request ? { request } : {}) }
|
||||
}
|
||||
}
|
||||
178
packages/server/src/host-lifetime/service-supervision-store.ts
Normal file
178
packages/server/src/host-lifetime/service-supervision-store.ts
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
import { constants } from "node:fs"
|
||||
import { lstat, mkdir, open, rename, rm, rmdir } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
import { mutateWindowsSupervisionFile } from "./service-supervision-windows-gate"
|
||||
import { withLinuxSupervisionLock } from "./service-supervision-linux-lock"
|
||||
import { withMacosSupervisionLock } from "./service-supervision-macos-native"
|
||||
import {
|
||||
applySupervisionControl, initialSupervisionManifest, parseSupervisionManifest, SUPERVISION_MANIFEST_BYTES,
|
||||
SupervisionError, type SupervisionControl, type SupervisionManifest, type SupervisionNamespace, type SupervisionTarget,
|
||||
} from "./service-supervision-contract"
|
||||
|
||||
/** Private service-wide intent storage. The root is provisioned by the execution
|
||||
* host independently of every CodeNomad profile. This store never starts/stops a
|
||||
* service, installs an OS resource, or acknowledges an unobserved side effect. */
|
||||
export class ServiceSupervisionStore {
|
||||
readonly directory: string
|
||||
private readonly initial: SupervisionManifest
|
||||
constructor(root: string, namespace: SupervisionNamespace, target: SupervisionTarget,
|
||||
private readonly policy: StoragePolicy = privateStorage, private readonly nativeHelper?: string,
|
||||
private readonly namespaceMutation?: (update: (bytes: string | null) => Promise<string>) => Promise<void>) {
|
||||
if (!path.isAbsolute(root)) throw new SupervisionError("unsafe-storage")
|
||||
this.initial = initialSupervisionManifest(namespace, target)
|
||||
this.directory = path.join(root, this.initial.key)
|
||||
}
|
||||
|
||||
private async prepare(): Promise<void> {
|
||||
try {
|
||||
const root = path.dirname(this.directory)
|
||||
// POSIX's privateStorage checks the leaf; check all ancestors for redirects
|
||||
// as well. The Windows evaluator additionally verifies their owner/DACL.
|
||||
for (let current = root; ;) {
|
||||
const stat = await lstat(current)
|
||||
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error()
|
||||
const parent = path.dirname(current)
|
||||
if (parent === current) break
|
||||
current = parent
|
||||
}
|
||||
await this.policy.verify(root, true)
|
||||
await mkdir(this.directory, { mode: 0o700 }).catch(error => { if (error.code !== "EEXIST") throw error })
|
||||
await this.policy.verify(this.directory, true)
|
||||
} catch { throw new SupervisionError("unsafe-storage") }
|
||||
}
|
||||
|
||||
async read(): Promise<SupervisionManifest> {
|
||||
await this.prepare()
|
||||
return this.readPrepared()
|
||||
}
|
||||
|
||||
private async readPrepared(): Promise<SupervisionManifest> {
|
||||
const file = path.join(this.directory, "manifest.json")
|
||||
try { await this.policy.verify(file, false) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return structuredClone(this.initial)
|
||||
throw new SupervisionError("unsafe-storage")
|
||||
}
|
||||
const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0))
|
||||
try {
|
||||
const stat = await handle.stat(), named = await lstat(file)
|
||||
if (!stat.isFile() || stat.size > SUPERVISION_MANIFEST_BYTES || stat.ino !== named.ino || stat.dev !== named.dev)
|
||||
throw new SupervisionError("unsafe-storage")
|
||||
const bytes = Buffer.alloc(SUPERVISION_MANIFEST_BYTES + 1)
|
||||
let size = 0
|
||||
while (size < bytes.length) {
|
||||
const result = await handle.read(bytes, size, bytes.length - size, null)
|
||||
if (!result.bytesRead) break
|
||||
size += result.bytesRead
|
||||
}
|
||||
if (size > SUPERVISION_MANIFEST_BYTES) throw new SupervisionError("invalid")
|
||||
let value: SupervisionManifest
|
||||
try { value = parseSupervisionManifest(JSON.parse(bytes.subarray(0, size).toString("utf8"))) }
|
||||
catch { throw new SupervisionError("invalid") }
|
||||
if (value.key !== this.initial.key) throw new SupervisionError("conflict")
|
||||
return value
|
||||
} finally { await handle.close() }
|
||||
}
|
||||
|
||||
/** Persist intent before calling an OS adapter. A lost reply is read by exact
|
||||
* request ID; an absent receipt remains unknown, never proof of no effect. */
|
||||
control(control: SupervisionControl, assertCurrent: () => void = () => {}): Promise<SupervisionManifest> {
|
||||
return this.mutate(current => {
|
||||
assertCurrent()
|
||||
return applySupervisionControl(current, control)
|
||||
})
|
||||
}
|
||||
|
||||
async request(requestID: string): Promise<SupervisionManifest | undefined> {
|
||||
if (!/^[a-f0-9-]{36}$/i.test(requestID)) throw new SupervisionError("invalid")
|
||||
const value = await this.read()
|
||||
return value.receipt?.control.requestID === requestID ? value : undefined
|
||||
}
|
||||
|
||||
/** The adapter calls this only after its exact resource/guard read-back. It
|
||||
* must use the same observed revision, including for an OFF release. */
|
||||
acknowledge(expectedRevision: number): Promise<SupervisionManifest> {
|
||||
return this.mutate(current => {
|
||||
if (current.revision !== expectedRevision) throw new SupervisionError("conflict")
|
||||
return parseSupervisionManifest({ ...current, reconciledRevision: expectedRevision,
|
||||
...(current.attempt?.revision === expectedRevision ? { attempt: { ...current.attempt, cause: null } } : {}) })
|
||||
})
|
||||
}
|
||||
|
||||
/** Persist only fixed diagnostics. Adapter exception text can contain secrets. */
|
||||
recordAttempt(expectedRevision: number, cause: "adapter-failed" | null): Promise<SupervisionManifest> {
|
||||
return this.mutate(current => {
|
||||
if (current.revision !== expectedRevision) throw new SupervisionError("conflict")
|
||||
return parseSupervisionManifest({ ...current, reconciledRevision: null,
|
||||
attempt: { revision: expectedRevision, at: new Date().toISOString(), cause } })
|
||||
})
|
||||
}
|
||||
|
||||
private async mutate(update: (value: SupervisionManifest) => SupervisionManifest): Promise<SupervisionManifest> {
|
||||
await this.prepare()
|
||||
if (process.platform === "win32") {
|
||||
const mutate = async () => {
|
||||
await this.readPrepared()
|
||||
const file = path.join(this.directory, "admission.lock")
|
||||
// Existing files must retain their private, non-linked identity. A new
|
||||
// handle inherits the verified directory's private DACL.
|
||||
await this.policy.verify(file, false).catch(error => {
|
||||
if (error.code !== "ENOENT") throw new SupervisionError("unsafe-storage")
|
||||
})
|
||||
let next: SupervisionManifest | undefined
|
||||
const publish = async (bytes: string | null) => {
|
||||
if (this.initial.namespace.platform !== "wsl") await this.policy.verify(file, false)
|
||||
const current = await this.readPrepared()
|
||||
if (bytes !== null) {
|
||||
let parsed: SupervisionManifest
|
||||
try { parsed = parseSupervisionManifest(JSON.parse(bytes)) } catch { throw new SupervisionError("invalid") }
|
||||
if (JSON.stringify(parsed) !== JSON.stringify(current)) throw new SupervisionError("conflict")
|
||||
} else if (current.revision !== 0) throw new SupervisionError("conflict")
|
||||
next = parseSupervisionManifest(update(current))
|
||||
return JSON.stringify(next)
|
||||
}
|
||||
if (this.initial.namespace.platform === "wsl") {
|
||||
if (!this.namespaceMutation) throw new SupervisionError("invalid")
|
||||
await this.namespaceMutation(publish)
|
||||
} else await mutateWindowsSupervisionFile(path.join(this.directory, "manifest.json"), publish)
|
||||
return next!
|
||||
}
|
||||
return mutate()
|
||||
}
|
||||
if (process.platform === "linux") return withLinuxSupervisionLock(path.join(this.directory, "admission.lock"),
|
||||
() => this.writeMutation(update), this.policy)
|
||||
if (process.platform === "darwin") return withMacosSupervisionLock(path.join(this.directory, "admission.lock"),
|
||||
() => this.writeMutation(update), this.policy, this.nativeHelper)
|
||||
const gate = path.join(this.directory, "manifest-mutation")
|
||||
// ponytail: POSIX metadata writers still require explicit abandoned-gate
|
||||
// recovery; use their OS advisory lock when the native adapter is integrated.
|
||||
try { await mkdir(gate, { mode: 0o700 }) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "EEXIST") throw new SupervisionError("busy")
|
||||
throw new SupervisionError("unsafe-storage")
|
||||
}
|
||||
try {
|
||||
await this.policy.verify(gate, true)
|
||||
return await this.writeMutation(update)
|
||||
} finally { await rmdir(gate) }
|
||||
}
|
||||
|
||||
private async writeMutation(update: (value: SupervisionManifest) => SupervisionManifest): Promise<SupervisionManifest> {
|
||||
let temporary: string | undefined
|
||||
try {
|
||||
const current = await this.readPrepared()
|
||||
const next = update(current)
|
||||
if (JSON.stringify(next) === JSON.stringify(current)) return next
|
||||
const bytes = JSON.stringify(parseSupervisionManifest(next))
|
||||
temporary = path.join(this.directory, `${randomUUID()}.tmp`)
|
||||
const handle = await open(temporary, "wx", 0o600)
|
||||
try { await handle.writeFile(bytes); await handle.sync() } finally { await handle.close() }
|
||||
await this.policy.verify(temporary, false)
|
||||
await this.policy.verify(this.directory, true)
|
||||
await rename(temporary, path.join(this.directory, "manifest.json"))
|
||||
return next
|
||||
} finally {
|
||||
if (temporary) await rm(temporary, { force: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { mkdir, mkdtemp, writeFile, rm, rename, realpath } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import os from "node:os"
|
||||
import { test } from "node:test"
|
||||
import { acquireSupervisionTarget, supervisionTargetReady } from "./service-supervision-target"
|
||||
|
||||
test("absolute targets with spaces pin bytes and npm package/installation provenance, with no fallback", async t => {
|
||||
const temp = path.join(os.tmpdir(), "opencode")
|
||||
await mkdir(temp, { recursive: true })
|
||||
const root = await realpath(await mkdtemp(path.join(temp, "supervision binary ")))
|
||||
t.after(() => rm(root, { recursive: true, force: true }))
|
||||
const binary = path.join(root, "npm prefix", ...(process.platform === "win32" ? [] : ["lib"]), "node_modules/@opencode/cli/bin/opencode.exe")
|
||||
const packageFile = path.resolve(binary, "../../package.json")
|
||||
const lock = path.join(root, "npm prefix/.codenomad-opencode-install.lock")
|
||||
await mkdir(path.dirname(binary), { recursive: true })
|
||||
await writeFile(binary, "complete isolated executable bytes")
|
||||
await writeFile(packageFile, JSON.stringify({ name: "@opencode/cli", version: "99.0.0", bin: { opencode: "./bin/opencode.exe" } }))
|
||||
const target = await acquireSupervisionTarget(binary)
|
||||
assert.equal(target.provenance, "npm")
|
||||
assert.equal(target.package?.file, packageFile)
|
||||
assert.equal(target.package?.lock, lock)
|
||||
assert.equal(await supervisionTargetReady(target), true)
|
||||
await writeFile(lock, "installing")
|
||||
assert.equal(await supervisionTargetReady(target), false)
|
||||
await assert.rejects(acquireSupervisionTarget(binary), /busy/)
|
||||
await rm(lock)
|
||||
const hidden = `${binary}.hidden`
|
||||
await rename(binary, hidden)
|
||||
assert.equal(await supervisionTargetReady(target), false)
|
||||
await writeFile(binary, "partial")
|
||||
assert.equal(await supervisionTargetReady(target), false)
|
||||
await rm(binary); await rename(hidden, binary)
|
||||
const roots = { config: path.join(root, "config/opencode"), state: path.join(root, "state/opencode") }
|
||||
for (const value of Object.values(roots)) await mkdir(value, { recursive: true })
|
||||
await writeFile(path.join(roots.config, "service.json"), JSON.stringify({ password: "isolated-fixture" }))
|
||||
await writeFile(binary, "complete future compatible executable")
|
||||
const calls: string[][] = []
|
||||
const replacement = { namespace: { platform: process.platform === "win32" ? "win32" as const : "linux" as const,
|
||||
host: "fixture", user: "fixture", configDirectory: roots.config, stateDirectory: roots.state, registrationName: "service.json" },
|
||||
run: async (args: string[]) => {
|
||||
calls.push(args)
|
||||
return args[0] === "debug" ? roots[args[2] as keyof typeof roots] : "isolated-fixture"
|
||||
} }
|
||||
assert.equal(await supervisionTargetReady(target), false)
|
||||
assert.equal(await supervisionTargetReady(target, undefined, replacement), true)
|
||||
assert.deepEqual(calls, [["debug", "paths", "config"], ["debug", "paths", "state"], ["service", "get", "password"]])
|
||||
assert.equal(await supervisionTargetReady(target, undefined, { ...replacement,
|
||||
namespace: { ...replacement.namespace, registrationName: "service-beta.json" } }), false)
|
||||
await writeFile(path.join(roots.config, "service-beta.json"), JSON.stringify({ password: "isolated-fixture" }))
|
||||
assert.equal(await supervisionTargetReady(target, undefined, replacement), false, "ambiguous channels are refused")
|
||||
await writeFile(packageFile, JSON.stringify({ name: "foreign-package", bin: { opencode: "./bin/opencode.exe" } }))
|
||||
assert.equal(await supervisionTargetReady(target), false)
|
||||
await assert.rejects(acquireSupervisionTarget(binary), /invalid/)
|
||||
await assert.rejects(acquireSupervisionTarget("opencode"), /invalid/)
|
||||
assert.equal(await supervisionTargetReady({ binary, provenance: "custom" }), false)
|
||||
})
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
import { createHash } from "node:crypto"
|
||||
import { constants } from "node:fs"
|
||||
import { lstat, open, realpath } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { SupervisionError, supervisionTargetSchema, type SupervisionTarget, type SupervisionNamespace } from "./service-supervision-contract"
|
||||
import { readNativeServiceChannel } from "../workspaces/native-service-registration"
|
||||
|
||||
/** Pin complete bytes, rather than a version label or a PATH command. Stable
|
||||
* rereads fence partial installs and replacements during namespace acquisition. */
|
||||
async function hash(file: string, limit = 256 * 1024 * 1024): Promise<string> {
|
||||
const before = await lstat(file)
|
||||
if (!before.isFile() || before.isSymbolicLink() || before.size < 1 || before.size > limit) throw new SupervisionError("invalid")
|
||||
const handle = await open(file, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0))
|
||||
try {
|
||||
const digest = createHash("sha256"), bytes = Buffer.alloc(1024 * 1024)
|
||||
let size = 0
|
||||
while (true) {
|
||||
const read = await handle.read(bytes, 0, bytes.length, null)
|
||||
if (!read.bytesRead) break
|
||||
size += read.bytesRead
|
||||
if (size > limit) throw new SupervisionError("invalid")
|
||||
digest.update(bytes.subarray(0, read.bytesRead))
|
||||
}
|
||||
const after = await lstat(file), opened = await handle.stat()
|
||||
if (size !== before.size || [after, opened].some(stat => stat.ino !== before.ino || stat.dev !== before.dev
|
||||
|| stat.size !== before.size || stat.mtimeMs !== before.mtimeMs || stat.ctimeMs !== before.ctimeMs)) throw new SupervisionError("conflict")
|
||||
return digest.digest("hex")
|
||||
} finally { await handle.close() }
|
||||
}
|
||||
async function unlocked(file: string) {
|
||||
try { await lstat(file); throw new SupervisionError("busy") }
|
||||
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error }
|
||||
}
|
||||
|
||||
export async function acquireSupervisionTarget(binary: string, map: (file: string) => string | Promise<string> = value => value,
|
||||
windows = process.platform === "win32"): Promise<SupervisionTarget> {
|
||||
const api = windows ? path.win32 : path.posix
|
||||
if (!api.isAbsolute(binary)) throw new SupervisionError("invalid")
|
||||
const file = await map(binary)
|
||||
if (await realpath(file) !== file) throw new SupervisionError("invalid")
|
||||
const target: SupervisionTarget = { binary, provenance: "custom" }
|
||||
// Only the exact published @opencode/cli layout qualifies as npm provenance.
|
||||
const root = api.dirname(api.dirname(binary))
|
||||
if (api.basename(root) === "cli" && api.basename(api.dirname(root)) === "@opencode"
|
||||
&& api.basename(api.dirname(api.dirname(root))) === "node_modules") {
|
||||
const prefix = api.dirname(api.dirname(api.dirname(root)))
|
||||
const lock = api.join(!windows && api.basename(prefix) === "lib" ? api.dirname(prefix) : prefix, ".codenomad-opencode-install.lock")
|
||||
await unlocked(await map(lock))
|
||||
const packageFile = api.join(root, "package.json"), hostPackage = await map(packageFile)
|
||||
const sha256 = await hash(hostPackage, 65536)
|
||||
const handle = await open(hostPackage, "r")
|
||||
let pkg: { name?: string; bin?: Record<string, string> }
|
||||
try {
|
||||
const bytes = Buffer.alloc(65537), read = await handle.read(bytes, 0, bytes.length, 0)
|
||||
if (read.bytesRead > 65536) throw new SupervisionError("invalid")
|
||||
pkg = JSON.parse(bytes.subarray(0, read.bytesRead).toString("utf8"))
|
||||
} finally { await handle.close() }
|
||||
if (pkg.name !== "@opencode/cli" || !pkg.bin || !Object.values(pkg.bin).some(relative => typeof relative === "string"
|
||||
&& api.resolve(root, relative) === binary)) throw new SupervisionError("invalid")
|
||||
target.provenance = "npm"; target.package = { file: packageFile, sha256, lock }
|
||||
}
|
||||
target.sha256 = await hash(file)
|
||||
if (!await supervisionTargetReady(target, map)) throw new SupervisionError("conflict")
|
||||
return supervisionTargetSchema.parse(target)
|
||||
}
|
||||
|
||||
export async function supervisionTargetReady(target: SupervisionTarget,
|
||||
map: (file: string) => string | Promise<string> = value => value,
|
||||
replacement?: { namespace: SupervisionNamespace; run(args: string[]): Promise<string> }): Promise<boolean> {
|
||||
try {
|
||||
if (!target.sha256) return false
|
||||
let exact = await hash(await map(target.binary)) === target.sha256
|
||||
if (target.package) {
|
||||
await unlocked(await map(target.package.lock))
|
||||
exact = await hash(await map(target.package.file), 65536) === target.package.sha256 && exact
|
||||
await unlocked(await map(target.package.lock))
|
||||
}
|
||||
if (exact) return true
|
||||
// A complete native npm upgrade may happen with CodeNomad closed. Preserve
|
||||
// its authorized package/path, then re-prove the consumed CLI namespace before
|
||||
// accepting the replacement. Custom images still need explicit publication.
|
||||
if (!replacement || target.provenance !== "npm" || !target.package) return false
|
||||
const { namespace: n, run } = replacement
|
||||
const next = await acquireSupervisionTarget(target.binary, map, n.platform === "win32")
|
||||
if (next.package?.file !== target.package.file || next.package.lock !== target.package.lock) return false
|
||||
for (const key of ["config", "state"] as const) {
|
||||
const root = await run(["debug", "paths", key])
|
||||
if (await realpath(await map(root)) !== await realpath(await map(n[`${key}Directory`]))) return false
|
||||
}
|
||||
const password = await run(["service", "get", "password"])
|
||||
if ((await readNativeServiceChannel(await map(n.configDirectory), password)).name !== n.registrationName) return false
|
||||
return supervisionTargetReady(next, map)
|
||||
} catch { return false }
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { execFile } from "node:child_process"
|
||||
import { access, mkdtemp, rm, writeFile } from "node:fs/promises"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import test from "node:test"
|
||||
import { promisify } from "node:util"
|
||||
|
||||
// Execute the complete shipped Recovery branch, with only its clock substituted.
|
||||
// Scheduler commands are in-process stubs: this test never creates an OS task.
|
||||
const fixture = String.raw`
|
||||
param([string]$Guard,[string]$Root)
|
||||
$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue'
|
||||
function Assert($ok,$message) { if (!$ok) { throw $message } }
|
||||
$source=[IO.File]::ReadAllText($Guard)
|
||||
$tokens=$null; $errors=$null
|
||||
$ast=[Management.Automation.Language.Parser]::ParseInput($source,[ref]$tokens,[ref]$errors)
|
||||
Assert ($errors.Count -eq 0) 'guard syntax'
|
||||
$reader=$ast.Find({param($n) $n -is [Management.Automation.Language.FunctionDefinitionAst] -and $n.Name -eq 'ReadJson'},$true)
|
||||
. ([scriptblock]::Create($reader.Extent.Text))
|
||||
$source=$source.Replace('[datetimeoffset]::UtcNow','$global:CooldownNow').Replace('[datetime]::UtcNow','$global:CooldownNow.UtcDateTime')
|
||||
[void][IO.Directory]::CreateDirectory($Root)
|
||||
$script=Join-Path $Root 'guard.ps1'; [IO.File]::WriteAllText($script,$source)
|
||||
$key='a'*64; $directory=Join-Path $Root $key; [void][IO.Directory]::CreateDirectory($directory)
|
||||
$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
||||
$descriptor=@{v=1;key=$key;directory=$directory;script=$script;powershell='unused.exe';node=$script;probe=$script}
|
||||
[IO.File]::WriteAllText((Join-Path $directory 'windows.json'),($descriptor|ConvertTo-Json -Compress))
|
||||
$manifest=@{v=1;key=$key;revision=1;desired='on';namespace=@{platform='win32';user=$sid}}
|
||||
[IO.File]::WriteAllText((Join-Path $directory 'manifest.json'),($manifest|ConvertTo-Json -Depth 4 -Compress))
|
||||
$last=Join-Path $directory 'last-start.json'
|
||||
$global:Starts=0; $global:Refuse=$false; $global:MainState='Ready'
|
||||
function Import-Module { param($Name) Assert ($Name.EndsWith('ScheduledTasks.psd1')) 'unexpected module' }
|
||||
function Get-ScheduledTask {
|
||||
param($TaskPath,$TaskName,$ErrorAction)
|
||||
$kind=if($TaskName.EndsWith('-Main')){'Main'}else{'Recovery'}
|
||||
return [pscustomobject]@{Description=$marker;Actions=@([pscustomobject]@{Execute=$d.powershell;Arguments=(Arguments $kind);WorkingDirectory=$Directory});Principal=@{UserId=$sid};State=$(if($kind -eq 'Main'){$global:MainState}else{'Ready'})}
|
||||
}
|
||||
function Export-ScheduledTask {
|
||||
param($TaskPath,$TaskName)
|
||||
$kind=if($TaskName.EndsWith('-Main')){'Main'}else{'Recovery'}
|
||||
$policy=if($kind -eq 'Main'){'IgnoreNew'}else{'Queue'}
|
||||
$limit=if($kind -eq 'Main'){'PT0S'}else{'PT10S'}
|
||||
$extra=if($kind -eq 'Recovery'){'<EventTrigger><Subscription>'+[Security.SecurityElement]::Escape((Subscription))+'</Subscription></EventTrigger><TimeTrigger><Repetition><Interval>PT1M</Interval></Repetition></TimeTrigger>'}else{''}
|
||||
return '<Task><Principals><Principal><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals><Settings><MultipleInstancesPolicy>'+$policy+'</MultipleInstancesPolicy><ExecutionTimeLimit>'+$limit+'</ExecutionTimeLimit><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries></Settings><Triggers><LogonTrigger><UserId>'+$sid+'</UserId></LogonTrigger>'+$extra+'</Triggers></Task>'
|
||||
}
|
||||
function Start-ScheduledTask {
|
||||
param($InputObject)
|
||||
$global:Starts++
|
||||
if($global:Refuse){throw 'test-start-refusal'}
|
||||
}
|
||||
$results=@(); $nativeTypes=@()
|
||||
function Check($name,$json,$now,$code,$starts) {
|
||||
$global:CooldownNow=[datetimeoffset]::Parse($now,[Globalization.CultureInfo]::InvariantCulture)
|
||||
if($null -eq $json){[IO.File]::Delete($last)}else{[IO.File]::WriteAllText($last,$json)}
|
||||
$before=$global:Starts
|
||||
$output=@(& $script -Directory $directory -Key $key -Mode Recovery)
|
||||
Assert ($LASTEXITCODE -eq $code) ($name+': exit '+$LASTEXITCODE+' expected '+$code+' '+($output -join ' '))
|
||||
Assert ($global:Starts-$before -eq $starts) ($name+': Start count')
|
||||
if($starts -eq 0 -and $null -ne $json){Assert ([IO.File]::ReadAllText($last) -ceq $json) ($name+': proof preserved')}
|
||||
if($starts -eq 1){
|
||||
$written=ReadJson $last 1024
|
||||
Assert ($written.key -eq $key) ($name+': written identity')
|
||||
$wire=[IO.File]::ReadAllText($last)
|
||||
Assert ($wire -match '"at":"[^"]+Z"') ($name+': UTC writer')
|
||||
}
|
||||
$script:results+=@{name=$name;code=$LASTEXITCODE;starts=$global:Starts-$before}
|
||||
}
|
||||
function Proof($at){return '{"key":"'+$key+'","at":"'+$at+'"}'}
|
||||
$now='2026-11-01T07:41:25.0669287Z'
|
||||
$cases=@(
|
||||
@('UTC expired','2026-11-01T07:41:05.5654997Z',$now,1),
|
||||
@('positive offset expired','2026-11-01T08:41:05.5654997+01:00',$now,1),
|
||||
@('negative offset expired','2026-11-01T00:41:05.5654997-07:00',$now,1),
|
||||
@('maximum positive offset','2026-11-01T21:41:05.5654997+14:00',$now,1),
|
||||
@('maximum negative offset','2026-10-31T17:41:05.5654997-14:00',$now,1),
|
||||
@('UTC recent','2026-11-01T07:41:22.0669287Z',$now,0),
|
||||
@('positive offset recent','2026-11-01T13:11:22.0669287+05:30',$now,0),
|
||||
@('negative offset recent','2026-11-01T04:11:22.0669287-03:30',$now,0),
|
||||
@('one tick before boundary','2026-11-01T07:41:20.0669288Z',$now,0),
|
||||
@('exact five seconds','2026-11-01T07:41:20.0669287Z',$now,1),
|
||||
@('clock rollback','2026-11-01T07:42:25.0669287Z',$now,0),
|
||||
@('DST spring recent','2026-03-29T01:59:59+01:00','2026-03-29T03:00:01+02:00',0),
|
||||
@('DST spring expired','2026-03-29T01:59:59+01:00','2026-03-29T03:00:05+02:00',1),
|
||||
@('DST fall recent','2026-10-25T02:59:59+02:00','2026-10-25T02:00:01+01:00',0),
|
||||
@('DST repeated hour expired','2026-10-25T02:00:00+02:00','2026-10-25T02:00:01+01:00',1)
|
||||
)
|
||||
foreach($case in $cases){
|
||||
$json=Proof $case[1]; [IO.File]::WriteAllText($last,$json)
|
||||
$nativeTypes+=(ReadJson $last 1024).at.GetType().FullName
|
||||
Check $case[0] $json $case[2] 0 $case[3]
|
||||
}
|
||||
$invalid=@('','garbage','2026-11-01T07:41:05','2026-11-01T07:41:05z','2026-11-01T07:41:05Z ',
|
||||
'2026-11-01T07:41:05+01:60','2026-11-01T07:41:05-01:60','2026-11-01T07:41:05+14:01',
|
||||
'2026-11-01T07:41:05+15:00','2026-11-01T07:41:05-15:00','2026-11-01T07:41:05+0100',
|
||||
'2026-11-01T07:41:05+01','2026-11-01T07:41:05+01:-1','2026-11-01T07:41:05.12345678Z',
|
||||
'2026-02-29T07:41:05Z','2026-11-01T07:41:60Z','2026-13-01T07:41:05Z')
|
||||
foreach($at in $invalid){Check ('invalid '+$at) (Proof $at) $now 1 0}
|
||||
foreach($value in @('null','123','true','[]','{}')){Check ('invalid type '+$value) ('{"key":"'+$key+'","at":'+$value+'}') $now 1 0}
|
||||
Check 'missing at' ('{"key":"'+$key+'"}') $now 1 0
|
||||
Check 'duplicate at' ('{"key":"'+$key+'","at":"2000-01-01T00:00:00Z","at":"2000-01-01T00:00:01Z"}') $now 1 0
|
||||
Check 'escaped at key' ('{"key":"'+$key+'","\u0061t":"2000-01-01T00:00:00Z"}') $now 1 0
|
||||
Check 'wrong key' ('{"key":"'+('b'*64)+'","at":"2000-01-01T00:00:00Z"}') $now 1 0
|
||||
Check 'invalid JSON' '{' $now 1 0
|
||||
Check 'over bound' (' '*1025) $now 1 0
|
||||
Check 'at first, whitespace' ('{ "at" : "2000-01-01T00:00:00Z", "key" : "'+$key+'" }') $now 0 1
|
||||
Check 'missing proof' $null $now 0 1
|
||||
$global:Refuse=$true
|
||||
Check 'transient Start refusal' (Proof '2026-11-01T07:41:05Z') $now 1 1
|
||||
$refused=[IO.File]::ReadAllText($last); $global:Refuse=$false
|
||||
Check 'tick within refused cooldown' $refused '2026-11-01T07:41:29.0669287Z' 0 0
|
||||
Check 'tick after refused cooldown' $refused '2026-11-01T07:41:30.0669287Z' 0 1
|
||||
$global:MainState='Running'
|
||||
Check 'active Main' (Proof '2000-01-01T00:00:00Z') $now 0 0
|
||||
@{version=$PSVersionTable.PSVersion.ToString();nativeTypes=@($nativeTypes|Select-Object -Unique);cases=$results;clockChanged=$false;osTasksCreated=0}|ConvertTo-Json -Depth 5 -Compress
|
||||
`
|
||||
|
||||
test("Recovery cooldown uses UTC instants and rejects invalid proof in Windows PowerShell and pwsh", {
|
||||
skip: process.platform !== "win32",
|
||||
}, async t => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "supervision-cooldown-"))
|
||||
t.after(() => rm(root, { recursive: true, force: true }))
|
||||
const guard = process.env.CODENOMAD_COOLDOWN_GUARD
|
||||
?? path.join(import.meta.dirname, "service-supervision-windows.ps1")
|
||||
const script = path.join(root, "test.ps1")
|
||||
await writeFile(script, fixture)
|
||||
const windows = process.env.SystemRoot ?? "C:\\Windows"
|
||||
const powershell = path.join(windows, "System32/WindowsPowerShell/v1.0/powershell.exe")
|
||||
const pwsh = path.join(process.env.ProgramFiles ?? "C:\\Program Files", "PowerShell/7/pwsh.exe")
|
||||
const shells = [powershell]
|
||||
try { await access(pwsh); shells.push(pwsh) } catch { t.diagnostic("pwsh 7 not installed") }
|
||||
for (const [index, shell] of shells.entries()) {
|
||||
await t.test(path.basename(shell), async () => {
|
||||
const output = path.join(root, String(index))
|
||||
const { stdout } = await promisify(execFile)(shell, ["-NoLogo", "-NoProfile", "-NonInteractive",
|
||||
"-ExecutionPolicy", "Bypass", "-File", script, "-Guard", guard, "-Root", output], {
|
||||
windowsHide: true, timeout: 30_000, maxBuffer: 256 * 1024,
|
||||
env: { SystemRoot: windows, TEMP: root, TMP: root },
|
||||
})
|
||||
const result = JSON.parse(stdout.trim())
|
||||
assert.ok(result.cases.length >= 45)
|
||||
assert.equal(result.clockChanged, false)
|
||||
assert.equal(result.osTasksCreated, 0)
|
||||
assert.ok(result.nativeTypes.includes(index === 0 ? "System.String" : "System.DateTime"))
|
||||
t.diagnostic(`${result.version}: ${result.cases.length} Recovery cases; ${result.nativeTypes.join(", ")}`)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { spawn } from "node:child_process"
|
||||
import path from "node:path"
|
||||
import { SupervisionError, SUPERVISION_MANIFEST_BYTES } from "./service-supervision-contract"
|
||||
|
||||
/** The process that holds FileShare.None also performs the atomic replacement.
|
||||
* Killing it releases the handle and prevents a late Node callback from writing
|
||||
* outside exclusion. Main/Recovery use the same admission.lock for Start. The
|
||||
* caller verifies the private directory and manifest before this transaction. */
|
||||
export async function mutateWindowsSupervisionFile(file: string,
|
||||
update: (bytes: string | null) => Promise<string>): Promise<void> {
|
||||
if (process.platform !== "win32" || !/^[A-Za-z]:\\/.test(file) || /[\x00-\x1f]/.test(file)
|
||||
|| path.win32.normalize(file) !== file || file.slice(2).includes(":")) throw new SupervisionError("unsafe-storage")
|
||||
const script = `$ErrorActionPreference='Stop'; $h=$null; $tmp=$null
|
||||
$file='${file.replace(/'/g, "''")}'; $gate=[IO.Path]::Combine([IO.Path]::GetDirectoryName($file),'admission.lock')
|
||||
try { $h=[IO.File]::Open($gate,'OpenOrCreate','ReadWrite','None') }
|
||||
catch [IO.IOException] { [Console]::Out.WriteLine('BUSY'); exit 0 }
|
||||
catch { exit 1 }
|
||||
try {
|
||||
$before=$null
|
||||
if ([IO.File]::Exists($file)) {
|
||||
if ((Get-Item -LiteralPath $file).Length -gt ${SUPERVISION_MANIFEST_BYTES}) { throw 'bounded' }
|
||||
$before=[IO.File]::ReadAllBytes($file)
|
||||
if ($before.Length -gt ${SUPERVISION_MANIFEST_BYTES}) { throw 'bounded' }
|
||||
}
|
||||
[Console]::Out.WriteLine('READY:'+$(if ($null -eq $before) { '-' } else { [Convert]::ToBase64String($before) }))
|
||||
[Console]::Out.Flush()
|
||||
$line=[Console]::In.ReadLine()
|
||||
if ($null -eq $line -or $line.Length -gt 90000) { throw 'cancelled' }
|
||||
$bytes=[Convert]::FromBase64String($line)
|
||||
if ($bytes.Length -gt ${SUPERVISION_MANIFEST_BYTES}) { throw 'bounded' }
|
||||
$tmp=[IO.Path]::Combine([IO.Path]::GetDirectoryName($file),[guid]::NewGuid().ToString()+'.tmp')
|
||||
$out=[IO.File]::Open($tmp,'CreateNew','Write','None')
|
||||
try { $out.Write($bytes,0,$bytes.Length); $out.Flush($true) } finally { $out.Dispose() }
|
||||
if ($null -eq $before) { [IO.File]::Move($tmp,$file) } else { [IO.File]::Replace($tmp,$file,[NullString]::Value) }
|
||||
[Console]::Out.WriteLine('COMMITTED')
|
||||
} catch { exit 1 }
|
||||
finally { if ($tmp -and [IO.File]::Exists($tmp)) { [IO.File]::Delete($tmp) }; $h.Dispose() }`
|
||||
const executable = path.join(process.env.SystemRoot || "C:/Windows", "System32/WindowsPowerShell/v1.0/powershell.exe")
|
||||
const child = spawn(executable, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand",
|
||||
Buffer.from(script, "utf16le").toString("base64")], { windowsHide: true, stdio: ["pipe", "pipe", "ignore"] })
|
||||
const exited = new Promise<number | null>(resolve => {
|
||||
child.once("exit", resolve); child.once("error", () => resolve(null))
|
||||
})
|
||||
// A broken pipe is a failed transaction, never an unhandled process error.
|
||||
child.stdin.on("error", () => {})
|
||||
let output = "", committed = false
|
||||
const timer = setTimeout(() => child.kill(), 10_000)
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
child.once("error", () => reject(new SupervisionError("unsafe-storage")))
|
||||
child.once("exit", () => { if (!committed) reject(new SupervisionError("busy")) })
|
||||
let requested = false
|
||||
child.stdout.on("data", bytes => {
|
||||
output += bytes.toString("utf8")
|
||||
if (output.length > 100_000) { reject(new SupervisionError("invalid")); child.kill(); return }
|
||||
const lines = output.split(/\r?\n/)
|
||||
if (!requested && lines.length > 1) {
|
||||
requested = true
|
||||
if (!lines[0].startsWith("READY:")) { reject(new SupervisionError("busy")); return }
|
||||
const data = lines[0].slice(6)
|
||||
void update(data === "-" ? null : Buffer.from(data, "base64").toString("utf8")).then(next => {
|
||||
if (child.exitCode !== null || child.killed) return
|
||||
if (Buffer.byteLength(next) > SUPERVISION_MANIFEST_BYTES) throw new SupervisionError("invalid")
|
||||
child.stdin.end(Buffer.from(next).toString("base64") + "\n")
|
||||
}).catch(reject)
|
||||
}
|
||||
if (lines.includes("COMMITTED")) { committed = true; resolve() }
|
||||
})
|
||||
})
|
||||
if (await exited !== 0) throw new SupervisionError("busy")
|
||||
} finally {
|
||||
child.stdin.end()
|
||||
if (!committed) child.kill()
|
||||
await exited
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import type { WindowsSupervisionRuntime } from "./service-supervision-windows"
|
||||
|
||||
/** Observation stays available when the durable action script is missing. The
|
||||
* exact owned OS definition, rather than a computed now+60s estimate, supplies
|
||||
* the independent retry date. This reader has no task mutation commands. */
|
||||
export async function readWindowsRecovery(runtime: WindowsSupervisionRuntime, directory: string, key: string): Promise<string | undefined> {
|
||||
const quote = (text: string) => `'${text.replaceAll("'", "''")}'`
|
||||
const name = `CodeNomad-OpenCode-${key}-Recovery`
|
||||
const args = `-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File "${runtime.script}" -Directory "${directory}" -Key ${key} -Mode Recovery`
|
||||
const script = `$ErrorActionPreference='Stop'; $ProgressPreference='SilentlyContinue';
|
||||
Import-Module (Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/Modules/ScheduledTasks/ScheduledTasks.psd1');
|
||||
$t=Get-ScheduledTask -TaskPath '\\' -TaskName ${quote(name)} -ErrorAction Stop;
|
||||
if($t.Description -ne ${quote(`CodeNomad OpenCode supervision v1 key=${key} root=${directory}`)} -or
|
||||
$t.Actions.Count -ne 1 -or $t.Actions[0].Execute -ne ${quote(runtime.powershell)} -or
|
||||
$t.Actions[0].Arguments -ne ${quote(args)} -or $t.Actions[0].WorkingDirectory -ne ${quote(directory)} -or
|
||||
$t.State -eq 'Disabled'){throw 'ownership'};
|
||||
$sid=[string]$t.Principal.UserId; if($sid -notmatch '^S-1-'){
|
||||
$sid=[Security.Principal.NTAccount]::new($sid).Translate([Security.Principal.SecurityIdentifier]).Value};
|
||||
if($sid -ne [Security.Principal.WindowsIdentity]::GetCurrent().User.Value){throw 'principal'};
|
||||
$text=Export-ScheduledTask -InputObject $t; if($text.Length -gt 32768){throw 'bounded'};
|
||||
$x=[xml]$text; if($x.Task.Settings.MultipleInstancesPolicy -ne 'Queue' -or
|
||||
$x.Task.Settings.ExecutionTimeLimit -ne 'PT10S' -or $x.Task.Settings.Enabled -eq 'false' -or
|
||||
$x.Task.Triggers.TimeTrigger.Enabled -eq 'false' -or $x.Task.Triggers.TimeTrigger.EndBoundary -or
|
||||
$x.Task.Triggers.TimeTrigger.Repetition.Interval -ne 'PT1M' -or
|
||||
$x.Task.Triggers.TimeTrigger.Repetition.Duration){throw 'policy'};
|
||||
$i=Get-ScheduledTaskInfo -InputObject $t; if($i.NextRunTime -le (Get-Date)){throw 'date'};
|
||||
$i.NextRunTime.ToUniversalTime().ToString('o')`
|
||||
const text = await new Promise<string>((resolve, reject) => execFile(runtime.powershell,
|
||||
["-NoProfile", "-NonInteractive", "-EncodedCommand", Buffer.from(script, "utf16le").toString("base64")],
|
||||
{ windowsHide: true, timeout: 5000, maxBuffer: 4096, env: { SystemRoot: runtime.environment.SystemRoot,
|
||||
WINDIR: runtime.environment.WINDIR } },
|
||||
(error, stdout) => error ? reject(new Error("supervision-recovery-unknown")) : resolve(stdout.trim())))
|
||||
return /^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d\.\d+Z$/.test(text) ? new Date(text).toISOString() : undefined
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
param([Parameter(Mandatory=$true)][string]$Directory,
|
||||
[Parameter(Mandatory=$true)][string]$Key,
|
||||
[ValidateSet('install','remove','observe','Main','Recovery')][string]$Mode,
|
||||
[long]$Revision=-1)
|
||||
$ErrorActionPreference='Stop'
|
||||
$ProgressPreference='SilentlyContinue'
|
||||
# Only fixed, owned paths are passed by the adapter. The directory has a private
|
||||
# DACL; user/profile environment and command strings never enter this script.
|
||||
if ($Key -notmatch '^[a-f0-9]{64}$' -or [IO.Path]::GetFileName($Directory) -ne $Key) { exit 1 }
|
||||
function ReadJson($file,$limit,$json=$null) {
|
||||
$item=Get-Item -LiteralPath $file -Force
|
||||
if ($item.PSIsContainer -or ($item.Attributes -band 1024) -ne 0 -or $item.Length -gt $limit) { throw 'invalid-file' }
|
||||
$bytes=[IO.File]::ReadAllBytes($file)
|
||||
if ($bytes.Length -gt $limit) { throw 'bounded' }
|
||||
$text=[Text.Encoding]::UTF8.GetString($bytes)
|
||||
if ($json) { $json.Value=$text }
|
||||
return $text|ConvertFrom-Json
|
||||
}
|
||||
function Manifest {
|
||||
$m=ReadJson (Join-Path $Directory 'manifest.json') 65536
|
||||
$owner=if($m.namespace.platform -eq 'wsl'){$d.wsl.owner}else{$m.namespace.user}
|
||||
if ($m.v -ne 1 -or $m.key -ne $Key -or $m.namespace.platform -notin @('win32','wsl') -or
|
||||
$owner -ne [Security.Principal.WindowsIdentity]::GetCurrent().User.Value -or
|
||||
($m.namespace.platform -eq 'wsl' -and (!$d.wsl -or $m.namespace.distro -ne $d.wsl.distro -or $m.namespace.user -ne $d.wsl.user)) -or
|
||||
$m.revision -isnot [long] -and $m.revision -isnot [int] -or $m.revision -lt 0 -or
|
||||
$m.desired -notin @('unset','on','off')) { throw 'manifest-identity' }
|
||||
return $m
|
||||
}
|
||||
function Gate { return [IO.File]::Open((Join-Path $Directory 'admission.lock'),'OpenOrCreate','ReadWrite','None') }
|
||||
function Atomic($name,$value) {
|
||||
$file=Join-Path $Directory $name; $tmp=Join-Path $Directory ([guid]::NewGuid().ToString()+'.tmp')
|
||||
try {
|
||||
$bytes=[Text.Encoding]::UTF8.GetBytes(($value|ConvertTo-Json -Depth 8 -Compress))
|
||||
if ($bytes.Length -gt 32768) { throw 'bounded-status' }
|
||||
[IO.File]::WriteAllBytes($tmp,$bytes)
|
||||
if ([IO.File]::Exists($file)) { [IO.File]::Replace($tmp,$file,[NullString]::Value) } else { [IO.File]::Move($tmp,$file) }
|
||||
} finally { if ([IO.File]::Exists($tmp)) { [IO.File]::Delete($tmp) } }
|
||||
}
|
||||
$d=ReadJson (Join-Path $Directory 'windows.json') 32768
|
||||
if ($d.v -ne 1 -or $d.key -ne $Key -or $d.directory -ne $Directory -or $d.script -ne $PSCommandPath) { exit 1 }
|
||||
$main="CodeNomad-OpenCode-$Key-Main"; $recovery="CodeNomad-OpenCode-$Key-Recovery"
|
||||
$marker="CodeNomad OpenCode supervision v1 key=$Key root=$Directory"
|
||||
function Arguments($kind) {
|
||||
return "-NoLogo -NoProfile -NonInteractive -ExecutionPolicy Bypass -File `"$($d.script)`" -Directory `"$Directory`" -Key $Key -Mode $kind"
|
||||
}
|
||||
Import-Module (Join-Path $env:SystemRoot 'System32/WindowsPowerShell/v1.0/Modules/ScheduledTasks/ScheduledTasks.psd1')
|
||||
function Subscription {
|
||||
return "<QueryList><Query Id=`"0`" Path=`"Microsoft-Windows-TaskScheduler/Operational`"><Select Path=`"Microsoft-Windows-TaskScheduler/Operational`">*[System[(EventID=201 or EventID=203)] and EventData[Data[@Name='TaskName']='\$main' and Data[@Name='ResultCode']!='0']]</Select></Query></QueryList>"
|
||||
}
|
||||
function Owned($name,$kind) {
|
||||
$t=Get-ScheduledTask -TaskPath '\' -TaskName $name -ErrorAction SilentlyContinue
|
||||
$sid=$null
|
||||
if ($t) {
|
||||
$sid=[string]$t.Principal.UserId
|
||||
if ($sid -notmatch '^S-1-') { $sid=[Security.Principal.NTAccount]::new($sid).Translate([Security.Principal.SecurityIdentifier]).Value }
|
||||
}
|
||||
if ($t -and ($t.Description -ne $marker -or $t.Actions.Count -ne 1 -or
|
||||
$t.Actions[0].Execute -ne $d.powershell -or $t.Actions[0].Arguments -ne (Arguments $kind) -or
|
||||
$t.Actions[0].WorkingDirectory -ne $Directory -or $sid -ne [Security.Principal.WindowsIdentity]::GetCurrent().User.Value)) { throw 'foreign-task' }
|
||||
if ($t) {
|
||||
$text=Export-ScheduledTask -TaskPath '\' -TaskName $name
|
||||
if ($text.Length -gt 32768) { throw 'bounded-task' }
|
||||
$xml=[xml]$text; $s=$xml.Task.Settings; $triggers=$xml.Task.Triggers
|
||||
$logonUser=[string]$triggers.LogonTrigger.UserId
|
||||
if ($logonUser -notmatch '^S-1-') { $logonUser=[Security.Principal.NTAccount]::new($logonUser).Translate([Security.Principal.SecurityIdentifier]).Value }
|
||||
$policy=if($kind -eq 'Main'){'IgnoreNew'}else{'Queue'}
|
||||
$limit=if($kind -eq 'Main'){'PT0S'}else{'PT10S'}
|
||||
if ($s.MultipleInstancesPolicy -ne $policy -or $s.ExecutionTimeLimit -ne $limit -or
|
||||
$s.DisallowStartIfOnBatteries -ne 'false' -or $s.StopIfGoingOnBatteries -ne 'false' -or
|
||||
$xml.Task.Principals.Principal.LogonType -ne 'InteractiveToken' -or
|
||||
[string]$xml.Task.Principals.Principal.RunLevel -notin @('','LeastPrivilege') -or
|
||||
[string]$triggers.LogonTrigger.Enabled -notin @('','true') -or $logonUser -ne $sid) { throw 'task-policy-mismatch' }
|
||||
foreach ($trigger in $triggers.ChildNodes) {
|
||||
if ($trigger.EndBoundary -or [string]$trigger.Delay -notin @('','PT0S') -or
|
||||
[string]$trigger.RandomDelay -notin @('','PT0S')) { throw 'task-trigger-boundary' }
|
||||
}
|
||||
if ($kind -eq 'Main') {
|
||||
if ($triggers.ChildNodes.Count -ne 1) { throw 'task-triggers-mismatch' }
|
||||
} elseif ($triggers.ChildNodes.Count -ne 3 -or [string]$triggers.EventTrigger.Enabled -notin @('','true') -or
|
||||
$triggers.EventTrigger.Subscription -ne (Subscription) -or [string]$triggers.TimeTrigger.Enabled -notin @('','true') -or
|
||||
$triggers.TimeTrigger.Repetition.Interval -ne 'PT1M' -or $triggers.TimeTrigger.Repetition.Duration) { throw 'task-triggers-mismatch' }
|
||||
}
|
||||
return $t
|
||||
}
|
||||
function Expected($desired) {
|
||||
$m=Manifest
|
||||
if ($m.revision -ne $Revision -or $m.desired -ne $desired) { throw 'revision-conflict' }
|
||||
return $m
|
||||
}
|
||||
function Tasks {
|
||||
$a=Owned $main 'Main'; $b=Owned $recovery 'Recovery'
|
||||
return @{main=if($a){[string]$a.State}else{'absent'};recovery=if($b){[string]$b.State}else{'absent'}}
|
||||
}
|
||||
try {
|
||||
if ($Mode -eq 'observe') { Tasks|ConvertTo-Json -Compress; exit 0 }
|
||||
if ($Mode -eq 'install') {
|
||||
$gate=Gate
|
||||
try {
|
||||
$m=Expected 'on'; $a=Owned $main 'Main'; $b=Owned $recovery 'Recovery'
|
||||
if (!(Get-WinEvent -ListLog 'Microsoft-Windows-TaskScheduler/Operational').IsEnabled) { throw 'event-log-disabled' }
|
||||
$sid=[Security.Principal.WindowsIdentity]::GetCurrent().User.Value
|
||||
$principal=New-ScheduledTaskPrincipal -UserId $sid -LogonType Interactive -RunLevel Limited
|
||||
if (!$a) {
|
||||
$settings=New-ScheduledTaskSettingsSet -ExecutionTimeLimit ([timespan]::Zero) -MultipleInstances IgnoreNew -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
Register-ScheduledTask -TaskPath '\' -TaskName $main -Action (New-ScheduledTaskAction -Execute $d.powershell -Argument (Arguments 'Main') -WorkingDirectory $Directory) -Trigger (New-ScheduledTaskTrigger -AtLogOn -User $sid) -Principal $principal -Settings $settings -Description $marker|Out-Null
|
||||
}
|
||||
if (!$b) {
|
||||
$subscription=Subscription
|
||||
$event=New-CimInstance -CimClass (Get-CimClass -Namespace Root/Microsoft/Windows/TaskScheduler -ClassName MSFT_TaskEventTrigger) -ClientOnly
|
||||
$event.Enabled=$true; $event.Subscription=$subscription
|
||||
$periodic=New-ScheduledTaskTrigger -Once -At ((Get-Date).AddMinutes(1)) -RepetitionInterval (New-TimeSpan -Minutes 1)
|
||||
$settings=New-ScheduledTaskSettingsSet -ExecutionTimeLimit (New-TimeSpan -Seconds 10) -MultipleInstances Queue -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
Register-ScheduledTask -TaskPath '\' -TaskName $recovery -Action (New-ScheduledTaskAction -Execute $d.powershell -Argument (Arguments 'Recovery') -WorkingDirectory $Directory) -Trigger @($event,(New-ScheduledTaskTrigger -AtLogOn -User $sid),$periodic) -Principal $principal -Settings $settings -Description $marker|Out-Null
|
||||
}
|
||||
foreach ($t in @((Owned $main 'Main'),(Owned $recovery 'Recovery'))) { Enable-ScheduledTask -InputObject $t|Out-Null }
|
||||
Start-ScheduledTask -InputObject (Owned $main 'Main')
|
||||
Tasks|ConvertTo-Json -Compress
|
||||
} finally { $gate.Dispose() }
|
||||
exit 0
|
||||
}
|
||||
if ($Mode -eq 'remove') {
|
||||
$gate=Gate
|
||||
try {
|
||||
$m=Expected 'off'; $a=Owned $main 'Main'; $b=Owned $recovery 'Recovery'
|
||||
foreach ($t in @($b,$a)) { if ($t) { Disable-ScheduledTask -InputObject $t|Out-Null } }
|
||||
} finally { $gate.Dispose() }
|
||||
# OFF is the guard's release request. Never End/Stop Main: its foreground
|
||||
# child must survive. A blocked release stays visible for an explicit retry.
|
||||
$deadline=[datetime]::UtcNow.AddSeconds(12)
|
||||
do {
|
||||
$null=Expected 'off'; $a=Owned $main 'Main'
|
||||
if (!$a -or $a.State -notin @('Running','Queued')) { break }
|
||||
Start-Sleep -Milliseconds 200
|
||||
} while ([datetime]::UtcNow -lt $deadline)
|
||||
if ($a -and $a.State -in @('Running','Queued')) { throw 'release-pending' }
|
||||
$gate=Gate
|
||||
try {
|
||||
$null=Expected 'off'
|
||||
foreach ($entry in @(@($recovery,'Recovery'),@($main,'Main'))) {
|
||||
$t=Owned $entry[0] $entry[1]
|
||||
if ($t) {
|
||||
if ($entry[1] -eq 'Recovery') { Stop-ScheduledTask -InputObject $t }
|
||||
Unregister-ScheduledTask -InputObject $t -Confirm:$false
|
||||
}
|
||||
}
|
||||
Tasks|ConvertTo-Json -Compress
|
||||
} finally { $gate.Dispose() }
|
||||
exit 0
|
||||
}
|
||||
if ($Mode -eq 'Recovery') {
|
||||
$m=Manifest; if ($m.desired -ne 'on') { exit 0 }; $rev=$m.revision
|
||||
$a=Owned $main 'Main'; $b=Owned $recovery 'Recovery'
|
||||
if (!$a -or !$b -or $a.State -in @('Running','Queued')) { exit 0 }
|
||||
$last=Join-Path $Directory 'last-start.json'
|
||||
if ([IO.File]::Exists($last)) {
|
||||
$json=$null; $record=ReadJson $last 1024 ([ref]$json)
|
||||
if ($record.key -ne $Key) { throw 'cooldown-identity' }
|
||||
# Validate the wire value: ConvertFrom-Json can return DateTime and even
|
||||
# normalize invalid offsets. Only the two-field proof written below is accepted.
|
||||
$proof=[regex]::Match($json,'\A\s*\{\s*(?:"key"\s*:\s*"[a-f0-9]{64}"\s*,\s*"at"\s*:\s*"(?<at>[^"\\]*)"|"at"\s*:\s*"(?<at>[^"\\]*)"\s*,\s*"key"\s*:\s*"[a-f0-9]{64}")\s*\}\s*\z')
|
||||
$at=[datetimeoffset]::MinValue
|
||||
if (!$proof.Success -or $proof.Groups['at'].Value -cnotmatch '\A[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\.[0-9]{1,7})?(?:Z|[+-](?:(?:0[0-9]|1[0-3]):[0-5][0-9]|14:00))\z' -or
|
||||
![datetimeoffset]::TryParse($proof.Groups['at'].Value,[Globalization.CultureInfo]::InvariantCulture,[Globalization.DateTimeStyles]::None,[ref]$at)) { throw 'cooldown-time' }
|
||||
if (([datetimeoffset]::UtcNow-$at.ToUniversalTime()).TotalSeconds -lt 5) { exit 0 }
|
||||
}
|
||||
# One bounded attempt; the independent OS tick retains the retry obligation.
|
||||
$gate=$null
|
||||
try {
|
||||
try { $gate=Gate } catch [IO.IOException] { exit 0 }
|
||||
$m=Manifest; if ($m.desired -ne 'on' -or $m.revision -ne $rev) { exit 0 }
|
||||
$a=Owned $main 'Main'; $b=Owned $recovery 'Recovery'
|
||||
if (!$a -or !$b -or $a.State -in @('Running','Queued')) { exit 0 }
|
||||
foreach ($file in @($d.script,$d.node,$d.probe)) { if (![IO.File]::Exists($file)) { throw 'artifact-missing' } }
|
||||
Atomic 'last-start.json' @{key=$Key;at=[datetime]::UtcNow.ToString('o')}
|
||||
Start-ScheduledTask -InputObject $a
|
||||
} finally { if ($gate) { $gate.Dispose() } }
|
||||
exit 0
|
||||
}
|
||||
} catch { @{state='failing';cause='windows-resource-operation';line=$_.InvocationInfo.ScriptLineNumber}|ConvertTo-Json -Compress; exit 1 }
|
||||
|
||||
# Main owns a foreground handle for the entire ON lifetime. CreateProcessW uses
|
||||
# only CREATE_UNICODE_ENVIRONMENT: no breakaway, detached flag or finally-kill.
|
||||
$guard=$null
|
||||
try { $guard=[IO.File]::Open((Join-Path $Directory 'guard.lock'),'OpenOrCreate','ReadWrite','None') } catch { exit 1 }
|
||||
Add-Type -TypeDefinition @'
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Runtime.InteropServices;
|
||||
public static class CodeNomadForeground {
|
||||
[StructLayout(LayoutKind.Sequential)] public struct Security { public int length; public IntPtr descriptor; public int inherit; }
|
||||
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)] public struct Startup {
|
||||
public int cb; public string reserved,desktop,title; public uint x,y,xSize,ySize,xCount,yCount,fill,flags;
|
||||
public short show,reserved2; public IntPtr reservedPtr,input,output,error;
|
||||
}
|
||||
[StructLayout(LayoutKind.Sequential)] public struct ProcessInfo { public IntPtr process,thread; public uint pid,tid; }
|
||||
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] public static extern bool CreateProcessW(string app,StringBuilder command,IntPtr pa,IntPtr ta,bool inherit,uint flags,IntPtr env,string cwd,ref Startup startup,out ProcessInfo process);
|
||||
[DllImport("kernel32.dll",CharSet=CharSet.Unicode,SetLastError=true)] public static extern IntPtr CreateFileW(string name,uint access,uint share,ref Security security,uint disposition,uint flags,IntPtr template);
|
||||
[DllImport("kernel32.dll",SetLastError=true)] public static extern bool CloseHandle(IntPtr handle);
|
||||
[DllImport("kernel32.dll",SetLastError=true)] public static extern uint WaitForSingleObject(IntPtr handle,uint milliseconds);
|
||||
}
|
||||
'@
|
||||
$child=[CodeNomadForeground+ProcessInfo]::new(); $out=[IntPtr]::Zero; $inputHandle=[IntPtr]::Zero; $memory=[IntPtr]::Zero
|
||||
$startIdentity='win32:'+([Diagnostics.Process]::GetCurrentProcess().StartTime.ToUniversalTime().Ticks.ToString())
|
||||
function Sample([bool]$checkTarget=$false) {
|
||||
$info=[Diagnostics.ProcessStartInfo]::new()
|
||||
$info.FileName=$d.node
|
||||
$info.Arguments="`"$($d.probe)`" `"$Directory`" $Key"
|
||||
if ($checkTarget) { $info.Arguments+=' --start' }
|
||||
$info.UseShellExecute=$false; $info.CreateNoWindow=$true; $info.RedirectStandardOutput=$true; $info.RedirectStandardError=$true
|
||||
$info.EnvironmentVariables.Clear()
|
||||
foreach ($p in $d.environment.PSObject.Properties) { $info.EnvironmentVariables[$p.Name]=[string]$p.Value }
|
||||
$p=[Diagnostics.Process]::Start($info)
|
||||
try {
|
||||
$stdout=$p.StandardOutput.ReadToEndAsync(); $stderr=$p.StandardError.ReadToEndAsync()
|
||||
if (!$p.WaitForExit($(if($checkTarget){12000}else{6000}))) { $p.Kill(); $p.WaitForExit(); throw 'probe-timeout' }
|
||||
if ($p.ExitCode -ne 0) { throw 'probe-failed' }
|
||||
$text=$stdout.GetAwaiter().GetResult()
|
||||
if ($text.Length -gt 32768) { throw 'probe-bounded' }
|
||||
return $text|ConvertFrom-Json
|
||||
} finally { $p.Dispose() }
|
||||
}
|
||||
function QuoteArgument([string]$value) {
|
||||
# Windows CommandLineToArgvW quoting, including embedded POSIX shell quotes.
|
||||
# wsl.exe also parses its leading switches directly from the raw command line.
|
||||
if ($value.Length -gt 0 -and $value -notmatch '[\s"]') { return $value }
|
||||
return '"'+[regex]::Replace([regex]::Replace($value,'(\\*)"','$1$1\"'),'(\\+)$','$1$1')+'"'
|
||||
}
|
||||
$statusText=''; $backoff=2; $next=[datetime]::MinValue; $lastAttempt=$null
|
||||
function Status($m,$health,$hosting,$cause,$retry) {
|
||||
$value=@{v=1;key=$Key;revision=$m.revision;guardPid=$PID;guardStart=$startIdentity;childWindowsPid=$child.pid;health=$health;mode=$hosting;cause=$cause;lastAttemptAt=$script:lastAttempt;retryAt=$retry}
|
||||
$text=$value|ConvertTo-Json -Depth 8 -Compress
|
||||
if ($text -ne $script:statusText) { Atomic 'windows-status.json' $value; $script:statusText=$text }
|
||||
}
|
||||
try {
|
||||
while ($true) {
|
||||
$m=Manifest; if ($m.desired -ne 'on') { break }
|
||||
$health=@{state='unknown'}; $cause=$null; $hosting='observed'
|
||||
if ($child.process -ne [IntPtr]::Zero) {
|
||||
$wait=[CodeNomadForeground]::WaitForSingleObject($child.process,0)
|
||||
if ($wait -eq 0) {
|
||||
foreach ($h in @($child.process,$child.thread)) { [void][CodeNomadForeground]::CloseHandle($h) }
|
||||
$child=[CodeNomadForeground+ProcessInfo]::new()
|
||||
$next=[datetime]::UtcNow.AddSeconds($backoff); $backoff=[math]::Min(30,$backoff*2)
|
||||
} elseif ($wait -ne 258) { throw 'child-state-unknown' } else { $hosting='hosted' }
|
||||
}
|
||||
$permitted=$false; $sample=$null
|
||||
try { $sample=Sample; if ($sample.revision -eq $m.revision) { $health=$sample.health; $permitted=$sample.permitted -eq $true } } catch { $cause='service-unknown' }
|
||||
if ($sample -and $sample.revision -eq $m.revision -and $sample.forwardingUnavailable -eq $true) { $cause='wsl-forwarding-unavailable' }
|
||||
if (!$permitted) { $cause='native-service-disabled-or-unknown' }
|
||||
if ($health.state -eq 'healthy') { $backoff=2; $next=[datetime]::MinValue }
|
||||
# A live hosted child, including slow boot/500/unknown health, is retained.
|
||||
if ($permitted -and $health.state -eq 'absent' -and $child.process -eq [IntPtr]::Zero -and [datetime]::UtcNow -ge $next) {
|
||||
$gate=$null
|
||||
try {
|
||||
$gate=Gate; $fresh=Manifest
|
||||
if ($fresh.desired -ne 'on' -or $fresh.revision -ne $m.revision) { continue }
|
||||
$again=Sample $true
|
||||
if ($again.revision -ne $fresh.revision -or $again.health.state -ne 'absent' -or $again.permitted -ne $true) { continue }
|
||||
$lastAttempt=[datetime]::UtcNow.ToString('o')
|
||||
if ($again.targetReady -ne $true) { throw 'binary-unavailable' }
|
||||
$binary=if($d.wsl){[string]$d.wsl.executable}else{[string]$fresh.target.binary}
|
||||
if ($binary -notmatch '^[A-Za-z]:\\' -or $binary -match '[\x00-\x1f"]' -or ![IO.File]::Exists($binary)) { throw 'binary-unavailable' }
|
||||
if ($out -eq [IntPtr]::Zero) {
|
||||
$security=[CodeNomadForeground+Security]::new(); $security.length=[Runtime.InteropServices.Marshal]::SizeOf($security); $security.inherit=1
|
||||
# Independent NUL handles avoid pipes tied to the guard or unbounded
|
||||
# user output logs. Native OpenCode owns its own log files.
|
||||
$out=[CodeNomadForeground]::CreateFileW('NUL',1073741824,3,[ref]$security,3,0,[IntPtr]::Zero)
|
||||
$inputHandle=[CodeNomadForeground]::CreateFileW('NUL',2147483648,3,[ref]$security,3,0,[IntPtr]::Zero)
|
||||
if ($out -eq [IntPtr](-1) -or $inputHandle -eq [IntPtr](-1)) { throw 'stdio-failed' }
|
||||
$block=(@($d.environment.PSObject.Properties|Sort-Object Name|ForEach-Object { "$($_.Name)=$($_.Value)" }) -join "`0")+"`0`0"
|
||||
$memory=[Runtime.InteropServices.Marshal]::StringToHGlobalUni($block)
|
||||
}
|
||||
$startup=[CodeNomadForeground+Startup]::new(); $startup.cb=[Runtime.InteropServices.Marshal]::SizeOf($startup)
|
||||
$startup.flags=0x100; $startup.input=$inputHandle; $startup.output=$out; $startup.error=$out
|
||||
$argv=if($d.wsl){@($again.launch)}else{@('serve','--service')}
|
||||
if ($d.wsl -and ($argv.Count -lt 12 -or $argv[0] -ne '--distribution' -or $argv[1] -ne $d.wsl.distro -or $argv[2] -ne '--user' -or $argv[3] -ne $d.wsl.account)) { throw 'wsl-launch-identity' }
|
||||
$command=if($d.wsl){[Text.StringBuilder]::new((( @($binary)+$argv | ForEach-Object { QuoteArgument ([string]$_) }) -join ' '))}else{[Text.StringBuilder]::new("`"$binary`" serve --service")}
|
||||
if (![CodeNomadForeground]::CreateProcessW($binary,$command,[IntPtr]::Zero,[IntPtr]::Zero,$true,0x400,$memory,$Directory,[ref]$startup,[ref]$child)) { throw 'spawn-failed' }
|
||||
$hosting='hosted'
|
||||
} catch { $cause='start-failed'; $next=[datetime]::UtcNow.AddSeconds($backoff); $backoff=[math]::Min(30,$backoff*2) }
|
||||
finally { if ($gate) { $gate.Dispose() } }
|
||||
}
|
||||
Status $m $health $hosting $cause $(if($next -gt [datetime]::UtcNow){$next.ToString('o')}else{$null})
|
||||
Start-Sleep -Milliseconds $(if($d.wsl){1000}else{300})
|
||||
}
|
||||
} catch {
|
||||
try { Atomic 'windows-status.json' @{v=1;key=$Key;revision=(Manifest).revision;guardPid=$PID;guardStart=$startIdentity;cause='guard-failed';line=$_.InvocationInfo.ScriptLineNumber} } catch {}
|
||||
exit 1
|
||||
}
|
||||
finally {
|
||||
if ($d.wsl -and $child.process -ne [IntPtr]::Zero) {
|
||||
# The Linux foreground bridge observes OFF and releases its managed child.
|
||||
# Wait for that acknowledgement; never terminate either side of the bridge.
|
||||
if ([CodeNomadForeground]::WaitForSingleObject($child.process,8000) -ne 0) { exit 1 }
|
||||
}
|
||||
foreach ($h in @($child.process,$child.thread,$out,$inputHandle)) { if ($h -ne [IntPtr]::Zero -and $h -ne [IntPtr](-1)) { [void][CodeNomadForeground]::CloseHandle($h) } }
|
||||
if ($memory -ne [IntPtr]::Zero) { [Runtime.InteropServices.Marshal]::FreeHGlobal($memory) }
|
||||
$guard.Dispose()
|
||||
}
|
||||
181
packages/server/src/host-lifetime/service-supervision-windows.ts
Normal file
181
packages/server/src/host-lifetime/service-supervision-windows.ts
Normal file
|
|
@ -0,0 +1,181 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import { lstat, open, readFile, link, rm } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { z } from "zod"
|
||||
import { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import { SupervisionError, type SupervisionObservation } from "./service-supervision-contract"
|
||||
import { readSupervisionHealth, supervisionReleasePreserved } from "./service-supervision-health"
|
||||
import { lookupProcess } from "./process-identity"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
import { readWindowsRecovery } from "./service-supervision-windows-recovery"
|
||||
import { supervisionArtifactPolicy } from "./service-supervision-runtime"
|
||||
import { wslRuntimeSchema, readWslSupervisionSample } from "./service-supervision-wsl"
|
||||
|
||||
const absolute = z.string().max(4096).refine(value => /^[A-Za-z]:\\/.test(value)
|
||||
&& path.win32.normalize(value) === value && !/[\x00-\x1f"]/.test(value) && !value.slice(2).includes(":"))
|
||||
// The execution-host resolver supplies only these native paths. Never persist a
|
||||
// profile's environment, PATH, OPENCODE_CONFIG_CONTENT or credentials.
|
||||
const environmentSchema = z.object({
|
||||
SystemRoot: absolute, WINDIR: absolute, COMSPEC: absolute, HOME: absolute, USERPROFILE: absolute,
|
||||
APPDATA: absolute, LOCALAPPDATA: absolute, XDG_CONFIG_HOME: absolute, XDG_STATE_HOME: absolute,
|
||||
XDG_DATA_HOME: absolute, XDG_CACHE_HOME: absolute, TEMP: absolute, TMP: absolute,
|
||||
OPENCODE_TEST_HOME: absolute.optional(),
|
||||
}).strict()
|
||||
const descriptorSchema = z.object({ v: z.literal(1), key: z.string().regex(/^[a-f0-9]{64}$/), directory: absolute,
|
||||
powershell: absolute, script: absolute, node: absolute, probe: absolute, environment: environmentSchema,
|
||||
wsl: wslRuntimeSchema.optional() }).strict()
|
||||
export type WindowsSupervisionRuntime = Pick<z.infer<typeof descriptorSchema>, "powershell" | "script" | "node" | "probe" | "environment" | "wsl">
|
||||
|
||||
export async function readWindowsSupervisionRuntime(store: ServiceSupervisionStore, policy: StoragePolicy = privateStorage): Promise<WindowsSupervisionRuntime | undefined> {
|
||||
const file = path.join(store.directory, "windows.json")
|
||||
try { await policy.verify(file, false) } catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined
|
||||
throw error
|
||||
}
|
||||
if ((await lstat(file)).size > 32768) throw new SupervisionError("invalid")
|
||||
const manifest = await store.read(), descriptor = descriptorSchema.parse(JSON.parse(await readFile(file, "utf8")))
|
||||
if (descriptor.key !== manifest.key || descriptor.directory !== store.directory) throw new SupervisionError("conflict")
|
||||
const { powershell, script, node, probe, environment, wsl } = descriptor
|
||||
return { powershell, script, node, probe, environment, ...(wsl ? { wsl } : {}) }
|
||||
}
|
||||
|
||||
/** Internal execution-host adapter. Its caller acquires the exact namespace and
|
||||
* durable, owned CodeNomad runtime artifacts, independently of Settings profiles.
|
||||
* Preference controls always go through the shared store before reconcile(). */
|
||||
export class WindowsServiceSupervision {
|
||||
constructor(private readonly store: ServiceSupervisionStore, private readonly runtime: WindowsSupervisionRuntime,
|
||||
private readonly policy: StoragePolicy = privateStorage, private readonly artifactPolicy: StoragePolicy = supervisionArtifactPolicy) {}
|
||||
|
||||
private async descriptor() {
|
||||
if (process.platform !== "win32") throw new SupervisionError("invalid")
|
||||
const manifest = await this.store.read()
|
||||
if (!["win32", "wsl"].includes(manifest.namespace.platform)) throw new SupervisionError("invalid")
|
||||
const descriptor = descriptorSchema.parse({ ...this.runtime, v: 1, key: manifest.key, directory: this.store.directory })
|
||||
if (Buffer.byteLength(JSON.stringify(descriptor)) > 32768) throw new SupervisionError("invalid")
|
||||
if (descriptor.wsl ? manifest.namespace.platform !== "wsl" || descriptor.wsl.distro !== manifest.namespace.distro
|
||||
|| descriptor.wsl.user !== manifest.namespace.user
|
||||
|| path.posix.join(descriptor.wsl.environment.XDG_CONFIG_HOME, "opencode") !== manifest.namespace.configDirectory
|
||||
|| path.posix.join(descriptor.wsl.environment.XDG_STATE_HOME, "opencode") !== manifest.namespace.stateDirectory
|
||||
: manifest.namespace.platform !== "win32" || path.win32.join(descriptor.environment.XDG_CONFIG_HOME, "opencode").toLowerCase() !== manifest.namespace.configDirectory.toLowerCase()
|
||||
|| path.win32.join(descriptor.environment.XDG_STATE_HOME, "opencode").toLowerCase() !== manifest.namespace.stateDirectory.toLowerCase())
|
||||
throw new SupervisionError("conflict")
|
||||
for (const file of [descriptor.powershell, descriptor.script, descriptor.node, descriptor.probe]) {
|
||||
const stat = await lstat(file)
|
||||
if (!stat.isFile() || stat.isSymbolicLink()) throw new SupervisionError("unsafe-storage")
|
||||
if (file !== descriptor.powershell) await this.artifactPolicy.verify(file, false)
|
||||
}
|
||||
return descriptor
|
||||
}
|
||||
|
||||
private async provision(): Promise<z.infer<typeof descriptorSchema>> {
|
||||
const descriptor = await this.descriptor(), file = path.join(this.store.directory, "windows.json")
|
||||
try {
|
||||
await this.policy.verify(file, false)
|
||||
const stat = await lstat(file)
|
||||
if (stat.size > 32768) throw new SupervisionError("invalid")
|
||||
const existing = descriptorSchema.parse(JSON.parse(await readFile(file, "utf8")))
|
||||
if (JSON.stringify(existing) !== JSON.stringify(descriptor)) throw new SupervisionError("conflict")
|
||||
return descriptor
|
||||
} catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error }
|
||||
// Publish a complete immutable descriptor without replacing a competing
|
||||
// writer. The temporary hard link is removed before the private-file check.
|
||||
const temporary = path.join(this.store.directory, `${randomUUID()}.tmp`)
|
||||
let exists = false
|
||||
try {
|
||||
const handle = await open(temporary, "wx", 0o600)
|
||||
try { await handle.writeFile(JSON.stringify(descriptor)); await handle.sync() } finally { await handle.close() }
|
||||
await this.policy.verify(temporary, false)
|
||||
await link(temporary, file).catch(error => { if (error.code !== "EEXIST") throw error; exists = true })
|
||||
} finally { await rm(temporary, { force: true }) }
|
||||
if (exists) return this.provision()
|
||||
await this.policy.verify(file, false)
|
||||
return descriptor
|
||||
}
|
||||
|
||||
/** Publishes inert private runtime metadata; installs no OS task or preference. */
|
||||
async prepareRuntime(): Promise<void> { await this.provision() }
|
||||
|
||||
private async run(mode: "install" | "remove" | "observe", revision: number) {
|
||||
const d = mode === "observe" ? await this.descriptor() : await this.provision()
|
||||
return new Promise<{ main: string; recovery: string }>((resolve, reject) => {
|
||||
execFile(d.powershell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", d.script,
|
||||
"-Directory", d.directory, "-Key", d.key, "-Mode", mode, "-Revision", String(revision)], {
|
||||
windowsHide: true, timeout: mode === "remove" ? 20_000 : 15_000, maxBuffer: 32768,
|
||||
env: { SystemRoot: d.environment.SystemRoot, WINDIR: d.environment.WINDIR,
|
||||
PSModulePath: path.join(d.environment.SystemRoot, "System32/WindowsPowerShell/v1.0/Modules") },
|
||||
}, (error, stdout) => {
|
||||
// Native stderr/exception text can contain private data. Only fixed
|
||||
// adapter failures cross this boundary; exact intent remains readable.
|
||||
if (error) {
|
||||
const failure = new SupervisionError("busy")
|
||||
try {
|
||||
Object.assign(failure, { cause: z.object({ state: z.literal("failing"), cause: z.literal("windows-resource-operation"),
|
||||
line: z.number().int().nonnegative() }).strict().parse(JSON.parse(stdout.trim())) })
|
||||
} catch { /* Preserve only the fixed error when native evidence is incomplete. */ }
|
||||
return reject(failure)
|
||||
}
|
||||
try { resolve(z.object({ main: z.string(), recovery: z.string() }).strict().parse(JSON.parse(stdout.trim()))) }
|
||||
catch { reject(new SupervisionError("invalid")) }
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async reconcile(expectedRevision: number): Promise<void> {
|
||||
const manifest = await this.store.read()
|
||||
if (manifest.revision !== expectedRevision) throw new SupervisionError("conflict")
|
||||
if (manifest.desired === "unset") return
|
||||
const releasing = manifest.desired === "off" && this.runtime.wsl
|
||||
? await readWslSupervisionSample(manifest, this.runtime.wsl) : undefined
|
||||
const tasks = await this.run(manifest.desired === "on" ? "install" : "remove", expectedRevision)
|
||||
if (manifest.desired === "on" ? [tasks.main, tasks.recovery].some(state => state === "absent" || state === "Disabled")
|
||||
: tasks.main !== "absent" || tasks.recovery !== "absent") throw new SupervisionError("busy")
|
||||
if (releasing && this.runtime.wsl) {
|
||||
const after = await readWslSupervisionSample(manifest, this.runtime.wsl)
|
||||
if (!supervisionReleasePreserved(releasing, after)) throw new SupervisionError("busy")
|
||||
}
|
||||
// Ack is separately serialized with OFF and Start; a changed intent wins.
|
||||
await this.store.acknowledge(expectedRevision)
|
||||
}
|
||||
|
||||
async observe(): Promise<SupervisionObservation> {
|
||||
const manifest = await this.store.read()
|
||||
const registration = path.join(manifest.namespace.stateDirectory, manifest.namespace.registrationName)
|
||||
const health = this.runtime.wsl ? await readWslSupervisionSample(manifest, this.runtime.wsl).then(value => value.health)
|
||||
.catch(() => ({ state: "unknown" as const })) : await readSupervisionHealth(registration)
|
||||
let artifactMissing = false
|
||||
for (const file of [this.runtime.script, this.runtime.node, this.runtime.probe]) {
|
||||
try { const stat = await lstat(file); if (!stat.isFile() || stat.isSymbolicLink()) artifactMissing = true }
|
||||
catch { artifactMissing = true }
|
||||
}
|
||||
let resources: SupervisionObservation["resources"] = "unknown", guard: SupervisionObservation["guard"] = "unknown"
|
||||
let detail: SupervisionObservation["detail"]
|
||||
try {
|
||||
const tasks = await this.run("observe", manifest.revision)
|
||||
resources = tasks.main === "absent" && tasks.recovery === "absent" ? "absent"
|
||||
: [tasks.main, tasks.recovery].every(state => !["absent", "Disabled"].includes(state)) ? "installed" : "unknown"
|
||||
guard = tasks.main !== "Running" ? "absent" : "unknown"
|
||||
if (tasks.main === "Running") {
|
||||
const file = path.join(this.store.directory, "windows-status.json")
|
||||
await this.policy.verify(file, false)
|
||||
if ((await lstat(file)).size > 32768) throw new Error()
|
||||
const status = z.object({ v: z.literal(1), key: z.literal(manifest.key), revision: z.literal(manifest.revision),
|
||||
guardPid: z.number().int().positive(), guardStart: z.string().regex(/^win32:\d+$/),
|
||||
cause: z.enum(["native-service-disabled-or-unknown", "start-failed", "guard-failed", "service-unknown", "wsl-forwarding-unavailable"]).nullable(),
|
||||
lastAttemptAt: z.string().datetime({ offset: true }).nullable().optional(),
|
||||
retryAt: z.string().datetime({ offset: true }).nullable().optional(),
|
||||
}).passthrough().parse(JSON.parse(await readFile(file, "utf8")))
|
||||
const process = await lookupProcess(status.guardPid)
|
||||
if (process.state === "live" && process.startIdentity === status.guardStart) {
|
||||
guard = "active"
|
||||
detail = { cause: status.cause, ...(status.lastAttemptAt ? { lastAttemptAt: status.lastAttemptAt } : {}),
|
||||
...(status.retryAt ? { retryAt: status.retryAt } : {}) }
|
||||
}
|
||||
}
|
||||
} catch { /* exact owned resource evidence is required */ }
|
||||
const recoveryNextTickAt = manifest.desired === "on" && (guard !== "active" || artifactMissing)
|
||||
? await readWindowsRecovery(this.runtime, this.store.directory, manifest.key).catch(() => undefined) : undefined
|
||||
return { revision: manifest.revision, resources, guard, health, ...(detail ? { detail } : {}), artifactMissing,
|
||||
...(recoveryNextTickAt ? { recoveryNextTickAt } : {}) }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { spawn } from "node:child_process"
|
||||
import path from "node:path"
|
||||
import { SupervisionError, SUPERVISION_MANIFEST_BYTES } from "./service-supervision-contract"
|
||||
import { wslRuntimeSchema, type WslSupervisionRuntime } from "./service-supervision-wsl"
|
||||
|
||||
/** The Linux lock holder also publishes. Its death closes publication authority:
|
||||
* a late Windows callback has only a broken pipe, never filesystem write access.
|
||||
* Fork admission and every manifest writer share this descriptor lock. */
|
||||
export async function mutateWslSupervisionFile(runtime: WslSupervisionRuntime,
|
||||
update: (bytes: string | null) => Promise<string>, launch: typeof spawn = spawn): Promise<void> {
|
||||
const d = wslRuntimeSchema.parse(runtime), directory = path.posix.dirname(d.manifest)
|
||||
const script = `set -eu; umask 077; file=$1; lock=$2; user=$3; tmp=
|
||||
[ "$(/usr/bin/id -u)" = "$user" ] || exit 1
|
||||
exec 9>>"$lock"; /usr/bin/flock -xn 9 || exit 1
|
||||
trap 'test -z "$tmp" || /bin/rm -f -- "$tmp"' EXIT
|
||||
if [ -e "$file" ]; then
|
||||
[ -f "$file" ] && [ ! -L "$file" ] && [ "$(/usr/bin/wc -c <"$file")" -le ${SUPERVISION_MANIFEST_BYTES} ] || exit 1
|
||||
printf 'READY:'; /usr/bin/base64 -w0 -- "$file"; printf '\\n'
|
||||
else printf 'READY:-\\n'; fi
|
||||
IFS= read -r next || exit 1
|
||||
[ "\${#next}" -le 90000 ] || exit 1
|
||||
case "$next" in *[!A-Za-z0-9+/=]*) exit 1;; esac
|
||||
tmp=$(/usr/bin/mktemp "$(/usr/bin/dirname -- "$file")/.manifest-XXXXXXXX")
|
||||
printf '%s' "$next" | /usr/bin/base64 -d >"$tmp"
|
||||
[ "$(/usr/bin/wc -c <"$tmp")" -le ${SUPERVISION_MANIFEST_BYTES} ] || exit 1
|
||||
/usr/bin/sync -f "$tmp"; /bin/mv -fT -- "$tmp" "$file"; tmp=
|
||||
/usr/bin/sync -f "$(/usr/bin/dirname -- "$file")"
|
||||
printf 'COMMITTED\\n'`
|
||||
const child = launch(d.executable, ["--distribution", d.distro, "--user", d.account, "--exec", "/bin/sh", "-c",
|
||||
script, "codenomad-mutation", d.manifest, path.posix.join(directory, "linux-admission.lock"), d.user], {
|
||||
windowsHide: true, stdio: ["pipe", "pipe", "ignore"],
|
||||
env: { SystemRoot: process.env.SystemRoot, WINDIR: process.env.SystemRoot, USERPROFILE: process.env.USERPROFILE,
|
||||
LOCALAPPDATA: process.env.LOCALAPPDATA },
|
||||
})
|
||||
const exit = new Promise<number | null>(resolve => { child.once("exit", resolve); child.once("error", () => resolve(null)) })
|
||||
child.stdin!.on("error", () => {})
|
||||
let output = "", requested = false, committed = false
|
||||
const timer = setTimeout(() => child.kill(), 10_000)
|
||||
try {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
child.once("error", () => reject(new SupervisionError("busy")))
|
||||
child.once("exit", () => { if (!committed) reject(new SupervisionError("busy")) })
|
||||
child.stdout!.on("data", bytes => {
|
||||
output += bytes.toString("utf8")
|
||||
if (output.length > 100_000) { reject(new SupervisionError("invalid")); child.kill(); return }
|
||||
const lines = output.split(/\r?\n/)
|
||||
if (!requested && lines.length > 1) {
|
||||
requested = true
|
||||
if (!lines[0]!.startsWith("READY:")) { reject(new SupervisionError("invalid")); return }
|
||||
const data = lines[0]!.slice(6)
|
||||
void update(data === "-" ? null : Buffer.from(data, "base64").toString("utf8")).then(next => {
|
||||
if (child.exitCode !== null || child.killed) return
|
||||
if (Buffer.byteLength(next) > SUPERVISION_MANIFEST_BYTES) throw new SupervisionError("invalid")
|
||||
child.stdin!.end(Buffer.from(next).toString("base64") + "\n")
|
||||
}).catch(reject)
|
||||
}
|
||||
if (lines.includes("COMMITTED")) { committed = true; resolve() }
|
||||
})
|
||||
})
|
||||
if (await exit !== 0) throw new SupervisionError("busy")
|
||||
} finally {
|
||||
child.stdin!.end()
|
||||
if (!committed) child.kill()
|
||||
await exit
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import { lstat, realpath } from "node:fs/promises"
|
||||
import path from "node:path"
|
||||
import os from "node:os"
|
||||
import { parseWslUncPath } from "../workspaces/spawn"
|
||||
import { readNativeServiceChannel } from "../workspaces/native-service-registration"
|
||||
import { runtimeIdentity } from "../opencode/compatibility/runtime"
|
||||
import { readNativeScheduleIncarnation } from "../opencode/missions/native-schedule-transport-identity"
|
||||
import type { ServiceConnection } from "../workspaces/opencode-service"
|
||||
import type { AcquiredSupervision } from "./service-supervision-service"
|
||||
import { SupervisionError, supervisionKey } from "./service-supervision-contract"
|
||||
import { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import { WindowsServiceSupervision, readWindowsSupervisionRuntime, type WindowsSupervisionRuntime } from "./service-supervision-windows"
|
||||
import { prepareSupervisionRoot, provisionSupervisionRuntime, supervisionArtifactPolicy } from "./service-supervision-runtime"
|
||||
import { privateStorage, type StoragePolicy } from "./storage"
|
||||
import { runSupervisionWsl, wslRuntimeSchema, type WslSupervisionRuntime } from "./service-supervision-wsl"
|
||||
import { mutateWslSupervisionFile } from "./service-supervision-wsl-lock"
|
||||
import { acquireSupervisionTarget, supervisionTargetReady } from "./service-supervision-target"
|
||||
|
||||
export async function acquireWslSupervision(input: {
|
||||
selectedBinary(): string; connection?: ServiceConnection; environment?: NodeJS.ProcessEnv
|
||||
root?: string; policy?: StoragePolicy; artifactPolicy?: StoragePolicy
|
||||
selection?: boolean
|
||||
/** Qualification-only native paths; never accepted by the Settings HTTP route. */
|
||||
nativeEnvironment?: WslSupervisionRuntime["environment"]
|
||||
}): Promise<AcquiredSupervision> {
|
||||
if (process.platform !== "win32") throw new Error("supervision-platform-unavailable")
|
||||
const selected = input.selectedBinary(), parsed = parseWslUncPath(selected)
|
||||
if (!parsed) throw new SupervisionError("invalid")
|
||||
const env = input.environment ?? process.env, system = env.SystemRoot
|
||||
if (!system || !path.win32.isAbsolute(system) || !env.LOCALAPPDATA) throw new SupervisionError("invalid")
|
||||
const powershell = path.join(system, "System32/WindowsPowerShell/v1.0/powershell.exe")
|
||||
const executable = path.join(system, "System32/wsl.exe")
|
||||
const current = () => {
|
||||
input.connection?.assertCurrent()
|
||||
if (input.selectedBinary() !== selected) throw new SupervisionError("conflict")
|
||||
}
|
||||
const ps = (script: string) => new Promise<string>((resolve, reject) => execFile(powershell,
|
||||
["-NoProfile", "-NonInteractive", "-Command", script], { windowsHide: true, timeout: 5000, maxBuffer: 65536 },
|
||||
(error, stdout) => error ? reject(new Error("supervision-wsl-identity")) : resolve(stdout.trim())))
|
||||
// Windows' registered distro spelling is stable across UNC aliases and case.
|
||||
const names: unknown = JSON.parse(await ps("ConvertTo-Json -InputObject @(Get-ChildItem 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Lxss' | ForEach-Object { (Get-ItemProperty $_.PSPath).DistributionName }) -Compress"))
|
||||
if (!Array.isArray(names)) throw new SupervisionError("invalid")
|
||||
const matches = names.filter((name): name is string => typeof name === "string" && name.toLowerCase() === parsed.distro.toLowerCase())
|
||||
if (matches.length !== 1) throw new SupervisionError("invalid")
|
||||
const distro = matches[0]!, owner = await ps("[Security.Principal.WindowsIdentity]::GetCurrent().User.Value")
|
||||
const initial = { executable, distro }
|
||||
const user = await runSupervisionWsl(initial, ["/usr/bin/id", "-u"])
|
||||
const account = await runSupervisionWsl(initial, ["/usr/bin/id", "-un"])
|
||||
const home = await runSupervisionWsl({ ...initial, account }, ["/bin/sh", "-c", 'printf "%s" "$HOME"'])
|
||||
const binary = await runSupervisionWsl({ ...initial, account }, ["/usr/bin/readlink", "-e", "--", parsed.linuxPath])
|
||||
const map = (native: string) => runSupervisionWsl({ ...initial, account }, ["/usr/bin/wslpath", "-w", native])
|
||||
const selectedTarget = await acquireSupervisionTarget(binary, map, false)
|
||||
const nativeEnv = input.nativeEnvironment ?? { HOME: home }
|
||||
const run = async (args: string[]) => {
|
||||
current()
|
||||
const text = await runSupervisionWsl({ ...initial, account }, ["/usr/bin/env", "-i",
|
||||
...Object.entries(nativeEnv).map(([key, value]) => `${key}=${value}`), "PATH=/usr/bin:/bin", binary, ...args])
|
||||
current()
|
||||
if (!text || /[\r\n\x00]/.test(text)) throw new SupervisionError("invalid")
|
||||
return text
|
||||
}
|
||||
const environment = { HOME: home } as WslSupervisionRuntime["environment"]
|
||||
const roots = {} as Record<"config" | "state" | "data" | "cache", string>
|
||||
for (const name of ["config", "state", "data", "cache"] as const) {
|
||||
const native = await run(["debug", "paths", name])
|
||||
roots[name] = await runSupervisionWsl({ ...initial, account }, ["/usr/bin/readlink", "-e", "--", native])
|
||||
if (path.posix.basename(roots[name]) !== "opencode") throw new SupervisionError("invalid")
|
||||
environment[`XDG_${name.toUpperCase()}_HOME` as "XDG_CONFIG_HOME"] = path.posix.dirname(roots[name])
|
||||
}
|
||||
if (input.nativeEnvironment) Object.assign(environment, input.nativeEnvironment)
|
||||
const password = await run(["service", "get", "password"])
|
||||
const channel = await readNativeServiceChannel(await map(roots.config), password)
|
||||
const connection = input.connection
|
||||
if (connection) {
|
||||
current()
|
||||
const incarnation = readNativeScheduleIncarnation(await map(path.posix.join(roots.state, channel.name)))
|
||||
const identity = runtimeIdentity(connection.endpoint)
|
||||
if (!identity || identity.pid !== incarnation.pid || connection.endpoint.url !== incarnation.url
|
||||
|| connection.endpoint.auth?.type !== "basic" || connection.endpoint.auth.password !== password) throw new SupervisionError("conflict")
|
||||
}
|
||||
const policy = input.policy ?? privateStorage, artifactPolicy = input.artifactPolicy ?? supervisionArtifactPolicy
|
||||
const root = input.root ?? path.join(env.LOCALAPPDATA, "CodeNomadServiceSupervision")
|
||||
const namespace = { platform: "wsl" as const, host: `${os.hostname()}:${owner}`, user, distro,
|
||||
configDirectory: roots.config, stateDirectory: roots.state, registrationName: channel.name }
|
||||
if (input.selection) await lstat(path.join(root, supervisionKey(namespace), "manifest.json")).catch(error => {
|
||||
if (error.code === "ENOENT") throw new SupervisionError("unknown-request"); throw error
|
||||
})
|
||||
current()
|
||||
await prepareSupervisionRoot(root, powershell, policy)
|
||||
// WSL distro registrations belong to a Windows SID. Two Windows accounts can
|
||||
// have identically named distros/UIDs/roots but must never share task names.
|
||||
let store = new ServiceSupervisionStore(root, namespace, selectedTarget, policy)
|
||||
const manifestPath = await runSupervisionWsl({ ...initial, account }, ["/usr/bin/wslpath", "-u", path.join(store.directory, "manifest.json")])
|
||||
const wsl = wslRuntimeSchema.parse({ ...initial, user, account, owner, environment, manifest: manifestPath })
|
||||
const initialManifest = await store.read()
|
||||
store = new ServiceSupervisionStore(root, initialManifest.namespace, initialManifest.target, policy, undefined,
|
||||
update => mutateWslSupervisionFile(wsl, update))
|
||||
let runtime = await readWindowsSupervisionRuntime(store, policy)
|
||||
if (!runtime) {
|
||||
const windowsEnvironment = { SystemRoot: system, WINDIR: system, COMSPEC: path.join(system, "System32/cmd.exe") } as WindowsSupervisionRuntime["environment"]
|
||||
for (const key of ["HOME", "USERPROFILE", "APPDATA", "LOCALAPPDATA", "TEMP", "TMP"] as const) {
|
||||
const value = env[key] ?? (key === "HOME" ? env.USERPROFILE : undefined)
|
||||
if (!value || !path.win32.isAbsolute(value)) throw new SupervisionError("invalid")
|
||||
windowsEnvironment[key] = await realpath(value)
|
||||
}
|
||||
for (const key of ["XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME"] as const) windowsEnvironment[key] = root
|
||||
runtime = { powershell, ...await provisionSupervisionRuntime(store.directory, artifactPolicy), environment: windowsEnvironment, wsl }
|
||||
} else if (JSON.stringify(runtime.wsl) !== JSON.stringify(wsl)) throw new SupervisionError("conflict")
|
||||
for (const file of [binary, roots.config, roots.state]) {
|
||||
// Explicit native canonicalization above; do not resolve Linux PIDs or paths
|
||||
// through Windows' process namespace or the selected profile's environment.
|
||||
if (!file.startsWith("/")) throw new SupervisionError("invalid")
|
||||
}
|
||||
if (!(await lstat(await map(binary))).isFile()) throw new SupervisionError("invalid")
|
||||
current()
|
||||
const adapter = new WindowsServiceSupervision(store, runtime, policy, artifactPolicy)
|
||||
await adapter.prepareRuntime()
|
||||
if (!await supervisionTargetReady(selectedTarget, map)) throw new SupervisionError("conflict")
|
||||
current()
|
||||
return { store, adapter, selectedTarget, assertCurrent: current }
|
||||
}
|
||||
|
|
@ -0,0 +1,143 @@
|
|||
import assert from "node:assert/strict"
|
||||
import { readFile, mkdtemp, rm, lstat, writeFile } from "node:fs/promises"
|
||||
import { spawn, type ChildProcess } from "node:child_process"
|
||||
import os from "node:os"
|
||||
import path from "node:path"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import test from "node:test"
|
||||
import { initialSupervisionManifest, supervisionKey } from "./service-supervision-contract"
|
||||
import { wslRuntimeSchema, wslSupervisionLaunch, wslSupervisionPaths, wslSupervisionLookup } from "./service-supervision-wsl"
|
||||
import { ServiceSupervisionStore } from "./service-supervision-store"
|
||||
import { mutateWindowsSupervisionFile } from "./service-supervision-windows-gate"
|
||||
import { mutateWslSupervisionFile } from "./service-supervision-wsl-lock"
|
||||
|
||||
const runtime = wslRuntimeSchema.parse({ executable: "C:\\Windows\\System32\\wsl.exe", distro: "Ubuntu", user: "1000", account: "u",
|
||||
owner: "S-1-5-21-1-2-3-1000", manifest: "/mnt/c/private/service/manifest.json",
|
||||
environment: { HOME: "/home/u", XDG_CONFIG_HOME: "/home/u/.config", XDG_STATE_HOME: "/home/u/.local/state",
|
||||
XDG_DATA_HOME: "/home/u/.local/share", XDG_CACHE_HOME: "/home/u/.cache" } })
|
||||
const namespace = { platform: "wsl" as const, host: "windows-host", user: "1000", distro: "Ubuntu",
|
||||
configDirectory: "/home/u/.config/opencode", stateDirectory: "/home/u/.local/state/opencode", registrationName: "service.json" }
|
||||
|
||||
test("WSL foreground bridge pins distro/UID and quotes all paths as argv; OFF releases without signaling managed child", () => {
|
||||
const binary = '/home/u/bin with spaces/open"code'
|
||||
const args = wslSupervisionLaunch(runtime, binary, 1)
|
||||
assert.deepEqual(args.slice(0, 7), ["--distribution", "Ubuntu", "--user", "u", "--exec", "/bin/sh", "-c"])
|
||||
assert.match(args[7]!, /trap "" HUP/)
|
||||
assert.match(args[7]!, /while kill -0/)
|
||||
assert.match(args[7]!, /on \|\| exit 0/)
|
||||
assert.doesNotMatch(args[7]!, /setsid|nohup|kill -[A-Z]|service stop/)
|
||||
assert.match(args[7]!, /\$\(\/usr\/bin\/id -u\)/)
|
||||
assert.equal(args[9], runtime.manifest)
|
||||
assert.deepEqual(args.slice(-3), [binary, "serve", "--service"])
|
||||
assert.ok(args.includes("HOME=/home/u"))
|
||||
assert.match(args[7]!, /\/usr\/bin\/flock -x 9/)
|
||||
assert.match(args[7]!, /9>&-/)
|
||||
assert.throws(() => wslSupervisionLaunch(runtime, "relative", 1))
|
||||
assert.throws(() => wslSupervisionLaunch(runtime, "/a/../b", 1))
|
||||
assert.throws(() => wslSupervisionLaunch(runtime, binary, -1))
|
||||
})
|
||||
|
||||
test("WSL runtime accepts only minimal native paths and keeps user/distro/roots separate from Windows identity", async () => {
|
||||
assert.throws(() => wslRuntimeSchema.parse({ ...runtime, environment: { ...runtime.environment, OPENAI_API_KEY: "secret" } }))
|
||||
assert.throws(() => wslRuntimeSchema.parse({ ...runtime, environment: { ...runtime.environment, HOME: "C:\\home" } }))
|
||||
assert.throws(() => wslRuntimeSchema.parse({ ...runtime, distro: "Ubuntu/other" }))
|
||||
assert.throws(() => wslRuntimeSchema.parse({ ...runtime, user: "pascal" }))
|
||||
for (const changed of [{ distro: "Other" }, { user: "1001" }, { stateDirectory: "/other/state/opencode" },
|
||||
{ host: "windows-host:S-1-5-21-1-2-3-1001" }])
|
||||
assert.notEqual(supervisionKey(namespace), supervisionKey({ ...namespace, ...changed }))
|
||||
const manifest = initialSupervisionManifest(namespace, { binary: "/bin/opencode", provenance: "custom" })
|
||||
for (const changed of [{ distro: "Other" }, { user: "1001" },
|
||||
{ environment: { ...runtime.environment, XDG_STATE_HOME: "/other/state" } }])
|
||||
await assert.rejects(wslSupervisionPaths(manifest, { ...runtime, ...changed }), /namespace/)
|
||||
assert.deepEqual(await wslSupervisionLookup(runtime)(-1), { state: "unknown" })
|
||||
})
|
||||
|
||||
test("WSL shares the Windows OS backstop and release contract", async () => {
|
||||
const script = await readFile(new URL("./service-supervision-windows.ps1", import.meta.url), "utf8")
|
||||
assert.match(script, /New-TimeSpan -Seconds 10\) -MultipleInstances Queue/)
|
||||
assert.match(script, /RepetitionInterval \(New-TimeSpan -Minutes 1\)/)
|
||||
assert.match(script, /\$d\.wsl\.owner/)
|
||||
assert.match(script, /\$again\.launch/)
|
||||
assert.match(script, /\$value -notmatch '\[\\s"\]'/)
|
||||
assert.match(script, /WaitForSingleObject\(\$child\.process,8000\)/)
|
||||
assert.doesNotMatch(script, /TerminateProcess|DETACHED_PROCESS[^\n]*true/)
|
||||
})
|
||||
|
||||
test("every WSL manifest mutation uses the namespace publisher; an absent publisher refuses writes", { skip: process.platform !== "win32" }, async t => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "wsl-store-"))
|
||||
t.after(() => rm(root, { recursive: true, force: true }))
|
||||
const policy = { async verify(file: string, directory: boolean) {
|
||||
const stat = await lstat(file)
|
||||
assert.ok(!stat.isSymbolicLink() && (directory ? stat.isDirectory() : stat.isFile() && stat.nlink === 1))
|
||||
} }
|
||||
let locks = 0, held = false
|
||||
const target = { binary: "/bin/opencode", provenance: "custom" as const }
|
||||
const store = new ServiceSupervisionStore(root, namespace, target, policy, undefined, async update => {
|
||||
locks++; held = true
|
||||
try { await mutateWindowsSupervisionFile(path.join(store.directory, "manifest.json"), update) } finally { held = false }
|
||||
})
|
||||
await store.control({ requestID: randomUUID(), action: "on", expectedRevision: 0 }, () => assert.equal(held, true))
|
||||
assert.equal(locks, 1)
|
||||
await store.acknowledge(1)
|
||||
await store.recordAttempt(1, null)
|
||||
assert.equal(locks, 3)
|
||||
const unfenced = new ServiceSupervisionStore(root, namespace, target, policy)
|
||||
await assert.rejects(unfenced.control({ requestID: randomUUID(), action: "off", expectedRevision: 1 }), /invalid/)
|
||||
assert.equal((await store.read()).desired, "on")
|
||||
await store.control({ requestID: randomUUID(), action: "off", expectedRevision: 1 })
|
||||
assert.equal(locks, 4)
|
||||
assert.equal((await store.read()).desired, "off")
|
||||
})
|
||||
|
||||
test("WSL publisher death before an asynchronous update returns cannot publish a late write", async t => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "wsl-publication-")), file = path.join(root, "manifest.json")
|
||||
t.after(() => rm(root, { recursive: true, force: true }))
|
||||
await writeFile(file, "original")
|
||||
let holder: ChildProcess | undefined, release!: () => void
|
||||
const blocked = new Promise<void>(resolve => { release = resolve })
|
||||
let entered!: () => void
|
||||
const updating = new Promise<void>(resolve => { entered = resolve })
|
||||
const launch: typeof spawn = ((_executable: string, args: string[]) => {
|
||||
const script = args[7]!
|
||||
assert.ok(script.indexOf("/usr/bin/flock -xn 9") < script.indexOf("READY:"))
|
||||
assert.ok(script.indexOf("/usr/bin/flock -xn 9") < script.indexOf("/bin/mv -fT"))
|
||||
assert.match(script, /exec 9>>/)
|
||||
assert.deepEqual(args.slice(-3), [runtime.manifest, "/mnt/c/private/service/linux-admission.lock", runtime.user])
|
||||
holder = spawn(process.execPath, ["-e", `const fs = require('node:fs'); process.stdout.write('READY:-\\n');
|
||||
process.stdin.once('data', bytes => { fs.writeFileSync(${JSON.stringify(file)}, Buffer.from(String(bytes).trim(), 'base64'));
|
||||
process.stdout.write('COMMITTED\\n'); process.stdin.destroy(); });`], { stdio: ["pipe", "pipe", "pipe"] })
|
||||
return holder
|
||||
}) as typeof spawn
|
||||
await mutateWslSupervisionFile(runtime, async () => "committed by holder", launch)
|
||||
assert.equal(await readFile(file, "utf8"), "committed by holder")
|
||||
await writeFile(file, "original")
|
||||
const result = mutateWslSupervisionFile(runtime, async () => { entered(); await blocked; return "late" }, launch)
|
||||
const rejected = assert.rejects(result, /busy/)
|
||||
await updating
|
||||
holder!.kill()
|
||||
await rejected
|
||||
release()
|
||||
await blocked
|
||||
assert.equal(await readFile(file, "utf8"), "original")
|
||||
})
|
||||
|
||||
test("WSL native qualification never terminates a general-purpose distro", async () => {
|
||||
const script = await readFile(new URL("../../../../scripts/test-service-supervision-wsl-native.mjs", import.meta.url), "utf8")
|
||||
assert.doesNotMatch(script, /--terminate|--shutdown/)
|
||||
})
|
||||
|
||||
test("Linux publication owns flock through flush/rename and excludes a second writer", { skip: process.platform !== "linux" }, async t => {
|
||||
const root = await mkdtemp(path.join(os.tmpdir(), "wsl-native-publisher-")), file = path.join(root, "manifest.json")
|
||||
t.after(() => rm(root, { recursive: true, force: true }))
|
||||
const native = { ...runtime, user: String(process.getuid!()), manifest: file }
|
||||
const launch: typeof spawn = ((_executable: string, args: string[]) => spawn("/bin/sh", args.slice(6),
|
||||
{ stdio: ["pipe", "pipe", "pipe"] })) as typeof spawn
|
||||
await writeFile(file, "original")
|
||||
await mutateWslSupervisionFile(native, async before => {
|
||||
assert.equal(before, "original")
|
||||
await assert.rejects(mutateWslSupervisionFile(native, async () => "competing", launch), /busy/)
|
||||
assert.equal(await readFile(file, "utf8"), "original")
|
||||
return "published"
|
||||
}, launch)
|
||||
assert.equal(await readFile(file, "utf8"), "published")
|
||||
})
|
||||
104
packages/server/src/host-lifetime/service-supervision-wsl.ts
Normal file
104
packages/server/src/host-lifetime/service-supervision-wsl.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
import { execFile } from "node:child_process"
|
||||
import path from "node:path"
|
||||
import { z } from "zod"
|
||||
import { Service } from "@opencode/client/service"
|
||||
import type { ProcessLookup } from "./process-identity"
|
||||
import { nativeSupervisionStartPermitted, readSupervisionSample } from "./service-supervision-health"
|
||||
import type { SupervisionManifest } from "./service-supervision-contract"
|
||||
import { readNativeScheduleIncarnation } from "../opencode/missions/native-schedule-transport-identity"
|
||||
import { supervisionTargetReady } from "./service-supervision-target"
|
||||
|
||||
const linuxPath = z.string().min(1).max(4096).refine(value => value.startsWith("/")
|
||||
&& path.posix.normalize(value) === value && !/[\\\x00-\x1f\x7f]/.test(value))
|
||||
export const wslRuntimeSchema = z.object({
|
||||
executable: z.string().max(4096).refine(value => /^[A-Za-z]:\\/.test(value)
|
||||
&& path.win32.normalize(value) === value && !/[\x00-\x1f"]/.test(value) && !value.slice(2).includes(":")),
|
||||
distro: z.string().min(1).max(240).regex(/^[^\\/\x00-\x1f"]+$/),
|
||||
user: z.string().regex(/^(?:0|[1-9]\d*)$/),
|
||||
account: z.string().min(1).max(256).regex(/^[a-zA-Z_][a-zA-Z0-9_.-]*\$?$/),
|
||||
owner: z.string().regex(/^S-1-\d+(?:-\d+)+$/),
|
||||
manifest: linuxPath,
|
||||
environment: z.object({ HOME: linuxPath, XDG_CONFIG_HOME: linuxPath, XDG_STATE_HOME: linuxPath,
|
||||
XDG_DATA_HOME: linuxPath, XDG_CACHE_HOME: linuxPath, OPENCODE_TEST_HOME: linuxPath.optional() }).strict(),
|
||||
}).strict()
|
||||
export type WslSupervisionRuntime = z.infer<typeof wslRuntimeSchema>
|
||||
|
||||
/** Exact distro/UID, fixed argv, no shell interpolation or Windows PID operations. */
|
||||
export function runSupervisionWsl(runtime: Pick<WslSupervisionRuntime, "executable" | "distro"> & { account?: string }, args: string[], timeout = 5000): Promise<string> {
|
||||
return new Promise((resolve, reject) => execFile(runtime.executable,
|
||||
["--distribution", runtime.distro, ...(runtime.account ? ["--user", runtime.account] : []), "--exec", ...args],
|
||||
{ windowsHide: true, timeout, maxBuffer: 65536, encoding: "utf8",
|
||||
env: { SystemRoot: process.env.SystemRoot, WINDIR: process.env.SystemRoot, USERPROFILE: process.env.USERPROFILE,
|
||||
LOCALAPPDATA: process.env.LOCALAPPDATA } },
|
||||
(error, stdout) => error ? reject(new Error("supervision-wsl-unavailable")) : resolve(stdout.trim())))
|
||||
}
|
||||
|
||||
export function wslSupervisionLookup(runtime: WslSupervisionRuntime): ProcessLookup {
|
||||
return async pid => {
|
||||
if (!Number.isSafeInteger(pid) || pid <= 0) return { state: "unknown" }
|
||||
try {
|
||||
const text = await runSupervisionWsl(runtime, ["/bin/sh", "-c",
|
||||
'if [ ! -d "/proc/$1" ]; then printf DEAD; else cat "/proc/$1/stat" /proc/sys/kernel/random/boot_id; fi', "codenomad-process", String(pid)])
|
||||
if (text === "DEAD") return { state: "dead" }
|
||||
const [stat, boot] = text.split("\n")
|
||||
const fields = stat?.slice(stat.lastIndexOf(")") + 1).trim().split(/\s+/)
|
||||
if (!fields || !/^\d+$/.test(fields[19] ?? "") || !/^[a-f0-9-]{36}$/.test(boot ?? "")) return { state: "unknown" }
|
||||
if (fields[0] === "Z" || fields[0] === "X") return { state: "dead" }
|
||||
return { state: "live", startIdentity: `linux:${boot}:${fields[19]}` }
|
||||
} catch { return { state: "unknown" } }
|
||||
}
|
||||
}
|
||||
|
||||
export async function wslSupervisionPaths(manifest: SupervisionManifest, runtime: WslSupervisionRuntime) {
|
||||
const n = manifest.namespace
|
||||
if (n.platform !== "wsl" || n.user !== runtime.user || n.distro !== runtime.distro
|
||||
|| path.posix.join(runtime.environment.XDG_CONFIG_HOME, "opencode") !== n.configDirectory
|
||||
|| path.posix.join(runtime.environment.XDG_STATE_HOME, "opencode") !== n.stateDirectory) throw new Error("supervision-wsl-namespace")
|
||||
// One bounded native inspection keeps the guard's probe deadline independent
|
||||
// of the startup cost of multiple wsl.exe clients.
|
||||
const rows = (await runSupervisionWsl(runtime, ["/bin/sh", "-c",
|
||||
'set -eu; /usr/bin/id -u; for p in "$@"; do /usr/bin/readlink -e -- "$p"; /usr/bin/wslpath -w "$p"; done',
|
||||
"codenomad-namespace", n.configDirectory, n.stateDirectory])).split(/\r?\n/)
|
||||
if (rows.length !== 5 || rows[0] !== n.user || rows[1] !== n.configDirectory || rows[3] !== n.stateDirectory
|
||||
|| [rows[2]!, rows[4]!].some(host => !path.win32.isAbsolute(host) || /[\r\n\x00]/.test(host))) throw new Error("supervision-wsl-namespace")
|
||||
return { config: path.win32.join(rows[2]!, n.registrationName), registration: path.win32.join(rows[4]!, n.registrationName) }
|
||||
}
|
||||
|
||||
/** Discovery runs from Windows: a healthy result also proves authenticated
|
||||
* localhost forwarding. An unreachable live Linux PID never authorizes a spawn. */
|
||||
export async function readWslSupervisionSample(manifest: SupervisionManifest, runtime: WslSupervisionRuntime,
|
||||
discover: typeof Service.discover = Service.discover, checkTarget = false) {
|
||||
const files = await wslSupervisionPaths(manifest, runtime)
|
||||
const { health, identity } = await readSupervisionSample(files.registration, { lookup: wslSupervisionLookup(runtime), discover })
|
||||
const permitted = await nativeSupervisionStartPermitted(files.config)
|
||||
let forwardingUnavailable = false
|
||||
if (health.state === "unhealthy") {
|
||||
// The official CLI's status is read-only and probes from Linux. A native
|
||||
// healthy URL plus failed Windows discovery isolates the forwarding path;
|
||||
// timeout/500/unknown inside Linux keeps ordinary unknown health instead.
|
||||
const native = await runSupervisionWsl(runtime, ["/usr/bin/env", "-i",
|
||||
...Object.entries(runtime.environment).map(([key, value]) => `${key}=${value}`),
|
||||
"PATH=/usr/bin:/bin", manifest.target.binary, "service", "status"], 3000).catch(() => undefined)
|
||||
forwardingUnavailable = native !== undefined && native === readNativeScheduleIncarnation(files.registration).url
|
||||
}
|
||||
return { revision: manifest.revision, desired: manifest.desired, binary: manifest.target.binary, health, identity, permitted,
|
||||
...(checkTarget ? { targetReady: await supervisionTargetReady(manifest.target,
|
||||
file => runSupervisionWsl(runtime, ["/usr/bin/wslpath", "-w", file]), { namespace: manifest.namespace,
|
||||
run: args => runSupervisionWsl(runtime, ["/usr/bin/env", "-i", ...Object.entries(runtime.environment).map(([key, value]) => `${key}=${value}`),
|
||||
"PATH=/usr/bin:/bin", manifest.target.binary, ...args], 2000) }) } : {}),
|
||||
forwardingUnavailable, launch: wslSupervisionLaunch(runtime, manifest.target.binary, manifest.revision) }
|
||||
}
|
||||
|
||||
/** The fixed shell is the Linux foreground bridge. Ignored HUP is inherited by
|
||||
* managed OpenCode; independent stdio and the Windows handle release preserve it
|
||||
* at OFF. ON keeps the wsl.exe client resident and observes its real exit. */
|
||||
export function wslSupervisionLaunch(runtime: WslSupervisionRuntime, binary: string, revision: number): string[] {
|
||||
if (!linuxPath.safeParse(binary).success || !Number.isSafeInteger(revision) || revision < 0) throw new Error("supervision-wsl-binary")
|
||||
return ["--distribution", runtime.distro, "--user", runtime.account, "--exec", "/bin/sh", "-c",
|
||||
// The fixed JSON token is only a release hint from our atomic private
|
||||
// manifest, never admission authority. Windows owns the Start exclusion.
|
||||
'trap "" HUP; manifest=$1; user=$2; home=$3; revision=$4; lock=$5; shift 5; [ "$(/usr/bin/id -u)" = "$user" ] && cd "$home" || exit 1; on() { /usr/bin/grep -Fq \'"desired":"on"\' "$manifest"; }; exec 9>>"$lock"; /usr/bin/flock -x 9 || exit 1; on && /usr/bin/grep -Fq "\\"revision\\":$revision,\\"desired\\":\\"on\\"" "$manifest" || exit 0; /usr/bin/env -i "$@" 9>&- </dev/null >/dev/null 2>&1 & child=$!; exec 9>&-; while kill -0 "$child" 2>/dev/null; do on || exit 0; /bin/sleep 1; done; wait "$child"',
|
||||
"codenomad-foreground", runtime.manifest, runtime.user, runtime.environment.HOME, String(revision),
|
||||
path.posix.join(path.posix.dirname(runtime.manifest), "linux-admission.lock"),
|
||||
...Object.entries(runtime.environment).map(([key, value]) => `${key}=${value}`), "PATH=/usr/bin:/bin", "LANG=C.UTF-8", binary, "serve", "--service"]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue