Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ allowlisted lifecycle shape into the shared durable session-summary queue.
`session.idle` becomes `RunEnd`, so one completed agent response can issue one
run receipt without pretending the multi-turn OpenCode conversation ended.
`session.deleted` remains the whole-session terminal boundary.
An auth or provider failure before a completed assistant response or tool
request does not create a run receipt.

The adapter observes session, user-message, permission, and tool lifecycle
events. Capture is metadata-only by default. Set
Expand Down Expand Up @@ -83,11 +85,15 @@ missing, mismatched, or unverified v2 activation fails closed; the task is not
prompted with ambient organizational authority.

For an interactive session without a Gateway activation, `session.created`
(or the first session-bound message/model request if that event was missed)
requests one context pack for the most specific configured anchor: task,
workstream, initiative, then workspace. Hydration is exactly once per native
OpenCode session. Internal model requests without a native session ID receive
no OrgX context or authority.
(or the first session-bound user message if that event was missed) first asks
the Wizard to claim any staged cwd-only context for the exact native session.
A successful local claim is delivered on that user turn without a network
refresh. Otherwise, the plugin requests one context pack for the most specific
configured anchor: task, workstream, initiative, then workspace. Hydration is
exactly once per native OpenCode session. The bounded context is retried after
a pre-inference failure, then stops being added after a completed assistant
response or tool request. Auxiliary title and small-model system transforms do
not receive it.

The full pack and any pending activation are stored outside the repository in
owner-only Wizard state. Each directory is keyed by the SHA-256 of the resolved
Expand All @@ -110,10 +116,15 @@ orgx-wizard sessions context set --file - --cwd <active-project> \
The digest is calculated from recursively key-sorted JSON (array order is
preserved and undefined values are omitted). The plugin accepts activation only
when the Wizard returns the v1 acknowledgement and v2 activation versions plus
the exact resolved cwd, source client, native session ID, and digest. It clears
that exact lease on `session.deleted`, and it clears stale authority when a
refresh fails or returns an invalid API envelope. If the Wizard is offline, the
plugin keeps the exact context in that session's private pending file.
the exact resolved cwd, source client, native session ID, and digest. After
model work, `session.deleted` clears that exact lease. If the session ends
before a completed assistant response or tool request, the Wizard atomically
returns the exact v2 lease to one cwd-only v1 activation before the plugin
clears private state. A newer cwd-only activation is never overwritten. If
release cannot be proven, the exact lease and private context remain intact.
The plugin also clears stale authority when a refresh fails or returns an
invalid API envelope. If the Wizard is offline, it keeps the exact context in
that session's private pending file.
Interactive sessions may continue with briefing explicitly marked
non-authoritative; Gateway dispatches fail before prompting. Context fetch and
activation each time out after 3 seconds by default.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@useorgx/orgx-opencode-plugin",
"version": "0.1.0-alpha.18",
"version": "0.1.0-alpha.19",
"publishConfig": {
"access": "public"
},
Expand Down
2 changes: 1 addition & 1 deletion plugin.manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"plugin_name": "@useorgx/orgx-opencode-plugin",
"version": "0.1.0-alpha.18",
"version": "0.1.0-alpha.19",
"manifest_fingerprint": "",
"signature": "",
"capabilities": [
Expand Down
9 changes: 5 additions & 4 deletions src/contextPackHydration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
sessionWorkContextSha256,
type SpawnLike,
} from './contextPackHydration.js';
import { normalizeAbsoluteHostPath } from './hostPath.js';

type HydrationInput = NonNullable<
Parameters<typeof hydrateContextPackImpl>[0]
Expand Down Expand Up @@ -346,7 +347,7 @@ describe('opencode context-pack hydration', () => {
'--file',
'-',
'--cwd',
projectDir,
normalizeAbsoluteHostPath(projectDir),
'--source-client',
'opencode',
'--session-id',
Expand Down Expand Up @@ -663,7 +664,7 @@ describe('opencode context-pack hydration', () => {
'context',
'clear',
'--cwd',
projectDir,
normalizeAbsoluteHostPath(projectDir),
'--source-client',
'opencode',
'--session-id',
Expand Down Expand Up @@ -780,7 +781,7 @@ describe('opencode context-pack hydration', () => {
'context',
'clear',
'--cwd',
projectDir,
normalizeAbsoluteHostPath(projectDir),
'--source-client',
'opencode',
'--session-id',
Expand Down Expand Up @@ -963,7 +964,7 @@ describe('opencode context-pack hydration', () => {
'context',
'clear',
'--cwd',
projectDir,
normalizeAbsoluteHostPath(projectDir),
'--source-client',
'opencode',
'--session-id',
Expand Down
24 changes: 24 additions & 0 deletions src/hostPath.test.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
import {
mkdirSync,
mkdtempSync,
realpathSync,
rmSync,
symlinkSync,
} from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';

import { normalizeAbsoluteHostPath } from './hostPath';
Expand Down Expand Up @@ -44,4 +53,19 @@ describe('normalizeAbsoluteHostPath', () => {
);
expect(normalizeAbsoluteHostPath('work/orgx', 'linux')).toBeNull();
});

it('canonicalizes a POSIX symlink alias through its longest existing ancestor', () => {
const root = mkdtempSync(join(tmpdir(), 'orgx-opencode-path-'));
const physical = join(root, 'physical');
const alias = join(root, 'alias');
mkdirSync(physical);
symlinkSync(physical, alias, 'dir');
try {
expect(normalizeAbsoluteHostPath(join(alias, 'future', 'repo'))).toBe(
join(realpathSync.native(physical), 'future', 'repo')
);
} finally {
rmSync(root, { recursive: true, force: true });
}
});
});
20 changes: 19 additions & 1 deletion src/hostPath.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
import { realpathSync } from 'node:fs';
import { platform as runtimePlatform } from 'node:os';
import { posix, win32 } from 'node:path';

const WINDOWS_DRIVE_ABSOLUTE = /^[A-Za-z]:[\\/]/;
const WINDOWS_UNC_ABSOLUTE = /^[\\/]{2}(?![?.][\\/])[^\\/]+[\\/][^\\/]+(?:[\\/]|$)/;
const WINDOWS_DEVICE_NAMESPACE = /^[\\/]{1,2}(?:[?.][\\/]|\?\?[\\/])/;

function canonicalPosixPath(value: string): string {
const absolute = posix.resolve(value);
let ancestor = absolute;
const missing: string[] = [];

while (true) {
try {
return posix.resolve(realpathSync.native(ancestor), ...missing);
} catch {
const parent = posix.dirname(ancestor);
if (parent === ancestor) return absolute;
missing.unshift(posix.basename(ancestor));
ancestor = parent;
}
}
}

/**
* Canonicalize an absolute path using the selected host's path contract.
*
Expand All @@ -19,7 +37,7 @@ export function normalizeAbsoluteHostPath(
if (typeof value !== 'string' || value.trim().length === 0) return null;

if (hostPlatform !== 'win32') {
return posix.isAbsolute(value) ? posix.resolve(value) : null;
return posix.isAbsolute(value) ? canonicalPosixPath(value) : null;
}

if (
Expand Down
2 changes: 1 addition & 1 deletion src/peer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ describe('startPeer', () => {
);

expect(presence?.body).toMatchObject({
gateway_version: '0.1.0-alpha.18',
gateway_version: '0.1.0-alpha.19',
metadata: {
execution_provider: null,
execution_provider_id: null,
Expand Down
Loading
Loading